Initial implementation of media profiles (#7)

* [WIP] Added basic video download method

* [WIP] Very-WIP first steps at parsing options and downloading

* Made my options safe by default and removed special safe versions

* Ran html generator for mediaprofile model - leaving as-is for now

* Addressed a bunch of TODO comments
This commit is contained in:
Kieran 2024-01-22 20:51:28 -08:00 committed by GitHub
parent a4f5024d8f
commit 4252c27f87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 728 additions and 4 deletions

View file

@ -12,7 +12,9 @@ config :pinchflat,
generators: [timestamp_type: :utc_datetime], generators: [timestamp_type: :utc_datetime],
# Specifying backend data here makes mocking and local testing SUPER easy # Specifying backend data here makes mocking and local testing SUPER easy
yt_dlp_executable: System.find_executable("yt-dlp"), yt_dlp_executable: System.find_executable("yt-dlp"),
yt_dlp_runner: Pinchflat.Downloader.Backends.YtDlp.CommandRunner yt_dlp_runner: Pinchflat.Downloader.Backends.YtDlp.CommandRunner,
# TODO: figure this out
media_directory: :not_implemented
# Configures the endpoint # Configures the endpoint
config :pinchflat, PinchflatWeb.Endpoint, config :pinchflat, PinchflatWeb.Endpoint,

View file

@ -1,5 +1,8 @@
import Config import Config
config :pinchflat,
media_directory: Path.join([System.tmp_dir!(), "yt-dlp"])
# Configure your database # Configure your database
config :pinchflat, Pinchflat.Repo, config :pinchflat, Pinchflat.Repo,
username: System.get_env("POSTGRES_USER"), username: System.get_env("POSTGRES_USER"),

View file

@ -2,7 +2,8 @@ import Config
config :pinchflat, config :pinchflat,
# Specifying backend data here makes mocking and local testing SUPER easy # Specifying backend data here makes mocking and local testing SUPER easy
yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]) yt_dlp_executable: Path.join([File.cwd!(), "/test/support/scripts/yt-dlp-mocks/repeater.sh"]),
media_directory: Path.join([System.tmp_dir!(), "yt-dlp"])
# Configure your database # Configure your database
# #

3
ideas.md Normal file
View file

@ -0,0 +1,3 @@
- Write media datbase ID as metadata/to file/whatever so it gives us an option to retroactively match media to the DB down the line. Useful if someone moves the media without informing the UI
- Use a UUID for the media database ID (or at least alongside it)
- Look into this and its recommended plugins https://hexdocs.pm/ex_check/readme.html

View file

@ -10,6 +10,10 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.CommandRunner do
@doc """ @doc """
Runs a yt-dlp command and returns the string output Runs a yt-dlp command and returns the string output
# IDEA: deduplicate command opts, keeping the last one on conflict
although possibly not needed (and a LOT easier) if yt-dlp
just ignores duplicate options (ie: look into that)
""" """
@impl BackendCommandRunner @impl BackendCommandRunner
def run(url, command_opts) do def run(url, command_opts) do

View file

@ -0,0 +1,22 @@
defmodule Pinchflat.Downloader.Backends.YtDlp.Video do
@moduledoc """
Contains utilities for working with singular videos
"""
@doc """
Downloads a single video (and possible metadata) directly to its
final destination. Returns the parsed JSON output from yt-dlp.
"""
def download(url, command_opts \\ []) do
opts = [:no_simulate, :dump_json] ++ command_opts
case backend_runner().run(url, opts) do
{:ok, output} -> Phoenix.json_library().decode(output)
err -> err
end
end
defp backend_runner do
Application.get_env(:pinchflat, :yt_dlp_runner)
end
end

View file

