Skip to content

Commit

Permalink
Upgrade user and recording controllers to Phx 1.7 style, refactor Rec…
Browse files Browse the repository at this point in the history
…ordings module
  • Loading branch information
ku1ik committed Mar 12, 2024
1 parent fe685e0 commit 12e49f0
Show file tree
Hide file tree
Showing 59 changed files with 1,303 additions and 1,091 deletions.
15 changes: 6 additions & 9 deletions lib/asciinema/png_generator/rsvg.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,12 @@ defmodule Asciinema.PngGenerator.Rsvg do
png_path = Path.join(tmp_dir_path, "tmp.png")

svg =
AsciinemaWeb.RecordingView.render(
"show.svg",
%{
asciicast: asciicast,
font_family: font_family(),
rx: 0,
ry: 0
}
)
AsciinemaWeb.RecordingSVG.show(%{
asciicast: asciicast,
font_family: font_family(),
rx: 0,
ry: 0
})

File.write!(svg_path, svg)

Expand Down
110 changes: 81 additions & 29 deletions lib/asciinema/recordings.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
defmodule Asciinema.Recordings do
require Logger
import Ecto.Changeset
import Ecto.Query, warn: false
alias Asciinema.{FileStore, Fonts, Repo, Themes, Vt}
alias Asciinema.Recordings.{Asciicast, Output, Paths, SnapshotUpdater, Text}
alias Asciinema.Recordings.{Asciicast, Markers, Output, Paths, Snapshot, SnapshotUpdater, Text}
alias Ecto.Changeset

def fetch_asciicast(id) do
Expand Down Expand Up @@ -124,18 +125,22 @@ defmodule Asciinema.Recordings do
:ok
end

def create_asciicast(user, params, overrides \\ %{})
def create_asciicast(user, upload, overrides \\ %{})

def create_asciicast(user, %Plug.Upload{filename: filename} = upload, overrides) do
asciicast = %Asciicast{
user_id: user.id,
filename: filename,
private: user.asciicasts_private_by_default
}
changeset =
change(
%Asciicast{
user_id: user.id,
filename: filename,
private: user.asciicasts_private_by_default,
secret_token: Crypto.random_token(25)
},
overrides
)

with {:ok, attrs} <- extract_metadata(upload),
attrs = Map.merge(attrs, overrides),
changeset = Asciicast.create_changeset(asciicast, attrs),
with {:ok, metadata} <- extract_metadata(upload),
changeset = apply_metadata(changeset, metadata),
{:ok, %Asciicast{} = asciicast} <- do_create_asciicast(changeset, upload) do
if asciicast.snapshot == nil do
:ok = SnapshotUpdater.update_snapshot(asciicast)
Expand Down Expand Up @@ -230,6 +235,27 @@ defmodule Asciinema.Recordings do
|> Enum.reduce(fn {t, _}, _prev_t -> t end)
end

defp apply_metadata(changeset, metadata) do
changeset
|> put_change(:version, metadata.version)
|> cast(metadata, [
:duration,
:cols,
:rows,
:terminal_type,
:command,
:shell,
:uname,
:recorded_at,
:theme_fg,
:theme_bg,
:theme_palette,
:idle_time_limit,
:title
])
|> validate_required([:duration, :cols, :rows])
end

defp decode_json(json) do
case Jason.decode(json) do
{:ok, thing} -> {:ok, thing}
Expand Down Expand Up @@ -266,17 +292,37 @@ defmodule Asciinema.Recordings do
end

