Start integrating playlist support (#13)

* [WIP] updated the output of VideoCollection to include playlists

* Updated source's name to collection_name; supported playlist ID/name fetching

* Hooked up collection_type to form; refactored enqueue_pending_media_downloads

* Added friendly_name to form

* Added media profile link to source view

* Updates comment
This commit is contained in:
Kieran 2024-02-05 11:38:19 -08:00 committed by GitHub
parent bdef6c75bb
commit ca01f17b58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 291 additions and 100 deletions

View file

@ -2,7 +2,6 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
use ExUnit.Case, async: true
import Mox
alias Pinchflat.MediaClient.SourceDetails
alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection
@channel_url "https://www.youtube.com/c/TheUselessTrials"
@ -45,19 +44,30 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollectionTest do
end
describe "get_source_details/1" do
test "it returns a %SourceDetails{} with data on success" do
test "it returns a map with data on success" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
Phoenix.json_library().encode(%{
channel: "TheUselessTrials",
channel_id: "UCQH2",
playlist_id: "PLQH2",
playlist_title: "TheUselessTrials - Videos"
})
end)
assert {:ok, res} = VideoCollection.get_source_details(@channel_url)
assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} = res
assert %{
channel_id: "UCQH2",
channel_name: "TheUselessTrials",
playlist_id: "PLQH2",
playlist_name: "TheUselessTrials - Videos"
} = 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"
assert opts == [:simulate, :skip_download, playlist_end: 1]
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title})j"
{:ok, "{}"}
end)

View file

@ -8,20 +8,13 @@ defmodule Pinchflat.MediaClient.SourceDetailsTest do
setup :verify_on_exit!
describe "new/2" do
test "it returns a struct with the given values" do
assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} =
SourceDetails.new("UCQH2", "TheUselessTrials")
end
end
describe "get_source_details/2" do
test "it passes the expected arguments to the backend" do
expect(YtDlpRunnerMock, :run, fn @channel_url, opts, ot ->
assert opts == [:skip_download, playlist_end: 1]
assert ot == "%(.{channel,channel_id})j"
assert opts == [:simulate, :skip_download, playlist_end: 1]
assert ot == "%(.{channel,channel_id,playlist_id,playlist_title})j"
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
{:ok, "{}"}
end)
assert {:ok, _} = SourceDetails.get_source_details(@channel_url)
@ -29,11 +22,22 @@ defmodule Pinchflat.MediaClient.SourceDetailsTest do
test "it returns a struct composed of the returned data" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
{:ok, "{\"channel\": \"TheUselessTrials\", \"channel_id\": \"UCQH2\"}"}
Phoenix.json_library().encode(%{
channel: "TheUselessTrials",
channel_id: "UCQH2",
playlist_id: "PLQH2",
playlist_title: "TheUselessTrials - Videos"
})
end)
assert {:ok, res} = SourceDetails.get_source_details(@channel_url)
assert %SourceDetails{id: "UCQH2", name: "TheUselessTrials"} = res
assert %{
channel_id: "UCQH2",
channel_name: "TheUselessTrials",
playlist_id: "PLQH2",
playlist_name: "TheUselessTrials - Videos"
} = res
end
end

View file