@ -0,0 +1,38 @@
defmodule Pinchflat.Downloader.VideoDownloader do
@moduledoc """
This is the integration layer for actually downloading videos.
It takes into account the media profile's settings in order
to download the video with the desired options.
Technically hardcodes the yt-dlp backend for now, but should leave
it open-ish for future expansion (just in case).
"""
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Downloader.Backends.YtDlp.Video, as: YtDlpVideo
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder, as: YtDlpOptionBuilder
@doc """
Downloads a single video based on the settings in the given media profile.
"""
def download_for_media_profile(url, %MediaProfile{} = media_profile, backend \\ :yt_dlp) do
option_builder = option_builder(backend)
video_backend = video_backend(backend)
{:ok, options} = option_builder.build(media_profile)
video_backend.download(url, options)
end
defp option_builder(backend) do
case backend do
:yt_dlp -> YtDlpOptionBuilder
end
end
defp video_backend(backend) do
case backend do
:yt_dlp -> YtDlpVideo
end
end
end

56
lib/pinchflat/profiles.ex Normal file
View file

@ -0,0 +1,56 @@
defmodule Pinchflat.Profiles do
@moduledoc """
The Profiles context.
"""
import Ecto.Query, warn: false
alias Pinchflat.Repo
alias Pinchflat.Profiles.MediaProfile
@doc """
Returns the list of media_profiles. Returns [%MediaProfile{}, ...]
"""
def list_media_profiles do
Repo.all(MediaProfile)
end
@doc """
Gets a single media_profile.
Returns %MediaProfile{}. Raises `Ecto.NoResultsError` if the Media profile does not exist.
"""
def get_media_profile!(id), do: Repo.get!(MediaProfile, id)
@doc """
Creates a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
"""
def create_media_profile(attrs \\ %{}) do
%MediaProfile{}
|> MediaProfile.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
"""
def update_media_profile(%MediaProfile{} = media_profile, attrs) do
media_profile
|> MediaProfile.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a media_profile. Returns {:ok, %MediaProfile{}} | {:error, %Ecto.Changeset{}}
"""
def delete_media_profile(%MediaProfile{} = media_profile) do
Repo.delete(media_profile)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking media_profile changes.
"""
def change_media_profile(%MediaProfile{} = media_profile, attrs \\ %{}) do
MediaProfile.changeset(media_profile, attrs)
end
end

View file

@ -0,0 +1,23 @@
defmodule Pinchflat.Profiles.MediaProfile do
@moduledoc """
A media profile is a set of settings that can be applied to many media sources
"""
use Ecto.Schema
import Ecto.Changeset
schema "media_profiles" do
field :name, :string
field :output_path_template, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(media_profile, attrs) do
media_profile
|> cast(attrs, [:name, :output_path_template])
|> validate_required([:name, :output_path_template])
|> unique_constraint(:name)
end
end

View file

@ -0,0 +1,42 @@
defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilder do
@moduledoc """
Builds the options for yt-dlp based on the given media profile.
IDEA: consider making this a behaviour so I can add other backends later
"""
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
@doc """
Builds the options for yt-dlp based on the given media profile.
IDEA: consider adding the ability to pass in a second argument to override
these options
"""
def build(%MediaProfile{} = media_profile) do
{:ok, output_path} = OutputPathBuilder.build(media_profile.output_path_template)
# NOTE: I'll be hardcoding most things for now (esp. options to help me test) -
# add more configuration later as I build out the models. Walk before you can run!
{:ok,
[
:write_thumbnail,
:write_subs,
:embed_metadata,
:embed_thumbnail,
:embed_subs,
:write_info_json,
:write_auto_subs,
:no_progress,
convert_thumbnails: "jpg",
sub_langs: "en.*",
output: Path.join(base_directory(), output_path)
]}
end
defp base_directory do
Application.get_env(:pinchflat, :media_directory)
end
end

View file

@ -0,0 +1,64 @@
defmodule Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder do
@moduledoc """
Builds yt-dlp-friendly output paths for downloaded media
IDEA: consider making this a behaviour so I can add other backends later
"""
alias Pinchflat.RenderedString.Parser, as: TemplateParser
@doc """
Builds the actual final filepath from a given template.
Translates liquid-style templates into yt-dlp-style templates,
leaving yt-dlp syntax intact.
"""
def build(template_string) do
TemplateParser.parse(template_string, full_yt_dlp_options_map())
end
defp full_yt_dlp_options_map do
Map.merge(
standard_yt_dlp_option_map(),
custom_yt_dlp_option_map()
)
end
defp standard_yt_dlp_option_map do
%{
# Uppercase "S" means "safe" - ie: filepath-safe
"id" => "%(id)S",
"ext" => "%(ext)S",
"title" => "%(title)S",
"fulltitle" => "%(fulltitle)S",
"uploader" => "%(uploader)S",
"creator" => "%(creator)S",
"upload_date" => "%(upload_date)S",
"release_date" => "%(release_date)S",
"duration" => "%(duration)S",
# For videos classified as an episode of a series:
"series" => "%(series)S",
"season" => "%(season)S",
"season_number" => "%(season_number)S",
"episode" => "%(episode)S",
"episode_number" => "%(episode_number)S",
"episode_id" => "%(episode_id)S",
# For videos classified as music:
"track" => "%(track)S",
"track_number" => "%(track_number)S",
"artist" => "%(artist)S",
"album" => "%(album)S",
"album_type" => "%(album_type)S",
"genre" => "%(genre)S"
}
end
defp custom_yt_dlp_option_map do
%{
# Individual parts of the upload date
"upload_year" => "%(upload_date>%Y)S",
"upload_month" => "%(upload_date>%m)S",
"upload_day" => "%(upload_date>%d)S"
}
end
end

View file

@ -0,0 +1,62 @@
defmodule PinchflatWeb.MediaProfiles.MediaProfileController do
use PinchflatWeb, :controller
alias Pinchflat.Profiles
alias Pinchflat.Profiles.MediaProfile
def index(conn, _params) do
media_profiles = Profiles.list_media_profiles()
render(conn, :index, media_profiles: media_profiles)
end
def new(conn, _params) do
changeset = Profiles.change_media_profile(%MediaProfile{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"media_profile" => media_profile_params}) do
case Profiles.create_media_profile(media_profile_params) do
{:ok, media_profile} ->
conn
|> put_flash(:info, "Media profile created successfully.")
|> redirect(to: ~p"/media_profiles/#{media_profile}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
media_profile = Profiles.get_media_profile!(id)
render(conn, :show, media_profile: media_profile)
end
def edit(conn, %{"id" => id}) do
media_profile = Profiles.get_media_profile!(id)
changeset = Profiles.change_media_profile(media_profile)
render(conn, :edit, media_profile: media_profile, changeset: changeset)
end
def update(conn, %{"id" => id, "media_profile" => media_profile_params}) do
media_profile = Profiles.get_media_profile!(id)
case Profiles.update_media_profile(media_profile, media_profile_params) do
{:ok, media_profile} ->
conn
|> put_flash(:info, "Media profile updated successfully.")
|> redirect(to: ~p"/media_profiles/#{media_profile}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, media_profile: media_profile, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
media_profile = Profiles.get_media_profile!(id)
{:ok, _media_profile} = Profiles.delete_media_profile(media_profile)
conn
|> put_flash(:info, "Media profile deleted successfully.")
|> redirect(to: ~p"/media_profiles")
end
end

View file

@ -0,0 +1,13 @@
defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do
use PinchflatWeb, :html
embed_templates "media_profile_html/*"
@doc """
Renders a media_profile form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def media_profile_form(assigns)
end

