From 46ceae47f00b6e01de0d08fc5d14145987ed702e Mon Sep 17 00:00:00 2001
From: Evan Strat <5790137+evan10s@users.noreply.github.com>
Date: Fri, 16 Feb 2024 23:26:17 -0500
Subject: [PATCH] Revert "Revert "Allow uploading replayed matches" (#96)"
This reverts commit d81034b52e51437e7e5a1382acc3729ea448d80d.
---
.../help/MissingMatchVideosHelp.vue | 10 ++++++++++
.../help/NameMatchVideoFilesHelp.vue | 10 +++++++++-
.../src/components/matches/MatchMetadata.vue | 19 +++++++++++++++++++
.../matches/MatchVideosUploader.vue | 13 ++++++-------
client/src/components/util/LoadingSpinner.vue | 19 +++++++++++++++++++
client/src/stores/match.ts | 11 +++++++++--
client/src/views/Home.vue | 5 ++++-
server/env/test.env.example | 2 ++
.../spec/tests/repos/MatchesServices.spec.ts | 1 +
server/src/models/Match.ts | 13 ++++++++-----
server/src/repos/FileStorageRepo.ts | 4 ++++
server/src/routes/matches.ts | 7 ++++---
server/src/services/MatchesService.ts | 15 +++++++++++----
server/src/tasks/uploadVideo.ts | 3 ++-
14 files changed, 108 insertions(+), 24 deletions(-)
create mode 100644 client/src/components/matches/MatchMetadata.vue
create mode 100644 client/src/components/util/LoadingSpinner.vue
diff --git a/client/src/components/help/MissingMatchVideosHelp.vue b/client/src/components/help/MissingMatchVideosHelp.vue
index c0817a9..58f68e2 100644
--- a/client/src/components/help/MissingMatchVideosHelp.vue
+++ b/client/src/components/help/MissingMatchVideosHelp.vue
@@ -9,9 +9,19 @@
Ensure your video files are named correctly (review How to name match video files).
+
+ Check the Replay metadata setting
+ (current value: {{ matchStore.isReplay ? "on" : "off" }}).
+ When the Replay setting is on, only video files for the replay of the match are returned. Replayed videos
+ should generally be named like Qualification 1 Replay.ext
. For full details, see
+ How to name match video files.
+
diff --git a/client/src/components/help/NameMatchVideoFilesHelp.vue b/client/src/components/help/NameMatchVideoFilesHelp.vue
index c37ca6d..407cd42 100644
--- a/client/src/components/help/NameMatchVideoFilesHelp.vue
+++ b/client/src/components/help/NameMatchVideoFilesHelp.vue
@@ -42,7 +42,15 @@ videos/
$Label/Final #.mp4
Examples: Overhead/Playoff 10.mp4, Feed B/Final 1.mp4
- Be sure to set your playoff type in Settings so we know how to parse your playoff matches!
+ Be sure to set your playoff type in Settings so we know how to parse your playoff matches!
+
+ Replayed matches:
+
+ For a replayed match, insert "Replay" (case-insensitive) between the match title and the file extension. For
+ instance, unlabeled/Qualification 1.mp4
would become
+ unlabeled/Qualification 1 Replay.mp4
. $LABEL/Playoff 1.mp4
would become
+ $LABEL/Playoff 1 Replay.mp4
+
diff --git a/client/src/components/matches/MatchMetadata.vue b/client/src/components/matches/MatchMetadata.vue
new file mode 100644
index 0000000..9f5148f
--- /dev/null
+++ b/client/src/components/matches/MatchMetadata.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/matches/MatchVideosUploader.vue b/client/src/components/matches/MatchVideosUploader.vue
index e9d14da..208f51a 100644
--- a/client/src/components/matches/MatchVideosUploader.vue
+++ b/client/src/components/matches/MatchVideosUploader.vue
@@ -2,11 +2,9 @@
Videos to upload
-
+
+
Replay
- No video files found for this match
+ No video files found
{
- return !matchStore.allMatchVideosUploaded;
+ return !matchStore.matchVideosLoading && !matchStore.allMatchVideosUploaded;
});
const queueAllBtnColor = computed(() => {
diff --git a/client/src/components/util/LoadingSpinner.vue b/client/src/components/util/LoadingSpinner.vue
new file mode 100644
index 0000000..7374ec0
--- /dev/null
+++ b/client/src/components/util/LoadingSpinner.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/client/src/stores/match.ts b/client/src/stores/match.ts
index 6b2eb32..1b0f039 100644
--- a/client/src/stores/match.ts
+++ b/client/src/stores/match.ts
@@ -1,5 +1,5 @@
import { acceptHMRUpdate, defineStore } from "pinia";
-import { computed, ref } from "vue";
+import { computed, ref, watch } from "vue";
import { MatchVideoInfo } from "@/types/MatchVideoInfo";
import { useSettingsStore } from "@/stores/settings";
import { useWorkerStore } from "@/stores/worker";
@@ -11,11 +11,13 @@ export const useMatchStore = defineStore("match", () => {
const settingsStore = useSettingsStore();
const workerStore = useWorkerStore();
const selectedMatchKey = ref(null);
+ const isReplay = ref(false);
const uploadInProgress = ref(false);
async function selectMatch(matchKey: string) {
selectedMatchKey.value = matchKey;
+ isReplay.value = false;
await getMatchVideos();
await getSuggestedDescription();
}
@@ -57,7 +59,7 @@ export const useMatchStore = defineStore("match", () => {
matchVideosLoading.value = true;
matchVideoError.value = "";
- const result = await fetch(`/api/v1/matches/${selectedMatchKey.value}/videos/recommend`);
+ const result = await fetch(`/api/v1/matches/${selectedMatchKey.value}/videos/recommend?isReplay=${isReplay.value}`);
if (!result.ok) {
const message = `Unable to retrieve video file suggestions for ${selectedMatchKey.value}`;
@@ -224,6 +226,10 @@ export const useMatchStore = defineStore("match", () => {
return !!job.linkedOnTheBlueAlliance && !!job.addedToYouTubePlaylist;
}
+ watch(isReplay, async (value, oldValue, onCleanup) => {
+ await getMatchVideos();
+ });
+
return {
advanceMatch,
allMatchVideosQueued,
@@ -234,6 +240,7 @@ export const useMatchStore = defineStore("match", () => {
descriptionLoading,
getMatchVideos,
getSuggestedDescription,
+ isReplay,
matchVideoError,
matchVideos,
matchVideosLoading,
diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue
index 50a4029..c6550da 100644
--- a/client/src/views/Home.vue
+++ b/client/src/views/Home.vue
@@ -14,8 +14,10 @@
- Video description
+ Video metadata
+
+ Description
@@ -80,6 +82,7 @@ import UploadErrors from "@/components/help/UploadErrors.vue";
import MissingPlaylistMapping from "@/components/help/MissingPlaylistMapping.vue";
import JobsList from "@/components/jobs/JobsList.vue";
import {useWorkerStore} from "@/stores/worker";
+import MatchMetadata from "@/components/matches/MatchMetadata.vue";
const error = ref("");
diff --git a/server/env/test.env.example b/server/env/test.env.example
index bd63863..eae9ad9 100644
--- a/server/env/test.env.example
+++ b/server/env/test.env.example
@@ -7,6 +7,8 @@ JET_LOGGER_FILEPATH=jet-logger.log
JET_LOGGER_TIMESTAMP=TRUE
JET_LOGGER_FORMAT=LINE
+WORKER_WEB_SERVER_URL=http://web:8080
+
POSTGRES_DB=match_uploader
POSTGRES_USER=match_uploader
POSTGRES_PASSWORD=
diff --git a/server/spec/tests/repos/MatchesServices.spec.ts b/server/spec/tests/repos/MatchesServices.spec.ts
index 211f244..ac694d2 100644
--- a/server/spec/tests/repos/MatchesServices.spec.ts
+++ b/server/spec/tests/repos/MatchesServices.spec.ts
@@ -23,6 +23,7 @@ describe("MatchesService", () => {
const files = await getLocalVideoFilesForMatch(
MatchKey.fromString("2023gadal_qm16", PlayoffsType.DoubleElimination),
+ false,
);
expect(files).toEqual([]);
mockFs.restore();
diff --git a/server/src/models/Match.ts b/server/src/models/Match.ts
index 8cf31a7..31d1d2d 100644
--- a/server/src/models/Match.ts
+++ b/server/src/models/Match.ts
@@ -3,9 +3,11 @@ import { CompLevel } from "@src/models/CompLevel";
export class Match {
key: MatchKey;
+ isReplay: boolean;
- constructor(key: MatchKey) {
+ constructor(key: MatchKey, isReplay: boolean = false) {
this.key = key;
+ this.isReplay = isReplay;
}
/**
@@ -17,24 +19,25 @@ export class Match {
*/
private generateMatchName(includeMatch: boolean, includeDoubleElimRound: boolean): string {
const match = includeMatch ? "Match " : "";
+ const replay = this.isReplay ? " Replay" : "";
const fullCompLevel = this.key.compLevel;
const playoffsRound = this.key.playoffsRound;
if (playoffsRound) {
const round = includeDoubleElimRound ? ` (R${playoffsRound})` : "";
- return `Playoff ${match}${this.key.setNumber}${round}`;
+ return `Playoff ${match}${this.key.setNumber}${round}${replay}`;
}
if (this.key.setNumber) {
if (fullCompLevel === CompLevel.Final) {
- return `${fullCompLevel} ${match}${this.key.matchNumber}`;
+ return `${fullCompLevel} ${match}${this.key.matchNumber}${replay}`;
}
- return `${fullCompLevel} ${this.key.setNumber} ${match}${this.key.matchNumber}`;
+ return `${fullCompLevel} ${this.key.setNumber} ${match}${this.key.matchNumber}${replay}`;
}
- return `${fullCompLevel} ${match}${this.key.matchNumber}`;
+ return `${fullCompLevel} ${match}${this.key.matchNumber}${replay}`;
}
get matchName(): string {
diff --git a/server/src/repos/FileStorageRepo.ts b/server/src/repos/FileStorageRepo.ts
index c293e11..f3822a4 100644
--- a/server/src/repos/FileStorageRepo.ts
+++ b/server/src/repos/FileStorageRepo.ts
@@ -12,17 +12,21 @@ const DEFAULT_ENCODING = "utf-8";
* @param dir What to set as the current working directory for glob lookup
* @param pattern Glob pattern to match files against
* @param depth Corresponds to fast-glob's `deep` option, see fast-glob docs for more information
+ * @param caseSensitiveMatch Corresponds to fast-glob's `caseSensitiveMatch` option, see fast-glob docs for more
+ * information
*/
export async function getFilesMatchingPattern(
dir: PathLike,
pattern: string,
depth: number = Infinity,
+ caseSensitiveMatch: boolean = true,
): Promise {
logger.info(`getFilesMatchingPattern: dir: ${dir}, pattern: ${pattern}, deep: ${depth}`);
return await fastGlob.async(pattern, {
cwd: dir.toString(), // cwd is relative to the directory the server is running out of
onlyFiles: true,
deep: depth,
+ caseSensitiveMatch,
});
}
diff --git a/server/src/routes/matches.ts b/server/src/routes/matches.ts
index 54c5e65..d4fe29d 100644
--- a/server/src/routes/matches.ts
+++ b/server/src/routes/matches.ts
@@ -6,7 +6,7 @@ import {
getLocalVideoFilesForMatch,
getMatchList,
} from "@src/services/MatchesService";
-import { matchedData, param, validationResult } from "express-validator";
+import { matchedData, param, query, validationResult } from "express-validator";
import MatchKey from "@src/models/MatchKey";
import { type MatchVideoInfo } from "@src/models/MatchVideoInfo";
import { Match } from "@src/models/Match";
@@ -48,6 +48,7 @@ matchesRouter.get(
"matchKey",
"Match key is required and must pass a format test. (See MatchKey class for regex.)",
).isString().matches(MatchKey.matchKeyRegex),
+ query("isReplay", "isReplay must be a boolean").isBoolean().toBoolean(),
recommendVideoFiles,
);
@@ -62,14 +63,14 @@ async function recommendVideoFiles(req: IReq, res: IRes): Promise {
return;
}
- const { matchKey } = matchedData(req);
+ const { matchKey, isReplay } = matchedData(req);
const { playoffsType: playoffsTypeRaw } = await getSettings();
const playoffsType = playoffsTypeRaw as PlayoffsType;
const matchKeyObject = MatchKey.fromString(matchKey as string, playoffsType);
- const recommendedVideoFiles = await getLocalVideoFilesForMatch(matchKeyObject);
+ const recommendedVideoFiles = await getLocalVideoFilesForMatch(matchKeyObject, isReplay as boolean);
res.json({
ok: true,
recommendedVideoFiles: recommendedVideoFiles.map((recommendation: MatchVideoInfo) => recommendation.toJson()),
diff --git a/server/src/services/MatchesService.ts b/server/src/services/MatchesService.ts
index 5aa40ee..af768eb 100644
--- a/server/src/services/MatchesService.ts
+++ b/server/src/services/MatchesService.ts
@@ -13,17 +13,24 @@ import { getFrcApiMatchNumber } from "@src/models/frcEvents/frcScoredMatch";
import { toFrcEventsUrlTournamentLevel } from "@src/models/CompLevel";
import Mustache from "mustache";
-export async function getLocalVideoFilesForMatch(matchKey: MatchKey): Promise {
+export async function getLocalVideoFilesForMatch(matchKey: MatchKey, isReplay: boolean): Promise {
const { eventName, videoSearchDirectory } = await getSettings();
- const match = new Match(matchKey);
+ const match = new Match(matchKey, isReplay);
const videoFileMatchingName = capitalizeFirstLetter(match.videoFileMatchingName);
const matchTitleName = capitalizeFirstLetter(match.verboseMatchName);
- const files = await getFilesMatchingPattern(videoSearchDirectory, `**/${videoFileMatchingName}.*`, 2);
+ const files = await getFilesMatchingPattern(
+ videoSearchDirectory,
+ `**/${videoFileMatchingName}.*`,
+ 2,
+ false,
+ );
const uploadedFiles = await getFilesMatchingPattern(
videoSearchDirectory,
`**/uploaded/${videoFileMatchingName}.*`,
- 3);
+ 3,
+ false,
+ );
files.push(...uploadedFiles);
return files
diff --git a/server/src/tasks/uploadVideo.ts b/server/src/tasks/uploadVideo.ts
index 8d1ce24..78b6a8c 100644
--- a/server/src/tasks/uploadVideo.ts
+++ b/server/src/tasks/uploadVideo.ts
@@ -62,7 +62,8 @@ async function moveToUploadedDirectory(
const toPath = path.join(videosDirectory, splitPath[0], "uploaded", splitPath[1]);
if (dryRun) {
- logger.info(`[Sandbox mode] Woul dhave moved video file ${fromPath} to uploaded directory ${toPath}`);
+ logger.info(`[Sandbox mode] Would have moved video file ${fromPath} to uploaded directory ${toPath}`);
+ return;
}
return await fs.move(fromPath, toPath);