Current section
Files
Jump to
Current section
Files
lib/subtitle/srt.ex
defmodule Subtitle.SRT do
alias Subtitle.SRT.Timing
alias Subtitle.Cue
require Logger
defstruct cues: []
def unmarshal(srt, _opts \\ []) do
srt = String.replace(srt, "\r", "")
with {:ok, cues} <- parse_body(srt, []) do
{:ok, %__MODULE__{cues: cues}}
end
end
def marshal!(srt) do
Enum.map_join(srt.cues, "\n\n", &marshal_cue/1)
end
defp parse_body(body, acc) do
case String.split(body, "\n\n", parts: 2) do
[""] ->
cues =
acc
# This parser allows empty cues, which are useless.
|> Enum.filter(fn %Cue{text: x} -> x != "" end)
|> Enum.reverse()
{:ok, cues}
[block | rest] ->
acc =
case parse_block(block) do
{:ok, cue} ->
[cue | acc]
{:error, reason} ->
Logger.warning("Parse SRT block: #{inspect(reason)}")
acc
end
body = if rest != [], do: List.first(rest), else: ""
parse_body(body, acc)
end
end
defp parse_block(candidate_cue) do
# Happens when a cue with no text is processed before.
candidate_cue = String.trim_leading(candidate_cue)
with {:ok, id, rest} <- parse_cue_id(candidate_cue),
{:ok, from, to, rest} <- parse_timings(rest) do
{:ok,
%Cue{
id: id,
from: from,
to: to,
text: String.trim(rest)
}}
else
{:error, reason} ->
{:error, reason}
end
end
defp parse_cue_id(cue) do
case String.split(cue, "\n", parts: 2) do
[""] ->
{:error, :invalid_cue}
[id_or_timing, body] ->
if String.contains?(id_or_timing, "-->") do
{:ok, "", cue}
else
{:ok, id_or_timing, body}
end
end
end
defp parse_timings(cue) do
with [timings, body] <- split_timings_body_safe(cue),
[raw_from, raw_to] <- String.split(timings, " --> ", parts: 2),
{:ok, from} <- Timing.parse(raw_from),
{:ok, to} <- Timing.parse(raw_to) do
{:ok, from, to, body}
else
_ -> {:error, :invalid_timings}
end
end
defp split_timings_body_safe(cue) do
if String.contains?(cue, "\n") do
String.split(cue, "\n", parts: 2)
else
[cue, ""]
end
end
defp marshal_cue(cue) do
from = Timing.from_ms(cue.from)
to = Timing.from_ms(cue.to)
timing_info = "#{from} --> #{to}"
id =
cond do
cue.id == nil -> nil
String.trim(cue.id) == "" -> nil
true -> String.trim(cue.id)
end
[id, timing_info, cue.text]
|> Enum.reject(&is_nil/1)
|> Enum.join("\n")
end
end