View file

@ -0,0 +1,8 @@
<.header>
Edit Media profile <%= @media_profile.id %>
<:subtitle>Use this form to manage media_profile records in your database.</:subtitle>
</.header>
<.media_profile_form changeset={@changeset} action={~p"/media_profiles/#{@media_profile}"} />
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back>

View file

@ -0,0 +1,30 @@
<.header>
Listing Media profiles
<:actions>
<.link href={~p"/media_profiles/new"}>
<.button>New Media profile</.button>
</.link>
</:actions>
</.header>
<.table
id="media_profiles"
rows={@media_profiles}
row_click={&JS.navigate(~p"/media_profiles/#{&1}")}
>
<:col :let={media_profile} label="Name"><%= media_profile.name %></:col>
<:col :let={media_profile} label="Output path template">
<%= media_profile.output_path_template %>
</:col>
<:action :let={media_profile}>
<div class="sr-only">
<.link navigate={~p"/media_profiles/#{media_profile}"}>Show</.link>
</div>
<.link navigate={~p"/media_profiles/#{media_profile}/edit"}>Edit</.link>
</:action>
<:action :let={media_profile}>
<.link href={~p"/media_profiles/#{media_profile}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

View file

@ -0,0 +1,10 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:output_path_template]} type="text" label="Output path template" />
<:actions>
<.button>Save Media profile</.button>
</:actions>
</.simple_form>

