Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7c30056
fix: reduce dashboard query timeout to 2s to handle busy SQLite
mjc Nov 10, 2025
724536a
fix: suppress DBConnection timeout errors in dashboard queries
mjc Nov 11, 2025
f14b5b6
fix: delegate encoder Broadway to Encode GenServer for single-worker …
mjc Nov 12, 2025
bc7b25b
build: compile Erlang/OTP 28.1.1 from source
mjc Nov 15, 2025
35985b5
fix: run dashboard queries synchronously in test mode
mjc Nov 15, 2025
74cfe32
Add custom Reencodarr favicon
mjc Nov 17, 2025
cf5221d
Update dashboard theme to match favicon aesthetic
mjc Nov 17, 2025
1a98473
test: achieve 100% coverage for SharedQueries module
mjc Nov 19, 2025
7bbc826
style: fix credo number formatting in video_upsert_test
mjc Nov 19, 2025
e9ef6d8
fix: correct Media test assertions and state requirements
mjc Nov 19, 2025
d3a32fa
feat: add Media module tests for queue, library, vmaf, and bulk opera…
mjc Nov 20, 2025
be5b8bb
feat: add comprehensive Media module tests for updates, upserts, and …
mjc Nov 20, 2025
3980d7b
fix: replace PostgreSQL array_length with SQLite json_array_length
mjc Nov 20, 2025
a0529f4
feat: add comprehensive Media module tests for queue, upsert, bulk op…
mjc Nov 20, 2025
efbee38
feat: add direct tests for upsert_video function
mjc Nov 20, 2025
4369b9b
feat: add comprehensive edge case tests for Media module
mjc Nov 20, 2025
99e6235
feat: add more Media module edge case and error path tests
mjc Nov 20, 2025
30acb6b
feat: add comprehensive tests for Media helper functions and edge cases
mjc Nov 20, 2025
a55aced
feat: add library and VMAF edge case tests
mjc Nov 20, 2025
9bafa9e
feat: add comprehensive edge case tests for Media module VMAF and tra…
mjc Nov 20, 2025
4d43e46
feat: add bulk operation and queue function tests for Media module
mjc Nov 20, 2025
c8628a3
fix: simplify dashboard queries and add state transitions for Broadwa…
mjc Nov 21, 2025
dec183d
feat: add exponential backoff with jitter for database busy errors
mjc Dec 1, 2025
0ec245e
fix: apply retry logic to CRF search VMAF upserts
mjc Dec 1, 2025
6d7069a
fix: catch checkout timeout errors in dashboard queries
mjc Dec 1, 2025
42c00df
fix: stop performance monitor from logging when analyzer is idle
mjc Dec 2, 2025
c0bf4d7
fix: validate required mediainfo fields before marking videos as anal…
mjc Dec 5, 2025
f67a5f8
fix: add Opus filename check to transition decision logic
mjc Dec 5, 2025
04a2141
refactor: rename analyzer functions for clarity
mjc Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ config :reencodarr, Reencodarr.Repo,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
# Increase pool size for better concurrency with Broadway pipelines
pool_size: 20
pool_size: 20,
# Increase checkout timeout for slow queries (especially JSON fragment queries in SQLite)
timeout: 30_000,
# DBConnection queue configuration for better handling under load
queue_target: 5_000,
queue_interval: 2_000

