From 605f872599b7e6e1d5ec10f088312cdddd2794bd Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Thu, 3 Oct 2024 18:34:32 +0300 Subject: [PATCH 01/17] add indicator for removed videos --- client/src/components/VideoInfoTile.tsx | 5 ++++- client/src/context/types.ts | 1 + client/src/pages/Admin.tsx | 1 + server/controllers/media/AddMedia.ts | 5 ----- server/controllers/media/GetJukeboxDataObject.ts | 11 ++++++++++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/client/src/components/VideoInfoTile.tsx b/client/src/components/VideoInfoTile.tsx index 7246ef0..f4a946c 100644 --- a/client/src/components/VideoInfoTile.tsx +++ b/client/src/components/VideoInfoTile.tsx @@ -8,6 +8,7 @@ interface VideoInfoTileProps { videoId: string; videoName: string; videoDuration: string; + videoExists?: boolean; thumbnail: string; isLoading: boolean; showControls?: @@ -31,6 +32,7 @@ const VideoInfoTile: React.FC = ({ isLoading, addVideo, removeVideo, + videoExists, videoInSelected, videoInMedia, disabledControls, @@ -43,7 +45,7 @@ const VideoInfoTile: React.FC = ({ } else { addVideo && addVideo(videoId); } - } + }; return (
@@ -87,6 +89,7 @@ const VideoInfoTile: React.FC = ({ )} {!isLoading ?

{videoDuration}

: } + {videoExists === false &&

Video not available

}
{showControls && (
diff --git a/client/src/context/types.ts b/client/src/context/types.ts index d0d170e..bb1a8dc 100644 --- a/client/src/context/types.ts +++ b/client/src/context/types.ts @@ -69,4 +69,5 @@ export type Video = { } }, duration: number; + exists?: boolean; } \ No newline at end of file diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 6a8d25f..a401b6d 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -53,6 +53,7 @@ const Admin = () => { videoId={video.id.videoId} videoName={video.snippet.title} videoDuration={convertMillisToMinutes(video.duration)} + videoExists={video.exists} thumbnail={video.snippet.thumbnails.high.url} videoInSelected={selectedVideoIds.find((v) => v === video.id.videoId) ? true : false} showControls={{ diff --git a/server/controllers/media/AddMedia.ts b/server/controllers/media/AddMedia.ts index b13e3c1..33e103e 100644 --- a/server/controllers/media/AddMedia.ts +++ b/server/controllers/media/AddMedia.ts @@ -67,11 +67,6 @@ export default async function AddMedia(req: Request, res: Response) { }) .then() .catch(() => console.error("Error: Cannot trigger particle")); -<<<<<<< dev - -======= - ->>>>>>> main videos.shift(); } } diff --git a/server/controllers/media/GetJukeboxDataObject.ts b/server/controllers/media/GetJukeboxDataObject.ts index 757cd5b..038b6a7 100644 --- a/server/controllers/media/GetJukeboxDataObject.ts +++ b/server/controllers/media/GetJukeboxDataObject.ts @@ -1,5 +1,6 @@ import { errorHandler, getCredentials, getDroppedAsset } from "../../utils/index.js"; import { Request, Response } from "express"; +import { checkYouTubeLinksExist, chunkArray, getAvailableVideos } from "../../utils/youtube/index.js"; export default async function GetJukeboxDataObject(req: Request, res: Response) { try { @@ -9,7 +10,6 @@ export default async function GetJukeboxDataObject(req: Request, res: Response) return res.status(404).json({ message: "Asset not found" }); } if (jukeboxAsset) { - jukeboxAsset .updateDataObject( {}, @@ -26,6 +26,15 @@ export default async function GetJukeboxDataObject(req: Request, res: Response) ) .then() .catch(() => console.error("Error sending analytics for views")); + + const videoIds = await getAvailableVideos(jukeboxAsset.dataObject.catalog); + jukeboxAsset.dataObject.catalog = jukeboxAsset.dataObject.catalog.map((video) => { + return { + ...video, + exists: videoIds.includes(video.id.videoId), + }; + }); + return res.status(200).json(jukeboxAsset.dataObject); } } catch (error: any) { From 9c99f6e686fdc244b409007ebc506ec697dd52ed Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Thu, 3 Oct 2024 18:34:58 +0300 Subject: [PATCH 02/17] change YT API & redis initialization --- server/external/google.ts | 4 +++- server/redis-sse/index.ts | 20 ++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/server/external/google.ts b/server/external/google.ts index 2528008..acc9999 100644 --- a/server/external/google.ts +++ b/server/external/google.ts @@ -10,4 +10,6 @@ async function initializeYouTube() { return yt; } -export default initializeYouTube; +const yt = await initializeYouTube(); + +export default yt; diff --git a/server/redis-sse/index.ts b/server/redis-sse/index.ts index 6005cd1..aef7dc3 100644 --- a/server/redis-sse/index.ts +++ b/server/redis-sse/index.ts @@ -13,20 +13,12 @@ const shouldSendEvent = ( ); }; -const connectionOpt = process.env.IS_LOCALHOST - ? { - password: process.env.REDIS_PASSWORD, - socket: { - host: process.env.REDIS_URL, - port: parseInt(process.env.REDIS_PORT!) || 6379, - }, - } - : { - url: process.env.REDIS_URL, - socket: { - tls: process.env.REDIS_URL!.startsWith("rediss"), - }, - }; +const connectionOpt = { + url: process.env.REDIS_URL, + socket: { + tls: process.env.REDIS_URL!.startsWith("rediss"), + }, +}; const redisObj = { publisher: createClient(connectionOpt), From 76bb39f0ad3bb953f70cc00f50c59e43a3cfcddd Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Thu, 3 Oct 2024 18:35:30 +0300 Subject: [PATCH 03/17] check if video exists before playing --- .../controllers/media/GetJukeboxDataObject.ts | 2 +- server/controllers/media/NextSong.ts | 24 ++++++++-- server/controllers/media/SearchVideos.ts | 3 +- server/utils/youtube/index.ts | 45 ++++++++++++++++++- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/server/controllers/media/GetJukeboxDataObject.ts b/server/controllers/media/GetJukeboxDataObject.ts index 038b6a7..3dcfa5e 100644 --- a/server/controllers/media/GetJukeboxDataObject.ts +++ b/server/controllers/media/GetJukeboxDataObject.ts @@ -1,6 +1,6 @@ import { errorHandler, getCredentials, getDroppedAsset } from "../../utils/index.js"; import { Request, Response } from "express"; -import { checkYouTubeLinksExist, chunkArray, getAvailableVideos } from "../../utils/youtube/index.js"; +import { getAvailableVideos } from "../../utils/youtube/index.js"; export default async function GetJukeboxDataObject(req: Request, res: Response) { try { diff --git a/server/controllers/media/NextSong.ts b/server/controllers/media/NextSong.ts index eb63660..c4905d6 100644 --- a/server/controllers/media/NextSong.ts +++ b/server/controllers/media/NextSong.ts @@ -2,6 +2,19 @@ import redisObj from "../../redis-sse/index.js"; import { World, getCredentials, getDroppedAsset } from "../../utils/index.js"; import { Request, Response } from "express"; import { Video } from "../../types/index.js"; +import { getAvailableVideos } from "../../utils/youtube/index.js"; + +const findNextAvailableSong = async (queue: string[], catalog: Video[]): Promise<[Video | null, -1]> => { + const videoIds = await getAvailableVideos(catalog); + + for (let i = 0; i < queue.length; i++) { + const video = catalog.find((v) => v.id.videoId === queue[i]); + if (video && videoIds.includes(video.id.videoId)) { + return [video, i]; + } + } + return [null, -1]; +}; export default async function NextSong(req: Request, res: Response) { const credentials = getCredentials(req.body); @@ -12,14 +25,17 @@ export default async function NextSong(req: Request, res: Response) { const { queue } = jukeboxAsset.dataObject; const timeFactor = new Date(Math.round(new Date().getTime() / 25000) * 25000); const lockId = `${jukeboxAsset.id}_${jukeboxAsset.mediaPlayTime}_${timeFactor}`; - const remainingQueue = queue.slice(1); + let remainingQueue = []; let nowPlaying = "-1" as "-1" | Video; const promises = []; const analytics = []; try { if (queue.length > 0) { - nowPlaying = jukeboxAsset.dataObject.catalog.find((video: Video) => video.id.videoId === queue[0]) as Video; + // nowPlaying = jukeboxAsset.dataObject.catalog.find((video: Video) => video.id.videoId === queue[0]) as Video; + const [nextSong, index] = await findNextAvailableSong(queue, jukeboxAsset.dataObject.catalog); + nowPlaying = nextSong; if (nowPlaying) { + remainingQueue = queue.slice(index + 1); const videoId = nowPlaying.id.videoId; // const videoTitle = nowPlaying.snippet.title; @@ -32,7 +48,7 @@ export default async function NextSong(req: Request, res: Response) { duration: 10, position: { x: jukeboxAsset.position.x, - y: jukeboxAsset.position.y - 130 + y: jukeboxAsset.position.y - 130, }, }) .then() @@ -50,6 +66,8 @@ export default async function NextSong(req: Request, res: Response) { }), ); analytics.push({ analyticName: "plays", urlSlug: credentials.urlSlug, uniqueKey: credentials.urlSlug }); + } else { + promises.push(jukeboxAsset.updateMediaType({ mediaType: "none" })); } } else { promises.push(jukeboxAsset.updateMediaType({ mediaType: "none" })); diff --git a/server/controllers/media/SearchVideos.ts b/server/controllers/media/SearchVideos.ts index 1d3dd76..5845658 100644 --- a/server/controllers/media/SearchVideos.ts +++ b/server/controllers/media/SearchVideos.ts @@ -1,5 +1,5 @@ import { youtube_v3 } from "@googleapis/youtube"; -import initializeYouTube from "../../external/google.js"; +import yt from "../../external/google.js"; import { Video } from "../../types"; import { YTDurationToMilliseconds } from "../../utils/youtube/index.js"; import { Request, Response } from "express"; @@ -20,7 +20,6 @@ async function getVideoDuration(videos: Video[], yt: youtube_v3.Youtube) { } export default async function SearchVideos(req: Request, res: Response) { - const yt = await initializeYouTube(); try { const { q, nextPageToken }: { q: string; nextPageToken: string } = req.body; diff --git a/server/utils/youtube/index.ts b/server/utils/youtube/index.ts index 2330aae..4b9abb5 100644 --- a/server/utils/youtube/index.ts +++ b/server/utils/youtube/index.ts @@ -1,4 +1,5 @@ -// @ts-nocheck +import yt from "../../external/google"; + const YTDurationToMilliseconds = (isoDurationString: string) => { const regex = /^P(?:([.,\d]+)Y)?(?:([.,\d]+)M)?(?:([.,\d]+)W)?(?:([.,\d]+)D)?T(?:([.,\d]+)H)?(?:([.,\d]+)M)?(?:([.,\d]+)S)?$/; @@ -30,4 +31,44 @@ const YTDurationToMilliseconds = (isoDurationString: string) => { } }; -export { YTDurationToMilliseconds }; +async function getAvailableVideos(catalog) { + const allVideoIds = catalog.map((video) => video.id.videoId); + const chunkedVideoIds = chunkArray(allVideoIds); + const existsOnYTPromises = []; + for (const chunk of chunkedVideoIds) { + existsOnYTPromises.push(checkYouTubeLinksExist(chunk)); + } + + const existsOnYT = await Promise.all(existsOnYTPromises); + const videoIds = existsOnYT.flat(); + return videoIds; +} + +async function checkYouTubeLinksExist(videoIds) { + try { + const response = await yt.videos.list({ + id: videoIds.join(","), + part: "status", + }); + + const videoData = response.data.items; + if (videoData.length > 0) { + return videoData.map((video) => video.id); // Returning found video IDs + } else { + return []; + } + } catch (error) { + console.error("Error fetching video details:", error); + return []; + } +} + +function chunkArray(arr) { + const chunks = []; + for (let i = 0; i < arr.length; i += 50) { + chunks.push(arr.slice(i, i + 50)); + } + return chunks; +} + +export { YTDurationToMilliseconds, getAvailableVideos }; From 35d4a6926596e9458bd60eb0ec83b002118f85f2 Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Thu, 3 Oct 2024 18:54:24 +0300 Subject: [PATCH 04/17] check if video exists only for admins --- .../controllers/media/GetJukeboxDataObject.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/server/controllers/media/GetJukeboxDataObject.ts b/server/controllers/media/GetJukeboxDataObject.ts index 3dcfa5e..a933358 100644 --- a/server/controllers/media/GetJukeboxDataObject.ts +++ b/server/controllers/media/GetJukeboxDataObject.ts @@ -1,6 +1,7 @@ import { errorHandler, getCredentials, getDroppedAsset } from "../../utils/index.js"; import { Request, Response } from "express"; import { getAvailableVideos } from "../../utils/youtube/index.js"; +import { checkIsAdmin } from "../../middleware/isAdmin.js"; export default async function GetJukeboxDataObject(req: Request, res: Response) { try { @@ -27,13 +28,16 @@ export default async function GetJukeboxDataObject(req: Request, res: Response) .then() .catch(() => console.error("Error sending analytics for views")); - const videoIds = await getAvailableVideos(jukeboxAsset.dataObject.catalog); - jukeboxAsset.dataObject.catalog = jukeboxAsset.dataObject.catalog.map((video) => { - return { - ...video, - exists: videoIds.includes(video.id.videoId), - }; - }); + const isAdmin = checkIsAdmin(credentials); + if (isAdmin) { + const videoIds = await getAvailableVideos(jukeboxAsset.dataObject.catalog); + jukeboxAsset.dataObject.catalog = jukeboxAsset.dataObject.catalog.map((video) => { + return { + ...video, + exists: videoIds.includes(video.id.videoId), + }; + }); + } return res.status(200).json(jukeboxAsset.dataObject); } From 045a7aed6dd18b0005a34df972b2998e8365115a Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Thu, 3 Oct 2024 19:17:16 +0300 Subject: [PATCH 05/17] lock data object before checking next song --- server/controllers/media/NextSong.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/controllers/media/NextSong.ts b/server/controllers/media/NextSong.ts index c4905d6..dd12a72 100644 --- a/server/controllers/media/NextSong.ts +++ b/server/controllers/media/NextSong.ts @@ -4,7 +4,7 @@ import { Request, Response } from "express"; import { Video } from "../../types/index.js"; import { getAvailableVideos } from "../../utils/youtube/index.js"; -const findNextAvailableSong = async (queue: string[], catalog: Video[]): Promise<[Video | null, -1]> => { +const findNextAvailableSong = async (queue: string[], catalog: Video[]): Promise<[Video | null, number]> => { const videoIds = await getAvailableVideos(catalog); for (let i = 0; i < queue.length; i++) { @@ -30,8 +30,18 @@ export default async function NextSong(req: Request, res: Response) { const promises = []; const analytics = []; try { + // Lock the asset dataObject before attempting to find the next available song + // This would reduce YouTube API quota usage + await jukeboxAsset.updateDataObject( + {}, + { + lock: { + lockId, + releaseLock: false, + }, + }, + ); if (queue.length > 0) { - // nowPlaying = jukeboxAsset.dataObject.catalog.find((video: Video) => video.id.videoId === queue[0]) as Video; const [nextSong, index] = await findNextAvailableSong(queue, jukeboxAsset.dataObject.catalog); nowPlaying = nextSong; if (nowPlaying) { @@ -82,10 +92,6 @@ export default async function NextSong(req: Request, res: Response) { }, { analytics, - lock: { - lockId, - releaseLock: false, - }, }, ), ); From 42ebf30d75fe35dc96b5d21e45d352b1b759f41a Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Thu, 3 Oct 2024 19:35:29 +0300 Subject: [PATCH 06/17] fix import error --- server/controllers/media/RemoveMedia.ts | 1 - server/utils/youtube/index.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/controllers/media/RemoveMedia.ts b/server/controllers/media/RemoveMedia.ts index fa661ca..febe5af 100644 --- a/server/controllers/media/RemoveMedia.ts +++ b/server/controllers/media/RemoveMedia.ts @@ -1,4 +1,3 @@ -import { checkIsAdmin } from "../../middleware/isAdmin.js"; import redisObj from "../../redis-sse/index.js"; import { Credentials, Video } from "../../types/index.js"; import { getDroppedAsset } from "../../utils/index.js"; diff --git a/server/utils/youtube/index.ts b/server/utils/youtube/index.ts index 4b9abb5..7b5712d 100644 --- a/server/utils/youtube/index.ts +++ b/server/utils/youtube/index.ts @@ -1,4 +1,4 @@ -import yt from "../../external/google"; +import yt from "../../external/google.js"; const YTDurationToMilliseconds = (isoDurationString: string) => { const regex = From a04dda0056506462d31660b7f42273f6421d3831 Mon Sep 17 00:00:00 2001 From: Terraform Date: Tue, 26 Nov 2024 13:31:26 -0800 Subject: [PATCH 07/17] Add production release CICD --- .github/workflows/aws_prod_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/aws_prod_release.yml b/.github/workflows/aws_prod_release.yml index 23df989..4b375da 100644 --- a/.github/workflows/aws_prod_release.yml +++ b/.github/workflows/aws_prod_release.yml @@ -92,7 +92,7 @@ jobs: - name: Build and version tag run: | - docker build --build-arg REF=${{ github.ref}} --build-arg COMMIT_HASH=${{ github.sha}} -t ${{ steps.metadata.outputs.tags }} . + docker build --build-arg REF=${{ github.ref}} --build-arg COMMIT_HASH=$(git rev-parse --short ${{ github.sha }}) -t ${{ steps.metadata.outputs.tags }} . docker tag ${{ steps.metadata.outputs.tags }} ${{ steps.metadata-latest.outputs.tags }} - name: push docker image to Amazon ECR From f6657015d1666e5598858312c79e0ecaf4971349 Mon Sep 17 00:00:00 2001 From: Terraform Date: Tue, 26 Nov 2024 13:31:28 -0800 Subject: [PATCH 08/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/aws_main_merge.yml diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml new file mode 100644 index 0000000..26320f3 --- /dev/null +++ b/.github/workflows/aws_main_merge.yml @@ -0,0 +1,44 @@ +name: "Create a new patch release on merge to main" +on: + push: + branches: + - main + workflow_dispatch: + +env: + ENV: "prod" + reponame: "${{ github.event.repository.name }}" + +concurrency: + group: ${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout +jobs: + Release_Patch: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + + - name: Checkout + uses: actions/checkout@v4 + + - name: Versioning + id: version + run: | + git config --global user.email devops@topia.io + git config --global user.name Devops + VERSION=$(npm version --workspaces --include-workspace-root true patch) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> $GITHUB_ENV + git add . && git commit -m "chore: bump version" && git push && git push --tags + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.version }} + name: Release ${{ env.version }} + generate_release_notes: true From 13f3a875cfb8593981c5f6ce03b2f253fac09a3d Mon Sep 17 00:00:00 2001 From: Terraform Date: Tue, 26 Nov 2024 18:51:42 -0800 Subject: [PATCH 09/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml index 26320f3..ca2d8a3 100644 --- a/.github/workflows/aws_main_merge.yml +++ b/.github/workflows/aws_main_merge.yml @@ -15,7 +15,8 @@ concurrency: permissions: id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout + contents: write # This is required for actions/checkout + jobs: Release_Patch: runs-on: ubuntu-latest From ce3ffa29d1d043b2ba7ddc904ed82ca80934385e Mon Sep 17 00:00:00 2001 From: Terraform Date: Tue, 26 Nov 2024 19:02:50 -0800 Subject: [PATCH 10/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml index ca2d8a3..fd843a6 100644 --- a/.github/workflows/aws_main_merge.yml +++ b/.github/workflows/aws_main_merge.yml @@ -35,7 +35,7 @@ jobs: VERSION=$(npm version --workspaces --include-workspace-root true patch) echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_ENV - git add . && git commit -m "chore: bump version" && git push && git push --tags + git add package.json && git commit -m "chore: bump version" && git push origin dev && git push --tags origin dev - name: Create Release uses: softprops/action-gh-release@v2 From dc1886e7d4179232f515c4d43583b2fcc0b1dc2f Mon Sep 17 00:00:00 2001 From: Terraform Date: Wed, 27 Nov 2024 04:01:14 -0800 Subject: [PATCH 11/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml index fd843a6..5a2d99f 100644 --- a/.github/workflows/aws_main_merge.yml +++ b/.github/workflows/aws_main_merge.yml @@ -35,7 +35,7 @@ jobs: VERSION=$(npm version --workspaces --include-workspace-root true patch) echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_ENV - git add package.json && git commit -m "chore: bump version" && git push origin dev && git push --tags origin dev + git commit -a -m "chore: bump version ${VERSION}" && git push origin dev && git push --tags origin dev - name: Create Release uses: softprops/action-gh-release@v2 From 26384f4f38be9ac6ef396308f7569a05b10c1881 Mon Sep 17 00:00:00 2001 From: Terraform Date: Mon, 2 Dec 2024 12:40:47 -0800 Subject: [PATCH 12/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml index 5a2d99f..e55d05c 100644 --- a/.github/workflows/aws_main_merge.yml +++ b/.github/workflows/aws_main_merge.yml @@ -35,7 +35,14 @@ jobs: VERSION=$(npm version --workspaces --include-workspace-root true patch) echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_ENV - git commit -a -m "chore: bump version ${VERSION}" && git push origin dev && git push --tags origin dev + + - name: Push to protected branch + uses: CasperWA/push-protected@v2 + with: + token: ${{ secrets.PAT }} + branch: main + unprotect_reviews: true + tags: true - name: Create Release uses: softprops/action-gh-release@v2 From 82a25046f7854d13d8d3e7f3bbd3e512fc15c6ad Mon Sep 17 00:00:00 2001 From: Terraform Date: Mon, 2 Dec 2024 13:33:45 -0800 Subject: [PATCH 13/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml index e55d05c..7609e6a 100644 --- a/.github/workflows/aws_main_merge.yml +++ b/.github/workflows/aws_main_merge.yml @@ -32,7 +32,8 @@ jobs: run: | git config --global user.email devops@topia.io git config --global user.name Devops - VERSION=$(npm version --workspaces --include-workspace-root true patch) + #VERSION=$(npm version --workspaces --include-workspace-root true patch) + VERSION=$(npm version patch) echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_ENV @@ -40,9 +41,8 @@ jobs: uses: CasperWA/push-protected@v2 with: token: ${{ secrets.PAT }} - branch: main + branch: dev unprotect_reviews: true - tags: true - name: Create Release uses: softprops/action-gh-release@v2 From 3067f9b63b600f77d4d4a3788fa5ffbe219ecf3b Mon Sep 17 00:00:00 2001 From: LinaBell Date: Mon, 2 Dec 2024 13:44:02 -0800 Subject: [PATCH 14/17] Add production release CICD --- .github/workflows/aws_main_merge.yml | 52 ---------------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/workflows/aws_main_merge.yml diff --git a/.github/workflows/aws_main_merge.yml b/.github/workflows/aws_main_merge.yml deleted file mode 100644 index 7609e6a..0000000 --- a/.github/workflows/aws_main_merge.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: "Create a new patch release on merge to main" -on: - push: - branches: - - main - workflow_dispatch: - -env: - ENV: "prod" - reponame: "${{ github.event.repository.name }}" - -concurrency: - group: ${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - id-token: write # This is required for requesting the JWT - contents: write # This is required for actions/checkout - -jobs: - Release_Patch: - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Versioning - id: version - run: | - git config --global user.email devops@topia.io - git config --global user.name Devops - #VERSION=$(npm version --workspaces --include-workspace-root true patch) - VERSION=$(npm version patch) - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "version=${VERSION}" >> $GITHUB_ENV - - - name: Push to protected branch - uses: CasperWA/push-protected@v2 - with: - token: ${{ secrets.PAT }} - branch: dev - unprotect_reviews: true - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ env.version }} - name: Release ${{ env.version }} - generate_release_notes: true From 7655beb926ba351cf2d3d6189ae65792f32c18cf Mon Sep 17 00:00:00 2001 From: Abel Belay Date: Fri, 31 Jan 2025 21:54:28 +0300 Subject: [PATCH 15/17] gives admins ability to skip to the next song --- client/src/context/actions.ts | 11 +++++++++++ client/src/pages/Home.tsx | 11 +++++++++-- package-lock.json | 8 ++++---- server/controllers/media/NextSong.ts | 7 ++++++- server/package.json | 2 +- server/router/routes.ts | 2 ++ 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/client/src/context/actions.ts b/client/src/context/actions.ts index dc695f3..e61c483 100644 --- a/client/src/context/actions.ts +++ b/client/src/context/actions.ts @@ -81,6 +81,16 @@ const checkInteractiveCredentials = async (backendAPI: AxiosInstance) => { } }; +const skipToNextSong = async (backendAPI: AxiosInstance) => { + try { + const result = await backendAPI.post("/next"); + return result.data; + } catch (error) { + console.error(error); + return null; + } +}; + export { checkInteractiveCredentials, searchCatalog, @@ -90,4 +100,5 @@ export { removeFromCatalog, addToQueue, removeFromQueue, + skipToNextSong }; diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 3576bca..1cc3f76 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -1,7 +1,7 @@ import Header from "@/components/Header"; import VideoInfoTile from "@/components/VideoInfoTile"; import { GlobalDispatchContext, GlobalStateContext } from "@/context/GlobalContext"; -import { removeFromQueue } from "@/context/actions"; +import { removeFromQueue, skipToNextSong } from "@/context/actions"; import { InitialState, REMOVE_FROM_QUEUE } from "@/context/types"; import { convertMillisToMinutes } from "@/utils/duration"; import { useContext, useState } from "react"; @@ -89,10 +89,17 @@ const Home: React.FC = () => { )} {!jukeboxLoading && ( -
+
Add a Song + {isAdmin && queue.length ? ( + + ) : ( + <> + )}
)}
diff --git a/package-lock.json b/package-lock.json index 7ef2ea6..48a6653 100644 --- a/package-lock.json +++ b/package-lock.json @@ -973,9 +973,9 @@ ] }, "node_modules/@rtsdk/topia": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.11.8.tgz", - "integrity": "sha512-YHrKXmnmJQifIh74JhIKNQA/bhCLjf8/poM3I3zdhj4pdHnSzDAbedzWSdHyjZO4gGanyA1mkK2lgcSpJb3bhQ==" + "version": "0.12.6", + "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.12.6.tgz", + "integrity": "sha512-YNMoz0Y/xZ1vZwFs+C+aXg8Qd7Z/QR32i5tIdYzbCUSxC173WgcyMmTW6cAy1RDFNxZQgPmH2H4ehieIpUxlJA==" }, "node_modules/@swc/core": { "version": "1.4.8", @@ -5899,7 +5899,7 @@ "version": "0.1.0", "dependencies": { "@googleapis/youtube": "^14.0.0", - "@rtsdk/topia": "^0.11.8", + "@rtsdk/topia": "^0.12.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/server/controllers/media/NextSong.ts b/server/controllers/media/NextSong.ts index dd12a72..75c106c 100644 --- a/server/controllers/media/NextSong.ts +++ b/server/controllers/media/NextSong.ts @@ -17,7 +17,12 @@ const findNextAvailableSong = async (queue: string[], catalog: Video[]): Promise }; export default async function NextSong(req: Request, res: Response) { - const credentials = getCredentials(req.body); + let credentials; + if (Object.keys(req.body).length) { + credentials = getCredentials(req.body); + } else { + credentials = getCredentials(req.query); + } const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); diff --git a/server/package.json b/server/package.json index 4a28a75..fb50706 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@googleapis/youtube": "^14.0.0", - "@rtsdk/topia": "^0.11.8", + "@rtsdk/topia": "^0.12.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/server/router/routes.ts b/server/router/routes.ts index 9c02600..86867f9 100644 --- a/server/router/routes.ts +++ b/server/router/routes.ts @@ -10,6 +10,7 @@ import AddMedia from "../controllers/media/AddMedia.js"; import sse from "../controllers/media/Events.js"; import RemoveMedia from "../controllers/media/RemoveMedia.js"; import { handleCheckInteractiveCredentials } from "../controllers/status/handleCheckInteractiveCredentials.js"; +import NextSong from "../controllers/media/NextSong.js"; const router = express.Router(); const SERVER_START_DATE = new Date(); @@ -52,5 +53,6 @@ router.get("/is-admin", isAdminCheck); router.post("/add-media", AddMedia); router.post("/remove-media", isAdmin, RemoveMedia); +router.post("/next", isAdmin, NextSong); export default router; From cc2834abc36a2e3c1113dbd5f7ab6e936f7d5c5b Mon Sep 17 00:00:00 2001 From: Terraform Date: Thu, 10 Apr 2025 12:25:27 -0700 Subject: [PATCH 16/17] Add code owners --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a3a580d..37f8642 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -@cpsiaki @LinaBell @liebeskind \ No newline at end of file +* @cpsiaki @LinaBell @liebeskind \ No newline at end of file From 0181f50882638d0cad1b9b92285cd880407b7f51 Mon Sep 17 00:00:00 2001 From: Lina Date: Wed, 23 Apr 2025 12:49:00 -0700 Subject: [PATCH 17/17] Update SDK --- package-lock.json | 8 ++++---- server/controllers/media/AddMedia.ts | 20 ++++++++++--------- .../controllers/media/GetJukeboxDataObject.ts | 8 +++++--- server/controllers/media/NextSong.ts | 10 ++++++++-- server/controllers/media/RemoveMedia.ts | 12 +++++++---- .../handleCheckInteractiveCredentials.ts | 1 + server/controllers/status/isAdminCheck.ts | 10 +++++----- server/middleware/isAdmin.ts | 14 ++++++------- server/package.json | 2 +- server/utils/droppedAssets/getDroppedAsset.ts | 8 +------- server/utils/visitors/getVisitor.ts | 14 ++++--------- 11 files changed, 54 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 48a6653..06569ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -973,9 +973,9 @@ ] }, "node_modules/@rtsdk/topia": { - "version": "0.12.6", - "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.12.6.tgz", - "integrity": "sha512-YNMoz0Y/xZ1vZwFs+C+aXg8Qd7Z/QR32i5tIdYzbCUSxC173WgcyMmTW6cAy1RDFNxZQgPmH2H4ehieIpUxlJA==" + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@rtsdk/topia/-/topia-0.15.4.tgz", + "integrity": "sha512-J1g/+yp61OvBx0YrloDiYiDeuO2/C64dFluaSx78+h+GSg7NSWINdQtrRl6qgcUTqNXMHbVZW4HwKAo58Fmx5A==" }, "node_modules/@swc/core": { "version": "1.4.8", @@ -5899,7 +5899,7 @@ "version": "0.1.0", "dependencies": { "@googleapis/youtube": "^14.0.0", - "@rtsdk/topia": "^0.12.6", + "@rtsdk/topia": "^0.15.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/server/controllers/media/AddMedia.ts b/server/controllers/media/AddMedia.ts index 33e103e..78e136e 100644 --- a/server/controllers/media/AddMedia.ts +++ b/server/controllers/media/AddMedia.ts @@ -7,6 +7,8 @@ import { Request, Response } from "express"; export default async function AddMedia(req: Request, res: Response) { const credentials = getCredentials(req.query); + const { interactiveNonce, profileId, urlSlug, visitorId } = credentials; + const { videos, type }: { videos: Video[] | string[]; type: "catalog" | "queue" } = req.body; const [isAdmin, jukeboxAsset] = await Promise.all([checkIsAdmin(credentials), getDroppedAsset(credentials)]); @@ -26,15 +28,15 @@ export default async function AddMedia(req: Request, res: Response) { analytics.push({ analyticName: "addsToCatalog", incrementBy: videos.length, - uniqueKey: credentials.urlSlug, - urlSlug: credentials.urlSlug, + uniqueKey: urlSlug, + urlSlug, }); } else if (type === "queue") { analytics.push({ analyticName: "addsToQueue", incrementBy: videos.length, - profileId: credentials.profileId, - uniqueKey: credentials.profileId, + profileId, + uniqueKey: profileId, }); } let firstVideo = null; @@ -42,7 +44,7 @@ export default async function AddMedia(req: Request, res: Response) { firstVideo = jukeboxAsset.dataObject.catalog.find((video: Video) => video.id.videoId === videos[0]); if (firstVideo) { const mediaLink = `https://www.youtube.com/watch?v=${firstVideo.id.videoId}`; - analytics.push({ analyticName: "plays", urlSlug: credentials.urlSlug, uniqueKey: credentials.urlSlug }); + analytics.push({ analyticName: "plays", urlSlug, uniqueKey: urlSlug }); promises.push( jukeboxAsset.updateMediaType({ mediaLink, @@ -55,7 +57,7 @@ export default async function AddMedia(req: Request, res: Response) { syncUserMedia: true, // Make it so everyone has the video synced instead of it playing from the beginning when they approach. }), ); - const world = World.create(credentials.urlSlug, { credentials }); + const world = World.create(urlSlug, { credentials }); world .triggerParticle({ name: "musicNote_float", @@ -91,9 +93,9 @@ export default async function AddMedia(req: Request, res: Response) { redisObj.publish(`${process.env.INTERACTIVE_KEY}_JUKEBOX`, { assetId: jukeboxAsset.id, videos: firstVideo ? [firstVideo.id.videoId, ...videos] : videos, - interactiveNonce: credentials.interactiveNonce, - urlSlug: credentials.urlSlug, - visitorId: credentials.visitorId, + interactiveNonce, + urlSlug, + visitorId, kind: type === "catalog" ? "addedToCatalog" : "addedToQueue", event: "mediaAction", }); diff --git a/server/controllers/media/GetJukeboxDataObject.ts b/server/controllers/media/GetJukeboxDataObject.ts index a933358..b376f97 100644 --- a/server/controllers/media/GetJukeboxDataObject.ts +++ b/server/controllers/media/GetJukeboxDataObject.ts @@ -6,6 +6,8 @@ import { checkIsAdmin } from "../../middleware/isAdmin.js"; export default async function GetJukeboxDataObject(req: Request, res: Response) { try { const credentials = getCredentials(req.query); + const { profileId, urlSlug } = credentials; + const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); @@ -18,9 +20,9 @@ export default async function GetJukeboxDataObject(req: Request, res: Response) analytics: [ { analyticName: "views", - profileId: credentials.profileId, - uniqueKey: credentials.profileId, - urlSlug: credentials.urlSlug, + profileId, + uniqueKey: profileId, + urlSlug, }, ], }, diff --git a/server/controllers/media/NextSong.ts b/server/controllers/media/NextSong.ts index 75c106c..42bc56a 100644 --- a/server/controllers/media/NextSong.ts +++ b/server/controllers/media/NextSong.ts @@ -23,6 +23,8 @@ export default async function NextSong(req: Request, res: Response) { } else { credentials = getCredentials(req.query); } + const { urlSlug } = credentials; + const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); @@ -34,6 +36,7 @@ export default async function NextSong(req: Request, res: Response) { let nowPlaying = "-1" as "-1" | Video; const promises = []; const analytics = []; + try { // Lock the asset dataObject before attempting to find the next available song // This would reduce YouTube API quota usage @@ -46,9 +49,11 @@ export default async function NextSong(req: Request, res: Response) { }, }, ); + if (queue.length > 0) { const [nextSong, index] = await findNextAvailableSong(queue, jukeboxAsset.dataObject.catalog); nowPlaying = nextSong; + if (nowPlaying) { remainingQueue = queue.slice(index + 1); const videoId = nowPlaying.id.videoId; @@ -56,7 +61,7 @@ export default async function NextSong(req: Request, res: Response) { const mediaLink = `https://www.youtube.com/watch?v=${videoId}`; - const world = World.create(credentials.urlSlug, { credentials }); + const world = World.create(urlSlug, { credentials }); world .triggerParticle({ name: "musicNote_float", @@ -68,6 +73,7 @@ export default async function NextSong(req: Request, res: Response) { }) .then() .catch(() => console.error("Cannot trigger particle")); + promises.push( jukeboxAsset.updateMediaType({ mediaLink, @@ -80,7 +86,7 @@ export default async function NextSong(req: Request, res: Response) { syncUserMedia: true, // Make it so everyone has the video synced instead of it playing from the beginning when they approach. }), ); - analytics.push({ analyticName: "plays", urlSlug: credentials.urlSlug, uniqueKey: credentials.urlSlug }); + analytics.push({ analyticName: "plays", urlSlug, uniqueKey: urlSlug }); } else { promises.push(jukeboxAsset.updateMediaType({ mediaType: "none" })); } diff --git a/server/controllers/media/RemoveMedia.ts b/server/controllers/media/RemoveMedia.ts index febe5af..36930c9 100644 --- a/server/controllers/media/RemoveMedia.ts +++ b/server/controllers/media/RemoveMedia.ts @@ -1,20 +1,22 @@ import redisObj from "../../redis-sse/index.js"; -import { Credentials, Video } from "../../types/index.js"; -import { getDroppedAsset } from "../../utils/index.js"; +import { Video } from "../../types/index.js"; +import { getCredentials, getDroppedAsset } from "../../utils/index.js"; import { Request, Response } from "express"; export default async function RemoveMedia(req: Request, res: Response) { - const { assetId, interactivePublicKey, interactiveNonce, urlSlug, visitorId } = req.query as Credentials; + const credentials = getCredentials(req.query); + const { interactiveNonce, urlSlug, visitorId } = credentials; const { videoIds, type }: { videoIds: string[]; type: "catalog" | "queue" } = req.body; - const credentials = { assetId, interactivePublicKey, interactiveNonce, urlSlug, visitorId }; const jukeboxAsset = await getDroppedAsset(credentials); if (jukeboxAsset.error) { return res.status(404).json({ message: "Asset not found" }); } + const timeFactor = new Date(Math.round(new Date().getTime() / 10000) * 10000); const lockId = `${jukeboxAsset.id}_${timeFactor}`; + try { const jukeboxUpdate: { catalog?: Video[]; @@ -29,6 +31,7 @@ export default async function RemoveMedia(req: Request, res: Response) { } else if (type === "queue") { jukeboxUpdate.queue = jukeboxAsset.dataObject.queue.filter((videoId: string) => !videoIds.includes(videoId)); } + await jukeboxAsset.updateDataObject( { ...jukeboxAsset.dataObject, @@ -41,6 +44,7 @@ export default async function RemoveMedia(req: Request, res: Response) { }, }, ); + redisObj.publish(`${process.env.INTERACTIVE_KEY}_JUKEBOX`, { assetId: jukeboxAsset.id, videos: videoIds, diff --git a/server/controllers/status/handleCheckInteractiveCredentials.ts b/server/controllers/status/handleCheckInteractiveCredentials.ts index 2e1baaf..faab2d2 100644 --- a/server/controllers/status/handleCheckInteractiveCredentials.ts +++ b/server/controllers/status/handleCheckInteractiveCredentials.ts @@ -13,6 +13,7 @@ export const handleCheckInteractiveCredentials = async ( // if key matches proceed with check using jwt created by topiaInit const user = User.create({ credentials }); await user.checkInteractiveCredentials(); + return res.json({ success: true }); } catch (error) { return errorHandler({ diff --git a/server/controllers/status/isAdminCheck.ts b/server/controllers/status/isAdminCheck.ts index 1fa42b9..a8e3e91 100644 --- a/server/controllers/status/isAdminCheck.ts +++ b/server/controllers/status/isAdminCheck.ts @@ -1,11 +1,10 @@ -import { Credentials } from "../../types/index.js"; -import { errorHandler, getVisitor } from "../../utils/index.js"; +import { errorHandler, getCredentials, getVisitor } from "../../utils/index.js"; import { Request, Response } from "express"; export default async function isAdminCheck(req: Request, res: Response) { try { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = req.query as Credentials; - const visitor = await getVisitor({ interactivePublicKey, interactiveNonce, urlSlug, visitorId }); + const credentials = getCredentials(req.query); + const visitor = await getVisitor(credentials); if (!visitor) { return res.status(404).json({ message: "Visitor not found" }); } else if (visitor.isAdmin) { @@ -18,7 +17,8 @@ export default async function isAdminCheck(req: Request, res: Response) { error, functionName: "isAdminCheck", message: "Error in Admin Check", - req, res + req, + res, }); } } diff --git a/server/middleware/isAdmin.ts b/server/middleware/isAdmin.ts index e2d8784..f4f61bf 100644 --- a/server/middleware/isAdmin.ts +++ b/server/middleware/isAdmin.ts @@ -1,19 +1,17 @@ -import { Credentials } from "../types/index.js"; -import { getVisitor } from "../utils/index.js"; +import { getCredentials, getVisitor } from "../utils/index.js"; import { Request, Response, NextFunction } from "express"; const checkIsAdmin = async (credentials) => { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = credentials; - const visitor = await getVisitor({ interactivePublicKey, interactiveNonce, urlSlug, visitorId }); + const visitor = await getVisitor(credentials); if (!visitor.isAdmin) { return false; } - return true -} + return true; +}; async function isAdmin(req: Request, res: Response, next: NextFunction) { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = req.query as Credentials; - const isAdmin = await checkIsAdmin({ interactivePublicKey, interactiveNonce, urlSlug, visitorId }); + const credentials = getCredentials(req.query); + const isAdmin = await checkIsAdmin(credentials); if (!isAdmin) { return res.status(401).json({ message: "Unauthorized" }); } diff --git a/server/package.json b/server/package.json index fb50706..d8efbcf 100644 --- a/server/package.json +++ b/server/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@googleapis/youtube": "^14.0.0", - "@rtsdk/topia": "^0.12.6", + "@rtsdk/topia": "^0.15.4", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.18.3", diff --git a/server/utils/droppedAssets/getDroppedAsset.ts b/server/utils/droppedAssets/getDroppedAsset.ts index 4a68212..f7e7c74 100644 --- a/server/utils/droppedAssets/getDroppedAsset.ts +++ b/server/utils/droppedAssets/getDroppedAsset.ts @@ -7,13 +7,7 @@ export const getDroppedAsset = async (credentials: Credentials) => { const { assetId, interactivePublicKey, interactiveNonce, urlSlug, visitorId } = credentials; if (!assetId || !interactivePublicKey || !interactiveNonce || !urlSlug || !visitorId) throw "Invalid credentials"; - const droppedAsset = await DroppedAsset.get(assetId, urlSlug, { - credentials: { - interactiveNonce, - interactivePublicKey, - visitorId, - }, - }); + const droppedAsset = await DroppedAsset.get(assetId, urlSlug, { credentials }); if (!droppedAsset) throw "Dropped asset not found"; diff --git a/server/utils/visitors/getVisitor.ts b/server/utils/visitors/getVisitor.ts index 4b6fb77..33fe2ae 100644 --- a/server/utils/visitors/getVisitor.ts +++ b/server/utils/visitors/getVisitor.ts @@ -1,18 +1,12 @@ -import { Visitor } from "../topiaInit.js" -import { errorHandler } from "../errorHandler.js" +import { Visitor } from "../topiaInit.js"; +import { errorHandler } from "../errorHandler.js"; import { Credentials } from "../../types/index.js"; export const getVisitor = async (credentials: Credentials) => { try { - const { interactivePublicKey, interactiveNonce, urlSlug, visitorId } = credentials; + const { urlSlug, visitorId } = credentials; - const visitor = await Visitor.get(parseInt(visitorId), urlSlug, { - credentials: { - interactiveNonce, - interactivePublicKey, - visitorId: parseInt(visitorId), - }, - }); + const visitor = await Visitor.get(visitorId, urlSlug, { credentials }); // @ts-ignore if (!visitor || !visitor.username) throw "Not in world";