View file

@ -0,0 +1,8 @@
<.header>
New Media profile
<:subtitle>Use this form to manage media_profile records in your database.</:subtitle>
</.header>
<.media_profile_form changeset={@changeset} action={~p"/media_profiles"} />
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back>

View file

@ -0,0 +1,16 @@
<.header>
Media profile <%= @media_profile.id %>
<:subtitle>This is a media_profile record from your database.</:subtitle>
<:actions>
<.link href={~p"/media_profiles/#{@media_profile}/edit"}>
<.button>Edit media_profile</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Name"><%= @media_profile.name %></:item>
<:item title="Output path template"><%= @media_profile.output_path_template %></:item>
</.list>
<.back navigate={~p"/media_profiles"}>Back to media_profiles</.back>

View file

@ -18,6 +18,8 @@ defmodule PinchflatWeb.Router do
pipe_through :browser pipe_through :browser
get "/", PageController, :home get "/", PageController, :home
resources "/media_profiles", MediaProfiles.MediaProfileController
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.

View file

@ -0,0 +1,14 @@
defmodule Pinchflat.Repo.Migrations.CreateMediaProfiles do
use Ecto.Migration
def change do
create table(:media_profiles) do
add :name, :string, null: false
add :output_path_template, :string, null: false
timestamps(type: :utc_datetime)
end
create unique_index(:media_profiles, [:name])
end
end

View file

@ -0,0 +1,25 @@
defmodule Pinchflat.Downloader.Backends.YtDlp.OutputPathBuilderTest do
use ExUnit.Case, async: true
alias Pinchflat.Profiles.Options.YtDlp.OutputPathBuilder
describe "build/1" do
test "it expands 'standard' curly brace variables in the template" do
assert {:ok, res} = OutputPathBuilder.build("/videos/{{ title }}.{{ ext }}")
assert res == "/videos/%(title)S.%(ext)S"
end
test "it expands 'custom' curly brace variables in the template" do
assert {:ok, res} = OutputPathBuilder.build("/videos/{{ upload_year }}.{{ ext }}")
assert res == "/videos/%(upload_date>%Y)S.%(ext)S"
end
test "it leaves yt-dlp variables alone" do
assert {:ok, res} = OutputPathBuilder.build("/videos/%(title)s.%(ext)s")
assert res == "/videos/%(title)s.%(ext)s"
end
end
end

View file

@ -2,7 +2,7 @@ defmodule Pinchflat.Downloader.Backends.YtDlp.VideoCollectionTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
import Mox import Mox
alias Pinchflat.Downloader.Backends.YtDlp.VideoCollection, as: VideoCollection alias Pinchflat.Downloader.Backends.YtDlp.VideoCollection
@channel_url "https://www.youtube.com/@TheUselessTrials" @channel_url "https://www.youtube.com/@TheUselessTrials"

View file

@ -0,0 +1,44 @@
defmodule Pinchflat.Downloader.Backends.YtDlp.VideoTest do
use ExUnit.Case, async: true
import Mox
alias Pinchflat.Downloader.Backends.YtDlp.Video
@video_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
setup :verify_on_exit!
describe "download/2" do
test "it calls the backend runner with the expected arguments" do
expect(CommandRunnerMock, :run, fn @video_url, opts ->
assert opts == [:no_simulate, :dump_json]
{:ok, "{}"}
end)
assert {:ok, _} = Video.download(@video_url)
end
test "it passes along additional options" do
expect(CommandRunnerMock, :run, fn _url, opts ->
assert opts == [:no_simulate, :dump_json, :custom_arg]
{:ok, "{}"}
end)
assert {:ok, _} = Video.download(@video_url, [:custom_arg])
end
test "it parses the output as JSON" do
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "{\"title\": \"Test\"}"} end)
assert {:ok, %{"title" => "Test"}} = Video.download(@video_url)
end
test "it directly passes along any errors" do
expect(CommandRunnerMock, :run, fn _url, _opts -> {:error, "Big issue", 1} end)
assert {:error, "Big issue", 1} = Video.download(@video_url)
end
end
end

