musora-content-services 2.7.1 → 2.7.2
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/CHANGELOG.md +2 -0
- package/docs/ContentOrganization.html +2 -2
- package/docs/Gamification.html +2 -2
- package/docs/UserManagementSystem.html +2 -2
- package/docs/api_types.js.html +2 -2
- package/docs/config.js.html +2 -2
- package/docs/content-org_content-org.js.html +2 -2
- package/docs/content-org_playlists-types.js.html +3 -5
- package/docs/content-org_playlists.js.html +23 -9
- package/docs/content.js.html +9 -2
- package/docs/gamification_awards.js.html +2 -2
- package/docs/gamification_gamification.js.html +2 -2
- package/docs/gamification_types.js.html +2 -2
- package/docs/global.html +5 -53
- package/docs/index.html +2 -2
- package/docs/module-Awards.html +2 -2
- package/docs/module-Config.html +2 -2
- package/docs/module-Content-Services-V2.html +8 -8
- package/docs/module-Interests.html +2 -2
- package/docs/module-Permissions.html +2 -2
- package/docs/module-Playlists.html +21 -20
- package/docs/module-Railcontent-Services.html +2 -2
- package/docs/module-Sanity-Services.html +30 -30
- package/docs/module-Sessions.html +2 -2
- package/docs/module-User-Activity.html +737 -58
- package/docs/module-UserManagement.html +2 -2
- package/docs/module-UserProfile.html +2 -2
- package/docs/railcontent.js.html +2 -2
- package/docs/sanity.js.html +24 -6
- package/docs/userActivity.js.html +360 -12
- package/docs/user_interests.js.html +2 -2
- package/docs/user_management.js.html +2 -2
- package/docs/user_permissions.js.html +2 -2
- package/docs/user_profile.js.html +4 -4
- package/docs/user_sessions.js.html +2 -2
- package/docs/user_types.js.html +2 -2
- package/docs/user_user-management-system.js.html +2 -2
- package/package.json +1 -1
- package/src/contentMetaData.js +2 -1
- package/src/contentTypeConfig.js +18 -2
- package/src/index.d.ts +10 -2
- package/src/index.js +10 -2
- package/src/services/content.js +7 -0
- package/src/services/contentProgress.js +45 -15
- package/src/services/sanity.js +22 -4
- package/src/services/userActivity.js +348 -10
- package/test/mockData/mockData_progress_content.json +182 -0
- package/test/mockData/mockData_sanity_progress_content.json +1451 -0
- package/test/progressRows.test.js +215 -0
- package/test/sanityQueryService.test.js +0 -0
|
@@ -11,10 +11,13 @@ import {
|
|
|
11
11
|
fetchRecentUserActivities,
|
|
12
12
|
} from './railcontent'
|
|
13
13
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
14
|
-
import { fetchByRailContentIds } from './sanity'
|
|
15
|
-
import {
|
|
14
|
+
import { fetchByRailContentIds, fetchShows } from './sanity'
|
|
15
|
+
import {fetchUserPlaylists} from "./content-org/playlists";
|
|
16
16
|
import { convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
|
|
17
17
|
import { globalConfig } from './config'
|
|
18
|
+
import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping} from "../contentTypeConfig";
|
|
19
|
+
import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
|
|
20
|
+
import {TabResponseType} from "../contentMetaData";
|
|
18
21
|
|
|
19
22
|
const DATA_KEY_PRACTICES = 'practices'
|
|
20
23
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
@@ -835,14 +838,6 @@ async function formatPracticeMeta(practices) {
|
|
|
835
838
|
})
|
|
836
839
|
}
|
|
837
840
|
|
|
838
|
-
export function getFormattedType(type) {
|
|
839
|
-
for (const [key, values] of Object.entries(lessonTypesMapping)) {
|
|
840
|
-
if (values.includes(type)) {
|
|
841
|
-
return key.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return null
|
|
845
|
-
}
|
|
846
841
|
|
|
847
842
|
/**
|
|
848
843
|
* Records a new user activity in the system.
|
|
@@ -887,3 +882,346 @@ export async function deleteUserActivity(id) {
|
|
|
887
882
|
const url = `/api/user-management-system/v1/activities/${id}`
|
|
888
883
|
return await fetchHandler(url, 'DELETE')
|
|
889
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
|
|
887
|
+
*
|
|
888
|
+
* @param {Object} [options={}] - Options for fetching progress rows.
|
|
889
|
+
* @param {string|null} [options.brand=null] - The brand context for progress data.
|
|
890
|
+
* @param {number} [options.limit=8] - Maximum number of progress rows to return.
|
|
891
|
+
* @returns {Promise<Object>} - A promise that resolves to an object containing progress rows formatted for UI.
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* getProgressRows({ brand: 'drumeo', limit: 10 })
|
|
895
|
+
* .then(data => console.log(data))
|
|
896
|
+
* .catch(error => console.error(error));
|
|
897
|
+
*/
|
|
898
|
+
export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
899
|
+
const excludedTypes = new Set([
|
|
900
|
+
'pack-bundle',
|
|
901
|
+
'learning-path-course',
|
|
902
|
+
'learning-path-level'
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
const recentPlaylists = await fetchUserPlaylists(brand, {
|
|
906
|
+
sort: '-last_progress',
|
|
907
|
+
limit: limit,
|
|
908
|
+
});
|
|
909
|
+
const playlists = recentPlaylists?.data || [];
|
|
910
|
+
const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
|
|
911
|
+
const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
|
|
912
|
+
const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
|
|
913
|
+
const excludedParents = new Set();
|
|
914
|
+
for (const item of playlistsContents) {
|
|
915
|
+
const contentId = item.id ?? item.railcontent_id;
|
|
916
|
+
excludedParents.add(contentId)
|
|
917
|
+
const parentIds = item.parent_content_data || [];
|
|
918
|
+
parentIds.forEach(id => excludedParents.add(id));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const progressContents = await getAllStartedOrCompleted({onlyIds: false, brand: brand, excludedIds: Array.from(excludedParents) });
|
|
922
|
+
const contents = await fetchByRailContentIds(Object.keys(progressContents), 'progress-tracker', brand);
|
|
923
|
+
const contentsMap = {};
|
|
924
|
+
contents.forEach(content => {
|
|
925
|
+
contentsMap[content.railcontent_id] = content;
|
|
926
|
+
});
|
|
927
|
+
const childToParentMap = {};
|
|
928
|
+
Object.values(contentsMap).forEach(content => {
|
|
929
|
+
if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
|
|
930
|
+
childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
const progressMap = new Map();
|
|
934
|
+
for (const [idStr, progress] of Object.entries(progressContents)) {
|
|
935
|
+
const id = parseInt(idStr);
|
|
936
|
+
const content = contentsMap[id];
|
|
937
|
+
if (!content || excludedTypes.has(content.type)) continue;
|
|
938
|
+
const parentId = childToParentMap[id];
|
|
939
|
+
// Handle children with parents
|
|
940
|
+
if (parentId) {
|
|
941
|
+
const parentContent = contentsMap[parentId];
|
|
942
|
+
if (!parentContent || excludedTypes.has(parentContent.type)) continue;
|
|
943
|
+
const existing = progressMap.get(parentId);
|
|
944
|
+
if (existing) {
|
|
945
|
+
// If childIndex isn't already set, set it
|
|
946
|
+
if (existing.childIndex === undefined) {
|
|
947
|
+
existing.childIndex = id;
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
progressMap.set(parentId, {
|
|
951
|
+
id: parentId,
|
|
952
|
+
raw: parentContent,
|
|
953
|
+
state: progress.status,
|
|
954
|
+
percent: progress.progress,
|
|
955
|
+
progressTimestamp: progress.last_update * 1000,
|
|
956
|
+
childIndex: id
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
// Handle standalone parents
|
|
962
|
+
if (!progressMap.has(id)) {
|
|
963
|
+
progressMap.set(id, {
|
|
964
|
+
id,
|
|
965
|
+
raw: content,
|
|
966
|
+
state: progress.status,
|
|
967
|
+
percent: progress.progress,
|
|
968
|
+
progressTimestamp: progress.last_update * 1000
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const progressList = Array.from(progressMap.values());
|
|
973
|
+
|
|
974
|
+
const combined = mergeAndSortItems([...progressList, ...eligiblePlaylistItems], limit);
|
|
975
|
+
const results = await Promise.all(
|
|
976
|
+
combined.slice(0, limit).map(item =>
|
|
977
|
+
item.type === 'playlist'
|
|
978
|
+
? processPlaylistItem(item)
|
|
979
|
+
: processContentItem(item)
|
|
980
|
+
)
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
return {
|
|
984
|
+
type: TabResponseType.PROGRESS_ROWS,
|
|
985
|
+
data: results
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function processContentItem(item) {
|
|
990
|
+
let data = item.raw;
|
|
991
|
+
const contentType = getFormattedType(data.type);
|
|
992
|
+
const status = item.state;
|
|
993
|
+
|
|
994
|
+
let ctaText = 'Continue';
|
|
995
|
+
if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
|
|
996
|
+
if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
|
|
997
|
+
if ((contentType === 'pack' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) && status === 'completed') {
|
|
998
|
+
ctaText = 'View Lessons';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (data.lesson_count > 0) {
|
|
1002
|
+
if (item.childIndex) {
|
|
1003
|
+
let nextId = item.childIndex
|
|
1004
|
+
const lessonIds = extractLessonIds(item);
|
|
1005
|
+
const progressOnItems = await getProgressStateByIds(lessonIds);
|
|
1006
|
+
const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1007
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type);
|
|
1008
|
+
nextId = nextByProgress ? nextByProgress : nextId;
|
|
1009
|
+
|
|
1010
|
+
const nestedLessons = data.lessons
|
|
1011
|
+
.filter(item => Array.isArray(item.lessons))
|
|
1012
|
+
.flatMap(parent =>
|
|
1013
|
+
parent.lessons.map(lesson => ({
|
|
1014
|
+
...lesson,
|
|
1015
|
+
parent: {
|
|
1016
|
+
id: parent.id,
|
|
1017
|
+
slug: parent.slug,
|
|
1018
|
+
title: parent.title,
|
|
1019
|
+
type: parent.type
|
|
1020
|
+
}
|
|
1021
|
+
}))
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons;
|
|
1025
|
+
const nextLesson = lessons.find(lesson => lesson.id === nextId);
|
|
1026
|
+
data.first_incomplete_child = nextLesson?.parent ?? nextLesson;
|
|
1027
|
+
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null;
|
|
1028
|
+
data.completed_children = completedCount;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if(contentType == 'show' && status == 'completed'){
|
|
1033
|
+
const shows = await fetchShows(data.brand, data.type)
|
|
1034
|
+
const showIds = shows.map(item => item.id);
|
|
1035
|
+
const progressOnItems = await getProgressStateByIds(showIds);
|
|
1036
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
|
|
1037
|
+
data = shows.find(lesson => lesson.id === nextByProgress);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
id: item.id,
|
|
1042
|
+
progressType: 'content',
|
|
1043
|
+
header: contentType,
|
|
1044
|
+
body: {
|
|
1045
|
+
progressPercent: item.percent,
|
|
1046
|
+
thumbnail: data.thumbnail,
|
|
1047
|
+
title: data.title,
|
|
1048
|
+
subtitle: !data.child_count || data.lesson_count === 1
|
|
1049
|
+
? `${data.difficulty_string} • ${data.artist_name}`
|
|
1050
|
+
: `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
|
|
1051
|
+
},
|
|
1052
|
+
cta: {
|
|
1053
|
+
text: ctaText,
|
|
1054
|
+
action: {
|
|
1055
|
+
type: data.type,
|
|
1056
|
+
brand: data.brand,
|
|
1057
|
+
id: data.id,
|
|
1058
|
+
slug: data.slug,
|
|
1059
|
+
child: data.first_incomplete_child
|
|
1060
|
+
? {
|
|
1061
|
+
id: data.first_incomplete_child.id,
|
|
1062
|
+
type: data.first_incomplete_child.type,
|
|
1063
|
+
brand: data.first_incomplete_child.brand,
|
|
1064
|
+
slug: data.first_incomplete_child.slug,
|
|
1065
|
+
child: data.second_incomplete_child
|
|
1066
|
+
? {
|
|
1067
|
+
id: data.second_incomplete_child.id,
|
|
1068
|
+
type: data.second_incomplete_child.type,
|
|
1069
|
+
brand: data.second_incomplete_child.brand,
|
|
1070
|
+
slug: data.second_incomplete_child.slug
|
|
1071
|
+
}
|
|
1072
|
+
: null
|
|
1073
|
+
}
|
|
1074
|
+
: null
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
progressTimestamp: item.progressTimestamp
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function processPlaylistItem(item) {
|
|
1082
|
+
const playlist = item.raw;
|
|
1083
|
+
const progressOnItems = await getProgressStateByIds(playlist.items.map(a => a.content_id));
|
|
1084
|
+
const allItemsCompleted = item.raw.items.every(i => {
|
|
1085
|
+
const itemId = i.content_id;
|
|
1086
|
+
const progress = progressOnItems[itemId];
|
|
1087
|
+
return progress && progress === 'completed';
|
|
1088
|
+
});
|
|
1089
|
+
let nextItem = playlist.items[0] ?? null;
|
|
1090
|
+
if (!allItemsCompleted) {
|
|
1091
|
+
const lastItemProgress = progressOnItems[playlist.last_engaged_on];
|
|
1092
|
+
const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
|
|
1093
|
+
if (lastItemProgress === 'completed') {
|
|
1094
|
+
nextItem = playlist.items[index + 1] ?? nextItem;
|
|
1095
|
+
} else {
|
|
1096
|
+
nextItem = playlist.items[index] ?? nextItem;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return {
|
|
1101
|
+
id: playlist.id,
|
|
1102
|
+
progressType: 'playlist',
|
|
1103
|
+
header: 'playlist',
|
|
1104
|
+
body: {
|
|
1105
|
+
first_items_thumbnail_url: playlist.first_items_thumbnail_url,
|
|
1106
|
+
title: playlist.name,
|
|
1107
|
+
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`
|
|
1108
|
+
},
|
|
1109
|
+
progressTimestamp: item.progressTimestamp,
|
|
1110
|
+
cta: {
|
|
1111
|
+
text: 'Continue',
|
|
1112
|
+
action: {
|
|
1113
|
+
brand: playlist.brand,
|
|
1114
|
+
id: playlist.id,
|
|
1115
|
+
itemId: nextItem.id,
|
|
1116
|
+
type: 'playlists',
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const getFormattedType = type => {
|
|
1123
|
+
for (const [key, values] of Object.entries(progressTypesMapping)) {
|
|
1124
|
+
if (values.includes(type)) {
|
|
1125
|
+
return key;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
function extractLessonIds(data) {
|
|
1132
|
+
const ids = [];
|
|
1133
|
+
function traverse(lessons) {
|
|
1134
|
+
for (const item of lessons) {
|
|
1135
|
+
if (item.id) {
|
|
1136
|
+
ids.push(item.id);
|
|
1137
|
+
}
|
|
1138
|
+
if (item.lessons) {
|
|
1139
|
+
traverse(item.lessons); // Recursively handle nested lessons
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (data.raw && Array.isArray(data.raw.lessons)) {
|
|
1144
|
+
traverse(data.raw.lessons);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return ids;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
async function getEligiblePlaylistItems(playlists) {
|
|
1152
|
+
const eligible = playlists.filter(p => p.last_progress && p.last_engaged_on);
|
|
1153
|
+
return Promise.all(
|
|
1154
|
+
eligible.map(async p => {
|
|
1155
|
+
const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z');
|
|
1156
|
+
const timestamp = utcDate.getTime();
|
|
1157
|
+
return {
|
|
1158
|
+
type: 'playlist',
|
|
1159
|
+
progressTimestamp: timestamp,
|
|
1160
|
+
last_engaged_on: p.last_engaged_on,
|
|
1161
|
+
raw: p
|
|
1162
|
+
};
|
|
1163
|
+
})
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function mergeAndSortItems(items, limit) {
|
|
1168
|
+
return items
|
|
1169
|
+
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp > 0)
|
|
1170
|
+
.sort((a, b) => b.progressTimestamp - a.progressTimestamp)
|
|
1171
|
+
.slice(0, limit + 5);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
1175
|
+
const ids = Object.keys(progressOnItems).map(Number);
|
|
1176
|
+
if (contentType === 'challenge') {
|
|
1177
|
+
// Return first incomplete lesson
|
|
1178
|
+
return ids.find(id => progressOnItems[id] !== 'completed') || null;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// For other types, find next incomplete after current
|
|
1182
|
+
const currentIndex = ids.indexOf(Number(currentContentId));
|
|
1183
|
+
if (currentIndex === -1) return null;
|
|
1184
|
+
|
|
1185
|
+
for (let i = currentIndex + 1; i < ids.length; i++) {
|
|
1186
|
+
const id = ids[i];
|
|
1187
|
+
if (progressOnItems[id] !== 'completed') {
|
|
1188
|
+
return id;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
return ids[0];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Pins a specific progress row for a user, scoped by brand.
|
|
1197
|
+
*
|
|
1198
|
+
* @param {string} brand - The brand context for the pin action.
|
|
1199
|
+
* @param {number|string} id - The ID of the progress item to pin.
|
|
1200
|
+
* @param {string} progressType - The type of progress (e.g., 'content', 'playlist').
|
|
1201
|
+
* @returns {Promise<Object>} - A promise resolving to the response from the pin API.
|
|
1202
|
+
*
|
|
1203
|
+
* @example
|
|
1204
|
+
* pinProgressRow('drumeo', 12345, 'content')
|
|
1205
|
+
* .then(response => console.log(response))
|
|
1206
|
+
* .catch(error => console.error(error));
|
|
1207
|
+
*/
|
|
1208
|
+
export async function pinProgressRow(brand, id, progressType) {
|
|
1209
|
+
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`;
|
|
1210
|
+
return await fetchHandler(url, 'PUT', null)
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Unpins the current pinned progress row for a user, scoped by brand.
|
|
1214
|
+
*
|
|
1215
|
+
* @param {string} brand - The brand context for the unpin action.
|
|
1216
|
+
* @returns {Promise<Object>} - A promise resolving to the response from the unpin API.
|
|
1217
|
+
*
|
|
1218
|
+
* @example
|
|
1219
|
+
* unpinProgressRow('drumeo')
|
|
1220
|
+
* .then(response => console.log(response))
|
|
1221
|
+
* .catch(error => console.error(error));
|
|
1222
|
+
*/
|
|
1223
|
+
export async function unpinProgressRow(brand) {
|
|
1224
|
+
const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`;
|
|
1225
|
+
return await fetchHandler(url, 'PUT', null)
|
|
1226
|
+
}
|
|
1227
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
{
|
|
2
|
+
"257242": {
|
|
3
|
+
"last_update": 1747657503,
|
|
4
|
+
"progress": 13,
|
|
5
|
+
"status": "started",
|
|
6
|
+
"brand": "drumeo"
|
|
7
|
+
},
|
|
8
|
+
"257243": {
|
|
9
|
+
"last_update": 1747657502,
|
|
10
|
+
"progress": 52,
|
|
11
|
+
"status": "started",
|
|
12
|
+
"brand": "drumeo"
|
|
13
|
+
},
|
|
14
|
+
"257245": {
|
|
15
|
+
"last_update": 1747657470,
|
|
16
|
+
"progress": 62,
|
|
17
|
+
"status": "started",
|
|
18
|
+
"brand": "drumeo"
|
|
19
|
+
},
|
|
20
|
+
"257249": {
|
|
21
|
+
"last_update": 1747657493,
|
|
22
|
+
"progress": 100,
|
|
23
|
+
"status": "completed",
|
|
24
|
+
"brand": "drumeo"
|
|
25
|
+
},
|
|
26
|
+
"257250": {
|
|
27
|
+
"last_update": 1747657502,
|
|
28
|
+
"progress": 100,
|
|
29
|
+
"status": "completed",
|
|
30
|
+
"brand": "drumeo"
|
|
31
|
+
},
|
|
32
|
+
"259238": {
|
|
33
|
+
"last_update": 1747657495,
|
|
34
|
+
"progress": 100,
|
|
35
|
+
"status": "completed",
|
|
36
|
+
"brand": "drumeo"
|
|
37
|
+
},
|
|
38
|
+
"259240": {
|
|
39
|
+
"last_update": 1747657505,
|
|
40
|
+
"progress": 100,
|
|
41
|
+
"status": "completed",
|
|
42
|
+
"brand": "drumeo"
|
|
43
|
+
},
|
|
44
|
+
"260677": {
|
|
45
|
+
"last_update": 1747318330,
|
|
46
|
+
"progress": 100,
|
|
47
|
+
"status": "completed",
|
|
48
|
+
"brand": "drumeo"
|
|
49
|
+
},
|
|
50
|
+
"274368": {
|
|
51
|
+
"last_update": 1747211391,
|
|
52
|
+
"progress": 20,
|
|
53
|
+
"status": "started",
|
|
54
|
+
"brand": "drumeo"
|
|
55
|
+
},
|
|
56
|
+
"274889": {
|
|
57
|
+
"last_update": 1747211391,
|
|
58
|
+
"progress": 100,
|
|
59
|
+
"status": "completed",
|
|
60
|
+
"brand": "drumeo"
|
|
61
|
+
},
|
|
62
|
+
"280498": {
|
|
63
|
+
"last_update": 1747726731,
|
|
64
|
+
"progress": 60,
|
|
65
|
+
"status": "started",
|
|
66
|
+
"brand": "drumeo"
|
|
67
|
+
},
|
|
68
|
+
"287853": {
|
|
69
|
+
"last_update": 1747647688,
|
|
70
|
+
"progress": 100,
|
|
71
|
+
"status": "completed",
|
|
72
|
+
"brand": "drumeo"
|
|
73
|
+
},
|
|
74
|
+
"287855": {
|
|
75
|
+
"last_update": 1747726731,
|
|
76
|
+
"progress": 100,
|
|
77
|
+
"status": "completed",
|
|
78
|
+
"brand": "drumeo"
|
|
79
|
+
},
|
|
80
|
+
"287858": {
|
|
81
|
+
"last_update": 1747653734,
|
|
82
|
+
"progress": 100,
|
|
83
|
+
"status": "completed",
|
|
84
|
+
"brand": "drumeo"
|
|
85
|
+
},
|
|
86
|
+
"316732": {
|
|
87
|
+
"last_update": 1747319258,
|
|
88
|
+
"progress": 0,
|
|
89
|
+
"status": "started",
|
|
90
|
+
"brand": "drumeo"
|
|
91
|
+
},
|
|
92
|
+
"316752": {
|
|
93
|
+
"last_update": 1747402511,
|
|
94
|
+
"progress": 80,
|
|
95
|
+
"status": "started",
|
|
96
|
+
"brand": "drumeo"
|
|
97
|
+
},
|
|
98
|
+
"316772": {
|
|
99
|
+
"last_update": 1747319687,
|
|
100
|
+
"progress": 100,
|
|
101
|
+
"status": "completed",
|
|
102
|
+
"brand": "drumeo"
|
|
103
|
+
},
|
|
104
|
+
"325826": {
|
|
105
|
+
"last_update": 1747324670,
|
|
106
|
+
"progress": 41,
|
|
107
|
+
"status": "started",
|
|
108
|
+
"brand": "drumeo"
|
|
109
|
+
},
|
|
110
|
+
"335025": {
|
|
111
|
+
"last_update": 1747315549,
|
|
112
|
+
"progress": 0,
|
|
113
|
+
"status": "started",
|
|
114
|
+
"brand": "drumeo"
|
|
115
|
+
},
|
|
116
|
+
"373171": {
|
|
117
|
+
"last_update": 1747211114,
|
|
118
|
+
"progress": 100,
|
|
119
|
+
"status": "completed",
|
|
120
|
+
"brand": "drumeo"
|
|
121
|
+
},
|
|
122
|
+
"373172": {
|
|
123
|
+
"last_update": 1747211114,
|
|
124
|
+
"progress": 100,
|
|
125
|
+
"status": "completed",
|
|
126
|
+
"brand": "drumeo"
|
|
127
|
+
},
|
|
128
|
+
"402009": {
|
|
129
|
+
"last_update": 1747293447,
|
|
130
|
+
"progress": 100,
|
|
131
|
+
"status": "completed",
|
|
132
|
+
"brand": "drumeo"
|
|
133
|
+
},
|
|
134
|
+
"402021": {
|
|
135
|
+
"last_update": 1747318769,
|
|
136
|
+
"progress": 100,
|
|
137
|
+
"status": "completed",
|
|
138
|
+
"brand": "drumeo"
|
|
139
|
+
},
|
|
140
|
+
"402199": {
|
|
141
|
+
"last_update": 1747318734,
|
|
142
|
+
"progress": 0,
|
|
143
|
+
"status": "started",
|
|
144
|
+
"brand": "drumeo"
|
|
145
|
+
},
|
|
146
|
+
"410255": {
|
|
147
|
+
"last_update": 1747211224,
|
|
148
|
+
"progress": 100,
|
|
149
|
+
"status": "completed",
|
|
150
|
+
"brand": "drumeo"
|
|
151
|
+
},
|
|
152
|
+
"410478": {
|
|
153
|
+
"last_update": 1747657306,
|
|
154
|
+
"progress": 22,
|
|
155
|
+
"status": "started",
|
|
156
|
+
"brand": "drumeo"
|
|
157
|
+
},
|
|
158
|
+
"410480": {
|
|
159
|
+
"last_update": 1747657295,
|
|
160
|
+
"progress": 100,
|
|
161
|
+
"status": "completed",
|
|
162
|
+
"brand": "drumeo"
|
|
163
|
+
},
|
|
164
|
+
"410481": {
|
|
165
|
+
"last_update": 1747657306,
|
|
166
|
+
"progress": 100,
|
|
167
|
+
"status": "completed",
|
|
168
|
+
"brand": "drumeo"
|
|
169
|
+
},
|
|
170
|
+
"411091": {
|
|
171
|
+
"last_update": 1747318457,
|
|
172
|
+
"progress": 100,
|
|
173
|
+
"status": "completed",
|
|
174
|
+
"brand": "drumeo"
|
|
175
|
+
},
|
|
176
|
+
"415585": {
|
|
177
|
+
"last_update": 1747319147,
|
|
178
|
+
"progress": 100,
|
|
179
|
+
"status": "completed",
|
|
180
|
+
"brand": "drumeo"
|
|
181
|
+
}
|
|
182
|
+
}
|