@ -29,7 +29,7 @@ defmodule Pinchflat.MediaSourceTest do
end
describe "create_source/1" do
test "creates a source and adds name + ID from runner response" do
test "creates a source and adds name + ID from runner response for channels" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
valid_attrs = %{
@ -39,20 +39,34 @@ defmodule Pinchflat.MediaSourceTest do
}
assert {:ok, %Source{} = source} = MediaSource.create_source(valid_attrs)
assert source.name == "some name"
assert String.starts_with?(source.collection_id, "some_source_id_")
assert source.collection_name == "some channel name"
assert String.starts_with?(source.collection_id, "some_channel_id_")
end
test "creates a source and adds name + ID for playlists" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
valid_attrs = %{
media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/playlist?list=abc123",
collection_type: "playlist"
}
assert {:ok, %Source{} = source} = MediaSource.create_source(valid_attrs)
assert source.collection_name == "some playlist name"
assert String.starts_with?(source.collection_id, "some_playlist_id_")
end
test "creation with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = MediaSource.create_source(@invalid_source_attrs)
end
test "creation enforces uniqueness of source_id scoped to the media_profile" do
test "creation enforces uniqueness of collection_id scoped to the media_profile" do
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
{:ok,
Phoenix.json_library().encode!(%{
channel: "some name",
channel_id: "some_source_id_12345678"
channel: "some channel name",
channel_id: "some_channel_id_12345678"
})}
end)
@ -70,8 +84,8 @@ defmodule Pinchflat.MediaSourceTest do
expect(YtDlpRunnerMock, :run, 2, fn _url, _opts, _ot ->
{:ok,
Phoenix.json_library().encode!(%{
channel: "some name",
channel_id: "some_source_id_12345678"
channel: "some channel name",
channel_id: "some_channel_id_12345678"
})}
end)
@ -159,21 +173,32 @@ defmodule Pinchflat.MediaSourceTest do
describe "update_source/2" do
test "updates with valid data updates the source" do
source = source_fixture()
update_attrs = %{name: "some updated name"}
update_attrs = %{collection_name: "some updated name"}
assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
assert source.name == "some updated name"
assert source.collection_name == "some updated name"
end
test "updating the original_url will re-fetch the source details" do
test "updating the original_url will re-fetch the source details for channels" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
source = source_fixture()
update_attrs = %{original_url: "https://www.youtube.com/channel/abc123"}
assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
assert source.name == "some name"
assert String.starts_with?(source.collection_id, "some_source_id_")
assert source.collection_name == "some channel name"
assert String.starts_with?(source.collection_id, "some_channel_id_")
end
test "updating the original_url will re-fetch the source details for playlists" do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
source = source_fixture(collection_type: "playlist")
update_attrs = %{original_url: "https://www.youtube.com/playlist?list=abc123"}
assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
assert source.collection_name == "some playlist name"
assert String.starts_with?(source.collection_id, "some_playlist_id_")
end
test "not updating the original_url will not re-fetch the source details" do
@ -275,16 +300,16 @@ defmodule Pinchflat.MediaSourceTest do
media_profile_id = media_profile.id
changeset =
MediaSource.change_source_from_url(%Source{}, %{
MediaSource.change_source_from_url(%Source{collection_type: :channel}, %{
original_url: "https://www.youtube.com/channel/abc123",
media_profile_id: media_profile.id
})
assert %Ecto.Changeset{} = changeset
assert String.starts_with?(changeset.changes.collection_id, "some_source_id_")
assert String.starts_with?(changeset.changes.collection_id, "some_channel_id_")
assert %{
name: "some name",
collection_name: "some channel name",
media_profile_id: ^media_profile_id,
original_url: "https://www.youtube.com/channel/abc123"
} = changeset.changes
@ -309,8 +334,10 @@ defmodule Pinchflat.MediaSourceTest do
{
:ok,
Phoenix.json_library().encode!(%{
channel: "some name",
channel_id: "some_source_id_#{:rand.uniform(1_000_000)}"
channel: "some channel name",
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}",
playlist_id: "some_playlist_id_#{:rand.uniform(1_000_000)}",
playlist_title: "some playlist name"
})
}
end

View file

@ -2,11 +2,14 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
use Pinchflat.DataCase
import Pinchflat.TasksFixtures
import Pinchflat.MediaFixtures
import Pinchflat.MediaSourceFixtures
alias Pinchflat.Tasks
alias Pinchflat.Tasks.Task
alias Pinchflat.Tasks.SourceTasks
alias Pinchflat.Workers.MediaIndexingWorker
alias Pinchflat.Workers.VideoDownloadWorker
describe "kickoff_indexing_task/1" do
test "it does not schedule a job if the interval is <= 0" do
@ -42,4 +45,35 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
assert_raise Ecto.NoResultsError, fn -> Repo.reload!(task) end
end
end
describe "enqueue_pending_media_downloads/1" do
test "it enqueues a job for each pending media item" do
source = source_fixture()
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
assert :ok = SourceTasks.enqueue_pending_media_downloads(source)
assert_enqueued(worker: VideoDownloadWorker, args: %{"id" => media_item.id})
end
test "it does not enqueue a job for media items with a filepath" do
source = source_fixture()
_media_item = media_item_fixture(source_id: source.id, media_filepath: "some/filepath.mp4")
assert :ok = SourceTasks.enqueue_pending_media_downloads(source)
refute_enqueued(worker: VideoDownloadWorker)
end
test "it attaches a task to each enqueued job" do
source = source_fixture()
media_item = media_item_fixture(source_id: source.id, media_filepath: nil)
assert [] = Tasks.list_tasks_for(:media_item_id, media_item.id)
assert :ok = SourceTasks.enqueue_pending_media_downloads(source)
assert [_] = Tasks.list_tasks_for(:media_item_id, media_item.id)
end
end
end

View file

@ -104,16 +104,17 @@ defmodule PinchflatWeb.SourceControllerTest do
end
defp create_source(_) do
source = source_fixture()
%{source: source}
%{source: source_fixture()}
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)}"
channel: "some channel name",
channel_id: "some_channel_id_#{:rand.uniform(1_000_000)}",
playlist_id: "some_playlist_id_#{:rand.uniform(1_000_000)}",
playlist_title: "some playlist name"
})
}
end

View file

@ -16,9 +16,10 @@ defmodule Pinchflat.MediaSourceFixtures do
%Source{}
|> Source.changeset(
Enum.into(attrs, %{
name: "Source ##{:rand.uniform(1_000_000)}",
collection_name: "Source ##{:rand.uniform(1_000_000)}",
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
collection_type: "channel",
friendly_name: "Cool and good internal name!",
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
index_frequency_minutes: 60