def change_asciicast(asciicast, attrs \\ %{}) do
Asciicast.update_changeset(asciicast, attrs)
asciicast
|> cast(attrs, [
:private,
:title,
:description,
:cols_override,
:rows_override,
:theme_name,
:idle_time_limit,
:speed,
:snapshot_at,
:terminal_line_height,
:terminal_font_family,
:markers
])
|> validate_required([:private])
|> validate_number(:cols_override, greater_than: 0, less_than: 1024)
|> validate_number(:rows_override, greater_than: 0, less_than: 512)
|> validate_number(:idle_time_limit, greater_than_or_equal_to: 0.5)
|> validate_inclusion(:theme_name, Themes.terminal_themes())
|> validate_number(:terminal_line_height,
greater_than_or_equal_to: 1.0,
less_than_or_equal_to: 2.0
)
|> validate_inclusion(:terminal_font_family, Fonts.terminal_font_families())
|> validate_number(:snapshot_at, greater_than: 0)
|> validate_change(:markers, &Markers.validate/2)
end

def update_asciicast(asciicast, attrs \\ %{}) do
changeset =
Asciicast.update_changeset(
asciicast,
attrs,
Fonts.terminal_font_families(),
Themes.terminal_themes()
)
changeset = change_asciicast(asciicast, attrs)

with {:ok, asciicast} <- Repo.update(changeset) do
if stale_snapshot?(changeset) do
Expand All @@ -293,13 +339,6 @@ defmodule Asciinema.Recordings do
changed?(changeset, :rows_override)
end

defp changed?(changeset, field) do
case Changeset.get_change(changeset, field, :none) do
:none -> false
_ -> true
end
end

def set_featured(asciicast, featured \\ true) do
asciicast
|> Changeset.change(%{featured: featured})
Expand Down Expand Up @@ -359,24 +398,37 @@ defmodule Asciinema.Recordings do
case cursor do
{x, y} ->
lines
|> AsciinemaWeb.RecordingView.split_segments()
|> Snapshot.split_segments()
|> List.update_at(y, fn line ->
List.update_at(line, x, fn {text, attrs} ->
attrs = Map.put(attrs, "inverse", !(attrs["inverse"] || false))
{text, attrs}
end)
end)
|> AsciinemaWeb.RecordingView.group_segments()
|> Snapshot.group_segments()

_ ->
lines
end