View file

@ -0,0 +1,35 @@
defmodule Pinchflat.Downloader.VideoDownloaderTest do
use ExUnit.Case, async: true
import Mox
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Downloader.VideoDownloader
@video_url "https://www.youtube.com/watch?v=TiZPUDkDYbk"
@media_profile %MediaProfile{
output_path_template: "videos/{{ title }}.%(ext)s"
}
setup :verify_on_exit!
describe "download_for_media_profile/3" do
test "it calls the backend runner with the arguments built from the media profile" do
expect(CommandRunnerMock, :run, fn @video_url, opts ->
assert :no_simulate in opts
assert :dump_json in opts
assert {:output, "/tmp/yt-dlp/videos/%(title)S.%(ext)s"} in opts
{:ok, "{}"}
end)
assert {:ok, _} = VideoDownloader.download_for_media_profile(@video_url, @media_profile)
end
test "it returns the parsed JSON output" do
expect(CommandRunnerMock, :run, fn _url, _opts -> {:ok, "{\"title\": \"Test\"}"} end)
assert {:ok, %{"title" => "Test"}} =
VideoDownloader.download_for_media_profile(@video_url, @media_profile)
end
end
end

View file

@ -0,0 +1,18 @@
defmodule Pinchflat.Profiles.Options.YtDlp.OptionBuilderTest do
use ExUnit.Case, async: true
alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Profiles.Options.YtDlp.OptionBuilder
@media_profile %MediaProfile{
output_path_template: "videos/{{ title }}.%(ext)s"
}
describe "build/1" do
test "it generates an expanded output path based on the given template" do
assert {:ok, res} = OptionBuilder.build(@media_profile)
assert {:output, "/tmp/yt-dlp/videos/%(title)S.%(ext)s"} in res
end
end
end

View file

@ -0,0 +1,70 @@
defmodule Pinchflat.ProfilesTest do
use Pinchflat.DataCase
alias Pinchflat.Profiles
describe "media_profiles" do
alias Pinchflat.Profiles.MediaProfile
import Pinchflat.ProfilesFixtures
@invalid_attrs %{name: nil, output_path_template: nil}
test "list_media_profiles/0 returns all media_profiles" do
media_profile = media_profile_fixture()
assert Profiles.list_media_profiles() == [media_profile]
end
test "get_media_profile!/1 returns the media_profile with given id" do
media_profile = media_profile_fixture()
assert Profiles.get_media_profile!(media_profile.id) == media_profile
end
test "create_media_profile/1 with valid data creates a media_profile" do
valid_attrs = %{name: "some name", output_path_template: "some output_path_template"}
assert {:ok, %MediaProfile{} = media_profile} = Profiles.create_media_profile(valid_attrs)
assert media_profile.name == "some name"
assert media_profile.output_path_template == "some output_path_template"
end
test "create_media_profile/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Profiles.create_media_profile(@invalid_attrs)
end
test "update_media_profile/2 with valid data updates the media_profile" do
media_profile = media_profile_fixture()
update_attrs = %{
name: "some updated name",
output_path_template: "some updated output_path_template"
}
assert {:ok, %MediaProfile{} = media_profile} =
Profiles.update_media_profile(media_profile, update_attrs)
assert media_profile.name == "some updated name"
assert media_profile.output_path_template == "some updated output_path_template"
end
test "update_media_profile/2 with invalid data returns error changeset" do
media_profile = media_profile_fixture()
assert {:error, %Ecto.Changeset{}} =
Profiles.update_media_profile(media_profile, @invalid_attrs)
assert media_profile == Profiles.get_media_profile!(media_profile.id)
end
test "delete_media_profile/1 deletes the media_profile" do
media_profile = media_profile_fixture()
assert {:ok, %MediaProfile{}} = Profiles.delete_media_profile(media_profile)
assert_raise Ecto.NoResultsError, fn -> Profiles.get_media_profile!(media_profile.id) end
end
test "change_media_profile/1 returns a media_profile changeset" do
media_profile = media_profile_fixture()
assert %Ecto.Changeset{} = Profiles.change_media_profile(media_profile)
end
end
end

