From cb608062c3d66f072d011a0c2c4043dff0eeaa79 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 6 Nov 2025 09:54:22 -0700 Subject: [PATCH 1/6] fix: add movieId to Radarr RenameFiles command payload The Radarr RenameFiles API command requires both movieId and files parameters in the JSON payload. Previously only files was sent, causing the rename operation to fail. This matches the Sonarr implementation which correctly includes seriesId. Verified against Radarr source code (RenameFilesCommand class). --- lib/reencodarr/services/radarr.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/reencodarr/services/radarr.ex b/lib/reencodarr/services/radarr.ex index 922ff877..cb69e1f1 100644 --- a/lib/reencodarr/services/radarr.ex +++ b/lib/reencodarr/services/radarr.ex @@ -122,6 +122,7 @@ defmodule Reencodarr.Services.Radarr do json_payload = %{ name: "RenameFiles", + movieId: movie_id, files: renameable_file_ids } From d9c23ccec7be1dc447a1daae9c87af3787fac6af Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 6 Nov 2025 10:01:42 -0700 Subject: [PATCH 2/6] fix: prevent GenStage buffer overflow in Broadway producers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed 'GenStage producer has discarded events from buffer' warnings by correcting the producer pattern in all three Broadway pipelines. **Root Cause:** Producers were emitting events in handle_info(:poll) without demand from downstream consumers. With max_demand=1 and concurrency=1, when a processor is busy (e.g., encoding in progress), it can't accept new events. The periodic :poll kept pushing events anyway, causing GenStage to discard them. **Solution:** Producers now only emit events in response to demand via handle_demand. The :poll handler only schedules the next poll - it doesn't push events. This follows the correct GenStage pattern: - Demand arrives → check for work → return events - Periodic poll → just reschedule → return empty list **Files Changed:** - lib/reencodarr/encoder/broadway/producer.ex - lib/reencodarr/crf_searcher/broadway/producer.ex - lib/reencodarr/analyzer/broadway/producer.ex All tests pass. --- lib/reencodarr/analyzer/broadway/producer.ex | 15 ++++----------- lib/reencodarr/crf_searcher/broadway/producer.ex | 12 +++--------- lib/reencodarr/encoder/broadway/producer.ex | 13 +++---------- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/lib/reencodarr/analyzer/broadway/producer.ex b/lib/reencodarr/analyzer/broadway/producer.ex index b75ed985..f8e90074 100644 --- a/lib/reencodarr/analyzer/broadway/producer.ex +++ b/lib/reencodarr/analyzer/broadway/producer.ex @@ -40,17 +40,10 @@ defmodule Reencodarr.Analyzer.Broadway.Producer do @impl GenStage def handle_info(:poll, state) do schedule_poll() - # Check if there's work and manually ask Broadway to pull - case Media.count_videos_needing_analysis() do - 0 -> - {:noreply, [], state} - - _count -> - # There's work available - return one video to wake up Broadway - videos = Media.get_videos_needing_analysis(1) - Logger.debug("Analyzer: poll wakeup -> #{length(videos)} videos") - {:noreply, videos, state} - end + # Just schedule the next poll - don't push events without demand + # Events are only emitted in response to demand via handle_demand + # This prevents buffer overflow warnings from GenStage + {:noreply, [], state} end @impl GenStage diff --git a/lib/reencodarr/crf_searcher/broadway/producer.ex b/lib/reencodarr/crf_searcher/broadway/producer.ex index 844e381f..37f1ce0c 100644 --- a/lib/reencodarr/crf_searcher/broadway/producer.ex +++ b/lib/reencodarr/crf_searcher/broadway/producer.ex @@ -29,15 +29,9 @@ defmodule Reencodarr.CrfSearcher.Broadway.Producer do @impl GenStage def handle_info(:poll, state) do schedule_poll() - # Check if there's work and CrfSearch is available, wake up Broadway if so - if CrfSearch.available?() do - case Media.get_videos_for_crf_search(1) do - [] -> {:noreply, [], state} - videos -> {:noreply, videos, state} - end - else - {:noreply, [], state} - end + # Just schedule the next poll - don't push events without demand + # Events are only emitted in response to demand via handle_demand + {:noreply, [], state} end @impl GenStage diff --git a/lib/reencodarr/encoder/broadway/producer.ex b/lib/reencodarr/encoder/broadway/producer.ex index 40e4ddad..bbb6ee76 100644 --- a/lib/reencodarr/encoder/broadway/producer.ex +++ b/lib/reencodarr/encoder/broadway/producer.ex @@ -35,16 +35,9 @@ defmodule Reencodarr.Encoder.Broadway.Producer do @impl GenStage def handle_info(:poll, state) do schedule_poll() - # Check if there's work and Encode is available, wake up Broadway if so - if Encode.available?() do - case Media.get_next_for_encoding(1) do - %Reencodarr.Media.Vmaf{} = vmaf -> {:noreply, [vmaf], state} - [%Reencodarr.Media.Vmaf{} = vmaf] -> {:noreply, [vmaf], state} - _ -> {:noreply, [], state} - end - else - {:noreply, [], state} - end + # Just schedule the next poll - don't push events without demand + # Events are only emitted in response to demand via handle_demand + {:noreply, [], state} end @impl GenStage From 92535cb5adc766dbe553c4f453176d1f5b0685f6 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 6 Nov 2025 10:06:39 -0700 Subject: [PATCH 3/6] fix: transition video to :encoding state when encoding starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Videos were showing in 'next in queue' even when currently being encoded because the encoder wasn't transitioning videos to :encoding state. **Root Cause:** The encoder Broadway pipeline was broadcasting :encoding_started events but never actually transitioning the video's state from :crf_searched to :encoding. This caused the video to remain in the encoding queue query results. **Solution:** Added state transition to :encoding when process_vmaf_encoding starts, matching the pattern used in CRF searcher which transitions to :crf_searching. **Impact:** - Encoding queue now correctly excludes currently encoding videos - 'Next in queue' shows actual next video, not current one - State machine flow is complete: crf_searched → encoding → encoded This fixes the issue where the dashboard showed currently processing items as 'next in queue' for the encoder. Also added VideoStateMachine and Repo aliases to satisfy credo. --- lib/reencodarr/encoder/broadway.ex | 21 +++++++++++++++++++++ lib/reencodarr/media/video_queries.ex | 2 ++ 2 files changed, 23 insertions(+) diff --git a/lib/reencodarr/encoder/broadway.ex b/lib/reencodarr/encoder/broadway.ex index cf59dd39..c418c3ab 100644 --- a/lib/reencodarr/encoder/broadway.ex +++ b/lib/reencodarr/encoder/broadway.ex @@ -20,7 +20,9 @@ defmodule Reencodarr.Encoder.Broadway do alias Reencodarr.AbAv1.ProgressParser alias Reencodarr.Dashboard.Events alias Reencodarr.Encoder.Broadway.Producer + alias Reencodarr.Media.VideoStateMachine alias Reencodarr.PostProcessor + alias Reencodarr.Repo @typedoc "VMAF struct for encoding processing" @type vmaf :: %{id: integer(), video: map()} @@ -169,6 +171,25 @@ defmodule Reencodarr.Encoder.Broadway do defp process_vmaf_encoding(vmaf, context) do Logger.info("Broadway: Starting encoding for VMAF #{vmaf.id}: #{vmaf.video.path}") + # Mark video as encoding NOW that we're actually starting + case VideoStateMachine.transition_to_encoding(vmaf.video) do + {:ok, changeset} -> + case Repo.update(changeset) do + {:ok, _updated_video} -> + :ok + + {:error, reason} -> + Logger.warning( + "Failed to mark video #{vmaf.video.id} as encoding: #{inspect(reason)}" + ) + end + + {:error, reason} -> + Logger.warning( + "Failed to transition video #{vmaf.video.id} to encoding: #{inspect(reason)}" + ) + end + # Broadcast initial encoding progress at 0% Events.broadcast_event(:encoding_started, %{ video_id: vmaf.video.id, diff --git a/lib/reencodarr/media/video_queries.ex b/lib/reencodarr/media/video_queries.ex index e708a164..411234e5 100644 --- a/lib/reencodarr/media/video_queries.ex +++ b/lib/reencodarr/media/video_queries.ex @@ -11,6 +11,7 @@ defmodule Reencodarr.Media.VideoQueries do @doc """ Gets videos ready for CRF search (state: analyzed). + Excludes videos already in crf_searching state to avoid showing currently processing videos. """ @spec videos_for_crf_search(integer()) :: [Video.t()] def videos_for_crf_search(limit \\ 10) do @@ -37,6 +38,7 @@ defmodule Reencodarr.Media.VideoQueries do @doc """ Counts the total number of videos ready for CRF search. + Excludes videos already in crf_searching state. """ @spec count_videos_for_crf_search() :: integer() def count_videos_for_crf_search do From c47f4bebbc748c1abef310aa65a258832abca70b Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 6 Nov 2025 10:20:36 -0700 Subject: [PATCH 4/6] chore: update dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated 17 packages: - credo 1.7.12 → 1.7.13 - dialyxir 1.4.6 → 1.4.7 - ecto 3.13.2 → 3.13.4 - ecto_sqlite3 0.21.0 → 0.22.0 - erlex 0.2.7 → 0.2.8 - expo 1.1.0 → 1.1.1 - exqlite 0.33.0 → 0.33.1 - gettext 1.0.0 → 1.0.1 - lazy_html 0.1.7 → 0.1.8 - meck 1.0.0 → 1.1.0 - phoenix_html 4.2.1 → 4.3.0 - phoenix_live_view 1.1.11 → 1.1.17 - phoenix_pubsub 2.1.3 → 2.2.0 - swoosh 1.19.5 → 1.19.8 - tailwind 0.4.0 → 0.4.1 - thousand_island 1.4.1 → 1.4.2 - websock_adapter 0.5.8 → 0.5.9 --- lib/reencodarr/sync.ex | 3 +- mix.lock | 34 ++++++++++----------- test/reencodarr/media/video_upsert_test.exs | 3 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/reencodarr/sync.ex b/lib/reencodarr/sync.ex index 2677d69e..24a3e4ce 100644 --- a/lib/reencodarr/sync.ex +++ b/lib/reencodarr/sync.ex @@ -6,9 +6,10 @@ defmodule Reencodarr.Sync do alias Reencodarr.Analyzer.Broadway, as: AnalyzerBroadway alias Reencodarr.Core.Parsers alias Reencodarr.Dashboard.Events + alias Reencodarr.{Media, Repo, Services} + alias Reencodarr.Media.{MediaInfoExtractor, VideoFileInfo, VideoUpsert} alias Reencodarr.Media.Video.MediaInfoConverter - alias Reencodarr.{Media, Repo, Services} # Public API def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__) diff --git a/mix.lock b/mix.lock index 91206baf..56917653 100644 --- a/mix.lock +++ b/mix.lock @@ -9,35 +9,35 @@ "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "contex": {:hex, :contex, "0.5.0", "5d8a6defbeb41f54adfcb0f85c4756d4f2b84aa5b0d809d45a5d2e90d91d0392", [:mix], [{:nimble_strftime, "~> 0.1.0", [hex: :nimble_strftime, repo: "hexpm", optional: false]}], "hexpm", "b7497a1790324d84247859df44ba4bcf2489d9bba1812a5375b2f2046b9e6fd7"}, - "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, - "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, + "ecto": {:hex, :ecto, "3.13.4", "27834b45d58075d4a414833d9581e8b7bb18a8d9f264a21e42f653d500dbeeb5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ad7d1505685dfa7aaf86b133d54f5ad6c42df0b4553741a1ff48796736e88b2"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.21.0", "8531f5044fb08289b3aacd21e383a9fb187e5a78981b9ed6d0929a78a25c2341", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "9c3e90ea33099ca0ddd160c8d9eaf80d7d4a9b110d325fa6ed0409858a714606"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, - "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, - "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, - "exqlite": {:hex, :exqlite, "0.33.0", "2cc96c4227fbb2d0864716def736dff18afb9949b1eaa74630822a0865b4b342", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8a7c2792e567bbebb4dafe96f6397f1c527edd7039d74f508a603817fbad2844"}, + "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, + "exqlite": {:hex, :exqlite, "0.33.1", "0465fdb997be174edeba6a27496fa27dfe8bc79ef1324a723daa8f0e8579da24", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "b3db0c9ae6e5ee7cf84dd0a1b6dc7566b80912eb7746d45370f5666ed66700f9"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "fuse": {:hex, :fuse, "2.5.0", "71afa90be21da4e64f94abba9d36472faa2d799c67fedc3bd1752a88ea4c4753", [:rebar3], [], "hexpm", "7f52a1c84571731ad3c91d569e03131cc220ebaa7e2a11034405f0bac46a4fef"}, "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, - "gettext": {:hex, :gettext, "1.0.0", "f8853ecb33e96361288f6239fafcfd50214b0a88ec38b5e452138d815d4877d8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "cc8196640756894a4fd75606067bed41a9863c0db09d6d6cc576e6170cffaa74"}, + "gettext": {:hex, :gettext, "1.0.1", "f5e374f4232c70fd6217d9ef11617cd8450a95b9fdabd3e4802e37a8b63f676b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "606cb4d7ca52c90223ec8505abaa0398e3f796313fbd7cbcfbdb5d556faaffb5"}, "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "lazy_html": {:hex, :lazy_html, "0.1.7", "53aa9ebdbde8aec7c8ee03a8bdaec38dd56302995b0baeebf8dbe7cbdd550400", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "e115944e6ddb887c45cadfd660348934c318abec0341f7b7156e912b98d3eb95"}, + "lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, - "meck": {:hex, :meck, "1.0.0", "24676cb6ee6951530093a93edcd410cfe4cb59fe89444b875d35c9d3909a15d0", [:rebar3], [], "hexpm", "680a9bcfe52764350beb9fb0335fb75fee8e7329821416cee0a19fec35433882"}, + "meck": {:hex, :meck, "1.1.0", "e65a3d84d7b418afcb2c8efde2b5106a98eefb7fe8e5f7e324fe9765122aefa2", [:rebar3], [], "hexpm", "0fdfbd4d4f9eb9251ba621f4740a6e450e2a7d0e5e07a37830c5cf632f2ca8bb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, @@ -49,11 +49,11 @@ "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.8.1", "865473a60a979551a4879db79fbfb4503e41cd809e77c85af79716578b6a456d", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "84d77d2b2e77c3c7e7527099bd01ef5c8560cd149c036d6b3a40745f11cd2fb2"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.11", "1b4d8fa56898d93b6f528c89227198a3fce7c5b242819b22ed9e92b73c1bb077", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "266823602e11a54e562ac03a25b3d232d79de12514262db7cfcbb83fdfd8fd57"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.17", "1d782b5901cf13b137c6d8c56542ff6cb618359b2adca7e185b21df728fa0c6c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fa82307dd9305657a8236d6b48e60ef2e8d9f742ee7ed832de4b8bcb7e0e5ed2"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, @@ -62,15 +62,15 @@ "req_fuse": {:hex, :req_fuse, "0.3.2", "8f96b26527deefe3d128496c058a23014754a569d12d281905d4c9e56bc3bae2", [:mix], [{:fuse, ">= 2.4.0", [hex: :fuse, repo: "hexpm", optional: false]}, {:req, ">= 0.4.14", [hex: :req, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "55cf642c03f10aed0dc4f97adc10f0985b355b377d2bc32bb0c569d82f3aa07e"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, - "swoosh": {:hex, :swoosh, "1.19.5", "5abd71be78302ba21be56a2b68d05c9946ff1f1bd254f949efef09d253b771ac", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c953f51ee0a8b237e0f4307c9cefd3eb1eb751c35fcdda2a8bccb991766473be"}, - "tailwind": {:hex, :tailwind, "0.4.0", "4b2606713080437e3d94a0fa26527e7425737abc001f172b484b42f43e3274c0", [:mix], [], "hexpm", "530bd35699333f8ea0e9038d7146c2f0932dfec2e3636bd4a8016380c4bc382e"}, + "swoosh": {:hex, :swoosh, "1.19.8", "0576f2ea96d1bb3a6e02cc9f79cbd7d497babc49a353eef8dce1a1f9f82d7915", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d7503c2daf0f9899afd8eba9923eeddef4b62e70816e1d3b6766e4d6c60e94ad"}, + "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, - "thousand_island": {:hex, :thousand_island, "1.4.1", "8df065e627407e281f7935da5ad0f3842d10eb721afa92e760b720d71e2e37aa", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "204a8640e5d2818589b87286ae66160978628d7edf6095181cbe0440765fb6c1"}, + "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, } diff --git a/test/reencodarr/media/video_upsert_test.exs b/test/reencodarr/media/video_upsert_test.exs index 7ec0045d..53a52469 100644 --- a/test/reencodarr/media/video_upsert_test.exs +++ b/test/reencodarr/media/video_upsert_test.exs @@ -2,8 +2,7 @@ defmodule Reencodarr.Media.VideoUpsertTest do use Reencodarr.DataCase import ExUnit.CaptureLog - alias Reencodarr.Media.VideoUpsert - alias Reencodarr.Media.{Library, Video} + alias Reencodarr.Media.{Library, Video, VideoUpsert} alias Reencodarr.Repo setup do From f43fa7096e783ecc01cae97bde598a6905d73555 Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 6 Nov 2025 18:24:40 -0700 Subject: [PATCH 5/6] Fix stuck pipelines by tracking pending demand The previous fix to prevent GenStage buffer overflow inadvertently broke the wake-up mechanism for Broadway pipelines. When Broadway finished all work and became idle, the :poll handler would see new work but couldn't emit events (to avoid buffer overflow). Solution: Track pending_demand in producer state and emit events during :poll if there's pending demand. This allows: - Broadway to express demand when it's ready - Producers to fulfill that demand immediately when work becomes available - No buffer overflow from pushing events without demand Changes: - Add pending_demand: 0 to initial state in all three producers - Accumulate demand in handle_demand/2 - Dispatch events if pending_demand > 0 during :poll - Track remaining demand after dispatching events --- flake.lock | 6 +-- flake.nix | 12 +----- lib/reencodarr/analyzer/broadway/producer.ex | 30 ++++++++++----- .../crf_searcher/broadway/producer.ex | 31 ++++++++------- lib/reencodarr/encoder/broadway/producer.ex | 38 +++++++++++-------- mix.exs | 2 +- 6 files changed, 67 insertions(+), 52 deletions(-) diff --git a/flake.lock b/flake.lock index 28293a93..ee27006b 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1758127371, - "narHash": "sha256-0VkGTLwl6E/MS+Yx0Eku2iwdTzgEsYxvjtjlMLq9Ekg=", + "lastModified": 1762451070, + "narHash": "sha256-sxqROO5LA9kYtu1hOEz6kAigHB8kvVJVRKGHBaZscJw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d48aa7a2404f69bc748a86da230baa6eb308eeb6", + "rev": "c64bcccaf5608a016d993a43fcecdc2dd2e108be", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index c067767c..f52d8d45 100644 --- a/flake.nix +++ b/flake.nix @@ -14,17 +14,10 @@ system: let pkgs = import nixpkgs {inherit system;}; lib = pkgs.lib; - # Use latest OTP 28.1 with Elixir 1.19.0-rc.0 for cutting-edge features + # Use latest stable OTP 28 with Elixir 1.19 erlang = pkgs.erlang_28; beamPackages = pkgs.beam.packagesWith erlang; - elixir = beamPackages.elixir.override { - erlang = erlang; - version = "1.19.0-rc.0"; - src = pkgs.fetchurl { - url = "https://github.com/elixir-lang/elixir/archive/refs/tags/v${elixir.version}.tar.gz"; - sha256 = "sha256-YvkDCI578h4SmtEA5XP2XQNjixDGIHdwIEuOa50Uh5E="; - }; - }; + elixir = beamPackages.elixir_1_19; in { # Docker image for the application packages.dockerImage = pkgs.dockerTools.buildLayeredImage { @@ -77,7 +70,6 @@ pkgs.curl pkgs.docker-compose pkgs.gnupg - pkgs.pinentry pkgs.pinentry-curses # Video processing tools for CI/dev pkgs.ab-av1 diff --git a/lib/reencodarr/analyzer/broadway/producer.ex b/lib/reencodarr/analyzer/broadway/producer.ex index f8e90074..fa877d43 100644 --- a/lib/reencodarr/analyzer/broadway/producer.ex +++ b/lib/reencodarr/analyzer/broadway/producer.ex @@ -23,15 +23,14 @@ defmodule Reencodarr.Analyzer.Broadway.Producer do def init(_opts) do # Poll every 2 seconds to check for new work schedule_poll() - {:producer, %{}} + {:producer, %{pending_demand: 0}} end @impl GenStage def handle_demand(demand, state) when demand > 0 do - # Fetch up to 5 videos for batch processing - videos = Media.get_videos_needing_analysis(min(demand, 5)) - Logger.debug("Analyzer: handle_demand(#{demand}) -> #{length(videos)} videos") - {:noreply, videos, state} + # Accumulate demand and fetch videos + new_demand = state.pending_demand + demand + dispatch(new_demand, state) end @impl GenStage @@ -40,15 +39,28 @@ defmodule Reencodarr.Analyzer.Broadway.Producer do @impl GenStage def handle_info(:poll, state) do schedule_poll() - # Just schedule the next poll - don't push events without demand - # Events are only emitted in response to demand via handle_demand - # This prevents buffer overflow warnings from GenStage - {:noreply, [], state} + # If there's pending demand, try to fulfill it + # This wakes up Broadway when new work appears + dispatch(state.pending_demand, state) end @impl GenStage def handle_info(_msg, state), do: {:noreply, [], state} + defp dispatch(demand, state) when demand > 0 do + # Fetch up to 5 videos for batch processing + videos = Media.get_videos_needing_analysis(min(demand, 5)) + remaining_demand = demand - length(videos) + + Logger.debug( + "Analyzer: dispatch(#{demand}) -> #{length(videos)} videos, #{remaining_demand} remaining" + ) + + {:noreply, videos, %{state | pending_demand: remaining_demand}} + end + + defp dispatch(_demand, state), do: {:noreply, [], state} + defp schedule_poll do Process.send_after(self(), :poll, 2000) end diff --git a/lib/reencodarr/crf_searcher/broadway/producer.ex b/lib/reencodarr/crf_searcher/broadway/producer.ex index 37f1ce0c..5cdac7da 100644 --- a/lib/reencodarr/crf_searcher/broadway/producer.ex +++ b/lib/reencodarr/crf_searcher/broadway/producer.ex @@ -18,34 +18,39 @@ defmodule Reencodarr.CrfSearcher.Broadway.Producer do def init(_opts) do # Poll every 2 seconds to check for new work schedule_poll() - {:producer, %{}} + {:producer, %{pending_demand: 0}} end @impl GenStage - def handle_demand(_demand, state) do - dispatch(state) + def handle_demand(demand, state) do + new_demand = state.pending_demand + demand + dispatch(new_demand, state) end @impl GenStage def handle_info(:poll, state) do schedule_poll() - # Just schedule the next poll - don't push events without demand - # Events are only emitted in response to demand via handle_demand - {:noreply, [], state} + # If there's pending demand, try to fulfill it + dispatch(state.pending_demand, state) end @impl GenStage def handle_info(_msg, state), do: {:noreply, [], state} - defp dispatch(state) do - if CrfSearch.available?() do - videos = Media.get_videos_for_crf_search(1) - {:noreply, videos, state} - else - {:noreply, [], state} - end + defp dispatch(demand, state) when demand > 0 do + videos = + if CrfSearch.available?() do + Media.get_videos_for_crf_search(1) + else + [] + end + + remaining_demand = demand - length(videos) + {:noreply, videos, %{state | pending_demand: remaining_demand}} end + defp dispatch(_demand, state), do: {:noreply, [], state} + defp schedule_poll do Process.send_after(self(), :poll, 2000) end diff --git a/lib/reencodarr/encoder/broadway/producer.ex b/lib/reencodarr/encoder/broadway/producer.ex index bbb6ee76..0c476117 100644 --- a/lib/reencodarr/encoder/broadway/producer.ex +++ b/lib/reencodarr/encoder/broadway/producer.ex @@ -24,38 +24,44 @@ defmodule Reencodarr.Encoder.Broadway.Producer do def init(_opts) do # Poll every 2 seconds to check for new work schedule_poll() - {:producer, %{}} + {:producer, %{pending_demand: 0}} end @impl GenStage - def handle_demand(_demand, state) do - dispatch(state) + def handle_demand(demand, state) do + new_demand = state.pending_demand + demand + dispatch(new_demand, state) end @impl GenStage def handle_info(:poll, state) do schedule_poll() - # Just schedule the next poll - don't push events without demand - # Events are only emitted in response to demand via handle_demand - {:noreply, [], state} + # If there's pending demand, try to fulfill it + dispatch(state.pending_demand, state) end @impl GenStage def handle_info(_msg, state), do: {:noreply, [], state} - defp dispatch(state) do - if Encode.available?() do - case Media.get_next_for_encoding(1) do - %Reencodarr.Media.Vmaf{} = vmaf -> {:noreply, [vmaf], state} - [%Reencodarr.Media.Vmaf{} = vmaf] -> {:noreply, [vmaf], state} - [] -> {:noreply, [], state} - nil -> {:noreply, [], state} + defp dispatch(demand, state) when demand > 0 do + vmaf_list = + if Encode.available?() do + case Media.get_next_for_encoding(1) do + %Reencodarr.Media.Vmaf{} = vmaf -> [vmaf] + [%Reencodarr.Media.Vmaf{} = vmaf] -> [vmaf] + [] -> [] + nil -> [] + end + else + [] end - else - {:noreply, [], state} - end + + remaining_demand = demand - length(vmaf_list) + {:noreply, vmaf_list, %{state | pending_demand: remaining_demand}} end + defp dispatch(_demand, state), do: {:noreply, [], state} + defp schedule_poll do Process.send_after(self(), :poll, 2000) end diff --git a/mix.exs b/mix.exs index 5033e2a7..83848a0d 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,7 @@ defmodule Reencodarr.MixProject do [ app: :reencodarr, version: "0.1.0", - elixir: "~> 1.14", + elixir: "~> 1.19", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), From 204981f988b72b709408629696c6b0fbb2c2c8da Mon Sep 17 00:00:00 2001 From: Mika Cohen Date: Thu, 6 Nov 2025 18:33:28 -0700 Subject: [PATCH 6/6] Update GitHub Actions to Elixir 1.19.2/OTP 28 with aggressive caching Switch from Alpine container to erlef/setup-beam for better version control and add comprehensive caching to dramatically speed up CI runs. Changes: - Use erlef/setup-beam@v1 for Elixir 1.19.2 and OTP 28.0 (strict versions) - Separate cache for Rust toolchain and ab-av1 binary - Cache ab-av1 binary independently (takes ~5-10 min to compile) - Cache Mix deps and _build together with OTP/Elixir version keys - Add PLT cache for future Dialyzer support - Skip deps install/compile on cache hit - Switch from Alpine to ubuntu-latest runner Cache strategy: - Rust toolchain: Cached separately from binaries - ab-av1 binary: Cached independently, only rebuild when cache misses - Mix: Include OTP/Elixir versions in key to invalidate on version changes - Build: Hash lib/ and config/ files to detect code changes This should reduce CI time from ~15+ minutes to ~2-3 minutes on cache hits. --- .github/workflows/elixir.yml | 120 +++++++++++++---------------------- 1 file changed, 44 insertions(+), 76 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index ea429a09..dc34bb9e 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -13,122 +13,88 @@ jobs: build: name: Build and test runs-on: ubuntu-latest - container: - image: alpine:3.21 env: MIX_ENV: test steps: + - uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.19.2' + otp-version: '28.0' + version-type: 'strict' + id: beam + + - name: Restore PLT cache + uses: actions/cache@v4 + id: plt-cache + with: + path: priv/plts + key: ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-plt-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-plt- + - name: Install system dependencies and FFmpeg run: | - # Add community repository for 3.21 packages - echo "https://dl-cdn.alpinelinux.org/alpine/v3.21/community" >> /etc/apk/repositories - apk update - - # Install basic system dependencies including GNU tar for GitHub Actions caching - apk add --no-cache \ - git \ - bash \ - curl \ - build-base \ - openssl-dev \ - ncurses-dev \ - zlib-dev \ + sudo apt-get update + sudo apt-get install -y \ mediainfo \ - cmake \ - make \ - gzip \ - tar - - # Verify tar installation for GitHub Actions caching compatibility - # GitHub Actions cache requires POSIX-compliant tar - which tar && tar --version | head -1 - - # Install FFmpeg with essential codec libraries (minimal set to avoid conflicts) - apk add --no-cache \ ffmpeg \ - x264-libs \ - x265-libs \ - svt-av1 - - - name: Install Erlang and Elixir - run: | - # Install Erlang 27 and Elixir 1.19 from Alpine 3.21 (should be available in community repo) - apk add --no-cache \ - erlang \ - elixir - - - uses: actions/checkout@v4 + build-essential - - name: Cache rustup and cargo installation + - name: Cache Rust toolchain uses: actions/cache@v4 + id: rust-toolchain-cache with: path: | ~/.rustup ~/.cargo/registry/index ~/.cargo/registry/cache - ~/.cargo/git - key: ${{ runner.os }}-alpine-rust-${{ hashFiles('**/Cargo.lock') }}-v1 + ~/.cargo/git/db + key: ${{ runner.os }}-rust-toolchain-v3 restore-keys: | - ${{ runner.os }}-alpine-rust- + ${{ runner.os }}-rust-toolchain- - name: Cache ab-av1 binary uses: actions/cache@v4 id: ab-av1-cache with: path: ~/.cargo/bin/ab-av1 - key: ${{ runner.os }}-alpine-ab-av1-${{ hashFiles('~/.cargo/bin/ab-av1') }}-v1 + key: ${{ runner.os }}-ab-av1-binary-v3 restore-keys: | - ${{ runner.os }}-alpine-ab-av1- + ${{ runner.os }}-ab-av1-binary- + + - name: Install Rust toolchain + if: steps.rust-toolchain-cache.outputs.cache-hit != 'true' + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - name: Install ab-av1 if: steps.ab-av1-cache.outputs.cache-hit != 'true' run: | - # Install rustup to get latest stable Rust (Alpine's is too old) - if [ ! -f ~/.cargo/env ]; then - apk add --no-cache curl - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - fi source ~/.cargo/env - # Add cargo to PATH for this and future steps - echo "$HOME/.cargo/bin" >> $GITHUB_PATH - # Install latest stable ab-av1 with newer Rust toolchain - cargo install ab-av1 + cargo install ab-av1 --locked - name: Setup cargo PATH run: | # Ensure cargo is available in PATH for subsequent steps echo "$HOME/.cargo/bin" >> $GITHUB_PATH + source ~/.cargo/env || true - - name: Cache Mix dependencies + - name: Cache Mix dependencies and build uses: actions/cache@v4 + id: mix-cache with: path: | deps - _build/test/lib - key: ${{ runner.os }}-alpine-mix-${{ hashFiles('mix.lock') }} - restore-keys: | - ${{ runner.os }}-alpine-mix- - - - name: Cache compiled dependencies - uses: actions/cache@v4 - with: - path: _build - key: ${{ runner.os }}-alpine-build-${{ hashFiles('mix.lock') }}-${{ hashFiles('lib/**/*.ex', 'test/**/*.exs', 'config/**/*.exs') }} - restore-keys: | - ${{ runner.os }}-alpine-build-${{ hashFiles('mix.lock') }}- - ${{ runner.os }}-alpine-build- - - - name: Cache native dependencies - uses: actions/cache@v4 - with: - path: | - deps/*/priv - deps/*/_build - key: ${{ runner.os }}-alpine-native-${{ hashFiles('mix.lock') }} + _build + key: ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('lib/**/*.ex', 'config/**/*.exs') }} restore-keys: | - ${{ runner.os }}-alpine-native- + ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('**/mix.lock') }}- + ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-mix- - name: Verify environment run: | @@ -139,9 +105,11 @@ jobs: echo 'ffmpeg version:' && ffmpeg -version | head -1 - name: Install dependencies + if: steps.mix-cache.outputs.cache-hit != 'true' run: mix deps.get - name: Compile dependencies + if: steps.mix-cache.outputs.cache-hit != 'true' run: mix deps.compile - name: Compile project