Skip to content
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.

4 changes: 2 additions & 2 deletions lib/reencodarr/ab_av1/progress_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ defmodule Reencodarr.AbAv1.ProgressParser do
# Alternative progress pattern without brackets: percent%, fps fps, eta time unit or eta Unknown/N/A
progress_alt:
~r/(?<percent>\d+(?:\.\d+)?)%,\s(?<fps>\d+(?:\.\d+)?)\sfps?,?\s?eta\s(?:(?<eta>\d+)\s(?<time_unit>(?:second|minute|hour|day|week|month|year)s?)|(?<eta_unknown>Unknown|N\/A|unknown))/,
# File size progress pattern: Encoded X GB (percent%)
file_size_progress: ~r/Encoded\s[\d.]+\s[KMGT]?B\s\((?<percent>\d+)%\)/
# File size progress pattern: Encoded X GB/GiB/MB/MiB etc (percent%)
file_size_progress: ~r/Encoded\s[\d.]+\s[KMGT]?i?B\s\((?<percent>\d+)%\)/
}

Process.put(:progress_parser_patterns, patterns)
Expand Down
17 changes: 12 additions & 5 deletions lib/reencodarr/analyzer/processing/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ defmodule Reencodarr.Analyzer.Processing.Pipeline do
@doc """
Process a single video with MediaInfo extraction.
"""
@spec process_single_video(map()) :: {:ok, map()} | {:skip, term()} | {:error, term()}
@spec process_single_video(map()) :: {:ok, map()} | {:error, term()}
def process_single_video(video_info) when is_map(video_info) do
Logger.debug("Processing single video: #{video_info.path}")

Expand All @@ -93,8 +93,11 @@ defmodule Reencodarr.Analyzer.Processing.Pipeline do
{:ok, {video_info, complete_params}}
else
{:error, reason} ->
Logger.debug("Skipping video #{video_info.path}: #{reason}")
{:skip, reason}
Logger.warning(
"Cannot analyze video #{video_info.path}: #{reason}. Will be marked as failed."
)

{:error, {video_info.path, reason}}

error ->
error_msg = inspect(error)
Expand Down Expand Up @@ -364,8 +367,11 @@ defmodule Reencodarr.Analyzer.Processing.Pipeline do
{:ok, {video_info, complete_params}}
else
{:error, reason} ->
Logger.debug("Skipping video #{video_info.path}: #{reason}")
{:skip, reason}
Logger.warning(
"Cannot analyze video #{video_info.path}: #{reason}. Will be marked as failed."
)

{:error, {video_info.path, reason}}
end
catch
:error, reason ->
Expand Down Expand Up @@ -438,6 +444,7 @@ defmodule Reencodarr.Analyzer.Processing.Pipeline do
defp extract_video_params(validated_mediainfo, path) do
case MediaInfoExtractor.extract_video_params(validated_mediainfo, path) do
video_params when is_map(video_params) -> {:ok, video_params}
{:error, reason} -> {:error, reason}
error -> {:error, "video parameter extraction failed: #{inspect(error)}"}
end
end
Expand Down
80 changes: 48 additions & 32 deletions lib/reencodarr/media/media_info_extractor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,62 @@ defmodule Reencodarr.Media.MediaInfoExtractor do
Extracts all needed video parameters directly from MediaInfo JSON.

