Packages
vtc
0.13.7
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.9
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.2
0.16.1
0.16.0
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.15
0.13.14
0.13.13
0.13.12
0.13.11
0.13.10
0.13.9
0.13.8
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.10
0.10.9
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.3
0.5.2
0.5.1
0.4.0
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
A SMPTE timecode library for Elixir
Current section
Files
Jump to
Current section
Files
lib/ecto/postgres/pg_framestamp.ex
use Vtc.Ecto.Postgres.Utils
defpgmodule Vtc.Ecto.Postgres.PgFramestamp do
@moduledoc """
Defines a composite type for storing rational values as a
[PgRational](`Vtc.Ecto.Postgres.PgRational`) real-world playbck seconds,
[PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`) pair.
These values are cast to
[Framestamp](`Vtc.Framestamp`) structs for use in application code.
The composite types is defined as follows:
```sql
CREATE TYPE framestamp as (
seconds rational,
rate framerate
)
```
```sql
SELECT ((18018, 5), ((24000, 1001), '{non_drop}'))::framestamp
```
## Field migrations
You can create `framerate` fields during a migration like so:
```elixir
alias Vtc.Framestamp
create table("events") do
add(:in, Framestamp.type())
add(:out, Framestamp.type())
end
```
[Framestamp](`Vtc.Framestamp`) re-exports the `Ecto.Type` implementation of this module,
and can be used any place this module would be used.
## Schema fields
Then in your schema module:
```elixir
defmodule MyApp.Event do
@moduledoc false
use Ecto.Schema
alias Vtc.Framestamp
@type t() :: %__MODULE__{
in: Framestamp.t(),
out: Framestamp.t()
}
schema "events" do
field(:in, Framestamp)
field(:out, Framestamp)
end
```
## Changesets
With the above setup, changesets should just work:
```elixir
def changeset(schema, attrs) do
schema
|> Changeset.cast(attrs, [:in, :out])
|> Changeset.validate_required([:in, :out])
end
```
Framerate values can be cast from the following values in changesets:
- [Framestamp](`Vtc.Framestamp`) structs.
- Maps with the following format:
```json
{
"smpte_timecode": "01:00:00:00",
"rate": {
"playback": [24000, 1001],
"ntsc": "non_drop"
}
}
```
Where `smpte_timecode` is properly formatted SMPTE timecode string and `playback`
is a map value supported by [PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`) changeset
casts.
## Fragments
Framestamp values must be explicitly cast using
[type/2](https://hexdocs.pm/ecto/Ecto.Query.html#module-interpolation-and-casting):
```elixir
framestamp = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
query = Query.from(f in fragment("SELECT ? as r", type(^framestamp, Framestamp)), select: f.r)
```
"""
use Ecto.Type
alias Ecto.Changeset
alias Vtc.Ecto.Postgres.PgFramerate
alias Vtc.Ecto.Postgres.PgRational
alias Vtc.Framerate
alias Vtc.Framestamp
alias Vtc.Source.Frames.SMPTETimecodeStr
@doc section: :ecto_migrations
@doc """
The database type for [PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`).
Can be used in migrations as the fields type.
"""
@impl Ecto.Type
@spec type() :: atom()
def type, do: :framestamp
@typedoc """
Type of the raw composite value that will be sent to / received from the database.
"""
@type db_record() :: {PgRational.db_record(), PgFramerate.db_record()}
# Handles casting PgRational fields in `Ecto.Changeset`s.
@doc false
@impl Ecto.Type
@spec cast(Framestamp.t() | %{String.t() => any()} | %{atom() => any()}) :: {:ok, Framerate.t()} | :error
def cast(%Framestamp{} = framestamp), do: {:ok, framestamp}
def cast(json) when is_map(json) do
schema = %{
smpte_timecode: :string,
rate: Framerate
}
changeset =
{%{}, schema}
|> Changeset.cast(json, [:smpte_timecode, :rate])
|> Changeset.validate_required([:smpte_timecode, :rate])
with {:ok, %{smpte_timecode: timecode_str, rate: rate}} <- Changeset.apply_action(changeset, :loaded),
{:ok, _} = result <- Framestamp.with_frames(%SMPTETimecodeStr{in: timecode_str}, rate) do
result
else
_ -> :error
end
end
def cast(_), do: :error
# Handles converting database records into Ratio structs to be used by the
# application.
@doc false
@impl Ecto.Type
@spec load(db_record()) :: {:ok, Framestamp.t()} | :error
def load({seconds, rate}) do
with {:ok, seconds} <- PgRational.load(seconds),
{:ok, framerate} <- PgFramerate.load(rate),
{:ok, _} = result <- Framestamp.with_seconds(seconds, framerate, round: :off) do
result
else
_ -> :error
end
end
def load(_), do: :error
# Handles converting Ratio structs into database records.
@doc false
@impl Ecto.Type
@spec dump(Framestamp.t()) :: {:ok, db_record()} | :error
def dump(%Framestamp{} = framestamp) do
with {:ok, seconds} <- PgRational.dump(framestamp.seconds),
{:ok, framerate} <- PgFramerate.dump(framestamp.rate) do
{:ok, {seconds, framerate}}
end
end
def dump(_), do: :error
end