@trafficgroup/knex-rel 0.1.7 → 0.1.8
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/dist/dao/VideoMinuteResultDAO.d.ts +37 -0
- package/dist/dao/VideoMinuteResultDAO.js +146 -0
- package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
- package/dist/dao/batch/batch.dao.d.ts +27 -0
- package/dist/dao/batch/batch.dao.js +135 -0
- package/dist/dao/batch/batch.dao.js.map +1 -0
- package/dist/dao/video/video.dao.d.ts +23 -0
- package/dist/dao/video/video.dao.js +80 -0
- package/dist/dao/video/video.dao.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/batch/batch.interfaces.d.ts +13 -0
- package/dist/interfaces/batch/batch.interfaces.js +3 -0
- package/dist/interfaces/batch/batch.interfaces.js.map +1 -0
- package/dist/interfaces/video/video.interfaces.d.ts +8 -0
- package/migrations/20251020225758_migration.ts +135 -0
- package/package.json +1 -1
- package/plan.md +684 -0
- package/src/dao/VideoMinuteResultDAO.ts +232 -0
- package/src/dao/batch/batch.dao.ts +121 -0
- package/src/dao/video/video.dao.ts +90 -0
- package/src/index.ts +11 -1
- package/src/interfaces/batch/batch.interfaces.ts +14 -0
- package/src/interfaces/video/video.interfaces.ts +12 -1
|
@@ -68,6 +68,33 @@ interface IGroupedResponse {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
interface IStudyTimeGroupResult {
|
|
72
|
+
absoluteTime: string; // ISO 8601 start of bucket
|
|
73
|
+
groupIndex: number;
|
|
74
|
+
label: string; // Formatted label
|
|
75
|
+
results: ITMCResult | IATRResult;
|
|
76
|
+
minuteCount: number;
|
|
77
|
+
videoCount: number;
|
|
78
|
+
contributingVideos: string[]; // Video UUIDs
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface IGroupedStudyResponse {
|
|
82
|
+
success: boolean;
|
|
83
|
+
data: IStudyTimeGroupResult[];
|
|
84
|
+
groupingMinutes: number;
|
|
85
|
+
study: {
|
|
86
|
+
uuid: string;
|
|
87
|
+
name: string;
|
|
88
|
+
type: "TMC" | "ATR";
|
|
89
|
+
status: string;
|
|
90
|
+
};
|
|
91
|
+
videoCount: number;
|
|
92
|
+
dateRange: {
|
|
93
|
+
earliest: string;
|
|
94
|
+
latest: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
72
99
|
private knex = KnexManager.getConnection();
|
|
73
100
|
private tableName = "video_minute_results";
|
|
@@ -486,6 +513,182 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
486
513
|
};
|
|
487
514
|
}
|
|
488
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Get grouped minute results by study UUID with time block aggregation across all videos
|
|
518
|
+
* @param studyUuid - The UUID of the study
|
|
519
|
+
* @param groupingMinutes - Number of minutes to group together (default: 15)
|
|
520
|
+
*/
|
|
521
|
+
async getGroupedMinuteResultsByStudyUuid(
|
|
522
|
+
studyUuid: string,
|
|
523
|
+
groupingMinutes: number = 15,
|
|
524
|
+
): Promise<IGroupedStudyResponse> {
|
|
525
|
+
// Step 1: Fetch study and validate
|
|
526
|
+
const study = await this.knex("study").where("uuid", studyUuid).first();
|
|
527
|
+
|
|
528
|
+
if (!study) {
|
|
529
|
+
throw new Error(`Study with UUID ${studyUuid} not found`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Step 2: Fetch all COMPLETED videos in the study with recordingStartedAt
|
|
533
|
+
const videos = await this.knex("video as v")
|
|
534
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
535
|
+
.select("v.id", "v.uuid", "v.name", "v.recordingStartedAt", "v.videoType")
|
|
536
|
+
.where("f.studyId", study.id)
|
|
537
|
+
.where("v.status", "COMPLETED")
|
|
538
|
+
.whereNotNull("v.recordingStartedAt")
|
|
539
|
+
.orderBy("v.recordingStartedAt", "asc");
|
|
540
|
+
|
|
541
|
+
// If no completed videos with recordingStartedAt, return empty result
|
|
542
|
+
if (videos.length === 0) {
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
data: [],
|
|
546
|
+
groupingMinutes,
|
|
547
|
+
study: {
|
|
548
|
+
uuid: study.uuid,
|
|
549
|
+
name: study.name,
|
|
550
|
+
type: study.type,
|
|
551
|
+
status: study.status,
|
|
552
|
+
},
|
|
553
|
+
videoCount: 0,
|
|
554
|
+
dateRange: {
|
|
555
|
+
earliest: "",
|
|
556
|
+
latest: "",
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Get video IDs for minute results query
|
|
562
|
+
const videoIds = videos.map((v) => v.id);
|
|
563
|
+
|
|
564
|
+
// Calculate date range
|
|
565
|
+
const earliestVideo = videos[0];
|
|
566
|
+
const latestVideo = videos[videos.length - 1];
|
|
567
|
+
const earliestTime = new Date(earliestVideo.recordingStartedAt);
|
|
568
|
+
|
|
569
|
+
// Step 3: Fetch and normalize all minute results with absolute times
|
|
570
|
+
const minuteResults = await this.knex("video_minute_results as vmr")
|
|
571
|
+
.innerJoin("video as v", "vmr.video_id", "v.id")
|
|
572
|
+
.select(
|
|
573
|
+
"vmr.minute_number",
|
|
574
|
+
"vmr.results",
|
|
575
|
+
"v.uuid as videoUuid",
|
|
576
|
+
"v.recordingStartedAt",
|
|
577
|
+
this.knex.raw(
|
|
578
|
+
'"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"',
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
.whereIn("v.id", videoIds)
|
|
582
|
+
.orderBy("absoluteTime", "asc");
|
|
583
|
+
|
|
584
|
+
// If no minute results found, return empty result
|
|
585
|
+
if (minuteResults.length === 0) {
|
|
586
|
+
return {
|
|
587
|
+
success: true,
|
|
588
|
+
data: [],
|
|
589
|
+
groupingMinutes,
|
|
590
|
+
study: {
|
|
591
|
+
uuid: study.uuid,
|
|
592
|
+
name: study.name,
|
|
593
|
+
type: study.type,
|
|
594
|
+
status: study.status,
|
|
595
|
+
},
|
|
596
|
+
videoCount: videos.length,
|
|
597
|
+
dateRange: {
|
|
598
|
+
earliest: earliestVideo.recordingStartedAt.toISOString(),
|
|
599
|
+
latest: latestVideo.recordingStartedAt.toISOString(),
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Step 4: Group by time buckets in TypeScript
|
|
605
|
+
interface IBucket {
|
|
606
|
+
groupIndex: number;
|
|
607
|
+
absoluteTime: Date;
|
|
608
|
+
results: any[];
|
|
609
|
+
videoUuids: Set<string>;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const buckets = new Map<number, IBucket>();
|
|
613
|
+
|
|
614
|
+
for (const minute of minuteResults) {
|
|
615
|
+
const absoluteTime = new Date(minute.absoluteTime);
|
|
616
|
+
const minutesSinceEarliest = Math.floor(
|
|
617
|
+
(absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60),
|
|
618
|
+
);
|
|
619
|
+
const bucketIndex = Math.floor(minutesSinceEarliest / groupingMinutes);
|
|
620
|
+
|
|
621
|
+
if (!buckets.has(bucketIndex)) {
|
|
622
|
+
// Calculate bucket start time
|
|
623
|
+
const bucketStartMinutes = bucketIndex * groupingMinutes;
|
|
624
|
+
const bucketStartTime = new Date(
|
|
625
|
+
earliestTime.getTime() + bucketStartMinutes * 60 * 1000,
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
buckets.set(bucketIndex, {
|
|
629
|
+
groupIndex: bucketIndex,
|
|
630
|
+
absoluteTime: bucketStartTime,
|
|
631
|
+
results: [],
|
|
632
|
+
videoUuids: new Set<string>(),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const bucket = buckets.get(bucketIndex)!;
|
|
637
|
+
bucket.results.push(minute.results);
|
|
638
|
+
bucket.videoUuids.add(minute.videoUuid);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Step 5: Aggregate using existing methods based on study type
|
|
642
|
+
const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(
|
|
643
|
+
buckets.values(),
|
|
644
|
+
)
|
|
645
|
+
.sort((a, b) => a.groupIndex - b.groupIndex)
|
|
646
|
+
.map((bucket) => {
|
|
647
|
+
let aggregatedResult: ITMCResult | IATRResult;
|
|
648
|
+
|
|
649
|
+
if (study.type === "TMC") {
|
|
650
|
+
aggregatedResult = this.aggregateTMCResults(bucket.results);
|
|
651
|
+
} else {
|
|
652
|
+
aggregatedResult = this.aggregateATRResults(bucket.results);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
absoluteTime: bucket.absoluteTime.toISOString(),
|
|
657
|
+
groupIndex: bucket.groupIndex,
|
|
658
|
+
label: this.formatStudyTimeLabel(
|
|
659
|
+
bucket.absoluteTime,
|
|
660
|
+
groupingMinutes,
|
|
661
|
+
),
|
|
662
|
+
results: aggregatedResult,
|
|
663
|
+
minuteCount: bucket.results.length,
|
|
664
|
+
videoCount: bucket.videoUuids.size,
|
|
665
|
+
contributingVideos: Array.from(bucket.videoUuids),
|
|
666
|
+
};
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Calculate latest time from last minute result
|
|
670
|
+
const lastMinute = minuteResults[minuteResults.length - 1];
|
|
671
|
+
const latestTime = new Date(lastMinute.absoluteTime);
|
|
672
|
+
|
|
673
|
+
// Step 6: Format response
|
|
674
|
+
return {
|
|
675
|
+
success: true,
|
|
676
|
+
data: aggregatedGroups,
|
|
677
|
+
groupingMinutes,
|
|
678
|
+
study: {
|
|
679
|
+
uuid: study.uuid,
|
|
680
|
+
name: study.name,
|
|
681
|
+
type: study.type,
|
|
682
|
+
status: study.status,
|
|
683
|
+
},
|
|
684
|
+
videoCount: videos.length,
|
|
685
|
+
dateRange: {
|
|
686
|
+
earliest: earliestTime.toISOString(),
|
|
687
|
+
latest: latestTime.toISOString(),
|
|
688
|
+
},
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
489
692
|
/**
|
|
490
693
|
* Aggregate minute results based on video type (TMC or ATR)
|
|
491
694
|
*/
|
|
@@ -728,6 +931,35 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
|
|
|
728
931
|
|
|
729
932
|
return `${formatMinute(startMinute)}:00 - ${formatMinute(endMinute)}:59`;
|
|
730
933
|
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Format time label for study-level results (time only, no dates)
|
|
937
|
+
* Used when results are already grouped by date in the UI
|
|
938
|
+
*/
|
|
939
|
+
private formatStudyTimeLabel(
|
|
940
|
+
startTime: Date,
|
|
941
|
+
durationMinutes: number,
|
|
942
|
+
): string {
|
|
943
|
+
const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
|
|
944
|
+
|
|
945
|
+
const formatTime = (date: Date): string => {
|
|
946
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
947
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
948
|
+
return `${hours}:${minutes}`;
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
return `${formatTime(startTime)} - ${formatTime(endTime)}`;
|
|
952
|
+
}
|
|
731
953
|
}
|
|
732
954
|
|
|
955
|
+
// Export interfaces for external use
|
|
956
|
+
export type {
|
|
957
|
+
IStudyTimeGroupResult,
|
|
958
|
+
IGroupedStudyResponse,
|
|
959
|
+
IGroupedResponse,
|
|
960
|
+
IGroupedResult,
|
|
961
|
+
ITMCResult,
|
|
962
|
+
IATRResult,
|
|
963
|
+
};
|
|
964
|
+
|
|
733
965
|
export default VideoMinuteResultDAO;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Knex } from "knex";
|
|
2
|
+
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
3
|
+
import { IBatch } from "../../interfaces/batch/batch.interfaces";
|
|
4
|
+
import KnexManager from "../../KnexConnection";
|
|
5
|
+
|
|
6
|
+
export class BatchDAO implements IBaseDAO<IBatch> {
|
|
7
|
+
private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
|
|
8
|
+
|
|
9
|
+
async create(item: IBatch): Promise<IBatch> {
|
|
10
|
+
const [createdBatch] = await this._knex("video_batch")
|
|
11
|
+
.insert(item)
|
|
12
|
+
.returning("*");
|
|
13
|
+
return createdBatch;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getById(id: number): Promise<IBatch | null> {
|
|
17
|
+
const batch = await this._knex("video_batch as b")
|
|
18
|
+
.innerJoin("folders as f", "b.folderId", "f.id")
|
|
19
|
+
.select("b.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
20
|
+
.where("b.id", id)
|
|
21
|
+
.first();
|
|
22
|
+
return batch || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getByUuid(uuid: string): Promise<IBatch | null> {
|
|
26
|
+
const batch = await this._knex("video_batch as b")
|
|
27
|
+
.innerJoin("folders as f", "b.folderId", "f.id")
|
|
28
|
+
.select("b.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
29
|
+
.where("b.uuid", uuid)
|
|
30
|
+
.first();
|
|
31
|
+
return batch || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async update(id: number, item: Partial<IBatch>): Promise<IBatch | null> {
|
|
35
|
+
const [updatedBatch] = await this._knex("video_batch")
|
|
36
|
+
.where({ id })
|
|
37
|
+
.update(item)
|
|
38
|
+
.returning("*");
|
|
39
|
+
return updatedBatch || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async delete(id: number): Promise<boolean> {
|
|
43
|
+
const result = await this._knex("video_batch").where({ id }).del();
|
|
44
|
+
return result > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getAll(
|
|
48
|
+
page: number,
|
|
49
|
+
limit: number,
|
|
50
|
+
folderId?: number | null,
|
|
51
|
+
): Promise<IDataPaginator<IBatch>> {
|
|
52
|
+
const offset = (page - 1) * limit;
|
|
53
|
+
|
|
54
|
+
const query = this._knex("video_batch as b")
|
|
55
|
+
.innerJoin("folders as f", "b.folderId", "f.id")
|
|
56
|
+
.select("b.*", this._knex.raw("to_jsonb(f.*) as folder"));
|
|
57
|
+
if (folderId !== undefined && folderId !== null) {
|
|
58
|
+
query.where("b.folderId", folderId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
62
|
+
const totalCount = +countResult.count;
|
|
63
|
+
const batches = await query.clone().limit(limit).offset(offset);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
data: batches,
|
|
68
|
+
page,
|
|
69
|
+
limit,
|
|
70
|
+
count: batches.length,
|
|
71
|
+
totalCount,
|
|
72
|
+
totalPages: Math.ceil(totalCount / limit),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get batches by folder ID
|
|
78
|
+
*/
|
|
79
|
+
async getByFolderId(folderId: number): Promise<IBatch[]> {
|
|
80
|
+
return this._knex("video_batch as b")
|
|
81
|
+
.innerJoin("folders as f", "b.folderId", "f.id")
|
|
82
|
+
.select("b.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
83
|
+
.where("b.folderId", folderId)
|
|
84
|
+
.orderBy("b.created_at", "desc");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Update batch progress
|
|
89
|
+
*/
|
|
90
|
+
async updateProgress(
|
|
91
|
+
id: number,
|
|
92
|
+
completedVideos: number,
|
|
93
|
+
failedVideos: number,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
await this._knex("video_batch").where({ id }).update({
|
|
96
|
+
completedVideos,
|
|
97
|
+
failedVideos,
|
|
98
|
+
updated_at: this._knex.fn.now(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mark batch as completed
|
|
104
|
+
*/
|
|
105
|
+
async markCompleted(id: number): Promise<void> {
|
|
106
|
+
await this._knex("video_batch").where({ id }).update({
|
|
107
|
+
status: "COMPLETED",
|
|
108
|
+
updated_at: this._knex.fn.now(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Mark batch as failed
|
|
114
|
+
*/
|
|
115
|
+
async markFailed(id: number): Promise<void> {
|
|
116
|
+
await this._knex("video_batch").where({ id }).update({
|
|
117
|
+
status: "FAILED",
|
|
118
|
+
updated_at: this._knex.fn.now(),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -257,4 +257,94 @@ export class VideoDAO implements IBaseDAO<IVideo> {
|
|
|
257
257
|
|
|
258
258
|
return rows.map((row) => row.id);
|
|
259
259
|
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get videos by batch ID
|
|
263
|
+
*/
|
|
264
|
+
async getByBatchId(batchId: number): Promise<IVideo[]> {
|
|
265
|
+
return this._knex("video as v")
|
|
266
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
267
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
268
|
+
.where("v.batchId", batchId)
|
|
269
|
+
.orderBy("v.created_at", "asc");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get videos sorted chronologically by recording start time for a study
|
|
274
|
+
*/
|
|
275
|
+
async getChronologicalVideos(
|
|
276
|
+
studyId: number,
|
|
277
|
+
startDate?: Date,
|
|
278
|
+
endDate?: Date,
|
|
279
|
+
): Promise<IDataPaginator<IVideo>> {
|
|
280
|
+
let query = this._knex("video as v")
|
|
281
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
282
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
283
|
+
.where("f.studyId", studyId)
|
|
284
|
+
.whereNotNull("v.recordingStartedAt");
|
|
285
|
+
|
|
286
|
+
if (startDate) {
|
|
287
|
+
query = query.where("v.recordingStartedAt", ">=", startDate);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (endDate) {
|
|
291
|
+
query = query.where("v.recordingStartedAt", "<=", endDate);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
295
|
+
const totalCount = +countResult.count;
|
|
296
|
+
const videos = await query.clone().orderBy("v.recordingStartedAt", "asc");
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
success: true,
|
|
300
|
+
data: videos,
|
|
301
|
+
page: 1,
|
|
302
|
+
limit: totalCount,
|
|
303
|
+
count: videos.length,
|
|
304
|
+
totalCount,
|
|
305
|
+
totalPages: 1,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Update recording start time for a video
|
|
311
|
+
*/
|
|
312
|
+
async updateRecordingStartTime(
|
|
313
|
+
id: number,
|
|
314
|
+
recordingStartedAt: Date,
|
|
315
|
+
): Promise<void> {
|
|
316
|
+
await this._knex("video").where({ id }).update({
|
|
317
|
+
recordingStartedAt,
|
|
318
|
+
updated_at: this._knex.fn.now(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Update trim settings for a video
|
|
324
|
+
*/
|
|
325
|
+
async updateTrimSettings(
|
|
326
|
+
id: number,
|
|
327
|
+
trimEnabled: boolean,
|
|
328
|
+
trimPeriods: { startTime: string; endTime: string }[] | null,
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
await this._knex("video")
|
|
331
|
+
.where({ id })
|
|
332
|
+
.update({
|
|
333
|
+
trimEnabled,
|
|
334
|
+
trimPeriods: trimPeriods ? JSON.stringify(trimPeriods) : null,
|
|
335
|
+
updated_at: this._knex.fn.now(),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get videos with trimming enabled for a folder
|
|
341
|
+
*/
|
|
342
|
+
async getVideosWithTrimming(folderId: number): Promise<IVideo[]> {
|
|
343
|
+
return this._knex("video as v")
|
|
344
|
+
.innerJoin("folders as f", "v.folderId", "f.id")
|
|
345
|
+
.select("v.*", this._knex.raw("to_jsonb(f.*) as folder"))
|
|
346
|
+
.where("v.folderId", folderId)
|
|
347
|
+
.where("v.trimEnabled", true)
|
|
348
|
+
.orderBy("v.recordingStartedAt", "asc");
|
|
349
|
+
}
|
|
260
350
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// DAOs
|
|
2
2
|
export { AuthDAO } from "./dao/auth/auth.dao";
|
|
3
|
+
export { BatchDAO } from "./dao/batch/batch.dao";
|
|
3
4
|
export { CameraDAO } from "./dao/camera/camera.dao";
|
|
4
5
|
export { ChatDAO } from "./dao/chat/chat.dao";
|
|
5
6
|
export { FolderDAO } from "./dao/folder/folder.dao";
|
|
@@ -14,6 +15,7 @@ export { VideoMinuteResultDAO } from "./dao/VideoMinuteResultDAO";
|
|
|
14
15
|
// Interfaces
|
|
15
16
|
export { IDataPaginator } from "./d.types";
|
|
16
17
|
export { IAuth } from "./interfaces/auth/auth.interfaces";
|
|
18
|
+
export { IBatch } from "./interfaces/batch/batch.interfaces";
|
|
17
19
|
export { ICamera } from "./interfaces/camera/camera.interfaces";
|
|
18
20
|
export {
|
|
19
21
|
IChat,
|
|
@@ -36,12 +38,20 @@ export {
|
|
|
36
38
|
export { IStudy } from "./interfaces/study/study.interfaces";
|
|
37
39
|
export { IUser } from "./interfaces/user/user.interfaces";
|
|
38
40
|
export { IUserPushNotificationToken } from "./interfaces/user-push-notification-token/user-push-notification-token.interfaces";
|
|
39
|
-
export { IVideo } from "./interfaces/video/video.interfaces";
|
|
41
|
+
export { IVideo, ITrimPeriod } from "./interfaces/video/video.interfaces";
|
|
40
42
|
export {
|
|
41
43
|
IVideoMinuteResult,
|
|
42
44
|
IVideoMinuteResultInput,
|
|
43
45
|
IVideoMinuteBatch,
|
|
44
46
|
} from "./entities/VideoMinuteResult";
|
|
47
|
+
export type {
|
|
48
|
+
IStudyTimeGroupResult,
|
|
49
|
+
IGroupedStudyResponse,
|
|
50
|
+
IGroupedResponse,
|
|
51
|
+
IGroupedResult,
|
|
52
|
+
ITMCResult,
|
|
53
|
+
IATRResult,
|
|
54
|
+
} from "./dao/VideoMinuteResultDAO";
|
|
45
55
|
|
|
46
56
|
import KnexManager from "./KnexConnection";
|
|
47
57
|
export { KnexManager };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IFolder } from "../folder/folder.interfaces";
|
|
2
|
+
|
|
3
|
+
export interface IBatch {
|
|
4
|
+
id: number;
|
|
5
|
+
uuid: string;
|
|
6
|
+
folderId: number;
|
|
7
|
+
status: "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
|
|
8
|
+
totalVideos: number;
|
|
9
|
+
completedVideos: number;
|
|
10
|
+
failedVideos: number;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
folder?: IFolder;
|
|
14
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { IFolder } from "../folder/folder.interfaces";
|
|
2
2
|
|
|
3
|
+
export interface ITrimPeriod {
|
|
4
|
+
startTime: string;
|
|
5
|
+
endTime: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export interface IVideo {
|
|
4
9
|
id: number;
|
|
5
10
|
uuid: string;
|
|
@@ -16,7 +21,7 @@ export interface IVideo {
|
|
|
16
21
|
progress: number;
|
|
17
22
|
remainingTime: string;
|
|
18
23
|
results: Record<string, any>;
|
|
19
|
-
durationSeconds?: number;
|
|
24
|
+
durationSeconds?: number;
|
|
20
25
|
|
|
21
26
|
// HLS streaming support
|
|
22
27
|
isHlsEnabled: boolean;
|
|
@@ -26,6 +31,12 @@ export interface IVideo {
|
|
|
26
31
|
// Enhanced metadata for hybrid streaming
|
|
27
32
|
streamingMetadata: Record<string, any> | null;
|
|
28
33
|
|
|
34
|
+
// Recording start time and video trimming support
|
|
35
|
+
recordingStartedAt?: Date;
|
|
36
|
+
trimEnabled?: boolean;
|
|
37
|
+
trimPeriods?: ITrimPeriod[] | null;
|
|
38
|
+
batchId?: number | null;
|
|
39
|
+
|
|
29
40
|
created_at: string;
|
|
30
41
|
updated_at: string;
|
|
31
42
|
folder?: IFolder;
|