Returns a flat map with all the fields we need, avoiding repeated track traversal.
Validates that required fields (width, height, bitrate) are present and valid.
"""
@spec extract_video_params(map(), String.t()) :: map()
@spec extract_video_params(map(), String.t()) :: map() | {:error, String.t()}
def extract_video_params(mediainfo, path) do
tracks = extract_tracks_safely(mediainfo, path)

general = find_track(tracks, "General")
video_track = find_track(tracks, "Video")
audio_tracks = filter_tracks(tracks, "Audio")

%{
# Core video info
width: get_int_field(video_track, "Width", 0),
height: get_int_field(video_track, "Height", 0),
frame_rate: get_float_field(video_track, "FrameRate", 0.0),
duration: get_float_field(general, "Duration", 0.0),
size: get_int_field(general, "FileSize", 0),
bitrate: get_int_field(general, "OverallBitRate", 0),

# Codecs
video_codecs: [get_string_field(video_track, "CodecID", "")],
audio_codecs: extract_audio_codecs_safely(audio_tracks, general),

# Audio info - use actual count of audio tracks found to ensure consistency
audio_count: length(audio_tracks),
max_audio_channels: calculate_max_audio_channels(audio_tracks),
atmos: detect_atmos(audio_tracks),

# Text/subtitle info
text_count: get_int_field(general, "TextCount", 0),
text_codecs: [],

# Video counts
video_count: get_int_field(general, "VideoCount", 0),

# HDR info
hdr: extract_hdr_info(video_track),

# Title fallback
title: get_string_field(general, "Title", Path.basename(path))
}
width = get_int_field(video_track, "Width", 0)
height = get_int_field(video_track, "Height", 0)
bitrate = get_int_field(general, "OverallBitRate", 0)

# Validate that we have required fields with valid values
case {width, height, bitrate} do
{w, h, _b} when w <= 0 or h <= 0 ->
Logger.warning(
"Invalid video dimensions for #{path}: width=#{w}, height=#{h}. MediaInfo may not have found video track."
)

{:error, "invalid video dimensions"}

_ ->
%{
# Core video info
width: width,
height: height,
frame_rate: get_float_field(video_track, "FrameRate", 0.0),
duration: get_float_field(general, "Duration", 0.0),
size: get_int_field(general, "FileSize", 0),
bitrate: bitrate,

# Codecs
video_codecs: [get_string_field(video_track, "CodecID", "")],
audio_codecs: extract_audio_codecs_safely(audio_tracks, general),

# Audio info - use actual count of audio tracks found to ensure consistency
audio_count: length(audio_tracks),
max_audio_channels: calculate_max_audio_channels(audio_tracks),
atmos: detect_atmos(audio_tracks),

# Text/subtitle info
text_count: get_int_field(general, "TextCount", 0),
text_codecs: [],

# Video counts
video_count: get_int_field(general, "VideoCount", 0),

# HDR info
hdr: extract_hdr_info(video_track),

# Title fallback
title: get_string_field(general, "Title", Path.basename(path))
}
end
end

# === Private Helper Functions ===
Expand Down
70 changes: 41 additions & 29 deletions lib/reencodarr/media/media_info_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,55 @@ defmodule Reencodarr.Media.MediaInfoUtils do
Extracts all needed video parameters directly from MediaInfo JSON.

Returns a flat map with all the fields we need, avoiding repeated track traversal.
Validates that required fields (width, height, bitrate) are present and valid.
"""
@spec extract_video_params(map(), String.t()) :: map()
@spec extract_video_params(map(), String.t()) :: map() | {:error, String.t()}
def extract_video_params(mediainfo, path) do
tracks = extract_tracks_safely(mediainfo, path)

general = find_track(tracks, "General")
video_track = find_track(tracks, "Video")
audio_tracks = filter_tracks(tracks, "Audio")

%{
# Core video info
width: get_int_field(video_track, "Width", 0),
height: get_int_field(video_track, "Height", 0),
frame_rate: get_float_field(video_track, "FrameRate", 0.0),
duration: get_float_field(general, "Duration", 0.0),
size: get_int_field(general, "FileSize", 0),
bitrate: get_int_field(general, "OverallBitRate", 0),

# Codecs
video_codecs: [get_string_field(video_track, "CodecID", "")],
audio_codecs: extract_audio_codecs_safely(audio_tracks, general),

# Audio info - use actual count of audio tracks found to ensure consistency
audio_count: length(audio_tracks),
max_audio_channels: calculate_max_audio_channels(audio_tracks),
atmos: detect_atmos(audio_tracks),

# Text/subtitle info
text_count: get_int_field(general, "TextCount", 0),
text_codecs: [],

# Video counts
video_count: get_int_field(general, "VideoCount", 0),

# HDR info
hdr: MediaInfo.parse_hdr_from_video(video_track)
}
width = get_int_field(video_track, "Width", 0)
height = get_int_field(video_track, "Height", 0)
bitrate = get_int_field(general, "OverallBitRate", 0)

