From de452f401b9c0d671a92f7c153f4eea65a31a298 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 13:51:43 -0700 Subject: [PATCH 1/9] Update frontend to make use of feed's bucket for HLS stream --- ui/src/components/Player/DetectionsPlayer.tsx | 6 +++--- ui/src/components/Player/Player.tsx | 16 ++++++++++++--- ui/src/hooks/useTimestampFetcher.ts | 20 ++++++++++++------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/ui/src/components/Player/DetectionsPlayer.tsx b/ui/src/components/Player/DetectionsPlayer.tsx index f42f2a3e..a0f1127f 100644 --- a/ui/src/components/Player/DetectionsPlayer.tsx +++ b/ui/src/components/Player/DetectionsPlayer.tsx @@ -5,7 +5,7 @@ import dynamic from "next/dynamic"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Feed } from "@/graphql/generated"; -import { getHlsURI } from "@/hooks/useTimestampFetcher"; +import { getHlsURI, S3_BUCKET } from "@/hooks/useTimestampFetcher"; import { mobileOnly } from "@/styles/responsive"; import { type PlayerStatus } from "./Player"; @@ -24,7 +24,7 @@ export function DetectionsPlayer({ endOffset, onAudioPlay, }: { - feed: Pick; + feed: Pick; marks: { label: string; value: number }[]; timestamp: number; startOffset: number; @@ -38,7 +38,7 @@ export function DetectionsPlayer({ const sliderMax = endOffset - startOffset; const sliderValue = playerTime - startOffset; - const hlsURI = getHlsURI(feed.nodeName, timestamp); + const hlsURI = getHlsURI(feed.bucket || S3_BUCKET, feed.nodeName, timestamp); const playerOptions = useMemo( () => ({ diff --git a/ui/src/components/Player/Player.tsx b/ui/src/components/Player/Player.tsx index d672dbc9..f12cba5c 100644 --- a/ui/src/components/Player/Player.tsx +++ b/ui/src/components/Player/Player.tsx @@ -6,7 +6,7 @@ import Marquee from "react-fast-marquee"; import type { Feed } from "@/graphql/generated"; import useFeedPresence from "@/hooks/useFeedPresence"; -import { useTimestampFetcher } from "@/hooks/useTimestampFetcher"; +import { S3_BUCKET, useTimestampFetcher } from "@/hooks/useTimestampFetcher"; import fin512 from "@/public/photos/fin-512x512.png"; import { displayDesktopOnly, @@ -33,13 +33,23 @@ export default function Player({ }: { currentFeed?: Pick< Feed, - "id" | "slug" | "nodeName" | "name" | "latLng" | "imageUrl" | "thumbUrl" + | "id" + | "slug" + | "nodeName" + | "name" + | "latLng" + | "imageUrl" + | "thumbUrl" + | "bucket" >; }) { const [playerStatus, setPlayerStatus] = useState("idle"); const playerRef = useRef(null); - const { timestamp, hlsURI } = useTimestampFetcher(currentFeed?.nodeName); + const { timestamp, hlsURI } = useTimestampFetcher( + currentFeed?.bucket || S3_BUCKET, + currentFeed?.nodeName, + ); const feedPresence = useFeedPresence(currentFeed?.slug); const listenerCount = feedPresence?.metas.length ?? 0; diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index 2fa88106..e7304a69 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -3,11 +3,16 @@ import { useEffect, useState } from "react"; if (!process.env.NEXT_PUBLIC_S3_BUCKET) { throw new Error("NEXT_PUBLIC_S3_BUCKET is not set"); } -const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; -const S3_BUCKET_BASE = `https://s3-us-west-2.amazonaws.com/${S3_BUCKET}`; +export const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; -export const getHlsURI = (nodeName: string, timestamp: number) => - `${S3_BUCKET_BASE}/${nodeName}/hls/${timestamp}/live.m3u8`; +const bucketBaseString = (bucket: string) => + `https://s3.amazonaws.com/${bucket}`; + +export const getHlsURI = ( + bucket: string, + nodeName: string, + timestamp: number, +) => `${bucketBaseString(bucket)}/${nodeName}/hls/${timestamp}/live.m3u8`; /** * @typedef {Object} TimestampFetcherOptions @@ -29,16 +34,17 @@ export const getHlsURI = (nodeName: string, timestamp: number) => * @returns {TimestampFetcherResult} The latest timestamp, HLS URI, and AWS console URI */ export function useTimestampFetcher( + bucket: string, nodeName?: string, { onStart, onStop }: { onStart?: () => void; onStop?: () => void } = {}, ) { const [timestamp, setTimestamp] = useState(); const hlsURI = - nodeName && timestamp ? getHlsURI(nodeName, timestamp) : undefined; + nodeName && timestamp ? getHlsURI(bucket, nodeName, timestamp) : undefined; const awsConsoleUri = nodeName && timestamp - ? `https://s3.console.aws.amazon.com/s3/buckets/${S3_BUCKET}/${nodeName}/hls/${timestamp}/` + ? `https://s3.console.aws.amazon.com/s3/buckets/${bucket}/${nodeName}/hls/${timestamp}/` : undefined; useEffect(() => { @@ -46,7 +52,7 @@ export function useTimestampFetcher( let intervalId: NodeJS.Timeout | undefined; const fetchTimestamp = (feed: string) => { - const timestampURI = `${S3_BUCKET_BASE}/${feed}/latest.txt`; + const timestampURI = `${bucketBaseString(bucket)}/${feed}/latest.txt`; const xhr = new XMLHttpRequest(); currentXhr = xhr; From 117dc928809409d6ebc278701657c20d02ee69a6 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 14:50:44 -0700 Subject: [PATCH 2/9] Update Feed bucket to non-null --- server/lib/orcasite/radio/feed.ex | 2 +- ...0909214708_update_feed_bucket_non_null.exs | 21 ++ .../repo/feeds/20240909214708.json | 317 ++++++++++++++++++ ui/src/components/Player/DetectionsPlayer.tsx | 4 +- ui/src/components/Player/Player.tsx | 4 +- ui/src/graphql/generated/index.ts | 36 +- ui/src/hooks/useTimestampFetcher.ts | 12 +- 7 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 server/priv/repo/migrations/20240909214708_update_feed_bucket_non_null.exs create mode 100644 server/priv/resource_snapshots/repo/feeds/20240909214708.json diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index 5723cd5a..2fb0b0df 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -35,7 +35,7 @@ defmodule Orcasite.Radio.Feed do attribute :intro_html, :string, default: "", public?: true attribute :image_url, :string, default: "", public?: true attribute :visible, :boolean, default: true, public?: true - attribute :bucket, :string, public?: true + attribute :bucket, :string, public?: true, allow_nil?: false attribute :bucket_region, :string, public?: true attribute :cloudfront_url, :string, public?: true attribute :dataplicity_id, :string, public?: true diff --git a/server/priv/repo/migrations/20240909214708_update_feed_bucket_non_null.exs b/server/priv/repo/migrations/20240909214708_update_feed_bucket_non_null.exs new file mode 100644 index 00000000..7ed62b39 --- /dev/null +++ b/server/priv/repo/migrations/20240909214708_update_feed_bucket_non_null.exs @@ -0,0 +1,21 @@ +defmodule Orcasite.Repo.Migrations.UpdateFeedBucketNonNull do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:feeds) do + modify :bucket, :text, null: false + end + end + + def down do + alter table(:feeds) do + modify :bucket, :text, null: true + end + end +end diff --git a/server/priv/resource_snapshots/repo/feeds/20240909214708.json b/server/priv/resource_snapshots/repo/feeds/20240909214708.json new file mode 100644 index 00000000..d8086d70 --- /dev/null +++ b/server/priv/resource_snapshots/repo/feeds/20240909214708.json @@ -0,0 +1,317 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "node_name", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "slug", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "location_point", + "type": "geometry" + }, + { + "allow_nil?": true, + "default": "\"\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "intro_html", + "type": "text" + }, + { + "allow_nil?": true, + "default": "\"\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "image_url", + "type": "text" + }, + { + "allow_nil?": true, + "default": "true", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "visible", + "type": "boolean" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "bucket", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "bucket_region", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "cloudfront_url", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "dataplicity_id", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "orcahello_id", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "name" + ], + "fields": [ + { + "type": "atom", + "value": "name" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + }, + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "node_name" + ], + "fields": [ + { + "type": "atom", + "value": "node_name" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + }, + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "visible" + ], + "fields": [ + { + "type": "atom", + "value": "visible" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + }, + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "slug" + ], + "fields": [ + { + "type": "atom", + "value": "slug" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + }, + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "dataplicity_id" + ], + "fields": [ + { + "type": "atom", + "value": "dataplicity_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + }, + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "orcahello_id" + ], + "fields": [ + { + "type": "atom", + "value": "orcahello_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "7FDF1C3B2AAFBFB7E5DFBE36CD16249F0B95A8F78F8927EE095669FC0F259BB4", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "feeds_unique_slug_index", + "keys": [ + { + "type": "atom", + "value": "slug" + } + ], + "name": "unique_slug", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Orcasite.Repo", + "schema": null, + "table": "feeds" +} \ No newline at end of file diff --git a/ui/src/components/Player/DetectionsPlayer.tsx b/ui/src/components/Player/DetectionsPlayer.tsx index a0f1127f..b0a8161f 100644 --- a/ui/src/components/Player/DetectionsPlayer.tsx +++ b/ui/src/components/Player/DetectionsPlayer.tsx @@ -5,7 +5,7 @@ import dynamic from "next/dynamic"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Feed } from "@/graphql/generated"; -import { getHlsURI, S3_BUCKET } from "@/hooks/useTimestampFetcher"; +import { getHlsURI } from "@/hooks/useTimestampFetcher"; import { mobileOnly } from "@/styles/responsive"; import { type PlayerStatus } from "./Player"; @@ -38,7 +38,7 @@ export function DetectionsPlayer({ const sliderMax = endOffset - startOffset; const sliderValue = playerTime - startOffset; - const hlsURI = getHlsURI(feed.bucket || S3_BUCKET, feed.nodeName, timestamp); + const hlsURI = getHlsURI(feed.bucket, feed.nodeName, timestamp); const playerOptions = useMemo( () => ({ diff --git a/ui/src/components/Player/Player.tsx b/ui/src/components/Player/Player.tsx index f12cba5c..f9aea1eb 100644 --- a/ui/src/components/Player/Player.tsx +++ b/ui/src/components/Player/Player.tsx @@ -6,7 +6,7 @@ import Marquee from "react-fast-marquee"; import type { Feed } from "@/graphql/generated"; import useFeedPresence from "@/hooks/useFeedPresence"; -import { S3_BUCKET, useTimestampFetcher } from "@/hooks/useTimestampFetcher"; +import { useTimestampFetcher } from "@/hooks/useTimestampFetcher"; import fin512 from "@/public/photos/fin-512x512.png"; import { displayDesktopOnly, @@ -47,7 +47,7 @@ export default function Player({ const playerRef = useRef(null); const { timestamp, hlsURI } = useTimestampFetcher( - currentFeed?.bucket || S3_BUCKET, + currentFeed?.bucket, currentFeed?.nodeName, ); diff --git a/ui/src/graphql/generated/index.ts b/ui/src/graphql/generated/index.ts index 48c19287..45a230fa 100644 --- a/ui/src/graphql/generated/index.ts +++ b/ui/src/graphql/generated/index.ts @@ -438,9 +438,10 @@ export type DetectionSortInput = { export type Feed = { __typename?: "Feed"; - bucket?: Maybe; + bucket: Scalars["String"]["output"]; bucketRegion?: Maybe; cloudfrontUrl?: Maybe; + dataplicityId?: Maybe; feedSegments: Array; feedStreams: Array; id: Scalars["ID"]["output"]; @@ -452,6 +453,7 @@ export type Feed = { name: Scalars["String"]["output"]; nodeName: Scalars["String"]["output"]; online?: Maybe; + orcahelloId?: Maybe; slug: Scalars["String"]["output"]; thumbUrl?: Maybe; visible?: Maybe; @@ -476,7 +478,7 @@ export type FeedFilterBucket = { greaterThan?: InputMaybe; greaterThanOrEqual?: InputMaybe; ilike?: InputMaybe; - in?: InputMaybe>>; + in?: InputMaybe>; isNil?: InputMaybe; lessThan?: InputMaybe; lessThanOrEqual?: InputMaybe; @@ -510,6 +512,19 @@ export type FeedFilterCloudfrontUrl = { notEq?: InputMaybe; }; +export type FeedFilterDataplicityId = { + eq?: InputMaybe; + greaterThan?: InputMaybe; + greaterThanOrEqual?: InputMaybe; + ilike?: InputMaybe; + in?: InputMaybe>>; + isNil?: InputMaybe; + lessThan?: InputMaybe; + lessThanOrEqual?: InputMaybe; + like?: InputMaybe; + notEq?: InputMaybe; +}; + export type FeedFilterId = { isNil?: InputMaybe; }; @@ -532,6 +547,7 @@ export type FeedFilterInput = { bucket?: InputMaybe; bucketRegion?: InputMaybe; cloudfrontUrl?: InputMaybe; + dataplicityId?: InputMaybe; feedSegments?: InputMaybe; feedStreams?: InputMaybe; id?: InputMaybe; @@ -543,6 +559,7 @@ export type FeedFilterInput = { not?: InputMaybe>; online?: InputMaybe; or?: InputMaybe>; + orcahelloId?: InputMaybe; slug?: InputMaybe; visible?: InputMaybe; }; @@ -601,6 +618,19 @@ export type FeedFilterOnline = { notEq?: InputMaybe; }; +export type FeedFilterOrcahelloId = { + eq?: InputMaybe; + greaterThan?: InputMaybe; + greaterThanOrEqual?: InputMaybe; + ilike?: InputMaybe; + in?: InputMaybe>>; + isNil?: InputMaybe; + lessThan?: InputMaybe; + lessThanOrEqual?: InputMaybe; + like?: InputMaybe; + notEq?: InputMaybe; +}; + export type FeedFilterSlug = { eq?: InputMaybe; greaterThan?: InputMaybe; @@ -851,6 +881,7 @@ export type FeedSortField = | "BUCKET" | "BUCKET_REGION" | "CLOUDFRONT_URL" + | "DATAPLICITY_ID" | "ID" | "IMAGE_URL" | "INTRO_HTML" @@ -858,6 +889,7 @@ export type FeedSortField = | "NAME" | "NODE_NAME" | "ONLINE" + | "ORCAHELLO_ID" | "SLUG" | "VISIBLE"; diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index e7304a69..5b8eefec 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -3,10 +3,10 @@ import { useEffect, useState } from "react"; if (!process.env.NEXT_PUBLIC_S3_BUCKET) { throw new Error("NEXT_PUBLIC_S3_BUCKET is not set"); } -export const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; +const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; const bucketBaseString = (bucket: string) => - `https://s3.amazonaws.com/${bucket}`; + `https://${bucket}.s3.amazonaws.com/`; export const getHlsURI = ( bucket: string, @@ -34,14 +34,16 @@ export const getHlsURI = ( * @returns {TimestampFetcherResult} The latest timestamp, HLS URI, and AWS console URI */ export function useTimestampFetcher( - bucket: string, + bucket?: string, nodeName?: string, { onStart, onStop }: { onStart?: () => void; onStop?: () => void } = {}, ) { const [timestamp, setTimestamp] = useState(); const hlsURI = - nodeName && timestamp ? getHlsURI(bucket, nodeName, timestamp) : undefined; + nodeName && timestamp + ? getHlsURI(bucket ?? S3_BUCKET, nodeName, timestamp) + : undefined; const awsConsoleUri = nodeName && timestamp ? `https://s3.console.aws.amazon.com/s3/buckets/${bucket}/${nodeName}/hls/${timestamp}/` @@ -52,7 +54,7 @@ export function useTimestampFetcher( let intervalId: NodeJS.Timeout | undefined; const fetchTimestamp = (feed: string) => { - const timestampURI = `${bucketBaseString(bucket)}/${feed}/latest.txt`; + const timestampURI = `${bucketBaseString(bucket ?? S3_BUCKET)}/${feed}/latest.txt`; const xhr = new XMLHttpRequest(); currentXhr = xhr; From 9d7b2d5fa944ebd000507650a2668d7efd403bad Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 14:54:07 -0700 Subject: [PATCH 3/9] Fix double slash in bucketBase --- ui/src/hooks/useTimestampFetcher.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index 5b8eefec..96c96420 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -5,14 +5,13 @@ if (!process.env.NEXT_PUBLIC_S3_BUCKET) { } const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; -const bucketBaseString = (bucket: string) => - `https://${bucket}.s3.amazonaws.com/`; +const bucketBase = (bucket: string) => `https://${bucket}.s3.amazonaws.com`; export const getHlsURI = ( bucket: string, nodeName: string, timestamp: number, -) => `${bucketBaseString(bucket)}/${nodeName}/hls/${timestamp}/live.m3u8`; +) => `${bucketBase(bucket)}/${nodeName}/hls/${timestamp}/live.m3u8`; /** * @typedef {Object} TimestampFetcherOptions @@ -29,6 +28,7 @@ export const getHlsURI = ( /** * Starts a timer that fetches the latest timestamp from a feed + * @param {string} bucket The bucket name of the node * @param {string} nodeName The name of the feed to fetch from, as defined in the S3 bucket * @param {TimestampFetcherOptions} options Callbacks for when the fetcher starts and stops * @returns {TimestampFetcherResult} The latest timestamp, HLS URI, and AWS console URI @@ -54,7 +54,7 @@ export function useTimestampFetcher( let intervalId: NodeJS.Timeout | undefined; const fetchTimestamp = (feed: string) => { - const timestampURI = `${bucketBaseString(bucket ?? S3_BUCKET)}/${feed}/latest.txt`; + const timestampURI = `${bucketBase(bucket ?? S3_BUCKET)}/${feed}/latest.txt`; const xhr = new XMLHttpRequest(); currentXhr = xhr; From b0c7c65020f44d08b64952699b72e4a800bf8922 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 15:12:13 -0700 Subject: [PATCH 4/9] Add missing type param for selecting bucket field in detections table --- ui/src/components/DetectionsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/DetectionsTable.tsx b/ui/src/components/DetectionsTable.tsx index 1637497a..8ba673ed 100644 --- a/ui/src/components/DetectionsTable.tsx +++ b/ui/src/components/DetectionsTable.tsx @@ -42,7 +42,7 @@ export default function DetectionsTable({ onDetectionUpdate, }: { detections: Detection[]; - feed: Pick; + feed: Pick; candidate: Pick; onDetectionUpdate: () => void; }) { From 6d5a835b6bd9a91ed30b855e4e7ec997a8809f7b Mon Sep 17 00:00:00 2001 From: Paul Cretu Date: Mon, 9 Sep 2024 22:43:23 +0000 Subject: [PATCH 5/9] Fix type & lint errors and refactor --- ui/src/components/layouts/MapLayout.tsx | 2 ++ ui/src/graphql/generated/index.ts | 4 ++++ ui/src/graphql/queries/getCandidate.graphql | 1 + ui/src/graphql/queries/getFeed.graphql | 1 + ui/src/hooks/useTimestampFetcher.ts | 18 +++++++++--------- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ui/src/components/layouts/MapLayout.tsx b/ui/src/components/layouts/MapLayout.tsx index 759d4d21..76493db2 100644 --- a/ui/src/components/layouts/MapLayout.tsx +++ b/ui/src/components/layouts/MapLayout.tsx @@ -22,6 +22,8 @@ const feedFromSlug = (feedSlug: string) => ({ name: feedSlug, slug: feedSlug, nodeName: feedSlug, + // TODO: pass in bucket from dynamic feed instead of env/hardcoding + bucket: process.env.NEXT_PUBLIC_S3_BUCKET ?? "audio-orcasound-net", // TODO: figure out which coordinates to use for dynamic feeds latLng: { lat: 47.6, lng: -122.3 }, }); diff --git a/ui/src/graphql/generated/index.ts b/ui/src/graphql/generated/index.ts index 45a230fa..3b5862df 100644 --- a/ui/src/graphql/generated/index.ts +++ b/ui/src/graphql/generated/index.ts @@ -1908,6 +1908,7 @@ export type CandidateQuery = { slug: string; name: string; nodeName: string; + bucket: string; }; detections: Array<{ __typename?: "Detection"; @@ -1955,6 +1956,7 @@ export type FeedQuery = { thumbUrl?: string | null; imageUrl?: string | null; mapUrl?: string | null; + bucket: string; latLng: { __typename?: "LatLng"; lat: number; lng: number }; }; }; @@ -2623,6 +2625,7 @@ export const CandidateDocument = ` slug name nodeName + bucket } detections { id @@ -2740,6 +2743,7 @@ export const FeedDocument = ` thumbUrl imageUrl mapUrl + bucket } } `; diff --git a/ui/src/graphql/queries/getCandidate.graphql b/ui/src/graphql/queries/getCandidate.graphql index 53772904..c8e70924 100644 --- a/ui/src/graphql/queries/getCandidate.graphql +++ b/ui/src/graphql/queries/getCandidate.graphql @@ -11,6 +11,7 @@ query candidate($id: ID!) { slug name nodeName + bucket } detections { id diff --git a/ui/src/graphql/queries/getFeed.graphql b/ui/src/graphql/queries/getFeed.graphql index 7bb68bcd..93a9d36a 100644 --- a/ui/src/graphql/queries/getFeed.graphql +++ b/ui/src/graphql/queries/getFeed.graphql @@ -12,5 +12,6 @@ query feed($slug: String!) { thumbUrl imageUrl mapUrl + bucket } } diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index 96c96420..f2d128c1 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -3,15 +3,17 @@ import { useEffect, useState } from "react"; if (!process.env.NEXT_PUBLIC_S3_BUCKET) { throw new Error("NEXT_PUBLIC_S3_BUCKET is not set"); } -const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; -const bucketBase = (bucket: string) => `https://${bucket}.s3.amazonaws.com`; +const getBucketBase = (bucket: string) => `https://${bucket}.s3.amazonaws.com`; + +const getTimestampURI = (bucket: string, nodeName: string) => + `${getBucketBase(bucket)}/${nodeName}/latest.txt`; export const getHlsURI = ( bucket: string, nodeName: string, timestamp: number, -) => `${bucketBase(bucket)}/${nodeName}/hls/${timestamp}/live.m3u8`; +) => `${getBucketBase(bucket)}/${nodeName}/hls/${timestamp}/live.m3u8`; /** * @typedef {Object} TimestampFetcherOptions @@ -34,16 +36,14 @@ export const getHlsURI = ( * @returns {TimestampFetcherResult} The latest timestamp, HLS URI, and AWS console URI */ export function useTimestampFetcher( - bucket?: string, + bucket: string, nodeName?: string, { onStart, onStop }: { onStart?: () => void; onStop?: () => void } = {}, ) { const [timestamp, setTimestamp] = useState(); const hlsURI = - nodeName && timestamp - ? getHlsURI(bucket ?? S3_BUCKET, nodeName, timestamp) - : undefined; + nodeName && timestamp ? getHlsURI(bucket, nodeName, timestamp) : undefined; const awsConsoleUri = nodeName && timestamp ? `https://s3.console.aws.amazon.com/s3/buckets/${bucket}/${nodeName}/hls/${timestamp}/` @@ -54,7 +54,7 @@ export function useTimestampFetcher( let intervalId: NodeJS.Timeout | undefined; const fetchTimestamp = (feed: string) => { - const timestampURI = `${bucketBase(bucket ?? S3_BUCKET)}/${feed}/latest.txt`; + const timestampURI = getTimestampURI(bucket, feed); const xhr = new XMLHttpRequest(); currentXhr = xhr; @@ -90,7 +90,7 @@ export function useTimestampFetcher( stopFetcher(); onStop?.(); }; - }, [nodeName, onStart, onStop]); + }, [nodeName, onStart, onStop, bucket]); return { timestamp, hlsURI, awsConsoleUri }; } From db4fab7eabff96bb154dc8513634b4b208222138 Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 16:22:11 -0700 Subject: [PATCH 6/9] Fix optional bucket name in useTimestampFetcher --- ui/src/hooks/useTimestampFetcher.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index f2d128c1..0f9df248 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -4,6 +4,8 @@ if (!process.env.NEXT_PUBLIC_S3_BUCKET) { throw new Error("NEXT_PUBLIC_S3_BUCKET is not set"); } +const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; + const getBucketBase = (bucket: string) => `https://${bucket}.s3.amazonaws.com`; const getTimestampURI = (bucket: string, nodeName: string) => @@ -36,14 +38,16 @@ export const getHlsURI = ( * @returns {TimestampFetcherResult} The latest timestamp, HLS URI, and AWS console URI */ export function useTimestampFetcher( - bucket: string, + bucket?: string, nodeName?: string, { onStart, onStop }: { onStart?: () => void; onStop?: () => void } = {}, ) { const [timestamp, setTimestamp] = useState(); const hlsURI = - nodeName && timestamp ? getHlsURI(bucket, nodeName, timestamp) : undefined; + nodeName && timestamp + ? getHlsURI(bucket ?? S3_BUCKET, nodeName, timestamp) + : undefined; const awsConsoleUri = nodeName && timestamp ? `https://s3.console.aws.amazon.com/s3/buckets/${bucket}/${nodeName}/hls/${timestamp}/` @@ -54,7 +58,7 @@ export function useTimestampFetcher( let intervalId: NodeJS.Timeout | undefined; const fetchTimestamp = (feed: string) => { - const timestampURI = getTimestampURI(bucket, feed); + const timestampURI = getTimestampURI(bucket ?? S3_BUCKET, feed); const xhr = new XMLHttpRequest(); currentXhr = xhr; From c2e9445ab873ef68e0a2b7c8dc91f5f4c6e730de Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 16:35:27 -0700 Subject: [PATCH 7/9] Fix elixir test with missing bucket in feeds --- server/test/orcasite_web/graphql/moderator_test.exs | 1 + server/test/orcasite_web/graphql/radio_test.exs | 1 + 2 files changed, 2 insertions(+) diff --git a/server/test/orcasite_web/graphql/moderator_test.exs b/server/test/orcasite_web/graphql/moderator_test.exs index 642d3a86..7bf17c0f 100644 --- a/server/test/orcasite_web/graphql/moderator_test.exs +++ b/server/test/orcasite_web/graphql/moderator_test.exs @@ -29,6 +29,7 @@ defmodule OrcasiteWeb.ModeratorTest do lat_lng_string: "48.5583362, -123.1735774", name: "Orcasound Lab (Haro Strait)", node_name: "rpi_orcasound_lab", + bucket: "streaming-orcasound-net", slug: "orcasound-lab" } ) diff --git a/server/test/orcasite_web/graphql/radio_test.exs b/server/test/orcasite_web/graphql/radio_test.exs index d940405b..603cd7ed 100644 --- a/server/test/orcasite_web/graphql/radio_test.exs +++ b/server/test/orcasite_web/graphql/radio_test.exs @@ -10,6 +10,7 @@ defmodule OrcasiteWeb.RadioTest do lat_lng_string: "48.5583362, -123.1735774", name: "Orcasound Lab (Haro Strait)", node_name: "rpi_orcasound_lab", + bucket: "streaming-orcasound-net", slug: "orcasound-lab" } ) From aa017a70bbb2274be5127313112e54450e4dc92c Mon Sep 17 00:00:00 2001 From: Paul Cretu Date: Mon, 9 Sep 2024 23:53:45 +0000 Subject: [PATCH 8/9] Refactor to remove bucket from env --- ui/src/hooks/useTimestampFetcher.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index 0f9df248..d4e54c92 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -4,8 +4,6 @@ if (!process.env.NEXT_PUBLIC_S3_BUCKET) { throw new Error("NEXT_PUBLIC_S3_BUCKET is not set"); } -const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET; - const getBucketBase = (bucket: string) => `https://${bucket}.s3.amazonaws.com`; const getTimestampURI = (bucket: string, nodeName: string) => @@ -45,11 +43,11 @@ export function useTimestampFetcher( const [timestamp, setTimestamp] = useState(); const hlsURI = - nodeName && timestamp - ? getHlsURI(bucket ?? S3_BUCKET, nodeName, timestamp) + nodeName && bucket && timestamp + ? getHlsURI(bucket, nodeName, timestamp) : undefined; const awsConsoleUri = - nodeName && timestamp + nodeName && bucket && timestamp ? `https://s3.console.aws.amazon.com/s3/buckets/${bucket}/${nodeName}/hls/${timestamp}/` : undefined; @@ -57,9 +55,7 @@ export function useTimestampFetcher( let currentXhr: XMLHttpRequest | undefined; let intervalId: NodeJS.Timeout | undefined; - const fetchTimestamp = (feed: string) => { - const timestampURI = getTimestampURI(bucket ?? S3_BUCKET, feed); - + const fetchTimestamp = (timestampURI: string) => { const xhr = new XMLHttpRequest(); currentXhr = xhr; xhr.open("GET", timestampURI); @@ -75,11 +71,15 @@ export function useTimestampFetcher( }; const startFetcher = () => { - if (!nodeName) return; + if (!nodeName || !bucket) return; + const timestampURI = getTimestampURI(bucket, nodeName); onStart?.(); - fetchTimestamp(nodeName); - const newIntervalId = setInterval(() => fetchTimestamp(nodeName), 10000); + fetchTimestamp(timestampURI); + const newIntervalId = setInterval( + () => fetchTimestamp(timestampURI), + 10000, + ); intervalId = newIntervalId; }; From 1d6de969e00e0370720e15b067af46b833c8655c Mon Sep 17 00:00:00 2001 From: Paul Cretu Date: Mon, 9 Sep 2024 23:57:31 +0000 Subject: [PATCH 9/9] Caps for consistency --- ui/src/hooks/useTimestampFetcher.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/hooks/useTimestampFetcher.ts b/ui/src/hooks/useTimestampFetcher.ts index d4e54c92..f66be190 100644 --- a/ui/src/hooks/useTimestampFetcher.ts +++ b/ui/src/hooks/useTimestampFetcher.ts @@ -25,7 +25,7 @@ export const getHlsURI = ( * @typedef {Object} TimestampFetcherResult * @property {number} timestamp The latest timestamp * @property {string} hlsURI The URI to the latest HLS stream - * @property {string} awsConsoleUri The URI to the AWS console for the latest HLS stream + * @property {string} awsConsoleURI The URI to the AWS console for the latest HLS stream */ /** @@ -46,7 +46,7 @@ export function useTimestampFetcher( nodeName && bucket && timestamp ? getHlsURI(bucket, nodeName, timestamp) : undefined; - const awsConsoleUri = + const awsConsoleURI = nodeName && bucket && timestamp ? `https://s3.console.aws.amazon.com/s3/buckets/${bucket}/${nodeName}/hls/${timestamp}/` : undefined; @@ -96,5 +96,5 @@ export function useTimestampFetcher( }; }, [nodeName, onStart, onStop, bucket]); - return { timestamp, hlsURI, awsConsoleUri }; + return { timestamp, hlsURI, awsConsoleURI }; }