# For development, we disable any cache and enable
# debugging and code reloading.
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@
system: let
pkgs = import nixpkgs {inherit system;};
lib = pkgs.lib;
# Use latest stable OTP 28 with Elixir 1.19
erlang = pkgs.erlang_28;
# Build Erlang 28.1.1 from source
erlang = pkgs.beam.interpreters.erlang_28.overrideAttrs (oldAttrs: rec {
version = "28.1.1";
src = pkgs.fetchFromGitHub {
owner = "erlang";
repo = "otp";
rev = "OTP-${version}";
hash = "sha256-2Yop9zpx3dY549NFsjBRghb5vw+SnUSEmv6VIA0m5yQ=";
};
});
beamPackages = pkgs.beam.packagesWith erlang;
elixir = beamPackages.elixir_1_19;
in {
Expand Down
29 changes: 29 additions & 0 deletions lib/reencodarr/ab_av1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,35 @@ defmodule Reencodarr.AbAv1 do
"""
@spec encode(Media.Vmaf.t()) :: :ok | {:error, atom()}
def encode(vmaf) do
# Skip MP4 files - compatibility issues to be resolved later
video_id = Map.get(vmaf, :video_id) || Map.get(vmaf, "video_id")

if is_integer(video_id) do
try do
video = Media.get_video!(video_id)

if is_binary(video.path) and String.ends_with?(video.path, ".mp4") do
# Skip MP4 files - compatibility issues
Logger.info("Skipping encode for MP4 file (compatibility issues): #{video.path}")
# Mark as failed to skip future encoding attempts
case Media.mark_as_failed(video) do
{:ok, _updated} -> :ok
error -> error
end
else
do_queue_encode(vmaf)
end
rescue
Ecto.NoResultsError ->
# Video doesn't exist - fall back to normal validation/queuing
do_queue_encode(vmaf)
end
else
do_queue_encode(vmaf)
end
end

defp do_queue_encode(vmaf) do
case QueueManager.validate_encode_request(vmaf) do
{:ok, validated_vmaf} ->
message = QueueManager.build_encode_message(validated_vmaf)
Expand Down
28 changes: 11 additions & 17 deletions lib/reencodarr/ab_av1/crf_search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ defmodule Reencodarr.AbAv1.CrfSearch do
alias Reencodarr.AbAv1.Helper
alias Reencodarr.AbAv1.OutputParser
alias Reencodarr.Core.Parsers
alias Reencodarr.Core.Retry
alias Reencodarr.Core.Time
alias Reencodarr.Dashboard.Events
alias Reencodarr.ErrorHelpers
alias Reencodarr.Formatters
alias Reencodarr.Media
alias Reencodarr.Media.VideoStateMachine
alias Reencodarr.Repo

require Logger
Expand Down Expand Up @@ -151,7 +151,7 @@ defmodule Reencodarr.AbAv1.CrfSearch do
@impl true
def handle_cast({:crf_search, video, vmaf_percent}, %{port: :none} = state) do
# Mark video as crf_searching NOW that we're actually starting
case VideoStateMachine.transition_to_crf_searching(video) do
case Media.mark_as_crf_searching(video) do
{:ok, _updated_video} ->
:ok

Expand Down Expand Up @@ -241,32 +241,26 @@ defmodule Reencodarr.AbAv1.CrfSearch do
full_line = buffer <> line

try do
process_line(full_line, video, args, target_vmaf)
# Use retry logic for database operations
Retry.retry_on_db_busy(fn ->
process_line(full_line, video, args, target_vmaf)
end)

# Add the line to our output buffer for failure tracking
new_output_buffer = [full_line | output_buffer]

{:noreply, %{state | partial_line_buffer: "", output_buffer: new_output_buffer}}
rescue
# If retries are exhausted, log and continue
e in Exqlite.Error ->
# Database is busy — don't crash. Requeue the same port message with a short delay.
Logger.warning(
"CrfSearch: Database busy while upserting VMAF, requeuing line: #{inspect(e)}"
)

# Re-send the exact same message after a short backoff to retry
Process.send_after(self(), {port, {:data, {:eol, line}}}, 200)
Logger.error("CrfSearch: Database error after retries, skipping line: #{inspect(e)}")

# Keep state intact — we'll retry later
{:noreply, state}
{:noreply, %{state | partial_line_buffer: ""}}

e in DBConnection.ConnectionError ->
Logger.warning(
"CrfSearch: DB connection error while upserting VMAF, requeuing line: #{inspect(e)}"
)
Logger.error("CrfSearch: DB connection error after retries, skipping line: #{inspect(e)}")

Process.send_after(self(), {port, {:data, {:eol, line}}}, 200)
{:noreply, state}
{:noreply, %{state | partial_line_buffer: ""}}
end
end

Expand Down
47 changes: 29 additions & 18 deletions lib/reencodarr/ab_av1/encode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -273,28 +273,39 @@ defmodule Reencodarr.AbAv1.Encode do

# Private Helper Functions
defp prepare_encode_state(vmaf, state) do
args = build_encode_args(vmaf)
output_file = Path.join(Helper.temp_dir(), "#{vmaf.video.id}.mkv")
# Mark video as encoding BEFORE starting the port to prevent duplicate dispatches
case Media.mark_as_encoding(vmaf.video) do
{:ok, _updated_video} ->
args = build_encode_args(vmaf)
output_file = Path.join(Helper.temp_dir(), "#{vmaf.video.id}.mkv")

port = Helper.open_port(args)
port = Helper.open_port(args)

# Broadcast encoding started to Dashboard Events
Events.broadcast_event(:encoding_started, %{
video_id: vmaf.video.id,
filename: Path.basename(vmaf.video.path)
})
# Broadcast encoding started to Dashboard Events
Events.broadcast_event(:encoding_started, %{
video_id: vmaf.video.id,
filename: Path.basename(vmaf.video.path)
})

# Set up a periodic timer to check if we're still alive and potentially emit progress
# Check every 10 seconds
Process.send_after(self(), :periodic_check, 10_000)
# Set up a periodic timer to check if we're still alive and potentially emit progress
# Check every 10 seconds
Process.send_after(self(), :periodic_check, 10_000)

%{
state
| port: port,
video: vmaf.video,
vmaf: vmaf,
output_file: output_file
}
%{
state
| port: port,
video: vmaf.video,
vmaf: vmaf,
output_file: output_file
}

{:error, reason} ->
Logger.error(
"Failed to mark video #{vmaf.video.id} as encoding: #{inspect(reason)}. Skipping encode."
)

state
end
end

defp build_encode_args(vmaf) do
Expand Down
Loading