# Validate that we have required fields with valid values
case {width, height, bitrate} do
{w, h, _b} when w <= 0 or h <= 0 ->
{:error, "invalid video dimensions"}

_ ->
%{
# Core video info
width: width,
height: height,
frame_rate: get_float_field(video_track, "FrameRate", 0.0),
duration: get_float_field(general, "Duration", 0.0),
size: get_int_field(general, "FileSize", 0),
bitrate: bitrate,

# Codecs
video_codecs: [get_string_field(video_track, "CodecID", "")],
audio_codecs: extract_audio_codecs_safely(audio_tracks, general),

# Audio info - use actual count of audio tracks found to ensure consistency
audio_count: length(audio_tracks),
max_audio_channels: calculate_max_audio_channels(audio_tracks),
atmos: detect_atmos(audio_tracks),

# Text/subtitle info
text_count: get_int_field(general, "TextCount", 0),
text_codecs: [],

# Video counts
video_count: get_int_field(general, "VideoCount", 0),

# HDR info
hdr: MediaInfo.parse_hdr_from_video(video_track)
}
end
end

@doc """
Expand Down
16 changes: 10 additions & 6 deletions lib/reencodarr/media/video.ex
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,16 @@ defmodule Reencodarr.Media.Video do

mediainfo ->
# Use the simpler extractor that avoids complex track traversal
params = MediaInfoExtractor.extract_video_params(mediainfo, get_field(changeset, :path))

changeset
|> cast(params, @mediainfo_params)
|> maybe_remove_size_zero()
|> maybe_remove_bitrate_zero()
case MediaInfoExtractor.extract_video_params(mediainfo, get_field(changeset, :path)) do
params when is_map(params) ->
changeset
|> cast(params, @mediainfo_params)
|> maybe_remove_size_zero()
|> maybe_remove_bitrate_zero()

{:error, reason} ->
add_error(changeset, :mediainfo, "invalid mediainfo structure: #{reason}")
end
end
end

Expand Down
51 changes: 46 additions & 5 deletions lib/reencodarr/post_processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ defmodule Reencodarr.PostProcessor do

defp process_reloaded_video(video, actual_path) do
case Media.mark_as_reencoded(video) do
{:ok, _} ->
{:ok, updated_video} ->
Logger.info("Successfully marked video #{video.id} as re-encoded")
finalize_and_sync(video, actual_path)
finalize_and_sync(updated_video, actual_path)

{:error, reason} ->
Logger.error("Failed to mark video #{video.id} as re-encoded: #{inspect(reason)}")
Expand All @@ -114,11 +114,52 @@ defmodule Reencodarr.PostProcessor do
)
end

# Always call Sync as per original logic
spawn_refresh_and_rename_task(video)
end

defp spawn_refresh_and_rename_task(video) do
Logger.info(
"Calling Sync.refresh_and_rename_from_video for video #{video.id} (path: #{video.path}) after finalization attempt."
"Spawning async Sync.refresh_and_rename_from_video for video #{video.id} (path: #{video.path})"
)

Sync.refresh_and_rename_from_video(video)
case Task.Supervisor.start_child(Reencodarr.TaskSupervisor, fn ->
handle_refresh_and_rename_result(video, Sync.refresh_and_rename_from_video(video))
end) do
{:ok, _pid} ->
{:ok, "refresh_and_rename started async"}

:ignore ->
Logger.warning(
"Task.Supervisor.start_child ignored starting Sync.refresh_and_rename_from_video for video #{video.id}"
)

{:ok, "refresh_and_rename task ignored"}

{:error, reason} ->
Logger.error(
"Failed to start async Sync.refresh_and_rename_from_video task for video #{video.id}: #{inspect(reason)}"
)

{:error, reason}
end
end

defp handle_refresh_and_rename_result(video, result) do
case result do
{:ok, outcome} ->
Logger.info(
"Sync.refresh_and_rename_from_video succeeded for video #{video.id}: #{inspect(outcome)}"
)

{:error, reason} ->
Logger.error(
"Sync.refresh_and_rename_from_video failed for video #{video.id}: #{inspect(reason)}"
)

other ->
Logger.warning(
"Sync.refresh_and_rename_from_video returned unexpected result for video #{video.id}: #{inspect(other)}"
)
end
end
end
Loading