Change Channel to Source (#12)

* Ensure channel detail lookup doesn't download the video - oops

* Ran the needful migrations for replacing channels with sources

* got media source test back working

* channel tasks test

* Media indexing worker test

* media tests

* Got all tests working except controller tests

* got all tests working

* Renamed Channel struct to Source

* renamed ChannelTasks

* More renaming; renamed channel details module

* Removed Channel yt-dlp module, instead throwing things in the VideoCollection module

* Renamed what looks like the last of the outstanding data
This commit is contained in:
Kieran
2024-02-02 17:45:21 -08:00
committed by GitHub
parent 977b69b7c3
commit 366fd80213
46 changed files with 805 additions and 762 deletions
+4 -4
View File
@@ -8,7 +8,7 @@ defmodule Pinchflat.Media do
alias Pinchflat.Repo
alias Pinchflat.Tasks
alias Pinchflat.Media.MediaItem
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaSource.Source
@doc """
Returns the list of media_items. Returns [%MediaItem{}, ...].
@@ -18,15 +18,15 @@ defmodule Pinchflat.Media do
end
@doc """
Returns a list of pending media_items for a given channel, where
Returns a list of pending media_items for a given source, where
pending means the `media_filepath` is `nil`.
Returns [%MediaItem{}, ...].
"""
def list_pending_media_items_for(%Channel{} = channel) do
def list_pending_media_items_for(%Source{} = source) do
from(
m in MediaItem,
where: m.channel_id == ^channel.id and is_nil(m.media_filepath)
where: m.source_id == ^source.id and is_nil(m.media_filepath)
)
|> Repo.all()
end
+5 -5
View File
@@ -7,11 +7,11 @@ defmodule Pinchflat.Media.MediaItem do
import Ecto.Changeset
alias Pinchflat.Tasks.Task
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaSource.Source
alias Pinchflat.Media.MediaMetadata
@required_fields ~w(media_id channel_id)a
@allowed_fields ~w(title media_id media_filepath channel_id subtitle_filepaths)a
@required_fields ~w(media_id source_id)a
@allowed_fields ~w(title media_id media_filepath source_id subtitle_filepaths)a
schema "media_items" do
field :title, :string
@@ -22,7 +22,7 @@ defmodule Pinchflat.Media.MediaItem do
# Will very likely revisit because I can't leave well-enough alone.
field :subtitle_filepaths, {:array, {:array, :string}}, default: []
belongs_to :channel, Channel
belongs_to :source, Source
has_one :metadata, MediaMetadata, on_replace: :update
@@ -37,6 +37,6 @@ defmodule Pinchflat.Media.MediaItem do
|> cast(attrs, @allowed_fields)
|> cast_assoc(:metadata, with: &MediaMetadata.changeset/2, required: false)
|> validate_required(@required_fields)
|> unique_constraint([:media_id, :channel_id])
|> unique_constraint([:media_id, :source_id])
end
end
@@ -1,32 +0,0 @@
defmodule Pinchflat.MediaClient.Backends.YtDlp.Channel do
@moduledoc """
Contains utilities for working with a channel's videos
"""
use Pinchflat.MediaClient.Backends.YtDlp.VideoCollection
alias Pinchflat.MediaClient.ChannelDetails
@doc """
Gets a channel's ID and name from its URL.
yt-dlp does not _really_ have channel-specific functions, so
instead we're fetching just the first video (using playlist_end: 1)
and parsing the channel ID and name from _its_ metadata
Returns {:ok, %ChannelDetails{}} | {:error, any, ...}.
"""
def get_channel_details(channel_url) do
opts = [playlist_end: 1]
with {:ok, output} <- backend_runner().run(channel_url, opts, "%(.{channel,channel_id})j"),
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
{:ok, ChannelDetails.new(parsed_json["channel_id"], parsed_json["channel"])}
else
err -> err
end
end
defp backend_runner do
Application.get_env(:pinchflat, :yt_dlp_runner)
end
end
@@ -1,28 +1,47 @@
defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollection do
@moduledoc """
Contains utilities for working with collections of videos (ie: channels, playlists).
Meant to be included in other modules but can be used on its own. Channels and playlists
will have many of their own methods, but also share a lot of methods. This module is for
those shared methods.
Contains utilities for working with collections of
videos (aka: a source [ie: channels, playlists]).
"""
defmacro __using__(_) do
quote do
@doc """
Returns a list of strings representing the video ids in the collection.
alias Pinchflat.MediaClient.SourceDetails
Returns {:ok, [binary()]} | {:error, any, ...}.
"""
def get_video_ids(url, command_opts \\ []) do
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
opts = command_opts ++ [:simulate, :skip_download]
@doc """
Returns a list of strings representing the video ids in the collection.
case runner.run(url, opts, "%(id)s") do
{:ok, output} -> {:ok, String.split(output, "\n", trim: true)}
res -> res
end
end
Returns {:ok, [binary()]} | {:error, any, ...}.
"""
def get_video_ids(url, command_opts \\ []) do
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
opts = command_opts ++ [:simulate, :skip_download]
case runner.run(url, opts, "%(id)s") do
{:ok, output} -> {:ok, String.split(output, "\n", trim: true)}
res -> res
end
end
@doc """
Gets a source's ID and name from its URL.
yt-dlp does not _really_ have source-specific functions, so
instead we're fetching just the first video (using playlist_end: 1)
and parsing the source ID and name from _its_ metadata
Returns {:ok, %SourceDetails{}} | {:error, any, ...}.
"""
def get_source_details(source_url) do
opts = [:skip_download, playlist_end: 1]
with {:ok, output} <- backend_runner().run(source_url, opts, "%(.{channel,channel_id})j"),
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
{:ok, SourceDetails.new(parsed_json["channel_id"], parsed_json["channel"])}
else
err -> err
end
end
defp backend_runner do
Application.get_env(:pinchflat, :yt_dlp_runner)
end
end
@@ -1,41 +0,0 @@
defmodule Pinchflat.MediaClient.ChannelDetails do
@moduledoc """
This is the integration layer for actually working with channels.
Technically hardcodes the yt-dlp backend for now, but should leave
it open-ish for future expansion (just in case).
"""
@enforce_keys [:id, :name]
defstruct [:id, :name]
alias Pinchflat.MediaClient.Backends.YtDlp.Channel, as: YtDlpChannel
@doc false
def new(id, name) do
%__MODULE__{id: id, name: name}
end
@doc """
Gets a channel's ID and name from its URL, using the given backend.
Returns {:ok, map()} | {:error, any, ...}.
"""
def get_channel_details(channel_url, backend \\ :yt_dlp) do
channel_module(backend).get_channel_details(channel_url)
end
@doc """
Returns a list of video IDs for the given channel URL, using the given backend.
Returns {:ok, list(binary())} | {:error, any, ...}.
"""
def get_video_ids(channel_url, backend \\ :yt_dlp) do
channel_module(backend).get_video_ids(channel_url)
end
defp channel_module(backend) do
case backend do
:yt_dlp -> YtDlpChannel
end
end
end
@@ -0,0 +1,41 @@
defmodule Pinchflat.MediaClient.SourceDetails do
@moduledoc """
This is the integration layer for actually working with sources.
Technically hardcodes the yt-dlp backend for now, but should leave
it open-ish for future expansion (just in case).
"""
@enforce_keys [:id, :name]
defstruct [:id, :name]
alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection, as: YtDlpSource
@doc false
def new(id, name) do
%__MODULE__{id: id, name: name}
end
@doc """
Gets a source's ID and name from its URL, using the given backend.
Returns {:ok, map()} | {:error, any, ...}.
"""
def get_source_details(source_url, backend \\ :yt_dlp) do
source_module(backend).get_source_details(source_url)
end
@doc """
Returns a list of video IDs for the given source URL, using the given backend.
Returns {:ok, list(binary())} | {:error, any, ...}.
"""
def get_video_ids(source_url, backend \\ :yt_dlp) do
source_module(backend).get_video_ids(source_url)
end
defp source_module(backend) do
case backend do
:yt_dlp -> YtDlpSource
end
end
end
@@ -25,8 +25,8 @@ defmodule Pinchflat.MediaClient.VideoDownloader do
Returns {:ok, %MediaItem{}} | {:error, any, ...any}
"""
def download_for_media_item(%MediaItem{} = media_item, backend \\ :yt_dlp) do
item_with_preloads = Repo.preload(media_item, [:metadata, channel: :media_profile])
media_profile = item_with_preloads.channel.media_profile
item_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile])
media_profile = item_with_preloads.source.media_profile
case download_for_media_profile(media_item.media_id, media_profile, backend) do
{:ok, parsed_json} ->
+53 -53
View File
@@ -8,34 +8,34 @@ defmodule Pinchflat.MediaSource do
alias Pinchflat.Tasks
alias Pinchflat.Media
alias Pinchflat.Tasks.ChannelTasks
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaClient.ChannelDetails
alias Pinchflat.Tasks.SourceTasks
alias Pinchflat.MediaSource.Source
alias Pinchflat.MediaClient.SourceDetails
@doc """
Returns the list of channels. Returns [%Channel{}, ...]
Returns the list of sources. Returns [%Source{}, ...]
"""
def list_channels do
Repo.all(Channel)
def list_sources do
Repo.all(Source)
end
@doc """
Gets a single channel.
Gets a single source.
Returns %Channel{}. Raises `Ecto.NoResultsError` if the Channel does not exist.
Returns %Source{}. Raises `Ecto.NoResultsError` if the Source does not exist.
"""
def get_channel!(id), do: Repo.get!(Channel, id)
def get_source!(id), do: Repo.get!(Source, id)
@doc """
Creates a channel. May attempt to pull additional channel details from the
original_url (if provided). Will attempt to start indexing the channel's
Creates a source. May attempt to pull additional source details from the
original_url (if provided). Will attempt to start indexing the source's
media if successfully inserted.
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
"""
def create_channel(attrs) do
%Channel{}
|> change_channel_from_url(attrs)
def create_source(attrs) do
%Source{}
|> change_source_from_url(attrs)
|> commit_and_start_indexing()
end
@@ -45,12 +45,12 @@ defmodule Pinchflat.MediaSource do
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
"""
def index_media_items(%Channel{} = channel) do
{:ok, media_ids} = ChannelDetails.get_video_ids(channel.original_url)
def index_media_items(%Source{} = source) do
{:ok, media_ids} = SourceDetails.get_video_ids(source.original_url)
media_ids
|> Enum.map(fn media_id ->
attrs = %{channel_id: channel.id, media_id: media_id}
attrs = %{source_id: source.id, media_id: media_id}
case Media.create_media_item(attrs) do
{:ok, media_item} -> media_item
@@ -60,67 +60,67 @@ defmodule Pinchflat.MediaSource do
end
@doc """
Updates a channel. May attempt to pull additional channel details from the
original_url (if changed). May attempt to start indexing the channel's
Updates a source. May attempt to pull additional source details from the
original_url (if changed). May attempt to start indexing the source's
media if the indexing frequency has been changed.
Existing indexing tasks will be cancelled if the indexing frequency has been
changed (logic in `ChannelTasks.kickoff_indexing_task`)
changed (logic in `SourceTasks.kickoff_indexing_task`)
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
"""
def update_channel(%Channel{} = channel, attrs) do
channel
|> change_channel_from_url(attrs)
def update_source(%Source{} = source, attrs) do
source
|> change_source_from_url(attrs)
|> commit_and_start_indexing()
end
@doc """
Deletes a channel and it's associated tasks (of any state).
Deletes a source and it's associated tasks (of any state).
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
"""
def delete_channel(%Channel{} = channel) do
Tasks.delete_tasks_for(channel)
Repo.delete(channel)
def delete_source(%Source{} = source) do
Tasks.delete_tasks_for(source)
Repo.delete(source)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking channel changes.
Returns an `%Ecto.Changeset{}` for tracking source changes.
"""
def change_channel(%Channel{} = channel, attrs \\ %{}) do
Channel.changeset(channel, attrs)
def change_source(%Source{} = source, attrs \\ %{}) do
Source.changeset(source, attrs)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking channel changes and additionally
fetches channel details from the original_url (if provided). If the channel
Returns an `%Ecto.Changeset{}` for tracking source changes and additionally
fetches source details from the original_url (if provided). If the source
details cannot be fetched, an error is added to the changeset.
Note that this fetches channel details as long as the `original_url` is present.
Note that this fetches source details as long as the `original_url` is present.
This means that it'll go for it even if a changeset is otherwise invalid. This
is pretty easy to change, but for MVP I'm not concerned.
"""
def change_channel_from_url(%Channel{} = channel, attrs) do
case change_channel(channel, attrs) do
def change_source_from_url(%Source{} = source, attrs) do
case change_source(source, attrs) do
%Ecto.Changeset{changes: %{original_url: _}} = changeset ->
add_channel_details_to_changeset(channel, changeset)
add_source_details_to_changeset(source, changeset)
changeset ->
changeset
end
end
defp add_channel_details_to_changeset(channel, changeset) do
defp add_source_details_to_changeset(source, changeset) do
%Ecto.Changeset{changes: changes} = changeset
case ChannelDetails.get_channel_details(changes.original_url) do
{:ok, %ChannelDetails{} = channel_details} ->
change_channel(
channel,
case SourceDetails.get_source_details(changes.original_url) do
{:ok, %SourceDetails{} = source_details} ->
change_source(
source,
Map.merge(changes, %{
name: channel_details.name,
channel_id: channel_details.id
name: source_details.name,
collection_id: source_details.id
})
)
@@ -128,7 +128,7 @@ defmodule Pinchflat.MediaSource do
Ecto.Changeset.add_error(
changeset,
:original_url,
"could not fetch channel details from URL",
"could not fetch source details from URL",
error: runner_error
)
end
@@ -136,27 +136,27 @@ defmodule Pinchflat.MediaSource do
defp commit_and_start_indexing(changeset) do
case Repo.insert_or_update(changeset) do
{:ok, %Channel{} = channel} ->
maybe_run_indexing_task(changeset, channel)
{:ok, %Source{} = source} ->
maybe_run_indexing_task(changeset, source)
{:ok, channel}
{:ok, source}
err ->
err
end
end
defp maybe_run_indexing_task(changeset, channel) do
defp maybe_run_indexing_task(changeset, source) do
case changeset.data do
# If the changeset is new (not persisted), attempt indexing no matter what
%{__meta__: %{state: :built}} ->
ChannelTasks.kickoff_indexing_task(channel)
SourceTasks.kickoff_indexing_task(source)
# If the record has been persisted, only attempt indexing if the
# indexing frequency has been changed
%{__meta__: %{state: :loaded}} ->
if Map.has_key?(changeset.changes, :index_frequency_minutes) do
ChannelTasks.kickoff_indexing_task(channel)
SourceTasks.kickoff_indexing_task(source)
end
end
end
@@ -1,6 +1,6 @@
defmodule Pinchflat.MediaSource.Channel do
defmodule Pinchflat.MediaSource.Source do
@moduledoc """
The Channel schema.
The Source schema.
"""
use Ecto.Schema
@@ -9,29 +9,30 @@ defmodule Pinchflat.MediaSource.Channel do
alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.MediaProfile
@allowed_fields ~w(name channel_id index_frequency_minutes original_url media_profile_id)a
@allowed_fields ~w(name collection_id collection_type index_frequency_minutes original_url media_profile_id)a
@required_fields @allowed_fields -- ~w(index_frequency_minutes)a
schema "channels" do
schema "sources" do
field :name, :string
field :channel_id, :string
field :collection_id, :string
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
field :index_frequency_minutes, :integer
# This should only be used for user reference going forward
# as the channel_id should be used for all API calls
# as the collection_id should be used for all API calls
field :original_url, :string
belongs_to :media_profile, MediaProfile
has_many :media_items, MediaItem
has_many :media_items, MediaItem, foreign_key: :source_id
timestamps(type: :utc_datetime)
end
@doc false
def changeset(channel, attrs) do
channel
def changeset(source, attrs) do
source
|> cast(attrs, @allowed_fields)
|> validate_required(@required_fields)
|> unique_constraint([:channel_id, :media_profile_id])
|> unique_constraint([:collection_id, :media_profile_id])
end
end
+2 -2
View File
@@ -6,7 +6,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
use Ecto.Schema
import Ecto.Changeset
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaSource.Source
@allowed_fields ~w(
name
@@ -27,7 +27,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
field :embed_subs, :boolean, default: true
field :sub_langs, :string, default: "en"
has_many :channels, Channel
has_many :sources, Source
timestamps(type: :utc_datetime)
end
+5 -5
View File
@@ -8,7 +8,7 @@ defmodule Pinchflat.Tasks do
alias Pinchflat.Tasks.Task
alias Pinchflat.Media.MediaItem
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaSource.Source
@doc """
Returns the list of tasks. Returns [%Task{}, ...]
@@ -57,7 +57,7 @@ defmodule Pinchflat.Tasks do
@doc """
Creates a task.
Accepts map() | %Oban.Job{}, %Channel{} | %Oban.Job{}, %MediaItem{}.
Accepts map() | %Oban.Job{}, %Source{} | %Oban.Job{}, %MediaItem{}.
Returns {:ok, %Task{}} | {:error, %Ecto.Changeset{}}.
"""
def create_task(attrs) do
@@ -71,7 +71,7 @@ defmodule Pinchflat.Tasks do
def create_task(%Oban.Job{} = job, attached_record) do
attached_record_attr =
case attached_record do
%Channel{} = channel -> %{channel_id: channel.id}
%Source{} = source -> %{source_id: source.id}
%MediaItem{} = media_item -> %{media_item_id: media_item.id}
end
@@ -113,7 +113,7 @@ defmodule Pinchflat.Tasks do
def delete_tasks_for(attached_record) do
tasks =
case attached_record do
%Channel{} = channel -> list_tasks_for(:channel_id, channel.id)
%Source{} = source -> list_tasks_for(:source_id, source.id)
%MediaItem{} = media_item -> list_tasks_for(:media_item_id, media_item.id)
end
@@ -130,7 +130,7 @@ defmodule Pinchflat.Tasks do
def delete_pending_tasks_for(attached_record) do
tasks =
case attached_record do
%Channel{} = channel -> list_pending_tasks_for(:channel_id, channel.id)
%Source{} = source -> list_pending_tasks_for(:source_id, source.id)
%MediaItem{} = media_item -> list_pending_tasks_for(:media_item_id, media_item.id)
end
@@ -1,28 +1,28 @@
defmodule Pinchflat.Tasks.ChannelTasks do
defmodule Pinchflat.Tasks.SourceTasks do
@moduledoc """
This module contains methods for managing tasks (workers) related to channels.
This module contains methods for managing tasks (workers) related to sources.
"""
alias Pinchflat.Tasks
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaSource.Source
alias Pinchflat.Workers.MediaIndexingWorker
@doc """
Starts tasks for indexing a channel's media.
Starts tasks for indexing a source's media.
Returns {:ok, :should_not_index} | {:ok, %Task{}}.
"""
def kickoff_indexing_task(%Channel{} = channel) do
Tasks.delete_pending_tasks_for(channel)
def kickoff_indexing_task(%Source{} = source) do
Tasks.delete_pending_tasks_for(source)
if channel.index_frequency_minutes <= 0 do
if source.index_frequency_minutes <= 0 do
{:ok, :should_not_index}
else
channel
source
|> Map.take([:id])
# Schedule this one immediately, but future ones will be on an interval
|> MediaIndexingWorker.new()
|> Tasks.create_job_with_task(channel)
|> Tasks.create_job_with_task(source)
|> case do
# This should never return {:error, :duplicate_job} since we just deleted
# any pending tasks. I'm being assertive about it so it's obvious if I'm wrong
+3 -3
View File
@@ -7,11 +7,11 @@ defmodule Pinchflat.Tasks.Task do
import Ecto.Changeset
alias Pinchflat.Media.MediaItem
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaSource.Source
schema "tasks" do
belongs_to :job, Oban.Job
belongs_to :channel, Channel
belongs_to :source, Source
belongs_to :media_item, MediaItem
timestamps(type: :utc_datetime)
@@ -20,7 +20,7 @@ defmodule Pinchflat.Tasks.Task do
@doc false
def changeset(task, attrs) do
task
|> cast(attrs, [:job_id, :channel_id, :media_item_id])
|> cast(attrs, [:job_id, :source_id, :media_item_id])
|> validate_required([:job_id])
end
end
+15 -15
View File
@@ -14,10 +14,10 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
@impl Oban.Worker
@doc """
The ID is that of a channel _record_, not a YouTube channel ID. Indexes
the provided channel, kicks off downloads for each new MediaItem, and
The ID is that of a source _record_, not a YouTube channel/playlist ID. Indexes
the provided source, kicks off downloads for each new MediaItem, and
reschedules the job to run again in the future (as determined by the
channel's `index_frequency_minutes` field).
souce's `index_frequency_minutes` field).
README: Re-scheduling here works a little different than you may expect.
The reschedule time is relative to the time the job has actually _completed_.
@@ -37,24 +37,24 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
Returns :ok | {:ok, %Task{}}
"""
def perform(%Oban.Job{args: %{"id" => channel_id}}) do
channel = MediaSource.get_channel!(channel_id)
def perform(%Oban.Job{args: %{"id" => source_id}}) do
source = MediaSource.get_source!(source_id)
if channel.index_frequency_minutes <= 0 do
if source.index_frequency_minutes <= 0 do
:ok
else
index_media_and_reschedule(channel)
index_media_and_reschedule(source)
end
end
defp index_media_and_reschedule(channel) do
MediaSource.index_media_items(channel)
enqueue_video_downloads(channel)
defp index_media_and_reschedule(source) do
MediaSource.index_media_items(source)
enqueue_video_downloads(source)
channel
source
|> Map.take([:id])
|> MediaIndexingWorker.new(schedule_in: channel.index_frequency_minutes * 60)
|> Tasks.create_job_with_task(channel)
|> MediaIndexingWorker.new(schedule_in: source.index_frequency_minutes * 60)
|> Tasks.create_job_with_task(source)
|> case do
{:ok, task} -> {:ok, task}
{:error, :duplicate_job} -> {:ok, :job_exists}
@@ -67,8 +67,8 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
# or somehow got de-queued.
#
# I'm not sure of a case where this would happen, but it's cheap insurance.
defp enqueue_video_downloads(channel) do
channel
defp enqueue_video_downloads(source) do
source
|> Media.list_pending_media_items_for()
|> Enum.each(fn media_item ->
media_item
@@ -1,75 +0,0 @@
defmodule PinchflatWeb.MediaSources.ChannelController do
use PinchflatWeb, :controller
alias Pinchflat.Profiles
alias Pinchflat.MediaSource
alias Pinchflat.MediaSource.Channel
def index(conn, _params) do
channels = MediaSource.list_channels()
render(conn, :index, channels: channels)
end
def new(conn, _params) do
changeset = MediaSource.change_channel(%Channel{})
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
end
def create(conn, %{"channel" => channel_params}) do
case MediaSource.create_channel(channel_params) do
{:ok, channel} ->
conn
|> put_flash(:info, "Channel created successfully.")
|> redirect(to: ~p"/media_sources/channels/#{channel}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
end
end
def show(conn, %{"id" => id}) do
channel = MediaSource.get_channel!(id)
render(conn, :show, channel: channel)
end
def edit(conn, %{"id" => id}) do
channel = MediaSource.get_channel!(id)
changeset = MediaSource.change_channel(channel)
render(conn, :edit, channel: channel, changeset: changeset, media_profiles: media_profiles())
end
def update(conn, %{"id" => id, "channel" => channel_params}) do
channel = MediaSource.get_channel!(id)
case MediaSource.update_channel(channel, channel_params) do
{:ok, channel} ->
conn
|> put_flash(:info, "Channel updated successfully.")
|> redirect(to: ~p"/media_sources/channels/#{channel}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit,
channel: channel,
changeset: changeset,
media_profiles: media_profiles()
)
end
end
def delete(conn, %{"id" => id}) do
channel = MediaSource.get_channel!(id)
{:ok, _channel} = MediaSource.delete_channel(channel)
conn
|> put_flash(:info, "Channel deleted successfully.")
|> redirect(to: ~p"/media_sources/channels")
end
defp media_profiles do
Profiles.list_media_profiles()
end
end
@@ -1,12 +0,0 @@
<.header>
Edit Channel <%= @channel.id %>
<:subtitle>Use this form to manage channel records in your database.</:subtitle>
</.header>
<.channel_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/media_sources/channels/#{@channel}"}
/>
<.back navigate={~p"/media_sources/channels"}>Back to channels</.back>
@@ -1,28 +0,0 @@
<.header>
Listing Channels
<:actions>
<.link href={~p"/media_sources/channels/new"}>
<.button>New Channel</.button>
</.link>
</:actions>
</.header>
<.table id="channels" rows={@channels} row_click={&JS.navigate(~p"/media_sources/channels/#{&1}")}>
<:col :let={channel} label="Name"><%= channel.name %></:col>
<:col :let={channel} label="Channel"><%= channel.channel_id %></:col>
<:action :let={channel}>
<div class="sr-only">
<.link navigate={~p"/media_sources/channels/#{channel}"}>Show</.link>
</div>
<.link navigate={~p"/media_sources/channels/#{channel}/edit"}>Edit</.link>
</:action>
<:action :let={channel}>
<.link
href={~p"/media_sources/channels/#{channel}"}
method="delete"
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>
@@ -1,12 +0,0 @@
<.header>
New Channel
<:subtitle>Use this form to manage channel records in your database.</:subtitle>
</.header>
<.channel_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/media_sources/channels"}
/>
<.back navigate={~p"/media_sources/channels"}>Back to channels</.back>
@@ -1,17 +0,0 @@
<.header>
Channel <%= @channel.id %>
<:subtitle>This is a channel record from your database.</:subtitle>
<:actions>
<.link href={~p"/media_sources/channels/#{@channel}/edit"}>
<.button>Edit channel</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Channel Name"><%= @channel.name %></:item>
<:item title="Channel ID"><%= @channel.channel_id %></:item>
<:item title="Original URL"><%= @channel.original_url %></:item>
</.list>
<.back navigate={~p"/media_sources/channels"}>Back to channels</.back>
@@ -0,0 +1,75 @@
defmodule PinchflatWeb.MediaSources.SourceController do
use PinchflatWeb, :controller
alias Pinchflat.Profiles
alias Pinchflat.MediaSource
alias Pinchflat.MediaSource.Source
def index(conn, _params) do
sources = MediaSource.list_sources()
render(conn, :index, sources: sources)
end
def new(conn, _params) do
changeset = MediaSource.change_source(%Source{})
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
end
def create(conn, %{"source" => source_params}) do
case MediaSource.create_source(source_params) do
{:ok, source} ->
conn
|> put_flash(:info, "Source created successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset, media_profiles: media_profiles())
end
end
def show(conn, %{"id" => id}) do
source = MediaSource.get_source!(id)
render(conn, :show, source: source)
end
def edit(conn, %{"id" => id}) do
source = MediaSource.get_source!(id)
changeset = MediaSource.change_source(source)
render(conn, :edit, source: source, changeset: changeset, media_profiles: media_profiles())
end
def update(conn, %{"id" => id, "source" => source_params}) do
source = MediaSource.get_source!(id)
case MediaSource.update_source(source, source_params) do
{:ok, source} ->
conn
|> put_flash(:info, "Source updated successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit,
source: source,
changeset: changeset,
media_profiles: media_profiles()
)
end
end
def delete(conn, %{"id" => id}) do
source = MediaSource.get_source!(id)
{:ok, _source} = MediaSource.delete_source(source)
conn
|> put_flash(:info, "Source deleted successfully.")
|> redirect(to: ~p"/media_sources/sources")
end
defp media_profiles do
Profiles.list_media_profiles()
end
end
@@ -1,16 +1,16 @@
defmodule PinchflatWeb.MediaSources.ChannelHTML do
defmodule PinchflatWeb.MediaSources.SourceHTML do
use PinchflatWeb, :html
embed_templates "channel_html/*"
embed_templates "source_html/*"
@doc """
Renders a channel form.
Renders a source form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
attr :media_profiles, :list, required: true
def channel_form(assigns)
def source_form(assigns)
def friendly_index_frequencies do
[
@@ -0,0 +1,12 @@
<.header>
Edit Source <%= @source.id %>
<:subtitle>Use this form to manage source records in your database.</:subtitle>
</.header>
<.source_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/media_sources/sources/#{@source}"}
/>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>
@@ -0,0 +1,24 @@
<.header>
Listing Sources
<:actions>
<.link href={~p"/media_sources/sources/new"}>
<.button>New Source</.button>
</.link>
</:actions>
</.header>
<.table id="sources" rows={@sources} row_click={&JS.navigate(~p"/media_sources/sources/#{&1}")}>
<:col :let={source} label="Name"><%= source.name %></:col>
<:col :let={source} label="Source"><%= source.collection_id %></:col>
<:action :let={source}>
<div class="sr-only">
<.link navigate={~p"/media_sources/sources/#{source}"}>Show</.link>
</div>
<.link navigate={~p"/media_sources/sources/#{source}/edit"}>Edit</.link>
</:action>
<:action :let={source}>
<.link href={~p"/media_sources/sources/#{source}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>
@@ -0,0 +1,12 @@
<.header>
New Source
<:subtitle>Use this form to manage source records in your database.</:subtitle>
</.header>
<.source_form
changeset={@changeset}
media_profiles={@media_profiles}
action={~p"/media_sources/sources"}
/>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>
@@ -0,0 +1,17 @@
<.header>
Source <%= @source.id %>
<:subtitle>This is a source record from your database.</:subtitle>
<:actions>
<.link href={~p"/media_sources/sources/#{@source}/edit"}>
<.button>Edit source</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Source Name"><%= @source.name %></:item>
<:item title="Source ID"><%= @source.collection_id %></:item>
<:item title="Original URL"><%= @source.original_url %></:item>
</.list>
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>
@@ -10,7 +10,8 @@
label="Media Profile"
/>
<.input field={f[:original_url]} type="text" label="Channel URL" />
<.input field={f[:collection_type]} type="text" label="Collection Type" />
<.input field={f[:original_url]} type="text" label="Source URL" />
<.input
field={f[:index_frequency_minutes]}
@@ -20,6 +21,6 @@
/>
<:actions>
<.button>Save Channel</.button>
<.button>Save Source</.button>
</:actions>
</.simple_form>
+1 -1
View File
@@ -22,7 +22,7 @@ defmodule PinchflatWeb.Router do
resources "/media_profiles", MediaProfiles.MediaProfileController
scope "/media_sources", MediaSources do
resources "/channels", ChannelController
resources "/sources", SourceController
end
end