Enum.map(lines, fn chunks ->
Enum.map(chunks, &Tuple.to_list/1)
Enum.map(lines, fn segments ->
Enum.map(segments, &Tuple.to_list/1)
end)
end

def title(asciicast) do
cond do
asciicast.title not in [nil, ""] ->
asciicast.title

asciicast.command not in [nil, ""] && asciicast.command != asciicast.shell ->
asciicast.command

true ->
"untitled"
end
end

defdelegate text(asciicast), to: Text
defdelegate text_file_path(asciicast), to: Text

Expand Down
99 changes: 0 additions & 99 deletions lib/asciinema/recordings/asciicast.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule Asciinema.Recordings.Asciicast do
use Ecto.Schema
import Ecto.Changeset
alias Asciinema.Accounts.User
alias Asciinema.Recordings.Asciicast

Expand Down Expand Up @@ -62,104 +61,6 @@ defmodule Asciinema.Recordings.Asciicast do
end
end

defp changeset(struct, attrs) do
struct
|> cast(attrs, [:title, :private, :snapshot_at])
|> validate_required([:private])
end

def create_changeset(struct, attrs) do
struct
|> changeset(attrs)
|> cast(attrs, [
:version,
:duration,
:cols,
:rows,
:terminal_type,
:command,
:shell,
:uname,
:user_agent,
:recorded_at,
:theme_fg,
:theme_bg,
:theme_palette,
:idle_time_limit,
:snapshot
])
|> validate_required([:user_id, :version, :duration, :cols, :rows])
|> generate_secret_token
end

def update_changeset(struct, attrs, terminal_font_families \\ [], themes \\ []) do
struct
|> changeset(attrs)
|> cast(attrs, [
:description,
:cols_override,
:rows_override,
:theme_name,
:idle_time_limit,
:speed,
:terminal_line_height,
:terminal_font_family,
:markers
])
|> validate_number(:cols_override, greater_than: 0, less_than: 1024)
|> validate_number(:rows_override, greater_than: 0, less_than: 512)
|> validate_number(:idle_time_limit, greater_than_or_equal_to: 0.5)
|> validate_inclusion(:theme_name, themes)
|> validate_number(:terminal_line_height,
greater_than_or_equal_to: 1.0,
less_than_or_equal_to: 2.0
)
|> validate_inclusion(:terminal_font_family, terminal_font_families)
|> validate_number(:snapshot_at, greater_than: 0)
|> validate_change(:markers, &validate_markers/2)
end

defp validate_markers(_, markers) do
case parse_markers(markers) do
{:ok, _} -> []
{:error, index} -> [markers: "invalid syntax in line #{index + 1}"]
end
end

def parse_markers(markers) do
results =
markers
|> String.trim()
|> String.split("\n")
|> Enum.map(&parse_marker/1)

case Enum.find_index(results, fn result -> result == :error end) do
nil -> {:ok, results}
index -> {:error, index}
end
end

defp parse_marker(marker) do
parts =
marker
|> String.trim()
|> String.split(~r/\s+-\s+/, parts: 2)
|> Kernel.++([""])
|> Enum.take(2)

with [t, l] <- parts,
{t, ""} <- Float.parse(t),
true <- String.length(l) < 100 do
{t, l}
else
_ -> :error
end
end

defp generate_secret_token(changeset) do
put_change(changeset, :secret_token, Crypto.random_token(25))
end

def snapshot_at(%Asciicast{snapshot_at: snapshot_at, duration: duration}) do
snapshot_at || duration / 2
end
Expand Down
38 changes: 38 additions & 0 deletions lib/asciinema/recordings/markers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
defmodule Asciinema.Recordings.Markers do
def validate(_, markers) do
case parse(markers) do
{:ok, _} -> []
{:error, index} -> [markers: "invalid syntax in line #{index + 1}"]
end
end

def parse(markers) do
results =
markers
|> String.trim()
|> String.split("\n")
|> Enum.map(&parse_one/1)

case Enum.find_index(results, fn result -> result == :error end) do
nil -> {:ok, results}
index -> {:error, index}
end
end

defp parse_one(marker) do
parts =
marker
|> String.trim()
|> String.split(~r/\s+-\s+/, parts: 2)
|> Kernel.++([""])
|> Enum.take(2)

with [t, l] <- parts,
{t, ""} <- Float.parse(t),
true <- String.length(l) < 100 do
{t, l}
else
_ -> :error
end
end
end
33 changes: 33 additions & 0 deletions lib/asciinema/recordings/snapshot.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule Asciinema.Recordings.Snapshot do
def split_segments(lines) do
Enum.map(lines, fn line ->
Enum.flat_map(line, &split_segment/1)
end)
end

defp split_segment([text, attrs]), do: split_segment({text, attrs})

defp split_segment({text, attrs}) do
text
|> String.codepoints()
|> Enum.map(&{&1, attrs})
end

def group_segments(lines), do: Enum.map(lines, &group_line_segments/1)

defp group_line_segments([]), do: []

defp group_line_segments([first_segment | segments]) do
{segments, last_segment} =
Enum.reduce(segments, {[], first_segment}, fn {text, attrs},
{segments, {prev_text, prev_attrs}} ->
if attrs == prev_attrs do
{segments, {prev_text <> text, attrs}}
else
{[{prev_text, prev_attrs} | segments], {text, attrs}}
end
end)

Enum.reverse([last_segment | segments])
end
end
2 changes: 1 addition & 1 deletion lib/asciinema_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule AsciinemaWeb do
def new_controller do
quote do
use Phoenix.Controller,
formats: [:html, :json]
formats: [:html, :json, :svg]

import Plug.Conn
import AsciinemaWeb.Gettext
Expand Down
Loading

0 comments on commit 12e49f0

Please sign in to comment.