View file

@ -1,7 +1,7 @@
defmodule Pinchflat.Utils.StringUtilsTest do defmodule Pinchflat.Utils.StringUtilsTest do
use ExUnit.Case, async: true use ExUnit.Case, async: true
alias Pinchflat.Utils.StringUtils, as: StringUtils alias Pinchflat.Utils.StringUtils
describe "to_kebab_case/1" do describe "to_kebab_case/1" do
test "converts a space-delimited string to kebab-case" do test "converts a space-delimited string to kebab-case" do

View file

@ -0,0 +1,90 @@
defmodule PinchflatWeb.MediaProfileControllerTest do
use PinchflatWeb.ConnCase
import Pinchflat.ProfilesFixtures
@create_attrs %{name: "some name", output_path_template: "some output_path_template"}
@update_attrs %{
name: "some updated name",
output_path_template: "some updated output_path_template"
}
@invalid_attrs %{name: nil, output_path_template: nil}
describe "index" do
test "lists all media_profiles", %{conn: conn} do
conn = get(conn, ~p"/media_profiles")
assert html_response(conn, 200) =~ "Listing Media profiles"
end
end
describe "new media_profile" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/media_profiles/new")
assert html_response(conn, 200) =~ "New Media profile"
end
end
describe "create media_profile" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, ~p"/media_profiles", media_profile: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == ~p"/media_profiles/#{id}"
conn = get(conn, ~p"/media_profiles/#{id}")
assert html_response(conn, 200) =~ "Media profile #{id}"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, ~p"/media_profiles", media_profile: @invalid_attrs)
assert html_response(conn, 200) =~ "New Media profile"
end
end
describe "edit media_profile" do
setup [:create_media_profile]
test "renders form for editing chosen media_profile", %{
conn: conn,
media_profile: media_profile
} do
conn = get(conn, ~p"/media_profiles/#{media_profile}/edit")
assert html_response(conn, 200) =~ "Edit Media profile"
end
end
describe "update media_profile" do
setup [:create_media_profile]
test "redirects when data is valid", %{conn: conn, media_profile: media_profile} do
conn = put(conn, ~p"/media_profiles/#{media_profile}", media_profile: @update_attrs)
assert redirected_to(conn) == ~p"/media_profiles/#{media_profile}"
conn = get(conn, ~p"/media_profiles/#{media_profile}")
assert html_response(conn, 200) =~ "some updated name"
end
test "renders errors when data is invalid", %{conn: conn, media_profile: media_profile} do
conn = put(conn, ~p"/media_profiles/#{media_profile}", media_profile: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Media profile"
end
end
describe "delete media_profile" do
setup [:create_media_profile]
test "deletes chosen media_profile", %{conn: conn, media_profile: media_profile} do
conn = delete(conn, ~p"/media_profiles/#{media_profile}")
assert redirected_to(conn) == ~p"/media_profiles"
assert_error_sent 404, fn ->
get(conn, ~p"/media_profiles/#{media_profile}")
end
end
end
defp create_media_profile(_) do
media_profile = media_profile_fixture()
%{media_profile: media_profile}
end
end

View file

@ -0,0 +1,21 @@
defmodule Pinchflat.ProfilesFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `Pinchflat.Profiles` context.
"""
@doc """
Generate a media_profile.
"""
def media_profile_fixture(attrs \\ %{}) do
{:ok, media_profile} =
attrs
|> Enum.into(%{
name: "some name",
output_path_template: "some output_path_template"
})
|> Pinchflat.Profiles.create_media_profile()
media_profile
end
end