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:
parent
d25e65f822
commit
bdef6c75bb
46 changed files with 805 additions and 762 deletions
4
.iex.exs
4
.iex.exs
|
|
@ -3,7 +3,7 @@ alias Pinchflat.Repo
|
||||||
alias Pinchflat.Tasks.Task
|
alias Pinchflat.Tasks.Task
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Media.MediaMetadata
|
alias Pinchflat.Media.MediaMetadata
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
alias Pinchflat.Profiles.MediaProfile
|
||||||
|
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
|
|
@ -11,4 +11,4 @@ alias Pinchflat.Media
|
||||||
alias Pinchflat.Profiles
|
alias Pinchflat.Profiles
|
||||||
alias Pinchflat.MediaSource
|
alias Pinchflat.MediaSource
|
||||||
|
|
||||||
alias Pinchflat.MediaClient.{ChannelDetails, VideoDownloader}
|
alias Pinchflat.MediaClient.{SourceDetails, VideoDownloader}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule Pinchflat.Media do
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of media_items. Returns [%MediaItem{}, ...].
|
Returns the list of media_items. Returns [%MediaItem{}, ...].
|
||||||
|
|
@ -18,15 +18,15 @@ defmodule Pinchflat.Media do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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`.
|
pending means the `media_filepath` is `nil`.
|
||||||
|
|
||||||
Returns [%MediaItem{}, ...].
|
Returns [%MediaItem{}, ...].
|
||||||
"""
|
"""
|
||||||
def list_pending_media_items_for(%Channel{} = channel) do
|
def list_pending_media_items_for(%Source{} = source) do
|
||||||
from(
|
from(
|
||||||
m in MediaItem,
|
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()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ defmodule Pinchflat.Media.MediaItem do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Pinchflat.Tasks.Task
|
alias Pinchflat.Tasks.Task
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
alias Pinchflat.Media.MediaMetadata
|
alias Pinchflat.Media.MediaMetadata
|
||||||
|
|
||||||
@required_fields ~w(media_id channel_id)a
|
@required_fields ~w(media_id source_id)a
|
||||||
@allowed_fields ~w(title media_id media_filepath channel_id subtitle_filepaths)a
|
@allowed_fields ~w(title media_id media_filepath source_id subtitle_filepaths)a
|
||||||
|
|
||||||
schema "media_items" do
|
schema "media_items" do
|
||||||
field :title, :string
|
field :title, :string
|
||||||
|
|
@ -22,7 +22,7 @@ defmodule Pinchflat.Media.MediaItem do
|
||||||
# Will very likely revisit because I can't leave well-enough alone.
|
# Will very likely revisit because I can't leave well-enough alone.
|
||||||
field :subtitle_filepaths, {:array, {:array, :string}}, default: []
|
field :subtitle_filepaths, {:array, {:array, :string}}, default: []
|
||||||
|
|
||||||
belongs_to :channel, Channel
|
belongs_to :source, Source
|
||||||
|
|
||||||
has_one :metadata, MediaMetadata, on_replace: :update
|
has_one :metadata, MediaMetadata, on_replace: :update
|
||||||
|
|
||||||
|
|
@ -37,6 +37,6 @@ defmodule Pinchflat.Media.MediaItem do
|
||||||
|> cast(attrs, @allowed_fields)
|
|> cast(attrs, @allowed_fields)
|
||||||
|> cast_assoc(:metadata, with: &MediaMetadata.changeset/2, required: false)
|
|> cast_assoc(:metadata, with: &MediaMetadata.changeset/2, required: false)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> unique_constraint([:media_id, :channel_id])
|
|> unique_constraint([:media_id, :source_id])
|
||||||
end
|
end
|
||||||
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
|
defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollection do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Contains utilities for working with collections of videos (ie: channels, playlists).
|
Contains utilities for working with collections of
|
||||||
|
videos (aka: a source [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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defmacro __using__(_) do
|
alias Pinchflat.MediaClient.SourceDetails
|
||||||
quote do
|
|
||||||
@doc """
|
|
||||||
Returns a list of strings representing the video ids in the collection.
|
|
||||||
|
|
||||||
Returns {:ok, [binary()]} | {:error, any, ...}.
|
@doc """
|
||||||
"""
|
Returns a list of strings representing the video ids in the collection.
|
||||||
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
|
Returns {:ok, [binary()]} | {:error, any, ...}.
|
||||||
{:ok, output} -> {:ok, String.split(output, "\n", trim: true)}
|
"""
|
||||||
res -> res
|
def get_video_ids(url, command_opts \\ []) do
|
||||||
end
|
runner = Application.get_env(:pinchflat, :yt_dlp_runner)
|
||||||
end
|
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
|
||||||
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
|
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
|
|
||||||
41
lib/pinchflat/media_client/source_details.ex
Normal file
41
lib/pinchflat/media_client/source_details.ex
Normal file
|
|
@ -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}
|
Returns {:ok, %MediaItem{}} | {:error, any, ...any}
|
||||||
"""
|
"""
|
||||||
def download_for_media_item(%MediaItem{} = media_item, backend \\ :yt_dlp) do
|
def download_for_media_item(%MediaItem{} = media_item, backend \\ :yt_dlp) do
|
||||||
item_with_preloads = Repo.preload(media_item, [:metadata, channel: :media_profile])
|
item_with_preloads = Repo.preload(media_item, [:metadata, source: :media_profile])
|
||||||
media_profile = item_with_preloads.channel.media_profile
|
media_profile = item_with_preloads.source.media_profile
|
||||||
|
|
||||||
case download_for_media_profile(media_item.media_id, media_profile, backend) do
|
case download_for_media_profile(media_item.media_id, media_profile, backend) do
|
||||||
{:ok, parsed_json} ->
|
{:ok, parsed_json} ->
|
||||||
|
|
|
||||||
|
|
@ -8,34 +8,34 @@ defmodule Pinchflat.MediaSource do
|
||||||
|
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Media
|
alias Pinchflat.Media
|
||||||
alias Pinchflat.Tasks.ChannelTasks
|
alias Pinchflat.Tasks.SourceTasks
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
alias Pinchflat.MediaClient.ChannelDetails
|
alias Pinchflat.MediaClient.SourceDetails
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of channels. Returns [%Channel{}, ...]
|
Returns the list of sources. Returns [%Source{}, ...]
|
||||||
"""
|
"""
|
||||||
def list_channels do
|
def list_sources do
|
||||||
Repo.all(Channel)
|
Repo.all(Source)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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 """
|
@doc """
|
||||||
Creates a channel. May attempt to pull additional channel details from the
|
Creates a source. May attempt to pull additional source details from the
|
||||||
original_url (if provided). Will attempt to start indexing the channel's
|
original_url (if provided). Will attempt to start indexing the source's
|
||||||
media if successfully inserted.
|
media if successfully inserted.
|
||||||
|
|
||||||
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
|
Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
|
||||||
"""
|
"""
|
||||||
def create_channel(attrs) do
|
def create_source(attrs) do
|
||||||
%Channel{}
|
%Source{}
|
||||||
|> change_channel_from_url(attrs)
|
|> change_source_from_url(attrs)
|
||||||
|> commit_and_start_indexing()
|
|> commit_and_start_indexing()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -45,12 +45,12 @@ defmodule Pinchflat.MediaSource do
|
||||||
|
|
||||||
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
|
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
|
||||||
"""
|
"""
|
||||||
def index_media_items(%Channel{} = channel) do
|
def index_media_items(%Source{} = source) do
|
||||||
{:ok, media_ids} = ChannelDetails.get_video_ids(channel.original_url)
|
{:ok, media_ids} = SourceDetails.get_video_ids(source.original_url)
|
||||||
|
|
||||||
media_ids
|
media_ids
|
||||||
|> Enum.map(fn media_id ->
|
|> 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
|
case Media.create_media_item(attrs) do
|
||||||
{:ok, media_item} -> media_item
|
{:ok, media_item} -> media_item
|
||||||
|
|
@ -60,67 +60,67 @@ defmodule Pinchflat.MediaSource do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Updates a channel. May attempt to pull additional channel details from the
|
Updates a source. May attempt to pull additional source details from the
|
||||||
original_url (if changed). May attempt to start indexing the channel's
|
original_url (if changed). May attempt to start indexing the source's
|
||||||
media if the indexing frequency has been changed.
|
media if the indexing frequency has been changed.
|
||||||
|
|
||||||
Existing indexing tasks will be cancelled if the indexing frequency has been
|
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
|
def update_source(%Source{} = source, attrs) do
|
||||||
channel
|
source
|
||||||
|> change_channel_from_url(attrs)
|
|> change_source_from_url(attrs)
|
||||||
|> commit_and_start_indexing()
|
|> commit_and_start_indexing()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@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
|
def delete_source(%Source{} = source) do
|
||||||
Tasks.delete_tasks_for(channel)
|
Tasks.delete_tasks_for(source)
|
||||||
Repo.delete(channel)
|
Repo.delete(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking channel changes.
|
Returns an `%Ecto.Changeset{}` for tracking source changes.
|
||||||
"""
|
"""
|
||||||
def change_channel(%Channel{} = channel, attrs \\ %{}) do
|
def change_source(%Source{} = source, attrs \\ %{}) do
|
||||||
Channel.changeset(channel, attrs)
|
Source.changeset(source, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns an `%Ecto.Changeset{}` for tracking channel changes and additionally
|
Returns an `%Ecto.Changeset{}` for tracking source changes and additionally
|
||||||
fetches channel details from the original_url (if provided). If the channel
|
fetches source details from the original_url (if provided). If the source
|
||||||
details cannot be fetched, an error is added to the changeset.
|
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
|
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.
|
is pretty easy to change, but for MVP I'm not concerned.
|
||||||
"""
|
"""
|
||||||
def change_channel_from_url(%Channel{} = channel, attrs) do
|
def change_source_from_url(%Source{} = source, attrs) do
|
||||||
case change_channel(channel, attrs) do
|
case change_source(source, attrs) do
|
||||||
%Ecto.Changeset{changes: %{original_url: _}} = changeset ->
|
%Ecto.Changeset{changes: %{original_url: _}} = changeset ->
|
||||||
add_channel_details_to_changeset(channel, changeset)
|
add_source_details_to_changeset(source, changeset)
|
||||||
|
|
||||||
changeset ->
|
changeset ->
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
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
|
%Ecto.Changeset{changes: changes} = changeset
|
||||||
|
|
||||||
case ChannelDetails.get_channel_details(changes.original_url) do
|
case SourceDetails.get_source_details(changes.original_url) do
|
||||||
{:ok, %ChannelDetails{} = channel_details} ->
|
{:ok, %SourceDetails{} = source_details} ->
|
||||||
change_channel(
|
change_source(
|
||||||
channel,
|
source,
|
||||||
Map.merge(changes, %{
|
Map.merge(changes, %{
|
||||||
name: channel_details.name,
|
name: source_details.name,
|
||||||
channel_id: channel_details.id
|
collection_id: source_details.id
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ defmodule Pinchflat.MediaSource do
|
||||||
Ecto.Changeset.add_error(
|
Ecto.Changeset.add_error(
|
||||||
changeset,
|
changeset,
|
||||||
:original_url,
|
:original_url,
|
||||||
"could not fetch channel details from URL",
|
"could not fetch source details from URL",
|
||||||
error: runner_error
|
error: runner_error
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
@ -136,27 +136,27 @@ defmodule Pinchflat.MediaSource do
|
||||||
|
|
||||||
defp commit_and_start_indexing(changeset) do
|
defp commit_and_start_indexing(changeset) do
|
||||||
case Repo.insert_or_update(changeset) do
|
case Repo.insert_or_update(changeset) do
|
||||||
{:ok, %Channel{} = channel} ->
|
{:ok, %Source{} = source} ->
|
||||||
maybe_run_indexing_task(changeset, channel)
|
maybe_run_indexing_task(changeset, source)
|
||||||
|
|
||||||
{:ok, channel}
|
{:ok, source}
|
||||||
|
|
||||||
err ->
|
err ->
|
||||||
err
|
err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_run_indexing_task(changeset, channel) do
|
defp maybe_run_indexing_task(changeset, source) do
|
||||||
case changeset.data do
|
case changeset.data do
|
||||||
# If the changeset is new (not persisted), attempt indexing no matter what
|
# If the changeset is new (not persisted), attempt indexing no matter what
|
||||||
%{__meta__: %{state: :built}} ->
|
%{__meta__: %{state: :built}} ->
|
||||||
ChannelTasks.kickoff_indexing_task(channel)
|
SourceTasks.kickoff_indexing_task(source)
|
||||||
|
|
||||||
# If the record has been persisted, only attempt indexing if the
|
# If the record has been persisted, only attempt indexing if the
|
||||||
# indexing frequency has been changed
|
# indexing frequency has been changed
|
||||||
%{__meta__: %{state: :loaded}} ->
|
%{__meta__: %{state: :loaded}} ->
|
||||||
if Map.has_key?(changeset.changes, :index_frequency_minutes) do
|
if Map.has_key?(changeset.changes, :index_frequency_minutes) do
|
||||||
ChannelTasks.kickoff_indexing_task(channel)
|
SourceTasks.kickoff_indexing_task(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Pinchflat.MediaSource.Channel do
|
defmodule Pinchflat.MediaSource.Source do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
The Channel schema.
|
The Source schema.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
@ -9,29 +9,30 @@ defmodule Pinchflat.MediaSource.Channel do
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Profiles.MediaProfile
|
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
|
@required_fields @allowed_fields -- ~w(index_frequency_minutes)a
|
||||||
|
|
||||||
schema "channels" do
|
schema "sources" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :channel_id, :string
|
field :collection_id, :string
|
||||||
|
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
|
||||||
field :index_frequency_minutes, :integer
|
field :index_frequency_minutes, :integer
|
||||||
# This should only be used for user reference going forward
|
# 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
|
field :original_url, :string
|
||||||
|
|
||||||
belongs_to :media_profile, MediaProfile
|
belongs_to :media_profile, MediaProfile
|
||||||
|
|
||||||
has_many :media_items, MediaItem
|
has_many :media_items, MediaItem, foreign_key: :source_id
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(channel, attrs) do
|
def changeset(source, attrs) do
|
||||||
channel
|
source
|
||||||
|> cast(attrs, @allowed_fields)
|
|> cast(attrs, @allowed_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> unique_constraint([:channel_id, :media_profile_id])
|
|> unique_constraint([:collection_id, :media_profile_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -6,7 +6,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
|
|
||||||
@allowed_fields ~w(
|
@allowed_fields ~w(
|
||||||
name
|
name
|
||||||
|
|
@ -27,7 +27,7 @@ defmodule Pinchflat.Profiles.MediaProfile do
|
||||||
field :embed_subs, :boolean, default: true
|
field :embed_subs, :boolean, default: true
|
||||||
field :sub_langs, :string, default: "en"
|
field :sub_langs, :string, default: "en"
|
||||||
|
|
||||||
has_many :channels, Channel
|
has_many :sources, Source
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule Pinchflat.Tasks do
|
||||||
|
|
||||||
alias Pinchflat.Tasks.Task
|
alias Pinchflat.Tasks.Task
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of tasks. Returns [%Task{}, ...]
|
Returns the list of tasks. Returns [%Task{}, ...]
|
||||||
|
|
@ -57,7 +57,7 @@ defmodule Pinchflat.Tasks do
|
||||||
@doc """
|
@doc """
|
||||||
Creates a task.
|
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{}}.
|
Returns {:ok, %Task{}} | {:error, %Ecto.Changeset{}}.
|
||||||
"""
|
"""
|
||||||
def create_task(attrs) do
|
def create_task(attrs) do
|
||||||
|
|
@ -71,7 +71,7 @@ defmodule Pinchflat.Tasks do
|
||||||
def create_task(%Oban.Job{} = job, attached_record) do
|
def create_task(%Oban.Job{} = job, attached_record) do
|
||||||
attached_record_attr =
|
attached_record_attr =
|
||||||
case attached_record do
|
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}
|
%MediaItem{} = media_item -> %{media_item_id: media_item.id}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -113,7 +113,7 @@ defmodule Pinchflat.Tasks do
|
||||||
def delete_tasks_for(attached_record) do
|
def delete_tasks_for(attached_record) do
|
||||||
tasks =
|
tasks =
|
||||||
case attached_record do
|
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)
|
%MediaItem{} = media_item -> list_tasks_for(:media_item_id, media_item.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -130,7 +130,7 @@ defmodule Pinchflat.Tasks do
|
||||||
def delete_pending_tasks_for(attached_record) do
|
def delete_pending_tasks_for(attached_record) do
|
||||||
tasks =
|
tasks =
|
||||||
case attached_record do
|
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)
|
%MediaItem{} = media_item -> list_pending_tasks_for(:media_item_id, media_item.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
defmodule Pinchflat.Tasks.ChannelTasks do
|
defmodule Pinchflat.Tasks.SourceTasks do
|
||||||
@moduledoc """
|
@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.Tasks
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
alias Pinchflat.Workers.MediaIndexingWorker
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts tasks for indexing a channel's media.
|
Starts tasks for indexing a source's media.
|
||||||
|
|
||||||
Returns {:ok, :should_not_index} | {:ok, %Task{}}.
|
Returns {:ok, :should_not_index} | {:ok, %Task{}}.
|
||||||
"""
|
"""
|
||||||
def kickoff_indexing_task(%Channel{} = channel) do
|
def kickoff_indexing_task(%Source{} = source) do
|
||||||
Tasks.delete_pending_tasks_for(channel)
|
Tasks.delete_pending_tasks_for(source)
|
||||||
|
|
||||||
if channel.index_frequency_minutes <= 0 do
|
if source.index_frequency_minutes <= 0 do
|
||||||
{:ok, :should_not_index}
|
{:ok, :should_not_index}
|
||||||
else
|
else
|
||||||
channel
|
source
|
||||||
|> Map.take([:id])
|
|> Map.take([:id])
|
||||||
# Schedule this one immediately, but future ones will be on an interval
|
# Schedule this one immediately, but future ones will be on an interval
|
||||||
|> MediaIndexingWorker.new()
|
|> MediaIndexingWorker.new()
|
||||||
|> Tasks.create_job_with_task(channel)
|
|> Tasks.create_job_with_task(source)
|
||||||
|> case do
|
|> case do
|
||||||
# This should never return {:error, :duplicate_job} since we just deleted
|
# 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
|
# any pending tasks. I'm being assertive about it so it's obvious if I'm wrong
|
||||||
|
|
@ -7,11 +7,11 @@ defmodule Pinchflat.Tasks.Task do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
|
|
||||||
schema "tasks" do
|
schema "tasks" do
|
||||||
belongs_to :job, Oban.Job
|
belongs_to :job, Oban.Job
|
||||||
belongs_to :channel, Channel
|
belongs_to :source, Source
|
||||||
belongs_to :media_item, MediaItem
|
belongs_to :media_item, MediaItem
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
|
|
@ -20,7 +20,7 @@ defmodule Pinchflat.Tasks.Task do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(task, attrs) do
|
def changeset(task, attrs) do
|
||||||
task
|
task
|
||||||
|> cast(attrs, [:job_id, :channel_id, :media_item_id])
|
|> cast(attrs, [:job_id, :source_id, :media_item_id])
|
||||||
|> validate_required([:job_id])
|
|> validate_required([:job_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
@doc """
|
@doc """
|
||||||
The ID is that of a channel _record_, not a YouTube channel ID. Indexes
|
The ID is that of a source _record_, not a YouTube channel/playlist ID. Indexes
|
||||||
the provided channel, kicks off downloads for each new MediaItem, and
|
the provided source, kicks off downloads for each new MediaItem, and
|
||||||
reschedules the job to run again in the future (as determined by the
|
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.
|
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_.
|
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{}}
|
Returns :ok | {:ok, %Task{}}
|
||||||
"""
|
"""
|
||||||
def perform(%Oban.Job{args: %{"id" => channel_id}}) do
|
def perform(%Oban.Job{args: %{"id" => source_id}}) do
|
||||||
channel = MediaSource.get_channel!(channel_id)
|
source = MediaSource.get_source!(source_id)
|
||||||
|
|
||||||
if channel.index_frequency_minutes <= 0 do
|
if source.index_frequency_minutes <= 0 do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
index_media_and_reschedule(channel)
|
index_media_and_reschedule(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp index_media_and_reschedule(channel) do
|
defp index_media_and_reschedule(source) do
|
||||||
MediaSource.index_media_items(channel)
|
MediaSource.index_media_items(source)
|
||||||
enqueue_video_downloads(channel)
|
enqueue_video_downloads(source)
|
||||||
|
|
||||||
channel
|
source
|
||||||
|> Map.take([:id])
|
|> Map.take([:id])
|
||||||
|> MediaIndexingWorker.new(schedule_in: channel.index_frequency_minutes * 60)
|
|> MediaIndexingWorker.new(schedule_in: source.index_frequency_minutes * 60)
|
||||||
|> Tasks.create_job_with_task(channel)
|
|> Tasks.create_job_with_task(source)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, task} -> {:ok, task}
|
{:ok, task} -> {:ok, task}
|
||||||
{:error, :duplicate_job} -> {:ok, :job_exists}
|
{:error, :duplicate_job} -> {:ok, :job_exists}
|
||||||
|
|
@ -67,8 +67,8 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
|
||||||
# or somehow got de-queued.
|
# or somehow got de-queued.
|
||||||
#
|
#
|
||||||
# I'm not sure of a case where this would happen, but it's cheap insurance.
|
# I'm not sure of a case where this would happen, but it's cheap insurance.
|
||||||
defp enqueue_video_downloads(channel) do
|
defp enqueue_video_downloads(source) do
|
||||||
channel
|
source
|
||||||
|> Media.list_pending_media_items_for()
|
|> Media.list_pending_media_items_for()
|
||||||
|> Enum.each(fn media_item ->
|
|> Enum.each(fn media_item ->
|
||||||
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
|
use PinchflatWeb, :html
|
||||||
|
|
||||||
embed_templates "channel_html/*"
|
embed_templates "source_html/*"
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a channel form.
|
Renders a source form.
|
||||||
"""
|
"""
|
||||||
attr :changeset, Ecto.Changeset, required: true
|
attr :changeset, Ecto.Changeset, required: true
|
||||||
attr :action, :string, required: true
|
attr :action, :string, required: true
|
||||||
attr :media_profiles, :list, required: true
|
attr :media_profiles, :list, required: true
|
||||||
|
|
||||||
def channel_form(assigns)
|
def source_form(assigns)
|
||||||
|
|
||||||
def friendly_index_frequencies do
|
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"
|
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
|
<.input
|
||||||
field={f[:index_frequency_minutes]}
|
field={f[:index_frequency_minutes]}
|
||||||
|
|
@ -20,6 +21,6 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<:actions>
|
<:actions>
|
||||||
<.button>Save Channel</.button>
|
<.button>Save Source</.button>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.simple_form>
|
</.simple_form>
|
||||||
|
|
@ -22,7 +22,7 @@ defmodule PinchflatWeb.Router do
|
||||||
resources "/media_profiles", MediaProfiles.MediaProfileController
|
resources "/media_profiles", MediaProfiles.MediaProfileController
|
||||||
|
|
||||||
scope "/media_sources", MediaSources do
|
scope "/media_sources", MediaSources do
|
||||||
resources "/channels", ChannelController
|
resources "/sources", SourceController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule Pinchflat.Repo.Migrations.RenameChannelAndRelatedFields do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
# Creation
|
||||||
|
create table(:sources) do
|
||||||
|
add :name, :string, null: false
|
||||||
|
add :collection_type, :string, null: false
|
||||||
|
add :collection_id, :string, null: false
|
||||||
|
add :original_url, :string, null: false
|
||||||
|
add :media_profile_id, references(:media_profiles, on_delete: :restrict), null: false
|
||||||
|
add :index_frequency_minutes, :integer, default: 60 * 24, null: false
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:media_items) do
|
||||||
|
add :source_id, references(:sources, on_delete: :restrict), null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:tasks) do
|
||||||
|
# `restrict` because we need to be sure to delete pending tasks when a source is deleted
|
||||||
|
add :source_id, references(:sources, on_delete: :restrict), null: true
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:sources, [:media_profile_id])
|
||||||
|
create unique_index(:sources, [:collection_id, :media_profile_id])
|
||||||
|
|
||||||
|
create index(:media_items, [:source_id])
|
||||||
|
create unique_index(:media_items, [:media_id, :source_id])
|
||||||
|
|
||||||
|
# Deletion
|
||||||
|
drop index(:media_items, [:channel_id])
|
||||||
|
drop unique_index(:media_items, [:media_id, :channel_id])
|
||||||
|
|
||||||
|
alter table(:tasks) do
|
||||||
|
# `restrict` because we need to be sure to delete pending tasks when a source is deleted
|
||||||
|
remove :channel_id, references(:channels, on_delete: :restrict), null: true
|
||||||
|
end
|
||||||
|
|
||||||
|
alter table(:media_items) do
|
||||||
|
remove :channel_id, references(:channels, on_delete: :restrict), null: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
priv/repo/migrations/20240202043042_drop_channels_table.exs
Normal file
21
priv/repo/migrations/20240202043042_drop_channels_table.exs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Pinchflat.Repo.Migrations.DropChannelsTable do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
drop table(:channels)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
create table(:channels) do
|
||||||
|
add :name, :string, null: false
|
||||||
|
add :channel_id, :string, null: false
|
||||||
|
add :original_url, :string, null: false
|
||||||
|
add :media_profile_id, references(:media_profiles, on_delete: :restrict), null: false
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime)
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:channels, [:media_profile_id])
|
||||||
|
create unique_index(:channels, [:channel_id, :media_profile_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
defmodule Pinchflat.MediaClient.Backends.YtDlp.ChannelTest do
|
|
||||||
use ExUnit.Case, async: true
|
|
||||||
import Mox
|
|
||||||
|
|
||||||
alias Pinchflat.MediaClient.ChannelDetails
|
|
||||||
alias Pinchflat.MediaClient.Backends.YtDlp.Channel
|
|
||||||
|
|
||||||
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
|
||||||
|
|
||||||
setup :verify_on_exit!
|
|
||||||
|
|
||||||
describe "get_channel_details/1" do
|
|
||||||
test "it returns a %ChannelDetails{} with data on success" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
|
||||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, res} = Channel.get_channel_details(@channel_url)
|
|
||||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it passes the expected args to the backend runner" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
|
||||||
assert opts == [playlist_end: 1]
|
|
||||||
assert ot == "%(.{channel,channel_id})j"
|
|
||||||
|
|
||||||
{:ok, "{}"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
assert {:ok, _} = Channel.get_channel_details(@channel_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it returns an error if the runner returns an error" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = Channel.get_channel_details(@channel_url)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it returns an error if the output is not JSON" do
|
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end)
|
|
||||||
|
|
||||||
assert {:error, %Jason.DecodeError{}} = Channel.get_channel_details(@channel_url)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -2,13 +2,10 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
|
alias Pinchflat.MediaClient.SourceDetails
|
||||||
alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection
|
alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection
|
||||||
|
|
||||||
@channel_url "https://www.youtube.com/@TheUselessTrials"
|
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
||||||
|
|
||||||
defmodule VideoCollectionUser do
|
|
||||||
use VideoCollection
|
|
||||||
end
|
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
|
@ -16,7 +13,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
|
||||||
test "returns a list of video ids with no blank elements" do
|
test "returns a list of video ids with no blank elements" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "id1\nid2\n\nid3\n"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "id1\nid2\n\nid3\n"} end)
|
||||||
|
|
||||||
assert {:ok, ["id1", "id2", "id3"]} = VideoCollectionUser.get_video_ids(@channel_url)
|
assert {:ok, ["id1", "id2", "id3"]} = VideoCollection.get_video_ids(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it passes the expected default args" do
|
test "it passes the expected default args" do
|
||||||
|
|
@ -27,7 +24,7 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url)
|
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it passes the expected custom args" do
|
test "it passes the expected custom args" do
|
||||||
|
|
@ -37,13 +34,47 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = VideoCollectionUser.get_video_ids(@channel_url, [:custom_arg])
|
assert {:ok, _} = VideoCollection.get_video_ids(@channel_url, [:custom_arg])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns the error straight through when the command fails" do
|
test "returns the error straight through when the command fails" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
assert {:error, "Big issue", 1} = VideoCollectionUser.get_video_ids(@channel_url)
|
assert {:error, "Big issue", 1} = VideoCollection.get_video_ids(@channel_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_source_details/1" do
|
||||||
|
test "it returns a %SourceDetails{} with data on success" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
|
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, res} = VideoCollection.get_source_details(@channel_url)
|
||||||
|
assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it passes the expected args to the backend runner" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||||
|
assert opts == [:skip_download, playlist_end: 1]
|
||||||
|
assert ot == "%(.{channel,channel_id})j"
|
||||||
|
|
||||||
|
{:ok, "{}"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, _} = VideoCollection.get_source_details(@channel_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns an error if the runner returns an error" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:error, "Big issue", 1} end)
|
||||||
|
|
||||||
|
assert {:error, "Big issue", 1} = VideoCollection.get_source_details(@channel_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns an error if the output is not JSON" do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "Not JSON"} end)
|
||||||
|
|
||||||
|
assert {:error, %Jason.DecodeError{}} = VideoCollection.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
defmodule Pinchflat.MediaClient.SourceDetailsTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
alias Pinchflat.MediaClient.ChannelDetails
|
alias Pinchflat.MediaClient.SourceDetails
|
||||||
|
|
||||||
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
@channel_url "https://www.youtube.com/c/TheUselessTrials"
|
||||||
|
|
||||||
|
|
@ -10,21 +10,21 @@ defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
||||||
|
|
||||||
describe "new/2" do
|
describe "new/2" do
|
||||||
test "it returns a struct with the given values" do
|
test "it returns a struct with the given values" do
|
||||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} =
|
assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} =
|
||||||
ChannelDetails.new("UCQH2", "TheUselessTrials")
|
SourceDetails.new("UCQH2", "TheUselessTrials")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_channel_details/2" do
|
describe "get_source_details/2" do
|
||||||
test "it passes the expected arguments to the backend" do
|
test "it passes the expected arguments to the backend" do
|
||||||
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
|
||||||
assert opts == [playlist_end: 1]
|
assert opts == [:skip_download, playlist_end: 1]
|
||||||
assert ot == "%(.{channel,channel_id})j"
|
assert ot == "%(.{channel,channel_id})j"
|
||||||
|
|
||||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = ChannelDetails.get_channel_details(@channel_url)
|
assert {:ok, _} = SourceDetails.get_source_details(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns a struct composed of the returned data" do
|
test "it returns a struct composed of the returned data" do
|
||||||
|
|
@ -32,8 +32,8 @@ defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
||||||
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, res} = ChannelDetails.get_channel_details(@channel_url)
|
assert {:ok, res} = SourceDetails.get_source_details(@channel_url)
|
||||||
assert %ChannelDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} = res
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
||||||
{:ok, ""}
|
{:ok, ""}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, _} = ChannelDetails.get_video_ids(@channel_url)
|
assert {:ok, _} = SourceDetails.get_video_ids(@channel_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns a list of strings" do
|
test "it returns a list of strings" do
|
||||||
|
|
@ -54,7 +54,7 @@ defmodule Pinchflat.MediaClient.ChannelDetailsTest do
|
||||||
{:ok, "video1\nvideo2\nvideo3"}
|
{:ok, "video1\nvideo2\nvideo3"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert {:ok, ["video1", "video2", "video3"]} = ChannelDetails.get_video_ids(@channel_url)
|
assert {:ok, ["video1", "video2", "video3"]} = SourceDetails.get_video_ids(@channel_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -11,7 +11,7 @@ defmodule Pinchflat.MediaClient.VideoDownloaderTest do
|
||||||
media_item =
|
media_item =
|
||||||
Repo.preload(
|
Repo.preload(
|
||||||
media_item_fixture(%{title: nil, media_filepath: nil}),
|
media_item_fixture(%{title: nil, media_filepath: nil}),
|
||||||
[:metadata, channel: :media_profile]
|
[:metadata, source: :media_profile]
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, %{media_item: media_item}}
|
{:ok, %{media_item: media_item}}
|
||||||
|
|
|
||||||
|
|
@ -7,82 +7,85 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
|
|
||||||
alias Pinchflat.MediaSource
|
alias Pinchflat.MediaSource
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
alias Pinchflat.Workers.MediaIndexingWorker
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
|
||||||
@invalid_channel_attrs %{name: nil, channel_id: nil}
|
@invalid_source_attrs %{name: nil, collection_id: nil}
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "list_channels/0" do
|
describe "list_sources/0" do
|
||||||
test "it returns all channels" do
|
test "it returns all sources" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
assert MediaSource.list_channels() == [channel]
|
assert MediaSource.list_sources() == [source]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_channel!/1" do
|
describe "get_source!/1" do
|
||||||
test "it returns the channel with given id" do
|
test "it returns the source with given id" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
assert MediaSource.get_channel!(channel.id) == channel
|
assert MediaSource.get_source!(source.id) == source
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "create_channel/1" do
|
describe "create_source/1" do
|
||||||
test "creates a channel and adds name + ID from runner response" do
|
test "creates a source and adds name + ID from runner response" do
|
||||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
collection_type: "channel"
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %Channel{} = channel} = MediaSource.create_channel(valid_attrs)
|
assert {:ok, %Source{} = source} = MediaSource.create_source(valid_attrs)
|
||||||
assert channel.name == "some name"
|
assert source.name == "some name"
|
||||||
assert String.starts_with?(channel.channel_id, "some_channel_id_")
|
assert String.starts_with?(source.collection_id, "some_source_id_")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creation with invalid data returns error changeset" do
|
test "creation with invalid data returns error changeset" do
|
||||||
assert {:error, %Ecto.Changeset{}} = MediaSource.create_channel(@invalid_channel_attrs)
|
assert {:error, %Ecto.Changeset{}} = MediaSource.create_source(@invalid_source_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creation enforces uniqueness of channel_id scoped to the media_profile" do
|
test "creation enforces uniqueness of source_id scoped to the media_profile" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some name",
|
channel: "some name",
|
||||||
channel_id: "some_channel_id_12345678"
|
channel_id: "some_source_id_12345678"
|
||||||
})}
|
})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
valid_once_attrs = %{
|
valid_once_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
collection_type: "channel"
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %Channel{}} = MediaSource.create_channel(valid_once_attrs)
|
assert {:ok, %Source{}} = MediaSource.create_source(valid_once_attrs)
|
||||||
assert {:error, %Ecto.Changeset{}} = MediaSource.create_channel(valid_once_attrs)
|
assert {:error, %Ecto.Changeset{}} = MediaSource.create_source(valid_once_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creation lets you duplicate channel_ids as long as the media profile is different" do
|
test "creation lets you duplicate collection_ids as long as the media profile is different" do
|
||||||
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
|
||||||
{:ok,
|
{:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some name",
|
channel: "some name",
|
||||||
channel_id: "some_channel_id_12345678"
|
channel_id: "some_source_id_12345678"
|
||||||
})}
|
})}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
name: "some name",
|
name: "some name",
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
collection_type: "channel"
|
||||||
}
|
}
|
||||||
|
|
||||||
channel_1_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
|
source_1_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
|
||||||
channel_2_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
|
source_2_attrs = Map.merge(valid_attrs, %{media_profile_id: media_profile_fixture().id})
|
||||||
|
|
||||||
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_1_attrs)
|
assert {:ok, %Source{}} = MediaSource.create_source(source_1_attrs)
|
||||||
assert {:ok, %Channel{}} = MediaSource.create_channel(channel_2_attrs)
|
assert {:ok, %Source{}} = MediaSource.create_source(source_2_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creation will schedule the indexing task" do
|
test "creation will schedule the indexing task" do
|
||||||
|
|
@ -90,12 +93,13 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
|
|
||||||
valid_attrs = %{
|
valid_attrs = %{
|
||||||
media_profile_id: media_profile_fixture().id,
|
media_profile_id: media_profile_fixture().id,
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
|
collection_type: "channel"
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %Channel{} = channel} = MediaSource.create_channel(valid_attrs)
|
assert {:ok, %Source{} = source} = MediaSource.create_source(valid_attrs)
|
||||||
|
|
||||||
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -103,181 +107,181 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo2\nvideo3"} end)
|
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo2\nvideo3"} end)
|
||||||
|
|
||||||
{:ok, [channel: channel_fixture()]}
|
{:ok, [source: source_fixture()]}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a media_item record for each media ID returned", %{channel: channel} do
|
test "it creates a media_item record for each media ID returned", %{source: source} do
|
||||||
assert media_items = MediaSource.index_media_items(channel)
|
assert media_items = MediaSource.index_media_items(source)
|
||||||
|
|
||||||
assert Enum.count(media_items) == 3
|
assert Enum.count(media_items) == 3
|
||||||
assert ["video1", "video2", "video3"] == Enum.map(media_items, & &1.media_id)
|
assert ["video1", "video2", "video3"] == Enum.map(media_items, & &1.media_id)
|
||||||
assert Enum.all?(media_items, fn %MediaItem{} -> true end)
|
assert Enum.all?(media_items, fn %MediaItem{} -> true end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it attaches all media_items to the given channel", %{channel: channel} do
|
test "it attaches all media_items to the given source", %{source: source} do
|
||||||
channel_id = channel.id
|
source_id = source.id
|
||||||
assert media_items = MediaSource.index_media_items(channel)
|
assert media_items = MediaSource.index_media_items(source)
|
||||||
|
|
||||||
assert Enum.count(media_items) == 3
|
assert Enum.count(media_items) == 3
|
||||||
assert Enum.all?(media_items, fn %MediaItem{channel_id: ^channel_id} -> true end)
|
assert Enum.all?(media_items, fn %MediaItem{source_id: ^source_id} -> true end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it won't duplicate media_items based on media_id and channel", %{channel: channel} do
|
test "it won't duplicate media_items based on media_id and source", %{source: source} do
|
||||||
_first_run = MediaSource.index_media_items(channel)
|
_first_run = MediaSource.index_media_items(source)
|
||||||
_duplicate_run = MediaSource.index_media_items(channel)
|
_duplicate_run = MediaSource.index_media_items(source)
|
||||||
|
|
||||||
media_items = Repo.preload(channel, :media_items).media_items
|
media_items = Repo.preload(source, :media_items).media_items
|
||||||
assert Enum.count(media_items) == 3
|
assert Enum.count(media_items) == 3
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it can duplicate media_ids for different channels", %{channel: channel} do
|
test "it can duplicate media_ids for different sources", %{source: source} do
|
||||||
other_channel = channel_fixture()
|
other_source = source_fixture()
|
||||||
|
|
||||||
media_items = MediaSource.index_media_items(channel)
|
media_items = MediaSource.index_media_items(source)
|
||||||
media_items_other_channel = MediaSource.index_media_items(other_channel)
|
media_items_other_source = MediaSource.index_media_items(other_source)
|
||||||
|
|
||||||
assert Enum.count(media_items) == 3
|
assert Enum.count(media_items) == 3
|
||||||
assert Enum.count(media_items_other_channel) == 3
|
assert Enum.count(media_items_other_source) == 3
|
||||||
|
|
||||||
assert Enum.map(media_items, & &1.media_id) ==
|
assert Enum.map(media_items, & &1.media_id) ==
|
||||||
Enum.map(media_items_other_channel, & &1.media_id)
|
Enum.map(media_items_other_source, & &1.media_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns a list of media_items or changesets", %{channel: channel} do
|
test "it returns a list of media_items or changesets", %{source: source} do
|
||||||
first_run = MediaSource.index_media_items(channel)
|
first_run = MediaSource.index_media_items(source)
|
||||||
duplicate_run = MediaSource.index_media_items(channel)
|
duplicate_run = MediaSource.index_media_items(source)
|
||||||
|
|
||||||
assert Enum.all?(first_run, fn %MediaItem{} -> true end)
|
assert Enum.all?(first_run, fn %MediaItem{} -> true end)
|
||||||
assert Enum.all?(duplicate_run, fn %Ecto.Changeset{} -> true end)
|
assert Enum.all?(duplicate_run, fn %Ecto.Changeset{} -> true end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "update_channel/2" do
|
describe "update_source/2" do
|
||||||
test "updates with valid data updates the channel" do
|
test "updates with valid data updates the source" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{name: "some updated name"}
|
update_attrs = %{name: "some updated name"}
|
||||||
|
|
||||||
assert {:ok, %Channel{} = channel} = MediaSource.update_channel(channel, update_attrs)
|
assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
|
||||||
assert channel.name == "some updated name"
|
assert source.name == "some updated name"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updating the original_url will re-fetch the channel details" do
|
test "updating the original_url will re-fetch the source details" do
|
||||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||||
|
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
|
||||||
|
|
||||||
assert {:ok, %Channel{} = channel} = MediaSource.update_channel(channel, update_attrs)
|
assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
|
||||||
assert channel.name == "some name"
|
assert source.name == "some name"
|
||||||
assert String.starts_with?(channel.channel_id, "some_channel_id_")
|
assert String.starts_with?(source.collection_id, "some_source_id_")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "not updating the original_url will not re-fetch the channel details" do
|
test "not updating the original_url will not re-fetch the source details" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
|
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
|
||||||
|
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{name: "some updated name"}
|
update_attrs = %{name: "some updated name"}
|
||||||
|
|
||||||
assert {:ok, %Channel{}} = MediaSource.update_channel(channel, update_attrs)
|
assert {:ok, %Source{}} = MediaSource.update_source(source, update_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updating the index frequency will re-schedule the indexing task" do
|
test "updating the index frequency will re-schedule the indexing task" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{index_frequency_minutes: 123}
|
update_attrs = %{index_frequency_minutes: 123}
|
||||||
|
|
||||||
assert {:ok, %Channel{} = channel} = MediaSource.update_channel(channel, update_attrs)
|
assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
|
||||||
assert channel.index_frequency_minutes == 123
|
assert source.index_frequency_minutes == 123
|
||||||
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "not updating the index frequency will not re-schedule the indexing task" do
|
test "not updating the index frequency will not re-schedule the indexing task" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
update_attrs = %{name: "some updated name"}
|
update_attrs = %{name: "some updated name"}
|
||||||
|
|
||||||
assert {:ok, %Channel{}} = MediaSource.update_channel(channel, update_attrs)
|
assert {:ok, %Source{}} = MediaSource.update_source(source, update_attrs)
|
||||||
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates with invalid data returns error changeset" do
|
test "updates with invalid data returns error changeset" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
assert {:error, %Ecto.Changeset{}} =
|
assert {:error, %Ecto.Changeset{}} =
|
||||||
MediaSource.update_channel(channel, @invalid_channel_attrs)
|
MediaSource.update_source(source, @invalid_source_attrs)
|
||||||
|
|
||||||
assert channel == MediaSource.get_channel!(channel.id)
|
assert source == MediaSource.get_source!(source.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_channel/1" do
|
describe "delete_source/1" do
|
||||||
test "it deletes the channel" do
|
test "it deletes the source" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
assert {:ok, %Channel{}} = MediaSource.delete_channel(channel)
|
assert {:ok, %Source{}} = MediaSource.delete_source(source)
|
||||||
assert_raise Ecto.NoResultsError, fn -> MediaSource.get_channel!(channel.id) end
|
assert_raise Ecto.NoResultsError, fn -> MediaSource.get_source!(source.id) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns a channel changeset" do
|
test "it returns a source changeset" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
assert %Ecto.Changeset{} = MediaSource.change_channel(channel)
|
assert %Ecto.Changeset{} = MediaSource.change_source(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deletion also deletes all associated tasks" do
|
test "deletion also deletes all associated tasks" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
task = task_fixture(channel_id: channel.id)
|
task = task_fixture(source_id: source.id)
|
||||||
|
|
||||||
assert {:ok, %Channel{}} = MediaSource.delete_channel(channel)
|
assert {:ok, %Source{}} = MediaSource.delete_source(source)
|
||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "change_channel/2" do
|
describe "change_source/2" do
|
||||||
test "it returns a changeset" do
|
test "it returns a changeset" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
assert %Ecto.Changeset{} = MediaSource.change_channel(channel)
|
assert %Ecto.Changeset{} = MediaSource.change_source(source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "change_channel_from_url/2" do
|
describe "change_source_from_url/2" do
|
||||||
test "it returns a changeset" do
|
test "it returns a changeset" do
|
||||||
stub(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
stub(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
assert %Ecto.Changeset{} = MediaSource.change_channel_from_url(channel, %{})
|
assert %Ecto.Changeset{} = MediaSource.change_source_from_url(source, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not fetch channel details if the original_url isn't in the changeset" do
|
test "it does not fetch source details if the original_url isn't in the changeset" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
|
expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
|
||||||
|
|
||||||
changeset = MediaSource.change_channel_from_url(%Channel{}, %{name: "some updated name"})
|
changeset = MediaSource.change_source_from_url(%Source{}, %{name: "some updated name"})
|
||||||
|
|
||||||
assert %Ecto.Changeset{} = changeset
|
assert %Ecto.Changeset{} = changeset
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it fetches channel details if the original_url is in the changeset" do
|
test "it fetches source details if the original_url is in the changeset" do
|
||||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
MediaSource.change_channel_from_url(%Channel{}, %{
|
MediaSource.change_source_from_url(%Source{}, %{
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
original_url: "https://www.youtube.com/channel/abc123"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert %Ecto.Changeset{} = changeset
|
assert %Ecto.Changeset{} = changeset
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it adds channel details to the changeset, keeping the orignal details" do
|
test "it adds source details to the changeset, keeping the orignal details" do
|
||||||
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
|
||||||
|
|
||||||
media_profile = media_profile_fixture()
|
media_profile = media_profile_fixture()
|
||||||
media_profile_id = media_profile.id
|
media_profile_id = media_profile.id
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
MediaSource.change_channel_from_url(%Channel{}, %{
|
MediaSource.change_source_from_url(%Source{}, %{
|
||||||
original_url: "https://www.youtube.com/channel/abc123",
|
original_url: "https://www.youtube.com/channel/abc123",
|
||||||
media_profile_id: media_profile.id
|
media_profile_id: media_profile.id
|
||||||
})
|
})
|
||||||
|
|
||||||
assert %Ecto.Changeset{} = changeset
|
assert %Ecto.Changeset{} = changeset
|
||||||
assert String.starts_with?(changeset.changes.channel_id, "some_channel_id_")
|
assert String.starts_with?(changeset.changes.collection_id, "some_source_id_")
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
name: "some name",
|
name: "some name",
|
||||||
|
|
@ -292,12 +296,12 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
end)
|
end)
|
||||||
|
|
||||||
changeset =
|
changeset =
|
||||||
MediaSource.change_channel_from_url(%Channel{}, %{
|
MediaSource.change_source_from_url(%Source{}, %{
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
original_url: "https://www.youtube.com/channel/abc123"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert %Ecto.Changeset{} = changeset
|
assert %Ecto.Changeset{} = changeset
|
||||||
assert errors_on(changeset).original_url == ["could not fetch channel details from URL"]
|
assert errors_on(changeset).original_url == ["could not fetch source details from URL"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -306,7 +310,7 @@ defmodule Pinchflat.MediaSourceTest do
|
||||||
:ok,
|
:ok,
|
||||||
Phoenix.json_library().encode!(%{
|
Phoenix.json_library().encode!(%{
|
||||||
channel: "some name",
|
channel: "some name",
|
||||||
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}"
|
channel_id: "some_source_id_#{:rand.uniform(1_000_000)}"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,23 +30,23 @@ defmodule Pinchflat.MediaTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "list_pending_media_items_for/1" do
|
describe "list_pending_media_items_for/1" do
|
||||||
test "it returns pending media_items for a given channel" do
|
test "it returns pending media_items for a given source" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
media_item = media_item_fixture(%{channel_id: channel.id, media_filepath: nil})
|
media_item = media_item_fixture(%{source_id: source.id, media_filepath: nil})
|
||||||
|
|
||||||
assert Media.list_pending_media_items_for(channel) == [media_item]
|
assert Media.list_pending_media_items_for(source) == [media_item]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not return media_items with media_filepath" do
|
test "it does not return media_items with media_filepath" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
_media_item =
|
_media_item =
|
||||||
media_item_fixture(%{
|
media_item_fixture(%{
|
||||||
channel_id: channel.id,
|
source_id: source.id,
|
||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}"
|
media_filepath: "/video/#{Faker.File.file_name(:video)}"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert Media.list_pending_media_items_for(channel) == []
|
assert Media.list_pending_media_items_for(source) == []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ defmodule Pinchflat.MediaTest do
|
||||||
media_id: Faker.String.base64(12),
|
media_id: Faker.String.base64(12),
|
||||||
title: Faker.Commerce.product_name(),
|
title: Faker.Commerce.product_name(),
|
||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
channel_id: channel_fixture().id
|
source_id: source_fixture().id
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
assert {:ok, %MediaItem{} = media_item} = Media.create_media_item(valid_attrs)
|
||||||
|
|
@ -85,7 +85,7 @@ defmodule Pinchflat.MediaTest do
|
||||||
media_id: Faker.String.base64(12),
|
media_id: Faker.String.base64(12),
|
||||||
title: Faker.Commerce.product_name(),
|
title: Faker.Commerce.product_name(),
|
||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
channel_id: channel_fixture().id
|
source_id: source_fixture().id
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:ok, %MediaItem{} = media_item} = Media.update_media_item(media_item, update_attrs)
|
assert {:ok, %MediaItem{} = media_item} = Media.update_media_item(media_item, update_attrs)
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
defmodule Pinchflat.Tasks.ChannelTasksTest do
|
|
||||||
use Pinchflat.DataCase
|
|
||||||
|
|
||||||
import Pinchflat.TasksFixtures
|
|
||||||
import Pinchflat.MediaSourceFixtures
|
|
||||||
|
|
||||||
alias Pinchflat.Tasks.Task
|
|
||||||
alias Pinchflat.Tasks.ChannelTasks
|
|
||||||
alias Pinchflat.Workers.MediaIndexingWorker
|
|
||||||
|
|
||||||
describe "kickoff_indexing_task/1" do
|
|
||||||
test "it does not schedule a job if the interval is <= 0" do
|
|
||||||
channel = channel_fixture(index_frequency_minutes: -1)
|
|
||||||
|
|
||||||
assert {:ok, :should_not_index} = ChannelTasks.kickoff_indexing_task(channel)
|
|
||||||
|
|
||||||
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it schedules a job if the interval is > 0" do
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 1)
|
|
||||||
|
|
||||||
assert {:ok, _} = ChannelTasks.kickoff_indexing_task(channel)
|
|
||||||
|
|
||||||
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it creates and attaches a task if the interval is > 0" do
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 1)
|
|
||||||
|
|
||||||
assert {:ok, %Task{} = task} = ChannelTasks.kickoff_indexing_task(channel)
|
|
||||||
|
|
||||||
assert task.channel_id == channel.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it deletes any pending tasks for the channel" do
|
|
||||||
channel = channel_fixture()
|
|
||||||
task = task_fixture(channel_id: channel.id)
|
|
||||||
|
|
||||||
assert {:ok, _} = ChannelTasks.kickoff_indexing_task(channel)
|
|
||||||
|
|
||||||
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
45
test/pinchflat/tasks/source_tasks_test.exs
Normal file
45
test/pinchflat/tasks/source_tasks_test.exs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule Pinchflat.Tasks.SourceTasksTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Pinchflat.TasksFixtures
|
||||||
|
import Pinchflat.MediaSourceFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Tasks.Task
|
||||||
|
alias Pinchflat.Tasks.SourceTasks
|
||||||
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
|
||||||
|
describe "kickoff_indexing_task/1" do
|
||||||
|
test "it does not schedule a job if the interval is <= 0" do
|
||||||
|
source = source_fixture(index_frequency_minutes: -1)
|
||||||
|
|
||||||
|
assert {:ok, :should_not_index} = SourceTasks.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it schedules a job if the interval is > 0" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
|
|
||||||
|
assert {:ok, _} = SourceTasks.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it creates and attaches a task if the interval is > 0" do
|
||||||
|
source = source_fixture(index_frequency_minutes: 1)
|
||||||
|
|
||||||
|
assert {:ok, %Task{} = task} = SourceTasks.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
assert task.source_id == source.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it deletes any pending tasks for the source" do
|
||||||
|
source = source_fixture()
|
||||||
|
task = task_fixture(source_id: source.id)
|
||||||
|
|
||||||
|
assert {:ok, _} = SourceTasks.kickoff_indexing_task(source)
|
||||||
|
|
||||||
|
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -21,11 +21,11 @@ defmodule Pinchflat.TasksTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not delete the other record when a job gets deleted" do
|
test "it does not delete the other record when a job gets deleted" do
|
||||||
task = Repo.preload(task_fixture(), [:channel, :job])
|
task = Repo.preload(task_fixture(), [:source, :job])
|
||||||
|
|
||||||
{:ok, _} = Repo.delete(task.job)
|
{:ok, _} = Repo.delete(task.job)
|
||||||
|
|
||||||
assert Repo.reload!(task.channel)
|
assert Repo.reload!(task.source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -40,14 +40,14 @@ defmodule Pinchflat.TasksTest do
|
||||||
test "it lets you specify which record type/ID to join on" do
|
test "it lets you specify which record type/ID to join on" do
|
||||||
task = task_fixture()
|
task = task_fixture()
|
||||||
|
|
||||||
assert Tasks.list_tasks_for(:channel_id, task.channel_id) == [task]
|
assert Tasks.list_tasks_for(:source_id, task.source_id) == [task]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it lets you specify which job states to include" do
|
test "it lets you specify which job states to include" do
|
||||||
task = task_fixture()
|
task = task_fixture()
|
||||||
|
|
||||||
assert Tasks.list_tasks_for(:channel_id, task.channel_id, [:available]) == [task]
|
assert Tasks.list_tasks_for(:source_id, task.source_id, [:available]) == [task]
|
||||||
assert Tasks.list_tasks_for(:channel_id, task.channel_id, [:cancelled]) == []
|
assert Tasks.list_tasks_for(:source_id, task.source_id, [:cancelled]) == []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -55,14 +55,14 @@ defmodule Pinchflat.TasksTest do
|
||||||
test "it lists pending tasks" do
|
test "it lists pending tasks" do
|
||||||
task = task_fixture()
|
task = task_fixture()
|
||||||
|
|
||||||
assert Tasks.list_pending_tasks_for(:channel_id, task.channel_id) == [task]
|
assert Tasks.list_pending_tasks_for(:source_id, task.source_id) == [task]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not list non-pending tasks" do
|
test "it does not list non-pending tasks" do
|
||||||
task = Repo.preload(task_fixture(), :job)
|
task = Repo.preload(task_fixture(), :job)
|
||||||
:ok = Oban.cancel_job(task.job)
|
:ok = Oban.cancel_job(task.job)
|
||||||
|
|
||||||
assert Tasks.list_pending_tasks_for(:channel_id, task.channel_id) == []
|
assert Tasks.list_pending_tasks_for(:source_id, task.source_id) == []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -84,14 +84,14 @@ defmodule Pinchflat.TasksTest do
|
||||||
assert {:error, %Ecto.Changeset{}} = Tasks.create_task(@invalid_attrs)
|
assert {:error, %Ecto.Changeset{}} = Tasks.create_task(@invalid_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "accepts a job and channel" do
|
test "accepts a job and source" do
|
||||||
job = job_fixture()
|
job = job_fixture()
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
assert {:ok, %Task{} = task} = Tasks.create_task(job, channel)
|
assert {:ok, %Task{} = task} = Tasks.create_task(job, source)
|
||||||
|
|
||||||
assert task.job_id == job.id
|
assert task.job_id == job.id
|
||||||
assert task.channel_id == channel.id
|
assert task.source_id == source.id
|
||||||
end
|
end
|
||||||
|
|
||||||
test "accepts a job and media item" do
|
test "accepts a job and media item" do
|
||||||
|
|
@ -115,25 +115,25 @@ defmodule Pinchflat.TasksTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a task record if successful" do
|
test "it creates a task record if successful" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
assert {:ok, %Task{} = task} = Tasks.create_job_with_task(TestJobWorker.new(%{}), channel)
|
assert {:ok, %Task{} = task} = Tasks.create_job_with_task(TestJobWorker.new(%{}), source)
|
||||||
|
|
||||||
assert task.channel_id == channel.id
|
assert task.source_id == source.id
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns an error if the job already exists" do
|
test "it returns an error if the job already exists" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity])
|
job = TestJobWorker.new(%{foo: "bar"}, unique: [period: :infinity])
|
||||||
|
|
||||||
assert {:ok, %Task{}} = Tasks.create_job_with_task(job, channel)
|
assert {:ok, %Task{}} = Tasks.create_job_with_task(job, source)
|
||||||
assert {:error, :duplicate_job} = Tasks.create_job_with_task(job, channel)
|
assert {:error, :duplicate_job} = Tasks.create_job_with_task(job, source)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it returns an error if the job fails to enqueue" do
|
test "it returns an error if the job fails to enqueue" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
|
|
||||||
assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, channel)
|
assert {:error, %Ecto.Changeset{}} = Tasks.create_job_with_task(%Ecto.Changeset{}, source)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -155,11 +155,11 @@ defmodule Pinchflat.TasksTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_tasks_for/1" do
|
describe "delete_tasks_for/1" do
|
||||||
test "it deletes tasks attached to a channel" do
|
test "it deletes tasks attached to a source" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
task = task_fixture(channel_id: channel.id)
|
task = task_fixture(source_id: source.id)
|
||||||
|
|
||||||
assert :ok = Tasks.delete_tasks_for(channel)
|
assert :ok = Tasks.delete_tasks_for(source)
|
||||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -173,20 +173,20 @@ defmodule Pinchflat.TasksTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "delete_pending_tasks_for/1" do
|
describe "delete_pending_tasks_for/1" do
|
||||||
test "it deletes pending tasks attached to a channel" do
|
test "it deletes pending tasks attached to a source" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
task = task_fixture(channel_id: channel.id)
|
task = task_fixture(source_id: source.id)
|
||||||
|
|
||||||
assert :ok = Tasks.delete_pending_tasks_for(channel)
|
assert :ok = Tasks.delete_pending_tasks_for(source)
|
||||||
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
assert_raise Ecto.NoResultsError, fn -> Tasks.get_task!(task.id) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not delete non-pending tasks" do
|
test "it does not delete non-pending tasks" do
|
||||||
channel = channel_fixture()
|
source = source_fixture()
|
||||||
task = Repo.preload(task_fixture(channel_id: channel.id), :job)
|
task = Repo.preload(task_fixture(source_id: source.id), :job)
|
||||||
:ok = Oban.cancel_job(task.job)
|
:ok = Oban.cancel_job(task.job)
|
||||||
|
|
||||||
assert :ok = Tasks.delete_pending_tasks_for(channel)
|
assert :ok = Tasks.delete_pending_tasks_for(source)
|
||||||
assert Tasks.get_task!(task.id)
|
assert Tasks.get_task!(task.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,36 +12,36 @@ defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "perform/1" do
|
describe "perform/1" do
|
||||||
test "it does not do any indexing if the channel shouldn't be indexed" do
|
test "it does not do any indexing if the source shouldn't be indexed" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: -1)
|
source = source_fixture(index_frequency_minutes: -1)
|
||||||
|
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it does not reschedule if the channel shouldn't be indexed" do
|
test "it does not reschedule if the source shouldn't be indexed" do
|
||||||
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, 0, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: -1)
|
source = source_fixture(index_frequency_minutes: -1)
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => channel.id})
|
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it indexes the channel if it should be indexed" do
|
test "it indexes the source if it should be indexed" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it kicks off a download job for each pending media item" do
|
test "it kicks off a download job for each pending media item" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1"} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
@ -49,9 +49,9 @@ defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||||
test "it starts a job for any pending media item even if it's from another run" do
|
test "it starts a job for any pending media item even if it's from another run" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1"} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
media_item_fixture(%{channel_id: channel.id, media_filepath: nil})
|
media_item_fixture(%{source_id: source.id, media_filepath: nil})
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
assert [_, _] = all_enqueued(worker: VideoDownloadWorker)
|
assert [_, _] = all_enqueued(worker: VideoDownloadWorker)
|
||||||
end
|
end
|
||||||
|
|
@ -59,8 +59,8 @@ defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||||
test "it does not kick off a job for media items that could not be saved" do
|
test "it does not kick off a job for media items that could not be saved" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo1"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo1"} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
# Only one job should be enqueued, since the second video is a duplicate
|
# Only one job should be enqueued, since the second video is a duplicate
|
||||||
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
assert [_] = all_enqueued(worker: VideoDownloadWorker)
|
||||||
|
|
@ -69,41 +69,41 @@ defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||||
test "it reschedules the job based on the index frequency" do
|
test "it reschedules the job based on the index frequency" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
|
|
||||||
assert_enqueued(
|
assert_enqueued(
|
||||||
worker: MediaIndexingWorker,
|
worker: MediaIndexingWorker,
|
||||||
args: %{"id" => channel.id},
|
args: %{"id" => source.id},
|
||||||
scheduled_at: now_plus(channel.index_frequency_minutes, :minutes)
|
scheduled_at: now_plus(source.index_frequency_minutes, :minutes)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates a task for the rescheduled job" do
|
test "it creates a task for the rescheduled job" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, ""} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
task_count_fetcher = fn -> Enum.count(Tasks.list_tasks()) end
|
task_count_fetcher = fn -> Enum.count(Tasks.list_tasks()) end
|
||||||
|
|
||||||
assert_changed([from: 0, to: 1], task_count_fetcher, fn ->
|
assert_changed([from: 0, to: 1], task_count_fetcher, fn ->
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it creates the basic media_item records" do
|
test "it creates the basic media_item records" do
|
||||||
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo2"} end)
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "video1\nvideo2"} end)
|
||||||
|
|
||||||
channel = channel_fixture(index_frequency_minutes: 10)
|
source = source_fixture(index_frequency_minutes: 10)
|
||||||
|
|
||||||
media_item_fetcher = fn ->
|
media_item_fetcher = fn ->
|
||||||
channel
|
source
|
||||||
|> Repo.preload(:media_items)
|
|> Repo.preload(:media_items)
|
||||||
|> Map.get(:media_items)
|
|> Map.get(:media_items)
|
||||||
|> Enum.map(fn media_item -> media_item.media_id end)
|
|> Enum.map(fn media_item -> media_item.media_id end)
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_changed([from: [], to: ["video1", "video2"]], media_item_fetcher, fn ->
|
assert_changed([from: [], to: ["video1", "video2"]], media_item_fetcher, fn ->
|
||||||
perform_job(MediaIndexingWorker, %{id: channel.id})
|
perform_job(MediaIndexingWorker, %{id: source.id})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Pinchflat.Workers.VideoDownloadWorkerTest do
|
||||||
media_item =
|
media_item =
|
||||||
Repo.preload(
|
Repo.preload(
|
||||||
media_item_fixture(%{media_filepath: nil}),
|
media_item_fixture(%{media_filepath: nil}),
|
||||||
[:metadata, channel: :media_profile]
|
[:metadata, source: :media_profile]
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, %{media_item: media_item}}
|
{:ok, %{media_item: media_item}}
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
defmodule PinchflatWeb.ChannelControllerTest do
|
|
||||||
use PinchflatWeb.ConnCase
|
|
||||||
import Mox
|
|
||||||
|
|
||||||
import Pinchflat.ProfilesFixtures
|
|
||||||
import Pinchflat.MediaSourceFixtures
|
|
||||||
|
|
||||||
setup do
|
|
||||||
media_profile = media_profile_fixture()
|
|
||||||
|
|
||||||
{
|
|
||||||
:ok,
|
|
||||||
%{
|
|
||||||
create_attrs: %{
|
|
||||||
media_profile_id: media_profile.id,
|
|
||||||
original_url: "https://www.youtube.com/channel/abc123"
|
|
||||||
},
|
|
||||||
update_attrs: %{
|
|
||||||
original_url: "https://www.youtube.com/channel/321xyz"
|
|
||||||
},
|
|
||||||
invalid_attrs: %{original_url: nil, media_profile_id: nil}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
setup :verify_on_exit!
|
|
||||||
|
|
||||||
describe "index" do
|
|
||||||
test "lists all channels", %{conn: conn} do
|
|
||||||
conn = get(conn, ~p"/media_sources/channels")
|
|
||||||
assert html_response(conn, 200) =~ "Listing Channels"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "new channel" do
|
|
||||||
test "renders form", %{conn: conn} do
|
|
||||||
conn = get(conn, ~p"/media_sources/channels/new")
|
|
||||||
assert html_response(conn, 200) =~ "New Channel"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "create channel" do
|
|
||||||
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
|
||||||
conn = post(conn, ~p"/media_sources/channels", channel: create_attrs)
|
|
||||||
|
|
||||||
assert %{id: id} = redirected_params(conn)
|
|
||||||
assert redirected_to(conn) == ~p"/media_sources/channels/#{id}"
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/media_sources/channels/#{id}")
|
|
||||||
assert html_response(conn, 200) =~ "Channel #{id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors when data is invalid", %{conn: conn, invalid_attrs: invalid_attrs} do
|
|
||||||
conn = post(conn, ~p"/media_sources/channels", channel: invalid_attrs)
|
|
||||||
assert html_response(conn, 200) =~ "New Channel"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "edit channel" do
|
|
||||||
setup [:create_channel]
|
|
||||||
|
|
||||||
test "renders form for editing chosen channel", %{conn: conn, channel: channel} do
|
|
||||||
conn = get(conn, ~p"/media_sources/channels/#{channel}/edit")
|
|
||||||
assert html_response(conn, 200) =~ "Edit Channel"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update channel" do
|
|
||||||
setup [:create_channel]
|
|
||||||
|
|
||||||
test "redirects when data is valid", %{conn: conn, channel: channel, update_attrs: update_attrs} do
|
|
||||||
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
|
||||||
|
|
||||||
conn = put(conn, ~p"/media_sources/channels/#{channel}", channel: update_attrs)
|
|
||||||
assert redirected_to(conn) == ~p"/media_sources/channels/#{channel}"
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/media_sources/channels/#{channel}")
|
|
||||||
assert html_response(conn, 200) =~ "https://www.youtube.com/channel/321xyz"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors when data is invalid", %{
|
|
||||||
conn: conn,
|
|
||||||
channel: channel,
|
|
||||||
invalid_attrs: invalid_attrs
|
|
||||||
} do
|
|
||||||
conn = put(conn, ~p"/media_sources/channels/#{channel}", channel: invalid_attrs)
|
|
||||||
assert html_response(conn, 200) =~ "Edit Channel"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "delete channel" do
|
|
||||||
setup [:create_channel]
|
|
||||||
|
|
||||||
test "deletes chosen channel", %{conn: conn, channel: channel} do
|
|
||||||
conn = delete(conn, ~p"/media_sources/channels/#{channel}")
|
|
||||||
assert redirected_to(conn) == ~p"/media_sources/channels"
|
|
||||||
|
|
||||||
assert_error_sent 404, fn ->
|
|
||||||
get(conn, ~p"/media_sources/channels/#{channel}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_channel(_) do
|
|
||||||
channel = channel_fixture()
|
|
||||||
%{channel: channel}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp runner_function_mock(_url, _opts, _ot) do
|
|
||||||
{
|
|
||||||
:ok,
|
|
||||||
Phoenix.json_library().encode!(%{
|
|
||||||
channel: "some name",
|
|
||||||
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
120
test/pinchflat_web/controllers/source_controller_test.exs
Normal file
120
test/pinchflat_web/controllers/source_controller_test.exs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
defmodule PinchflatWeb.SourceControllerTest do
|
||||||
|
use PinchflatWeb.ConnCase
|
||||||
|
import Mox
|
||||||
|
|
||||||
|
import Pinchflat.ProfilesFixtures
|
||||||
|
import Pinchflat.MediaSourceFixtures
|
||||||
|
|
||||||
|
setup do
|
||||||
|
media_profile = media_profile_fixture()
|
||||||
|
|
||||||
|
{
|
||||||
|
:ok,
|
||||||
|
%{
|
||||||
|
create_attrs: %{
|
||||||
|
media_profile_id: media_profile.id,
|
||||||
|
collection_type: "channel",
|
||||||
|
original_url: "https://www.youtube.com/source/abc123"
|
||||||
|
},
|
||||||
|
update_attrs: %{
|
||||||
|
original_url: "https://www.youtube.com/source/321xyz"
|
||||||
|
},
|
||||||
|
invalid_attrs: %{original_url: nil, media_profile_id: nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
describe "index" do
|
||||||
|
test "lists all sources", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/media_sources/sources")
|
||||||
|
assert html_response(conn, 200) =~ "Listing Sources"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "new source" do
|
||||||
|
test "renders form", %{conn: conn} do
|
||||||
|
conn = get(conn, ~p"/media_sources/sources/new")
|
||||||
|
assert html_response(conn, 200) =~ "New Source"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "create source" do
|
||||||
|
test "redirects to show when data is valid", %{conn: conn, create_attrs: create_attrs} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||||
|
conn = post(conn, ~p"/media_sources/sources", source: create_attrs)
|
||||||
|
|
||||||
|
assert %{id: id} = redirected_params(conn)
|
||||||
|
assert redirected_to(conn) == ~p"/media_sources/sources/#{id}"
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/media_sources/sources/#{id}")
|
||||||
|
assert html_response(conn, 200) =~ "Source #{id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors when data is invalid", %{conn: conn, invalid_attrs: invalid_attrs} do
|
||||||
|
conn = post(conn, ~p"/media_sources/sources", source: invalid_attrs)
|
||||||
|
assert html_response(conn, 200) =~ "New Source"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "edit source" do
|
||||||
|
setup [:create_source]
|
||||||
|
|
||||||
|
test "renders form for editing chosen source", %{conn: conn, source: source} do
|
||||||
|
conn = get(conn, ~p"/media_sources/sources/#{source}/edit")
|
||||||
|
assert html_response(conn, 200) =~ "Edit Source"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "update source" do
|
||||||
|
setup [:create_source]
|
||||||
|
|
||||||
|
test "redirects when data is valid", %{conn: conn, source: source, update_attrs: update_attrs} do
|
||||||
|
expect(YtDlpRunnerMock, :run, 1, &runner_function_mock/3)
|
||||||
|
|
||||||
|
conn = put(conn, ~p"/media_sources/sources/#{source}", source: update_attrs)
|
||||||
|
assert redirected_to(conn) == ~p"/media_sources/sources/#{source}"
|
||||||
|
|
||||||
|
conn = get(conn, ~p"/media_sources/sources/#{source}")
|
||||||
|
assert html_response(conn, 200) =~ "https://www.youtube.com/source/321xyz"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "renders errors when data is invalid", %{
|
||||||
|
conn: conn,
|
||||||
|
source: source,
|
||||||
|
invalid_attrs: invalid_attrs
|
||||||
|
} do
|
||||||
|
conn = put(conn, ~p"/media_sources/sources/#{source}", source: invalid_attrs)
|
||||||
|
assert html_response(conn, 200) =~ "Edit Source"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "delete source" do
|
||||||
|
setup [:create_source]
|
||||||
|
|
||||||
|
test "deletes chosen source", %{conn: conn, source: source} do
|
||||||
|
conn = delete(conn, ~p"/media_sources/sources/#{source}")
|
||||||
|
assert redirected_to(conn) == ~p"/media_sources/sources"
|
||||||
|
|
||||||
|
assert_error_sent 404, fn ->
|
||||||
|
get(conn, ~p"/media_sources/sources/#{source}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_source(_) do
|
||||||
|
source = source_fixture()
|
||||||
|
%{source: source}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp runner_function_mock(_url, _opts, _ot) do
|
||||||
|
{
|
||||||
|
:ok,
|
||||||
|
Phoenix.json_library().encode!(%{
|
||||||
|
channel: "some name",
|
||||||
|
channel_id: "some_source_id_#{:rand.uniform(1_000_000)}"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Pinchflat.MediaFixtures do
|
||||||
media_id: Faker.String.base64(12),
|
media_id: Faker.String.base64(12),
|
||||||
title: Faker.Commerce.product_name(),
|
title: Faker.Commerce.product_name(),
|
||||||
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
media_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||||
channel_id: MediaSourceFixtures.channel_fixture().id
|
source_id: MediaSourceFixtures.source_fixture().id
|
||||||
})
|
})
|
||||||
|> Pinchflat.Media.create_media_item()
|
|> Pinchflat.Media.create_media_item()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,19 @@ defmodule Pinchflat.MediaSourceFixtures do
|
||||||
|
|
||||||
alias Pinchflat.Repo
|
alias Pinchflat.Repo
|
||||||
alias Pinchflat.ProfilesFixtures
|
alias Pinchflat.ProfilesFixtures
|
||||||
alias Pinchflat.MediaSource.Channel
|
alias Pinchflat.MediaSource.Source
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generate a channel.
|
Generate a source.
|
||||||
"""
|
"""
|
||||||
def channel_fixture(attrs \\ %{}) do
|
def source_fixture(attrs \\ %{}) do
|
||||||
{:ok, channel} =
|
{:ok, source} =
|
||||||
%Channel{}
|
%Source{}
|
||||||
|> Channel.changeset(
|
|> Source.changeset(
|
||||||
Enum.into(attrs, %{
|
Enum.into(attrs, %{
|
||||||
name: "Channel ##{:rand.uniform(1_000_000)}",
|
name: "Source ##{:rand.uniform(1_000_000)}",
|
||||||
channel_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
||||||
|
collection_type: "channel",
|
||||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||||
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
||||||
index_frequency_minutes: 60
|
index_frequency_minutes: 60
|
||||||
|
|
@ -25,6 +26,6 @@ defmodule Pinchflat.MediaSourceFixtures do
|
||||||
)
|
)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
||||||
channel
|
source
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Pinchflat.TasksFixtures do
|
||||||
{:ok, task} =
|
{:ok, task} =
|
||||||
attrs
|
attrs
|
||||||
|> Enum.into(%{
|
|> Enum.into(%{
|
||||||
channel_id: MediaSourceFixtures.channel_fixture().id,
|
source_id: MediaSourceFixtures.source_fixture().id,
|
||||||
job_id: JobFixtures.job_fixture().id
|
job_id: JobFixtures.job_fixture().id
|
||||||
})
|
})
|
||||||
|> Pinchflat.Tasks.create_task()
|
|> Pinchflat.Tasks.create_task()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue