@kano/stem-daw 0.1.0
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.
- package/README.md +253 -0
- package/dist/chat-actions-54Z6URC4.js +7 -0
- package/dist/chat-actions-54Z6URC4.js.map +1 -0
- package/dist/chunk-56PWIP7O.js +1029 -0
- package/dist/chunk-56PWIP7O.js.map +1 -0
- package/dist/chunk-AAVC7KUW.js +145 -0
- package/dist/chunk-AAVC7KUW.js.map +1 -0
- package/dist/chunk-KCOOE2OP.js +1764 -0
- package/dist/chunk-KCOOE2OP.js.map +1 -0
- package/dist/chunk-LO74ZJ4H.js +23923 -0
- package/dist/chunk-LO74ZJ4H.js.map +1 -0
- package/dist/chunk-OFGZURP6.js +247 -0
- package/dist/chunk-OFGZURP6.js.map +1 -0
- package/dist/chunk-OYNES5W3.js +3085 -0
- package/dist/chunk-OYNES5W3.js.map +1 -0
- package/dist/chunk-QQ5NZTHT.js +336 -0
- package/dist/chunk-QQ5NZTHT.js.map +1 -0
- package/dist/chunk-TBXCZFAY.js +13713 -0
- package/dist/chunk-TBXCZFAY.js.map +1 -0
- package/dist/chunk-U44X6QP5.js +281 -0
- package/dist/chunk-U44X6QP5.js.map +1 -0
- package/dist/chunk-UKMELGZL.js +27 -0
- package/dist/chunk-UKMELGZL.js.map +1 -0
- package/dist/components/DAWView.d.ts +19 -0
- package/dist/components/DAWView.js +11 -0
- package/dist/components/DAWView.js.map +1 -0
- package/dist/daw-controller-BjRWcTol.d.ts +339 -0
- package/dist/engine/daw-controller.d.ts +3 -0
- package/dist/engine/daw-controller.js +5 -0
- package/dist/engine/daw-controller.js.map +1 -0
- package/dist/engine/daw-import-stem-fm-config.d.ts +224 -0
- package/dist/engine/daw-import-stem-fm-config.js +7 -0
- package/dist/engine/daw-import-stem-fm-config.js.map +1 -0
- package/dist/fetchStationTracks-SKFT4V3U.js +3 -0
- package/dist/fetchStationTracks-SKFT4V3U.js.map +1 -0
- package/dist/index.d.ts +308 -0
- package/dist/index.js +332 -0
- package/dist/index.js.map +1 -0
- package/dist/interface-DaRj7RkY.d.ts +66 -0
- package/dist/interfaces-5ZlG0Y4Y.d.ts +549 -0
- package/dist/media-session-XTP6PP7Q.js +3 -0
- package/dist/media-session-XTP6PP7Q.js.map +1 -0
- package/dist/note-detection-PPLM7R2H.js +148 -0
- package/dist/note-detection-PPLM7R2H.js.map +1 -0
- package/dist/sampler-audio-B7MBG3YN.js +3 -0
- package/dist/sampler-audio-B7MBG3YN.js.map +1 -0
- package/dist/sampler-store-QPHANXYP.js +3 -0
- package/dist/sampler-store-QPHANXYP.js.map +1 -0
- package/dist/services/track-search-api.d.ts +152 -0
- package/dist/services/track-search-api.js +4 -0
- package/dist/services/track-search-api.js.map +1 -0
- package/dist/store/daw-auth-store.d.ts +31 -0
- package/dist/store/daw-auth-store.js +3 -0
- package/dist/store/daw-auth-store.js.map +1 -0
- package/dist/store/daw-session-store.d.ts +255 -0
- package/dist/store/daw-session-store.js +3 -0
- package/dist/store/daw-session-store.js.map +1 -0
- package/dist/vite/index.d.ts +46 -0
- package/dist/vite/index.js +94 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/workers/analysis-worker.js +379 -0
- package/dist/workers/buffer-player-processor-202602.lavv8e32-ts.js +1 -0
- package/dist/workers/daw-stem-processor.js +228 -0
- package/dist/workers/manifest.json +10 -0
- package/dist/workers/phase-vocoder3.js +920 -0
- package/dist/workers/realtime-pitch-shift-processor.js +2 -0
- package/package.json +151 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
import { AEV3Config, extractBareSessionStationId, resolveSingleBeatsAndKeysWithBeatsLink, replaceMp4ForLocalUse, calculateTempoOrigSzFromBeatDownBeat, calculateQuantisedTrackActualDurationFromBeatGrid, getFullDurationFromBeatsLinkSegmentInfo, getDurationFromBeatsLinkUpToLastDownbeat0, resolveMultipleBeatsAndKeysWithBeatsLink } from './chunk-TBXCZFAY.js';
|
|
2
|
+
|
|
3
|
+
// src/services/dropped-mix-ids.ts
|
|
4
|
+
var NEXT_MIX_ID_DROPPED_ON_RESOLVING_NEXT_MIX = [];
|
|
5
|
+
|
|
6
|
+
// src/services/fetchMix.tsx
|
|
7
|
+
var TRACK_CREATOR_ENDPOINT = `${import.meta.env?.VITE_KB_STEMIFY_API_URL || "https://api.stemplayer.com"}/track-creator`;
|
|
8
|
+
var LOGIN_LINK = `${import.meta.env?.VITE_KB_STEMIFY_API_URL || "https://api.stemplayer.com"}/accounts/user/login`;
|
|
9
|
+
var APPLY_BEAT_GRID_SMOOTHING = true;
|
|
10
|
+
var FETCH_MIX_CONSOLE_TIME = "";
|
|
11
|
+
var TRACK_ITEM_FRAGMENT = `fragment TrackItem on Track {
|
|
12
|
+
artistDisplayName
|
|
13
|
+
colors
|
|
14
|
+
name
|
|
15
|
+
trackId
|
|
16
|
+
trackMixType
|
|
17
|
+
trackStatus
|
|
18
|
+
isClaimed
|
|
19
|
+
duration
|
|
20
|
+
artwork: image
|
|
21
|
+
artworkGlowColor
|
|
22
|
+
url: url(codec: aac, container: mp4, profile: raw)
|
|
23
|
+
stems {
|
|
24
|
+
aac: url(codec: aac)
|
|
25
|
+
hls: url(codec: hls)
|
|
26
|
+
wav: url(codec: wav)
|
|
27
|
+
mp3: url(codec: mp3)
|
|
28
|
+
stemId
|
|
29
|
+
stemPosition
|
|
30
|
+
}
|
|
31
|
+
}`;
|
|
32
|
+
var MIX_ITEM_FRAGMENT = `fragment MixItem on Mix {
|
|
33
|
+
mixId
|
|
34
|
+
name
|
|
35
|
+
colors
|
|
36
|
+
mixPosition
|
|
37
|
+
tracks {
|
|
38
|
+
...TrackItem
|
|
39
|
+
}
|
|
40
|
+
popularSelectedStems {
|
|
41
|
+
vocals
|
|
42
|
+
other
|
|
43
|
+
drums
|
|
44
|
+
bass
|
|
45
|
+
}
|
|
46
|
+
}`;
|
|
47
|
+
var _FM_log = (...data) => {
|
|
48
|
+
return;
|
|
49
|
+
};
|
|
50
|
+
var _FM_log_table = (...data) => {
|
|
51
|
+
return;
|
|
52
|
+
};
|
|
53
|
+
var login = async () => {
|
|
54
|
+
const loginUrl = LOGIN_LINK;
|
|
55
|
+
try {
|
|
56
|
+
const cachedToken = localStorage.getItem("accessToken");
|
|
57
|
+
const tokenExpiry = localStorage.getItem("tokenExpiry");
|
|
58
|
+
if (cachedToken && tokenExpiry && (/* @__PURE__ */ new Date()).getTime() < parseInt(tokenExpiry, 10)) {
|
|
59
|
+
const cachedTokenObj = JSON.parse(cachedToken);
|
|
60
|
+
_FM_log("Using cached access token:", cachedTokenObj.accessToken.value);
|
|
61
|
+
return cachedTokenObj.accessToken.value;
|
|
62
|
+
}
|
|
63
|
+
let loginPayload;
|
|
64
|
+
try {
|
|
65
|
+
const username = import.meta.env?.VITE_STEM_API_USERNAME;
|
|
66
|
+
const password = import.meta.env?.VITE_STEM_API_PASSWORD;
|
|
67
|
+
if (!username || !password) {
|
|
68
|
+
throw new Error("Username or password not found in environment variables");
|
|
69
|
+
}
|
|
70
|
+
loginPayload = {
|
|
71
|
+
email: username,
|
|
72
|
+
password
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
_FM_log(error);
|
|
76
|
+
throw new Error("Username or password not found");
|
|
77
|
+
}
|
|
78
|
+
_FM_log("Starting login request...");
|
|
79
|
+
const loginResponse = await fetch(loginUrl, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json"
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(loginPayload)
|
|
85
|
+
});
|
|
86
|
+
if (!loginResponse.ok) {
|
|
87
|
+
throw new Error("Login failed");
|
|
88
|
+
}
|
|
89
|
+
const loginData = await loginResponse.json();
|
|
90
|
+
const accessTokenObj = loginData.data;
|
|
91
|
+
const expiryTime = (/* @__PURE__ */ new Date()).getTime() + 20 * 60 * 1e3;
|
|
92
|
+
_FM_log("Login successful. Access token:", accessTokenObj.accessToken.value);
|
|
93
|
+
localStorage.setItem("accessToken", JSON.stringify(accessTokenObj));
|
|
94
|
+
localStorage.setItem("tokenExpiry", expiryTime.toString());
|
|
95
|
+
return accessTokenObj.accessToken.value;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error("Error during login or fetching mix data:", error.message);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var getMixMetadata = async (authToken, mixId, stationId, smoothBeats = APPLY_BEAT_GRID_SMOOTHING, abortController) => {
|
|
102
|
+
const query = `query GetMix($id: ID!) {
|
|
103
|
+
mix(id: $id) {
|
|
104
|
+
...MixItem
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
${TRACK_ITEM_FRAGMENT}
|
|
109
|
+
|
|
110
|
+
${MIX_ITEM_FRAGMENT}`;
|
|
111
|
+
const variables = { "id": mixId };
|
|
112
|
+
const queryParams = new URLSearchParams({
|
|
113
|
+
query: query.trim(),
|
|
114
|
+
variables: JSON.stringify(variables),
|
|
115
|
+
operationName: "GetMix"
|
|
116
|
+
});
|
|
117
|
+
const url = `${TRACK_CREATOR_ENDPOINT}/graphql?${queryParams.toString()}`;
|
|
118
|
+
const headers = {
|
|
119
|
+
Authorization: `Bearer ${authToken}`
|
|
120
|
+
};
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(url, {
|
|
123
|
+
method: "GET",
|
|
124
|
+
headers,
|
|
125
|
+
signal: abortController?.signal
|
|
126
|
+
});
|
|
127
|
+
const responseData = await response.json();
|
|
128
|
+
if (response.ok) {
|
|
129
|
+
_FM_log("Successfully fetched the mix details.");
|
|
130
|
+
_FM_log(responseData);
|
|
131
|
+
if (!responseData?.data?.mix) {
|
|
132
|
+
console.error("Mix not found or returned null for this ID");
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
responseData.data.mix.remixes = [];
|
|
136
|
+
const trackIds = responseData.data.mix.tracks.map((t) => {
|
|
137
|
+
return { id: t.trackId, url: t.url };
|
|
138
|
+
});
|
|
139
|
+
const trackIds2 = responseData.data.mix.remixes?.length > 0 ? responseData.data.mix.remixes[0].tracksmap((t) => {
|
|
140
|
+
return { id: t.trackId, url: t.url };
|
|
141
|
+
}) : [];
|
|
142
|
+
const tracks = removeDuplicatesById([...trackIds, ...trackIds2]);
|
|
143
|
+
const trackBeatsCollection2 = await resolveMultipleBeatsAndKeysWithBeatsLink(tracks, authToken, smoothBeats, window.$currentBeatGridVersion);
|
|
144
|
+
responseData.data.mix.tracks.map((t) => {
|
|
145
|
+
const m_t = t;
|
|
146
|
+
if (trackBeatsCollection2.data[t.trackId]) {
|
|
147
|
+
replaceMp4Url(m_t);
|
|
148
|
+
addBeatsRelatedParam(m_t, trackBeatsCollection2.data[t.trackId]);
|
|
149
|
+
m_t.__falseTrack = false;
|
|
150
|
+
m_t.__durationForValidation1 = getFullDurationFromBeatsLinkSegmentInfo(m_t.segmentInfo);
|
|
151
|
+
m_t.__durationForValidation2 = getDurationFromBeatsLinkUpToLastDownbeat0(-1, m_t.beats);
|
|
152
|
+
} else {
|
|
153
|
+
m_t.__falseTrack = true;
|
|
154
|
+
m_t.__durationForValidation1 = 0;
|
|
155
|
+
m_t.__durationForValidation2 = 0;
|
|
156
|
+
}
|
|
157
|
+
return m_t;
|
|
158
|
+
});
|
|
159
|
+
return responseData;
|
|
160
|
+
} else {
|
|
161
|
+
console.error(`Failed to fetch mix details. ${FETCH_MIX_CONSOLE_TIME}`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.timeEnd(FETCH_MIX_CONSOLE_TIME);
|
|
166
|
+
console.error(`Error during mix detail retrieval: ${e}`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var getNextMixMetadata = async (authToken, mixId, stationId, direction, currentMixPosition = -1, smoothBeats = APPLY_BEAT_GRID_SMOOTHING, abortController) => {
|
|
171
|
+
const query = `query MixesToPlay($id: [ID]!, $first: Int, $afterId: ID, $sort: Sort) {
|
|
172
|
+
stations(ids: $id) {
|
|
173
|
+
stationId
|
|
174
|
+
lastModified
|
|
175
|
+
mixCount
|
|
176
|
+
mixes(first: $first, afterId: $afterId, sort: $sort) {
|
|
177
|
+
...MixItem
|
|
178
|
+
mixPosition
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
${TRACK_ITEM_FRAGMENT}
|
|
184
|
+
|
|
185
|
+
${MIX_ITEM_FRAGMENT}`;
|
|
186
|
+
const queryArtist = `query MixesToPlay($id: [ID]!, $first: Int, $afterId: ID) {
|
|
187
|
+
stations: libraryArtists(ids: $id) {
|
|
188
|
+
artist {
|
|
189
|
+
artistId
|
|
190
|
+
stationId: artistId
|
|
191
|
+
}
|
|
192
|
+
lastModified
|
|
193
|
+
mixCount
|
|
194
|
+
mixes(first: $first, afterId: $afterId) {
|
|
195
|
+
...MixItem
|
|
196
|
+
mixPosition
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
${TRACK_ITEM_FRAGMENT}
|
|
202
|
+
|
|
203
|
+
${MIX_ITEM_FRAGMENT}`;
|
|
204
|
+
const variables = { "afterId": mixId, "id": [stationId], "first": 3, "sort": direction === "nextMixId" ? "asc" : "desc" };
|
|
205
|
+
const queryParams = new URLSearchParams({
|
|
206
|
+
query: isStationIdLibraryArtistStation(stationId) ? queryArtist.trim() : query.trim(),
|
|
207
|
+
variables: JSON.stringify(variables),
|
|
208
|
+
operationName: "MixesToPlay"
|
|
209
|
+
});
|
|
210
|
+
const url = `${TRACK_CREATOR_ENDPOINT}/graphql?${queryParams.toString()}`;
|
|
211
|
+
const headers = {
|
|
212
|
+
Authorization: `Bearer ${authToken}`
|
|
213
|
+
};
|
|
214
|
+
FETCH_MIX_CONSOLE_TIME = `Getting mix details... ${stationId}/${mixId}`;
|
|
215
|
+
try {
|
|
216
|
+
const response = await fetch(url, {
|
|
217
|
+
method: "GET",
|
|
218
|
+
headers,
|
|
219
|
+
signal: abortController?.signal
|
|
220
|
+
});
|
|
221
|
+
const responseData = await response.json();
|
|
222
|
+
_FM_log("%cnext mix json", "color: magenta", responseData);
|
|
223
|
+
if (response.ok || responseData.data?.stations?.[0]?.mixes && responseData.data?.stations?.[0]?.mixes?.length === 0) {
|
|
224
|
+
_FM_log("Successfully fetched the mix details.");
|
|
225
|
+
_FM_log(responseData);
|
|
226
|
+
responseData.data?.stations?.[0]?.mixes.forEach((m) => m.remixes = []);
|
|
227
|
+
const trackIds = responseData.data?.stations?.[0]?.mixes.map((m) => m.tracks.map((t) => {
|
|
228
|
+
return { id: t.trackId, url: t.url };
|
|
229
|
+
}));
|
|
230
|
+
const tracks = removeDuplicatesById([...trackIds].flat(Infinity));
|
|
231
|
+
const trackBeatsCollection2 = await resolveMultipleBeatsAndKeysWithBeatsLink(tracks, authToken, smoothBeats, window.$currentBeatGridVersion);
|
|
232
|
+
responseData.data?.stations?.[0]?.mixes.forEach((m) => m.tracks.map((t) => {
|
|
233
|
+
const m_t = t;
|
|
234
|
+
if (trackBeatsCollection2.data[t.trackId]) {
|
|
235
|
+
replaceMp4Url(m_t);
|
|
236
|
+
addBeatsRelatedParam(m_t, trackBeatsCollection2.data[t.trackId]);
|
|
237
|
+
m_t.__falseTrack = false;
|
|
238
|
+
m_t.__durationForValidation1 = getFullDurationFromBeatsLinkSegmentInfo(m_t.segmentInfo);
|
|
239
|
+
m_t.__durationForValidation2 = getDurationFromBeatsLinkUpToLastDownbeat0(-1, m_t.beats);
|
|
240
|
+
} else {
|
|
241
|
+
m_t.__falseTrack = true;
|
|
242
|
+
m_t.__durationForValidation1 = 0;
|
|
243
|
+
m_t.__durationForValidation2 = 0;
|
|
244
|
+
}
|
|
245
|
+
return m_t;
|
|
246
|
+
}));
|
|
247
|
+
if (!responseData.data?.stations?.[0]?.stationId) {
|
|
248
|
+
if (responseData.data?.stations?.[0]?.artist?.stationId) {
|
|
249
|
+
responseData.data.stations[0].stationId = responseData.data?.stations?.[0]?.artist?.stationId;
|
|
250
|
+
} else {
|
|
251
|
+
responseData.data.stations[0].stationId = "";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const stationMixCount = responseData.data?.stations?.[0]?.mixCount || 0;
|
|
255
|
+
const mixesLength = responseData.data?.stations?.[0]?.mixes?.length;
|
|
256
|
+
_FM_log_table({
|
|
257
|
+
id: "autoplay",
|
|
258
|
+
stationId,
|
|
259
|
+
mixId,
|
|
260
|
+
currentMixPosition,
|
|
261
|
+
stationMixCount,
|
|
262
|
+
mixesLength,
|
|
263
|
+
"isNaN(currentMixPosition*1)": isNaN(currentMixPosition * 1)
|
|
264
|
+
});
|
|
265
|
+
if (responseData.data?.stations?.[0]?.mixes?.length === 0 || NEXT_MIX_ID_DROPPED_ON_RESOLVING_NEXT_MIX.includes(responseData.data?.stations?.[0]?.mixes?.[0]?.mixId)) {
|
|
266
|
+
if (isNaN(currentMixPosition * 1)) {
|
|
267
|
+
return getFirstOrLastMixMetadata(authToken, stationId, direction, smoothBeats);
|
|
268
|
+
} else {
|
|
269
|
+
if (currentMixPosition + 1 < stationMixCount) {
|
|
270
|
+
return getMixesAfterNMetadata(authToken, stationId, direction, currentMixPosition + 1, smoothBeats);
|
|
271
|
+
} else {
|
|
272
|
+
return getFirstOrLastMixMetadata(authToken, stationId, direction, smoothBeats);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return responseData;
|
|
277
|
+
} else {
|
|
278
|
+
console.error(`Failed to fetch mix details. ${FETCH_MIX_CONSOLE_TIME}`);
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {
|
|
282
|
+
console.error(`Error during mix detail retrieval: ${e}`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
var getFirstOrLastMixMetadata = async (authToken, stationId, direction, smoothBeats = APPLY_BEAT_GRID_SMOOTHING, abortController) => {
|
|
287
|
+
return getMixesAfterNMetadata(authToken, stationId, direction, 0, smoothBeats, abortController);
|
|
288
|
+
};
|
|
289
|
+
var getMixesAfterNMetadata = async (authToken, stationId, direction, mixPosition = 0, smoothBeats = APPLY_BEAT_GRID_SMOOTHING, abortController) => {
|
|
290
|
+
const query = `query MixesToPlay($id: [ID]!, $first: Int, $after: Int, $sort: Sort) {
|
|
291
|
+
stations(ids: $id) {
|
|
292
|
+
stationId
|
|
293
|
+
lastModified
|
|
294
|
+
mixCount
|
|
295
|
+
mixes(first: $first, after: $after, sort: $sort) {
|
|
296
|
+
...MixItem
|
|
297
|
+
mixPosition
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
${TRACK_ITEM_FRAGMENT}
|
|
303
|
+
|
|
304
|
+
${MIX_ITEM_FRAGMENT}`;
|
|
305
|
+
const queryArtist = `query MixesToPlay($id: [ID]!, $first: Int, $after: Int) {
|
|
306
|
+
stations: libraryArtists(ids: $id) {
|
|
307
|
+
artist {
|
|
308
|
+
artistId
|
|
309
|
+
stationId: artistId
|
|
310
|
+
}
|
|
311
|
+
lastModified
|
|
312
|
+
mixCount
|
|
313
|
+
mixes(first: $first, after: $after) {
|
|
314
|
+
...MixItem
|
|
315
|
+
mixPosition
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
${TRACK_ITEM_FRAGMENT}
|
|
321
|
+
|
|
322
|
+
${MIX_ITEM_FRAGMENT}`;
|
|
323
|
+
const variables = { "after": mixPosition, "id": [stationId], "first": 3, "sort": direction === "nextMixId" ? "asc" : "desc" };
|
|
324
|
+
const queryParams = new URLSearchParams({
|
|
325
|
+
query: isStationIdLibraryArtistStation(stationId) ? queryArtist.trim() : query.trim(),
|
|
326
|
+
variables: JSON.stringify(variables),
|
|
327
|
+
operationName: "MixesToPlay"
|
|
328
|
+
});
|
|
329
|
+
const url = `${TRACK_CREATOR_ENDPOINT}/graphql?${queryParams.toString()}`;
|
|
330
|
+
const headers = {
|
|
331
|
+
Authorization: `Bearer ${authToken}`
|
|
332
|
+
};
|
|
333
|
+
FETCH_MIX_CONSOLE_TIME = `Getting mix details... ${stationId}/first_item}`;
|
|
334
|
+
try {
|
|
335
|
+
const response = await fetch(url, {
|
|
336
|
+
method: "GET",
|
|
337
|
+
headers,
|
|
338
|
+
signal: abortController?.signal
|
|
339
|
+
});
|
|
340
|
+
const responseData = await response.json();
|
|
341
|
+
_FM_log("%cnext mix json", "color: magenta", responseData);
|
|
342
|
+
if (response.ok || responseData.data?.stations?.[0]?.mixes && responseData.data?.stations?.[0]?.mixes?.length === 0) {
|
|
343
|
+
_FM_log("Successfully fetched the mix details.");
|
|
344
|
+
_FM_log(responseData);
|
|
345
|
+
responseData.data?.stations?.[0]?.mixes.forEach((m) => m.remixes = []);
|
|
346
|
+
const trackIds = responseData.data?.stations?.[0]?.mixes.map((m) => m.tracks.map((t) => {
|
|
347
|
+
return { id: t.trackId, url: t.url };
|
|
348
|
+
}));
|
|
349
|
+
const tracks = removeDuplicatesById([...trackIds].flat(Infinity));
|
|
350
|
+
const trackBeatsCollection2 = await resolveMultipleBeatsAndKeysWithBeatsLink(tracks, authToken, smoothBeats, window.$currentBeatGridVersion);
|
|
351
|
+
responseData.data?.stations?.[0]?.mixes.forEach((m) => m.tracks.map((t) => {
|
|
352
|
+
const m_t = t;
|
|
353
|
+
if (trackBeatsCollection2.data[t.trackId]) {
|
|
354
|
+
replaceMp4Url(m_t);
|
|
355
|
+
addBeatsRelatedParam(m_t, trackBeatsCollection2.data[t.trackId]);
|
|
356
|
+
m_t.__falseTrack = false;
|
|
357
|
+
m_t.__durationForValidation1 = getFullDurationFromBeatsLinkSegmentInfo(m_t.segmentInfo);
|
|
358
|
+
m_t.__durationForValidation2 = getDurationFromBeatsLinkUpToLastDownbeat0(-1, m_t.beats);
|
|
359
|
+
} else {
|
|
360
|
+
m_t.__falseTrack = true;
|
|
361
|
+
m_t.__durationForValidation1 = 0;
|
|
362
|
+
m_t.__durationForValidation2 = 0;
|
|
363
|
+
}
|
|
364
|
+
return m_t;
|
|
365
|
+
}));
|
|
366
|
+
if (!responseData.data?.stations?.[0]?.stationId) {
|
|
367
|
+
if (responseData.data?.stations?.[0]?.artist?.stationId) {
|
|
368
|
+
responseData.data.stations[0].stationId = responseData.data?.stations?.[0]?.artist?.stationId;
|
|
369
|
+
} else {
|
|
370
|
+
responseData.data.stations[0].stationId = "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return responseData;
|
|
374
|
+
} else {
|
|
375
|
+
console.error(`Failed to fetch mix details. ${FETCH_MIX_CONSOLE_TIME}`);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {
|
|
379
|
+
console.timeEnd(FETCH_MIX_CONSOLE_TIME);
|
|
380
|
+
console.error(`Error during mix detail retrieval: ${e}`);
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
var extractMixIdVer2 = (url) => {
|
|
385
|
+
const pattern = /^https?:\/\/[^/]+\/(?:set|artist)\/([^/]+)\/mix\/([^/?#]+)(?:\/|$|\?)/i;
|
|
386
|
+
const match = url.match(pattern);
|
|
387
|
+
if (match) {
|
|
388
|
+
const mixValue = match[2];
|
|
389
|
+
return mixValue;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var extractStationIdVer2 = (url) => {
|
|
393
|
+
const pattern = /^https?:\/\/[^/]+\/(?:set|artist)\/([^/]+)\/mix\/([^/?#]+)(?:\/|$|\?)/i;
|
|
394
|
+
const match = url.match(pattern);
|
|
395
|
+
if (match) {
|
|
396
|
+
const mixValue = match[1];
|
|
397
|
+
return mixValue;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
var fetchMixDataWithSharedURL = async (url, smoothBeats = APPLY_BEAT_GRID_SMOOTHING, abortController) => {
|
|
401
|
+
if (!url) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const mixId = extractMixIdVer2(url);
|
|
405
|
+
const stationId = extractStationIdVer2(url);
|
|
406
|
+
if (mixId) {
|
|
407
|
+
const token = await login();
|
|
408
|
+
AEV3Config.getInstance().setAccessToken(token);
|
|
409
|
+
if (token) {
|
|
410
|
+
const fetchedData = await getMixMetadata(token, mixId, stationId, smoothBeats, abortController);
|
|
411
|
+
if (fetchedData) {
|
|
412
|
+
return JSON.stringify(fetchedData);
|
|
413
|
+
} else {
|
|
414
|
+
console.error("Failed to fetch mix data");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
console.error("Invalid URL or mixId not found");
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
var removeDuplicatesById = (arr) => {
|
|
422
|
+
const seen = /* @__PURE__ */ new Set();
|
|
423
|
+
return arr.filter((item) => {
|
|
424
|
+
if (seen.has(item.id)) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
seen.add(item.id);
|
|
428
|
+
return true;
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
var isStationIdLibraryArtistStation = (stationId) => {
|
|
432
|
+
return stationId?.includes("00000014-");
|
|
433
|
+
};
|
|
434
|
+
var replaceMp4Url = (m_t) => {
|
|
435
|
+
const mp4 = m_t.url;
|
|
436
|
+
m_t.url = replaceMp4ForLocalUse(mp4);
|
|
437
|
+
};
|
|
438
|
+
var addBeatsRelatedParam = (m_t, beats) => {
|
|
439
|
+
m_t.beats = beats.beats;
|
|
440
|
+
m_t.tempoOrigSz = calculateTempoOrigSzFromBeatDownBeat(m_t.beats.map((b) => b.timestamp));
|
|
441
|
+
m_t.duration = calculateQuantisedTrackActualDurationFromBeatGrid(-1, m_t.beats);
|
|
442
|
+
m_t.indices = beats.indices;
|
|
443
|
+
m_t.segmentInfo = beats.segmentInfo;
|
|
444
|
+
m_t.key = beats.key;
|
|
445
|
+
m_t.tonality = beats.tonality;
|
|
446
|
+
m_t.beatSource = beats.beatSource;
|
|
447
|
+
return m_t;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// src/daw/services/track-search-api.ts
|
|
451
|
+
var API_BASE = import.meta.env?.VITE_KB_STEMIFY_API_URL || "https://api.stemplayer.com";
|
|
452
|
+
var GQL_URL = `${API_BASE}/track-creator/graphql`;
|
|
453
|
+
var FEAT_API_URL = import.meta.env?.VITE_FEAT_API_URL || "https://173i5juztg.execute-api.us-west-2.amazonaws.com";
|
|
454
|
+
var SIMILARITY_MODES = [
|
|
455
|
+
{ id: "feature", label: "Audio DNA", description: "Audio fingerprint / text similarity" },
|
|
456
|
+
{ id: "remix", label: "Remix Match", description: "Server-side key/BPM/feature matching" }
|
|
457
|
+
];
|
|
458
|
+
var CAMELOT_MAP = {
|
|
459
|
+
"A:minor": "8A",
|
|
460
|
+
"A:major": "11B",
|
|
461
|
+
"A#:minor": "3A",
|
|
462
|
+
"A#:major": "6B",
|
|
463
|
+
"B:minor": "10A",
|
|
464
|
+
"B:major": "1B",
|
|
465
|
+
"C:minor": "5A",
|
|
466
|
+
"C:major": "8B",
|
|
467
|
+
"C#:minor": "12A",
|
|
468
|
+
"C#:major": "3B",
|
|
469
|
+
"D:minor": "7A",
|
|
470
|
+
"D:major": "10B",
|
|
471
|
+
"D#:minor": "2A",
|
|
472
|
+
"D#:major": "5B",
|
|
473
|
+
"E:minor": "9A",
|
|
474
|
+
"E:major": "12B",
|
|
475
|
+
"F:minor": "4A",
|
|
476
|
+
"F:major": "7B",
|
|
477
|
+
"F#:minor": "11A",
|
|
478
|
+
"F#:major": "2B",
|
|
479
|
+
"G:minor": "6A",
|
|
480
|
+
"G:major": "9B",
|
|
481
|
+
"G#:minor": "1A",
|
|
482
|
+
"G#:major": "4B"
|
|
483
|
+
};
|
|
484
|
+
function getCamelotTag(key, tonality) {
|
|
485
|
+
return CAMELOT_MAP[`${key}:${tonality}`] || `${key}${tonality === "minor" ? "m" : ""}`;
|
|
486
|
+
}
|
|
487
|
+
function parseCamelot(tag) {
|
|
488
|
+
const m = tag.match(/^(\d+)([AB])$/);
|
|
489
|
+
if (!m) return null;
|
|
490
|
+
return { num: parseInt(m[1], 10), letter: m[2] };
|
|
491
|
+
}
|
|
492
|
+
function scoreKeyCompatibility(refKey, refTonality, trackKey, trackTonality) {
|
|
493
|
+
const refTag = getCamelotTag(refKey, refTonality);
|
|
494
|
+
const trkTag = getCamelotTag(trackKey, trackTonality);
|
|
495
|
+
if (refTag === trkTag) return 1;
|
|
496
|
+
const ref = parseCamelot(refTag);
|
|
497
|
+
const trk = parseCamelot(trkTag);
|
|
498
|
+
if (!ref || !trk) return 0;
|
|
499
|
+
const numDist = Math.min(
|
|
500
|
+
Math.abs(ref.num - trk.num),
|
|
501
|
+
12 - Math.abs(ref.num - trk.num)
|
|
502
|
+
);
|
|
503
|
+
if (numDist === 0 && ref.letter !== trk.letter) return 0.9;
|
|
504
|
+
if (numDist === 1 && ref.letter === trk.letter) return 0.85;
|
|
505
|
+
if (numDist === 1 && ref.letter !== trk.letter) return 0.7;
|
|
506
|
+
if (numDist === 2 && ref.letter === trk.letter) return 0.5;
|
|
507
|
+
return Math.max(0, 0.4 - numDist * 0.05);
|
|
508
|
+
}
|
|
509
|
+
function scoreBpmCompatibility(refBpm, trackBpm) {
|
|
510
|
+
const candidates = [trackBpm, trackBpm * 2, trackBpm / 2];
|
|
511
|
+
let bestPct = Infinity;
|
|
512
|
+
let bestLabel = "1x";
|
|
513
|
+
for (const bpm of candidates) {
|
|
514
|
+
const pct = Math.abs(bpm - refBpm) / refBpm;
|
|
515
|
+
if (pct < bestPct) {
|
|
516
|
+
bestPct = pct;
|
|
517
|
+
bestLabel = bpm === trackBpm ? "1x" : bpm > trackBpm ? "2x" : "\xBDx";
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const score = Math.max(0, 1 - bestPct / 0.15);
|
|
521
|
+
return { score, ratio: bestLabel };
|
|
522
|
+
}
|
|
523
|
+
function computeCompatibility(ref, track) {
|
|
524
|
+
const keyScore = scoreKeyCompatibility(ref.key, ref.tonality, track.key, track.tonality);
|
|
525
|
+
const { score: bpmScore, ratio } = scoreBpmCompatibility(ref.bpm, track.bpm);
|
|
526
|
+
const bpmPct = Math.round((1 - Math.abs(ref.bpm - track.bpm) / ref.bpm) * 100);
|
|
527
|
+
const overall = keyScore * 0.5 + bpmScore * 0.5;
|
|
528
|
+
return {
|
|
529
|
+
bpmPct,
|
|
530
|
+
keyScore: Math.round(keyScore * 100),
|
|
531
|
+
overall: Math.round(overall * 100),
|
|
532
|
+
camelotTag: getCamelotTag(track.key, track.tonality),
|
|
533
|
+
bpmLabel: `${Math.round(track.bpm)} ${ratio !== "1x" ? `(${ratio})` : ""}`.trim()
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
var beatsCache = /* @__PURE__ */ new Map();
|
|
537
|
+
async function fetchBeatsRaw(trackId) {
|
|
538
|
+
const cached = beatsCache.get(trackId);
|
|
539
|
+
if (cached) return cached;
|
|
540
|
+
try {
|
|
541
|
+
const token = await getToken();
|
|
542
|
+
const url = `${API_BASE}/track-creator/tracks/${trackId}/beats?min_bars=16`;
|
|
543
|
+
const res = await fetch(url, {
|
|
544
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
545
|
+
});
|
|
546
|
+
if (!res.ok) return null;
|
|
547
|
+
const json = await res.json();
|
|
548
|
+
const d = json?.data;
|
|
549
|
+
if (!d?.bpm || !d?.key) return null;
|
|
550
|
+
const segments = Array.isArray(d.segmentInfo) ? d.segmentInfo : [];
|
|
551
|
+
const durationSec = typeof d.duration === "number" ? d.duration : segments.length > 0 ? segments[segments.length - 1][1] : null;
|
|
552
|
+
const record = {
|
|
553
|
+
info: { bpm: d.bpm, key: d.key, tonality: d.tonality || "minor" },
|
|
554
|
+
segmentInfo: segments,
|
|
555
|
+
durationSec
|
|
556
|
+
};
|
|
557
|
+
beatsCache.set(trackId, record);
|
|
558
|
+
return record;
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function fetchTrackMusicalInfo(trackId) {
|
|
564
|
+
const record = await fetchBeatsRaw(trackId);
|
|
565
|
+
return record ? record.info : null;
|
|
566
|
+
}
|
|
567
|
+
async function fetchTrackSegmentInfo(trackId) {
|
|
568
|
+
const record = await fetchBeatsRaw(trackId);
|
|
569
|
+
if (!record) return null;
|
|
570
|
+
return { segmentInfo: record.segmentInfo, durationSec: record.durationSec };
|
|
571
|
+
}
|
|
572
|
+
async function getToken() {
|
|
573
|
+
const token = await login();
|
|
574
|
+
if (!token) throw new Error("Authentication failed \u2014 check .env credentials");
|
|
575
|
+
AEV3Config.getInstance().setAccessToken(token);
|
|
576
|
+
return token;
|
|
577
|
+
}
|
|
578
|
+
var MIX_URL_PATTERN = /^https?:\/\/[^/]+\/(?:set|artist)\/([^/]+)\/mix\/([^/?#]+)/i;
|
|
579
|
+
var SESSION_TRACK_URL_PATTERN = /^https?:\/\/[^/]+\/session\/([^/]+)\/track\/([^/?#]+)/i;
|
|
580
|
+
function isMixUrl(input) {
|
|
581
|
+
return MIX_URL_PATTERN.test(input.trim());
|
|
582
|
+
}
|
|
583
|
+
function isSessionTrackUrl(input) {
|
|
584
|
+
const t = input.trim();
|
|
585
|
+
return SESSION_TRACK_URL_PATTERN.test(t) || extractBareSessionStationId(t) !== null;
|
|
586
|
+
}
|
|
587
|
+
function isStemUrl(input) {
|
|
588
|
+
const trimmed = input.trim();
|
|
589
|
+
return MIX_URL_PATTERN.test(trimmed) || SESSION_TRACK_URL_PATTERN.test(trimmed) || extractBareSessionStationId(trimmed) !== null;
|
|
590
|
+
}
|
|
591
|
+
var MIX_TRACK_FRAGMENT = `fragment TrackItem on Track {
|
|
592
|
+
trackId
|
|
593
|
+
name
|
|
594
|
+
artistDisplayName
|
|
595
|
+
artwork: image
|
|
596
|
+
artworkGlowColor
|
|
597
|
+
url: url(codec: aac, container: mp4, profile: raw)
|
|
598
|
+
}`;
|
|
599
|
+
var MIX_QUERY = `query GetMix($id: ID!) {
|
|
600
|
+
mix(id: $id) {
|
|
601
|
+
mixId
|
|
602
|
+
name
|
|
603
|
+
tracks { ...TrackItem }
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
${MIX_TRACK_FRAGMENT}`;
|
|
607
|
+
async function fetchTracksFromMixUrl(url) {
|
|
608
|
+
const mixId = extractMixIdVer2(url);
|
|
609
|
+
if (!mixId) throw new Error("Could not extract mix ID from URL");
|
|
610
|
+
const token = await getToken();
|
|
611
|
+
const params = new URLSearchParams({
|
|
612
|
+
query: MIX_QUERY.trim(),
|
|
613
|
+
variables: JSON.stringify({ id: mixId }),
|
|
614
|
+
operationName: "GetMix"
|
|
615
|
+
});
|
|
616
|
+
const res = await fetch(`${GQL_URL}?${params}`, {
|
|
617
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
618
|
+
});
|
|
619
|
+
if (!res.ok) throw new Error(`Mix fetch failed (${res.status})`);
|
|
620
|
+
const json = await res.json();
|
|
621
|
+
const tracks = json?.data?.mix?.tracks;
|
|
622
|
+
if (!Array.isArray(tracks) || tracks.length === 0) {
|
|
623
|
+
throw new Error("Mix contains no tracks");
|
|
624
|
+
}
|
|
625
|
+
return tracks.map((t) => ({
|
|
626
|
+
id: t.trackId,
|
|
627
|
+
title: t.name || "Untitled",
|
|
628
|
+
artist: t.artistDisplayName || "Unknown",
|
|
629
|
+
artwork: t.artwork || null,
|
|
630
|
+
artworkGlowColor: t.artworkGlowColor || null,
|
|
631
|
+
type: "MixTrack"
|
|
632
|
+
}));
|
|
633
|
+
}
|
|
634
|
+
async function fetchTracksFromSessionUrl(url) {
|
|
635
|
+
const { fetchStationTrackDataWithSharedURL } = await import('./fetchStationTracks-SKFT4V3U.js');
|
|
636
|
+
const jsonString = await fetchStationTrackDataWithSharedURL(url);
|
|
637
|
+
if (!jsonString) throw new Error("Could not fetch station track data from session URL");
|
|
638
|
+
const stObj = JSON.parse(jsonString);
|
|
639
|
+
const currentTracks = stObj?.data?.stations?.[0]?.currentTrack ?? [];
|
|
640
|
+
const nextTracks = stObj?.data?.stations?.[0]?.nextTracks ?? [];
|
|
641
|
+
const allEntries = [...currentTracks, ...nextTracks];
|
|
642
|
+
if (allEntries.length === 0) throw new Error("No tracks found for this session link");
|
|
643
|
+
return allEntries.filter((e) => e?.track && !e.track.__falseTrack).map((e) => ({
|
|
644
|
+
id: e.track.trackId,
|
|
645
|
+
title: e.track.name || "Untitled",
|
|
646
|
+
artist: e.track.artistDisplayName || "Unknown",
|
|
647
|
+
artwork: e.track.artwork || e.track.image || null,
|
|
648
|
+
artworkGlowColor: e.track.artworkGlowColor || null,
|
|
649
|
+
type: "StationTrack"
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
var SEARCH_QUERY = `
|
|
653
|
+
query SearchTracks($name: String, $first: Int, $after: Int) {
|
|
654
|
+
searchItems(filter: { name: $name, searchMode: all }, first: $first, after: $after) {
|
|
655
|
+
tracks {
|
|
656
|
+
... on Track {
|
|
657
|
+
title: name
|
|
658
|
+
id: trackId
|
|
659
|
+
artist: artistDisplayName
|
|
660
|
+
artwork: image
|
|
661
|
+
artworkGlowColor: artworkHighlightColor
|
|
662
|
+
type: __typename
|
|
663
|
+
}
|
|
664
|
+
... on ExternalSearchTrack {
|
|
665
|
+
title: name
|
|
666
|
+
id: trackId
|
|
667
|
+
artist: artistDisplayName
|
|
668
|
+
artwork: image
|
|
669
|
+
artworkGlowColor
|
|
670
|
+
type: __typename
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}`;
|
|
675
|
+
async function searchTracks(query, limit = 20, offset = 0) {
|
|
676
|
+
const token = await getToken();
|
|
677
|
+
const params = new URLSearchParams({
|
|
678
|
+
query: SEARCH_QUERY.trim(),
|
|
679
|
+
variables: JSON.stringify({ name: query, first: limit, after: offset }),
|
|
680
|
+
operationName: "SearchTracks"
|
|
681
|
+
});
|
|
682
|
+
const res = await fetch(`${GQL_URL}?${params}`, {
|
|
683
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
684
|
+
});
|
|
685
|
+
if (!res.ok) throw new Error(`Search failed (${res.status})`);
|
|
686
|
+
const json = await res.json();
|
|
687
|
+
const tracks = json?.data?.searchItems?.tracks ?? [];
|
|
688
|
+
return tracks.map((t) => ({
|
|
689
|
+
id: t.id,
|
|
690
|
+
title: t.title || "Untitled",
|
|
691
|
+
artist: t.artist || "Unknown",
|
|
692
|
+
artwork: t.artwork || null,
|
|
693
|
+
artworkGlowColor: t.artworkGlowColor || null,
|
|
694
|
+
type: t.type || "Track"
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
var TRACK_PREVIEW_QUERY = `
|
|
698
|
+
query GetTrackPreview($id: ID!) {
|
|
699
|
+
track(id: $id) {
|
|
700
|
+
trackId
|
|
701
|
+
duration
|
|
702
|
+
url: url(codec: aac, container: mp4, profile: raw)
|
|
703
|
+
}
|
|
704
|
+
}`;
|
|
705
|
+
var previewSourcesCache = /* @__PURE__ */ new Map();
|
|
706
|
+
async function fetchTrackPreviewSources(trackId) {
|
|
707
|
+
const cached = previewSourcesCache.get(trackId);
|
|
708
|
+
if (cached) return cached;
|
|
709
|
+
try {
|
|
710
|
+
const token = await getToken();
|
|
711
|
+
const params = new URLSearchParams({
|
|
712
|
+
query: TRACK_PREVIEW_QUERY.trim(),
|
|
713
|
+
variables: JSON.stringify({ id: trackId }),
|
|
714
|
+
operationName: "GetTrackPreview"
|
|
715
|
+
});
|
|
716
|
+
const res = await fetch(`${GQL_URL}?${params}`, {
|
|
717
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
718
|
+
});
|
|
719
|
+
if (!res.ok) return null;
|
|
720
|
+
const json = await res.json();
|
|
721
|
+
const t = json?.data?.track;
|
|
722
|
+
if (!t) return null;
|
|
723
|
+
const record = {
|
|
724
|
+
durationSec: typeof t.duration === "number" ? t.duration : null,
|
|
725
|
+
mp4Url: typeof t.url === "string" ? t.url : null
|
|
726
|
+
};
|
|
727
|
+
previewSourcesCache.set(trackId, record);
|
|
728
|
+
return record;
|
|
729
|
+
} catch {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
var TRACK_QUERY = `
|
|
734
|
+
query GetTrack($id: ID!) {
|
|
735
|
+
track(id: $id) {
|
|
736
|
+
trackId
|
|
737
|
+
name
|
|
738
|
+
artistDisplayName
|
|
739
|
+
artwork: image
|
|
740
|
+
artworkGlowColor: artworkHighlightColor
|
|
741
|
+
duration
|
|
742
|
+
url: url(codec: aac, container: mp4, profile: raw)
|
|
743
|
+
colors
|
|
744
|
+
trackMixType
|
|
745
|
+
trackStatus
|
|
746
|
+
isClaimed
|
|
747
|
+
stems {
|
|
748
|
+
aac: url(codec: aac)
|
|
749
|
+
hls: url(codec: hls)
|
|
750
|
+
wav: url(codec: wav)
|
|
751
|
+
mp3: url(codec: mp3)
|
|
752
|
+
stemId
|
|
753
|
+
stemPosition
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}`;
|
|
757
|
+
async function fetchTrackForDAW(trackId) {
|
|
758
|
+
const token = await getToken();
|
|
759
|
+
const params = new URLSearchParams({
|
|
760
|
+
query: TRACK_QUERY.trim(),
|
|
761
|
+
variables: JSON.stringify({ id: trackId }),
|
|
762
|
+
operationName: "GetTrack"
|
|
763
|
+
});
|
|
764
|
+
const res = await fetch(`${GQL_URL}?${params}`, {
|
|
765
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
766
|
+
});
|
|
767
|
+
if (!res.ok) throw new Error(`Track fetch failed (${res.status})`);
|
|
768
|
+
const json = await res.json();
|
|
769
|
+
const t = json?.data?.track;
|
|
770
|
+
if (!t) throw new Error("Track not found");
|
|
771
|
+
const beatsResolved = await resolveSingleBeatsAndKeysWithBeatsLink(
|
|
772
|
+
trackId,
|
|
773
|
+
token,
|
|
774
|
+
true,
|
|
775
|
+
// smooth beats
|
|
776
|
+
null,
|
|
777
|
+
// beatVersion — always server default, never the debug global
|
|
778
|
+
t.url
|
|
779
|
+
);
|
|
780
|
+
const beats = beatsResolved.data[trackId];
|
|
781
|
+
if (!beats) throw new Error(`Beat analysis unavailable for track ${trackId}`);
|
|
782
|
+
const track = t;
|
|
783
|
+
track.trackDisplayName = t.name;
|
|
784
|
+
const mp4Url = t.url;
|
|
785
|
+
if (!mp4Url) {
|
|
786
|
+
throw new Error(`Track ${trackId} has no MP4 URL`);
|
|
787
|
+
}
|
|
788
|
+
track.url = replaceMp4ForLocalUse(mp4Url) ?? mp4Url;
|
|
789
|
+
track.trackMixType = track.trackMixType || "seed";
|
|
790
|
+
track.color1Hex = t.colors?.[0] || t.artworkGlowColor || "";
|
|
791
|
+
track.color2Hex = t.colors?.[1] || "";
|
|
792
|
+
track.webMixChunkUrl = "";
|
|
793
|
+
track.webMixChunkCount = 0;
|
|
794
|
+
track.switchInTimes = [];
|
|
795
|
+
track.beats = beats.beats;
|
|
796
|
+
track.tempoOrigSz = calculateTempoOrigSzFromBeatDownBeat(track.beats.map((b) => b.timestamp));
|
|
797
|
+
track.duration = calculateQuantisedTrackActualDurationFromBeatGrid(-1, track.beats);
|
|
798
|
+
track.indices = beats.indices;
|
|
799
|
+
track.segmentInfo = beats.segmentInfo;
|
|
800
|
+
track.key = beats.key;
|
|
801
|
+
track.tonality = beats.tonality;
|
|
802
|
+
track.beatSource = beats.beatSource;
|
|
803
|
+
track.__falseTrack = false;
|
|
804
|
+
track.__durationForValidation1 = getFullDurationFromBeatsLinkSegmentInfo(track.segmentInfo);
|
|
805
|
+
track.__durationForValidation2 = getDurationFromBeatsLinkUpToLastDownbeat0(-1, track.beats);
|
|
806
|
+
return track;
|
|
807
|
+
}
|
|
808
|
+
function parseFeatureResults(data) {
|
|
809
|
+
if (!Array.isArray(data)) return [];
|
|
810
|
+
return data.filter((t) => t.uuid || t.id).map((t) => ({
|
|
811
|
+
id: t.uuid || t.id,
|
|
812
|
+
title: t.title || "Untitled",
|
|
813
|
+
artist: t.artist || "Unknown",
|
|
814
|
+
artwork: t.artwork || null,
|
|
815
|
+
artworkGlowColor: null,
|
|
816
|
+
type: "SimilarTrack"
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
async function fetchFeaturesByQuery(query, limit) {
|
|
820
|
+
const body = new FormData();
|
|
821
|
+
body.append("query", query);
|
|
822
|
+
body.append("num_results", String(limit));
|
|
823
|
+
body.append("include_pca", "true");
|
|
824
|
+
const res = await fetch(`${FEAT_API_URL}/searchfeatures`, {
|
|
825
|
+
method: "POST",
|
|
826
|
+
body
|
|
827
|
+
});
|
|
828
|
+
if (!res.ok) return [];
|
|
829
|
+
return parseFeatureResults(await res.json());
|
|
830
|
+
}
|
|
831
|
+
async function fetchFeaturesByUUID(uuid, limit) {
|
|
832
|
+
const body = new FormData();
|
|
833
|
+
body.append("uuid", uuid);
|
|
834
|
+
body.append("num_results", String(limit));
|
|
835
|
+
body.append("include_pca", "true");
|
|
836
|
+
const res = await fetch(`${FEAT_API_URL}/searchfeatures`, {
|
|
837
|
+
method: "POST",
|
|
838
|
+
body
|
|
839
|
+
});
|
|
840
|
+
if (!res.ok) return [];
|
|
841
|
+
return parseFeatureResults(await res.json());
|
|
842
|
+
}
|
|
843
|
+
async function fetchSimilarTracksByFeature(trackId, limit = 12) {
|
|
844
|
+
try {
|
|
845
|
+
const uuidResults = await fetchFeaturesByUUID(trackId, limit);
|
|
846
|
+
if (uuidResults.length > 0) return uuidResults;
|
|
847
|
+
const { tracks: sessionTracks } = (await import('./store/daw-session-store.js')).useDAWSessionStore.getState();
|
|
848
|
+
const sessionTrack = sessionTracks.find(
|
|
849
|
+
(t) => (t.trackData?.trackId || t.id) === trackId
|
|
850
|
+
);
|
|
851
|
+
if (sessionTrack) {
|
|
852
|
+
const parts = [sessionTrack.displayName, sessionTrack.artistName].filter(Boolean);
|
|
853
|
+
if (parts.length > 0) {
|
|
854
|
+
return await fetchFeaturesByQuery(parts.join(" "), limit);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return [];
|
|
858
|
+
} catch (err) {
|
|
859
|
+
console.error("[fetchSimilarTracksByFeature]", err);
|
|
860
|
+
return [];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
var STRICT_POOL_BBOX = {
|
|
864
|
+
maxSemitoneDiff: 1,
|
|
865
|
+
maxBarSizePctDiff: 8,
|
|
866
|
+
allowOppositeTonality: false
|
|
867
|
+
};
|
|
868
|
+
var SIMILAR_TRACKS_QUERY = `
|
|
869
|
+
query SimilarTracks(
|
|
870
|
+
$trackId: ID!,
|
|
871
|
+
$first: Int,
|
|
872
|
+
$after: Int,
|
|
873
|
+
$excludeTrackIds: [ID],
|
|
874
|
+
$strategy: SimilarTracksStrategy,
|
|
875
|
+
$ranking: SimilarTracksRankingInput
|
|
876
|
+
) {
|
|
877
|
+
similarTracks(
|
|
878
|
+
trackId: $trackId,
|
|
879
|
+
first: $first,
|
|
880
|
+
after: $after,
|
|
881
|
+
excludeTrackIds: $excludeTrackIds,
|
|
882
|
+
strategy: $strategy,
|
|
883
|
+
ranking: $ranking
|
|
884
|
+
) {
|
|
885
|
+
trackId
|
|
886
|
+
name
|
|
887
|
+
artistDisplayName
|
|
888
|
+
colors
|
|
889
|
+
trackStatus
|
|
890
|
+
duration
|
|
891
|
+
isClaimed
|
|
892
|
+
artwork: image
|
|
893
|
+
artworkGlowColor
|
|
894
|
+
}
|
|
895
|
+
}`;
|
|
896
|
+
function parseSimilarTracksResults(json) {
|
|
897
|
+
const tracks = json?.data?.similarTracks ?? [];
|
|
898
|
+
return tracks.filter((t) => t.trackId).map((t) => ({
|
|
899
|
+
id: t.trackId,
|
|
900
|
+
title: t.name || "Untitled",
|
|
901
|
+
artist: t.artistDisplayName || "Unknown",
|
|
902
|
+
artwork: t.artwork || null,
|
|
903
|
+
artworkGlowColor: t.artworkGlowColor || null,
|
|
904
|
+
type: "RemixMatch"
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
function buildRankingInput(ranking, strategy) {
|
|
908
|
+
const out = {};
|
|
909
|
+
if (ranking) {
|
|
910
|
+
if (ranking.collaborativeStrength !== void 0) {
|
|
911
|
+
out.collaborativeStrength = ranking.collaborativeStrength;
|
|
912
|
+
}
|
|
913
|
+
if (ranking.genreWeight !== void 0) {
|
|
914
|
+
out.genreWeight = ranking.genreWeight;
|
|
915
|
+
}
|
|
916
|
+
if (ranking.prioritizeUserLikes !== void 0) {
|
|
917
|
+
out.prioritizeUserLikes = ranking.prioritizeUserLikes;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (strategy === "embedding_rank_with_bbox") {
|
|
921
|
+
Object.assign(out, STRICT_POOL_BBOX);
|
|
922
|
+
}
|
|
923
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
924
|
+
}
|
|
925
|
+
async function fetchSimilarTracksByRemix(trackId, limit = 12, excludeTrackIds, offset = 0, args) {
|
|
926
|
+
try {
|
|
927
|
+
const token = await getToken();
|
|
928
|
+
const variables = {
|
|
929
|
+
trackId,
|
|
930
|
+
first: limit,
|
|
931
|
+
after: offset
|
|
932
|
+
};
|
|
933
|
+
if (excludeTrackIds?.length) {
|
|
934
|
+
variables.excludeTrackIds = excludeTrackIds;
|
|
935
|
+
}
|
|
936
|
+
if (args?.strategy) {
|
|
937
|
+
variables.strategy = args.strategy;
|
|
938
|
+
}
|
|
939
|
+
const ranking = buildRankingInput(args?.ranking, args?.strategy);
|
|
940
|
+
if (ranking) {
|
|
941
|
+
variables.ranking = ranking;
|
|
942
|
+
}
|
|
943
|
+
const params = new URLSearchParams({
|
|
944
|
+
query: SIMILAR_TRACKS_QUERY.trim(),
|
|
945
|
+
variables: JSON.stringify(variables),
|
|
946
|
+
operationName: "SimilarTracks"
|
|
947
|
+
});
|
|
948
|
+
const res = await fetch(`${GQL_URL}?${params}`, {
|
|
949
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
950
|
+
});
|
|
951
|
+
if (!res.ok) {
|
|
952
|
+
console.warn(`[fetchSimilarTracksByRemix] similarTracks returned ${res.status}`);
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
return parseSimilarTracksResults(await res.json());
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.error("[fetchSimilarTracksByRemix]", err);
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function fetchSimilarTracksFromStation(mixUrl, limit = 8) {
|
|
962
|
+
const stationId = extractStationIdVer2(mixUrl);
|
|
963
|
+
const mixId = extractMixIdVer2(mixUrl);
|
|
964
|
+
if (!stationId || !mixId) return [];
|
|
965
|
+
const token = await login();
|
|
966
|
+
if (!token) return [];
|
|
967
|
+
const seen = /* @__PURE__ */ new Set();
|
|
968
|
+
const results = [];
|
|
969
|
+
const addTracksFromMixes = (mixes) => {
|
|
970
|
+
if (!mixes) return;
|
|
971
|
+
for (const mix of mixes) {
|
|
972
|
+
const tracks = mix?.tracks ?? [];
|
|
973
|
+
for (const t of tracks) {
|
|
974
|
+
if (t?.trackId && !seen.has(t.trackId) && !t.__falseTrack) {
|
|
975
|
+
seen.add(t.trackId);
|
|
976
|
+
results.push({
|
|
977
|
+
id: t.trackId,
|
|
978
|
+
title: t.name || t.trackDisplayName || "Untitled",
|
|
979
|
+
artist: t.artistDisplayName || "Unknown",
|
|
980
|
+
artwork: t.artwork || t.image || null,
|
|
981
|
+
artworkGlowColor: t.artworkGlowColor || null,
|
|
982
|
+
type: t.trackMixType || "Track"
|
|
983
|
+
});
|
|
984
|
+
if (results.length >= limit) return;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
try {
|
|
990
|
+
const nextData = await getNextMixMetadata(
|
|
991
|
+
token,
|
|
992
|
+
mixId,
|
|
993
|
+
stationId,
|
|
994
|
+
"nextMixId",
|
|
995
|
+
-1,
|
|
996
|
+
true
|
|
997
|
+
);
|
|
998
|
+
addTracksFromMixes(nextData?.data?.stations?.[0]?.mixes);
|
|
999
|
+
if (results.length < limit) {
|
|
1000
|
+
const prevData = await getNextMixMetadata(
|
|
1001
|
+
token,
|
|
1002
|
+
mixId,
|
|
1003
|
+
stationId,
|
|
1004
|
+
"prevMixId",
|
|
1005
|
+
-1,
|
|
1006
|
+
true
|
|
1007
|
+
);
|
|
1008
|
+
addTracksFromMixes(prevData?.data?.stations?.[0]?.mixes);
|
|
1009
|
+
}
|
|
1010
|
+
if (results.length < limit) {
|
|
1011
|
+
const fallbackData = await getMixesAfterNMetadata(
|
|
1012
|
+
token,
|
|
1013
|
+
stationId,
|
|
1014
|
+
"nextMixId",
|
|
1015
|
+
0,
|
|
1016
|
+
true
|
|
1017
|
+
);
|
|
1018
|
+
addTracksFromMixes(fallbackData?.data?.stations?.[0]?.mixes);
|
|
1019
|
+
}
|
|
1020
|
+
return results.slice(0, limit);
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
console.error("[fetchSimilarTracksFromStation]", err);
|
|
1023
|
+
return [];
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
export { SIMILARITY_MODES, computeCompatibility, fetchMixDataWithSharedURL, fetchSimilarTracksByFeature, fetchSimilarTracksByRemix, fetchSimilarTracksFromStation, fetchTrackForDAW, fetchTrackMusicalInfo, fetchTrackPreviewSources, fetchTrackSegmentInfo, fetchTracksFromMixUrl, fetchTracksFromSessionUrl, getCamelotTag, isMixUrl, isSessionTrackUrl, isStemUrl, login, searchTracks };
|
|
1028
|
+
//# sourceMappingURL=chunk-56PWIP7O.js.map
|
|
1029
|
+
//# sourceMappingURL=chunk-56PWIP7O.js.map
|