From ca8499bf5d12801335b3ef785124299b6da07cdd Mon Sep 17 00:00:00 2001 From: Skander Mzali Date: Mon, 9 Sep 2024 09:38:13 -0700 Subject: [PATCH] Create a single audio_image per feed_segment within a given range for a feed --- server/config/config.exs | 1 + server/lib/orcasite/radio/audio_image.ex | 16 +- server/lib/orcasite/radio/aws_client.ex | 2 +- server/lib/orcasite/radio/feed.ex | 53 ++- server/lib/orcasite/radio/feed_segment.ex | 22 ++ server/lib/orcasite/validations/compare.ex | 21 ++ ...190249_add_unique_index_to_audio_image.exs | 21 ++ .../repo/audio_images/20240831190249.json | 338 ++++++++++++++++++ 8 files changed, 458 insertions(+), 16 deletions(-) create mode 100644 server/lib/orcasite/validations/compare.ex create mode 100644 server/priv/repo/migrations/20240831190249_add_unique_index_to_audio_image.exs create mode 100644 server/priv/resource_snapshots/repo/audio_images/20240831190249.json diff --git a/server/config/config.exs b/server/config/config.exs index 8f451f84..0d103a73 100644 --- a/server/config/config.exs +++ b/server/config/config.exs @@ -102,6 +102,7 @@ config :spark, :formatter, :identities, :attributes, :calculations, + :aggregates, :relationships, :authentication, :token, diff --git a/server/lib/orcasite/radio/audio_image.ex b/server/lib/orcasite/radio/audio_image.ex index 6ebeccea..4343c5a9 100644 --- a/server/lib/orcasite/radio/audio_image.ex +++ b/server/lib/orcasite/radio/audio_image.ex @@ -1,9 +1,4 @@ defmodule Orcasite.Radio.AudioImage do - require Ash.Resource.Change.Builtins - require Ash.Resource.Change.Builtins - require Ash.Resource.Change.Builtins - require Ash.Resource.Change.Builtins - use Ash.Resource, otp_app: :orcasite, domain: Orcasite.Radio, @@ -24,6 +19,10 @@ defmodule Orcasite.Radio.AudioImage do end end + identities do + identity :unique_audio_image, [:feed_id, :image_type, :start_time, :end_time] + end + attributes do uuid_primary_key :id attribute :image_type, Orcasite.Types.ImageType, public?: true @@ -80,7 +79,7 @@ defmodule Orcasite.Radio.AudioImage do defaults [:read, :destroy, create: :*, update: :*] create :for_feed_segment do - argument :feed_segment_id, :uuid, allow_nil?: false + argument :feed_segment_id, :string, allow_nil?: false argument :image_type, Orcasite.Types.ImageType do default :spectrogram @@ -152,6 +151,7 @@ defmodule Orcasite.Radio.AudioImage do image_key: image.object_path } |> Orcasite.Radio.AwsClient.generate_spectrogram() + |> IO.inspect(label: "gen spect result") |> case do {:ok, %{"errorMessage" => _} = error} -> image @@ -161,7 +161,7 @@ defmodule Orcasite.Radio.AudioImage do |> Ash.Changeset.force_change_attribute(:last_error, inspect(error)) |> Ash.update(authorize?: false) - {:error, :spectrogram_failed} + # {:error, :spectrogram_failed} {:ok, %{image_size: image_size, sample_rate: _sample_rate}} -> image @@ -182,7 +182,7 @@ defmodule Orcasite.Radio.AudioImage do |> Ash.Changeset.force_change_attribute(:last_error, inspect(error)) |> Ash.update(authorize?: false) - error + # error end end, prepend?: true diff --git a/server/lib/orcasite/radio/aws_client.ex b/server/lib/orcasite/radio/aws_client.ex index dd2d9b54..14f94f98 100644 --- a/server/lib/orcasite/radio/aws_client.ex +++ b/server/lib/orcasite/radio/aws_client.ex @@ -21,7 +21,7 @@ defmodule Orcasite.Radio.AwsClient do |> elem(1) |> String.to_integer() - {:ok, %{size: size}} + {:ok, %{file_size: size}} _ -> # Doesn't exist, make spectrogram diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index fa9c7808..83cca49d 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -1,4 +1,6 @@ defmodule Orcasite.Radio.Feed do + require Ash.Resource.Change.Builtins + use Ash.Resource, domain: Orcasite.Radio, extensions: [AshAdmin.Resource, AshUUID, AshGraphql.Resource, AshJsonApi.Resource], @@ -68,7 +70,7 @@ defmodule Orcasite.Radio.Feed do {Orcasite.Radio.Calculations.FeedImageUrl, object: "map.png"}, public?: true - # calculate :uuid, :string, Orcasite.Radio.Calculations.DecodeUUID + calculate :uuid, :string, Orcasite.Radio.Calculations.DecodeUUID end aggregates do @@ -78,14 +80,18 @@ defmodule Orcasite.Radio.Feed do end end - relationships do has_many :feed_streams, Orcasite.Radio.FeedStream do public? true end + has_many :feed_segments, Orcasite.Radio.FeedSegment do public? true end + + has_many :audio_images, Orcasite.Radio.AudioImage do + public? true + end end policies do @@ -167,6 +173,44 @@ defmodule Orcasite.Radio.Feed do change &change_lat_lng/2 end + + update :generate_spectrogram do + require_atomic? false + argument :start_time, :utc_datetime_usec, allow_nil?: false + argument :end_time, :utc_datetime_usec, allow_nil?: false + + validate {Orcasite.Validations.Compare, [lt: [:start_time, :end_time]]} + + change before_action(fn change, _context -> + # Get feed_segments between the start time and end time, and, for now + # create a single spectrogram per segment. Once we can create an + # audio image for multiple segments (with concatenation in the lambda), + # we can change this to a single spectrogram with all those audio segments + + feed_segments = + Orcasite.Radio.FeedSegment + |> Ash.Query.for_read(:for_feed_range, %{ + feed_id: change.data.id, + start_time: change.arguments.start_time, + end_time: change.arguments.end_time + }) + |> Ash.read!(authorize?: false) + + audio_image_inputs = feed_segments |> Enum.map(&%{feed_segment_id: &1.id}) + + change + |> Ash.Changeset.manage_relationship(:audio_images, audio_image_inputs, + on_no_match: {:create, :for_feed_segment}, + on_match: :ignore + ) + |> IO.inspect(label: "new change") + end) + end + end + + code_interface do + define :get_feed_by_slug, action: :get_by_slug, args: [:slug], get?: true + define :get_feed_by_node_name, action: :get_by_node_name, args: [:node_name], get?: true end admin do @@ -179,11 +223,6 @@ defmodule Orcasite.Radio.Feed do end end - code_interface do - define :get_feed_by_slug, action: :get_by_slug, args: [:slug], get?: true - define :get_feed_by_node_name, action: :get_by_node_name, args: [:node_name], get?: true - end - json_api do type "feed" diff --git a/server/lib/orcasite/radio/feed_segment.ex b/server/lib/orcasite/radio/feed_segment.ex index 36748da1..8e2b4e92 100644 --- a/server/lib/orcasite/radio/feed_segment.ex +++ b/server/lib/orcasite/radio/feed_segment.ex @@ -104,6 +104,28 @@ defmodule Orcasite.Radio.FeedSegment do ) end + read :for_feed_range do + argument :start_time, :utc_datetime_usec, allow_nil?: false + argument :end_time, :utc_datetime_usec, allow_nil?: false + argument :feed_id, :string, allow_nil?: false + + filter expr( + feed_id == ^arg(:feed_id) and + (fragment( + "(?) between (?) and (?)", + start_time, + ^arg(:start_time), + ^arg(:end_time) + ) or + fragment( + "(?) between (?) and (?)", + end_time, + ^arg(:start_time), + ^arg(:end_time) + )) + ) + end + create :create do primary? true upsert? true diff --git a/server/lib/orcasite/validations/compare.ex b/server/lib/orcasite/validations/compare.ex new file mode 100644 index 00000000..6e36ce91 --- /dev/null +++ b/server/lib/orcasite/validations/compare.ex @@ -0,0 +1,21 @@ +defmodule Orcasite.Validations.Compare do + use Ash.Resource.Validation + + def validate(changeset, [{op, [arg_key_1, arg_key_2]}], _context) do + arg_1 = changeset |> Ash.Changeset.get_argument(arg_key_1) + arg_2 = changeset |> Ash.Changeset.get_argument(arg_key_2) + + case {compare(arg_1, arg_2), op} do + {:eq, :gte} -> :ok + {:gt, :gte} -> :ok + {:lt, :lte} -> :ok + {:lt, :lt} -> :ok + {:eq, :eq} -> :ok + _ -> {:error, "#{arg_key_1} (#{arg_1}) is not #{op} #{arg_key_2} (#{arg_2})"} + end + end + + def compare(%DateTime{} = datetime_1, %DateTime{} = datetime_2) do + DateTime.compare(datetime_1, datetime_2) + end +end diff --git a/server/priv/repo/migrations/20240831190249_add_unique_index_to_audio_image.exs b/server/priv/repo/migrations/20240831190249_add_unique_index_to_audio_image.exs new file mode 100644 index 00000000..3b09dda7 --- /dev/null +++ b/server/priv/repo/migrations/20240831190249_add_unique_index_to_audio_image.exs @@ -0,0 +1,21 @@ +defmodule Orcasite.Repo.Migrations.AddUniqueIndexToAudioImage 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 + create unique_index(:audio_images, [:feed_id, :image_type, :start_time, :end_time], + name: "audio_images_unique_audio_image_index" + ) + end + + def down do + drop_if_exists unique_index(:audio_images, [:feed_id, :image_type, :start_time, :end_time], + name: "audio_images_unique_audio_image_index" + ) + end +end diff --git a/server/priv/resource_snapshots/repo/audio_images/20240831190249.json b/server/priv/resource_snapshots/repo/audio_images/20240831190249.json new file mode 100644 index 00000000..4f951823 --- /dev/null +++ b/server/priv/resource_snapshots/repo/audio_images/20240831190249.json @@ -0,0 +1,338 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "image_type", + "type": "text" + }, + { + "allow_nil?": false, + "default": "\"new\"", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "status", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "start_time", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "end_time", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "%{}", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "parameters", + "type": "map" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "image_size", + "type": "bigint" + }, + { + "allow_nil?": true, + "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": "object_path", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "last_error", + "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" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "audio_images_feed_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": null, + "table": "feeds" + }, + "size": null, + "source": "feed_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": false, + "concurrently": false, + "error_fields": [ + "start_time" + ], + "fields": [ + { + "type": "atom", + "value": "start_time" + } + ], + "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": [ + "end_time" + ], + "fields": [ + { + "type": "atom", + "value": "end_time" + } + ], + "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": [ + "feed_id" + ], + "fields": [ + { + "type": "atom", + "value": "feed_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": [ + "bucket" + ], + "fields": [ + { + "type": "atom", + "value": "bucket" + } + ], + "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": [ + "image_type" + ], + "fields": [ + { + "type": "atom", + "value": "image_type" + } + ], + "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": [ + "status" + ], + "fields": [ + { + "type": "atom", + "value": "status" + } + ], + "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": "5CB61EBC659C4653FE3AC17730B22034A3A12404A0C179E0A1D9E8FD2B9D3D30", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "audio_images_unique_audio_image_index", + "keys": [ + { + "type": "atom", + "value": "feed_id" + }, + { + "type": "atom", + "value": "image_type" + }, + { + "type": "atom", + "value": "start_time" + }, + { + "type": "atom", + "value": "end_time" + } + ], + "name": "unique_audio_image", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Orcasite.Repo", + "schema": null, + "table": "audio_images" +} \ No newline at end of file