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);