Squash all the commits from the other branch bc I broke things (#88)

This commit is contained in:
Kieran 2024-03-14 12:18:16 -07:00 committed by GitHub
parent 52e260fd57
commit c89b7768e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 347 additions and 23 deletions

View file

@ -48,7 +48,8 @@ config :pinchflat, Oban,
media_indexing: 2, media_indexing: 2,
media_collection_indexing: 2, media_collection_indexing: 2,
media_fetching: 2, media_fetching: 2,
media_local_metadata: 8 local_metadata: 8,
remote_metadata: 4
] ]
# Configures the mailer # Configures the mailer

View file

@ -2,7 +2,7 @@ defmodule Pinchflat.Boot.DataBackfillWorker do
@moduledoc false @moduledoc false
use Oban.Worker, use Oban.Worker,
queue: :media_local_metadata, queue: :local_metadata,
unique: [period: :infinity, states: [:available, :scheduled, :retryable]], unique: [period: :infinity, states: [:available, :scheduled, :retryable]],
tags: ["media_item", "media_metadata", "local_metadata", "data_backfill"] tags: ["media_item", "media_metadata", "local_metadata", "data_backfill"]

View file

@ -2,7 +2,7 @@ defmodule Pinchflat.Filesystem.FilesystemDataWorker do
@moduledoc false @moduledoc false
use Oban.Worker, use Oban.Worker,
queue: :media_local_metadata, queue: :local_metadata,
tags: ["media_item", "media_metadata", "local_metadata"], tags: ["media_item", "media_metadata", "local_metadata"],
max_attempts: 1 max_attempts: 1
@ -13,6 +13,10 @@ defmodule Pinchflat.Filesystem.FilesystemDataWorker do
@doc """ @doc """
For a given media item, compute and save metadata about the file on-disk. For a given media item, compute and save metadata about the file on-disk.
IDEA: does this have to be a standalone job? I originally split it out
so a failure here wouldn't cause a downloader job retry, but I can match
for failures so it doesn't retry.
Returns :ok Returns :ok
""" """
def perform(%Oban.Job{args: %{"id" => media_item_id}}) do def perform(%Oban.Job{args: %{"id" => media_item_id}}) do

View file

@ -144,6 +144,8 @@ defmodule Pinchflat.Media do
@doc """ @doc """
Produces a flat list of the filesystem paths for a media_item's downloaded files Produces a flat list of the filesystem paths for a media_item's downloaded files
NOTE: this can almost certainly be made private
Returns [binary()] Returns [binary()]
""" """
def media_filepaths(media_item) do def media_filepaths(media_item) do
@ -162,6 +164,8 @@ defmodule Pinchflat.Media do
Produces a flat list of the filesystem paths for a media_item's metadata files. Produces a flat list of the filesystem paths for a media_item's metadata files.
Returns an empty list if the media_item has no metadata. Returns an empty list if the media_item has no metadata.
NOTE: this can almost certainly be made private
Returns [binary()] | [] Returns [binary()] | []
""" """
def metadata_filepaths(media_item) do def metadata_filepaths(media_item) do
@ -227,6 +231,7 @@ defmodule Pinchflat.Media do
def delete_media_item(%MediaItem{} = media_item, opts \\ []) do def delete_media_item(%MediaItem{} = media_item, opts \\ []) do
delete_files = Keyword.get(opts, :delete_files, false) delete_files = Keyword.get(opts, :delete_files, false)
# NOTE: this should delete metadata no matter what
if delete_files do if delete_files do
{:ok, _} = delete_all_attachments(media_item) {:ok, _} = delete_all_attachments(media_item)
end end
@ -242,6 +247,7 @@ defmodule Pinchflat.Media do
MediaItem.changeset(media_item, attrs) MediaItem.changeset(media_item, attrs)
end end
# NOTE: refactor this
defp delete_all_attachments(media_item) do defp delete_all_attachments(media_item) do
media_item = Repo.preload(media_item, :metadata) media_item = Repo.preload(media_item, :metadata)

View file

@ -0,0 +1,36 @@
defmodule Pinchflat.Metadata.SourceMetadata do
@moduledoc """
The SourceMetadata schema.
Look. Don't @ me about Metadata vs. Metadatum. I'm very sensitive.
"""
use Ecto.Schema
import Ecto.Changeset
alias Pinchflat.Sources.Source
@allowed_fields ~w(metadata_filepath)a
@required_fields ~w(metadata_filepath)a
schema "source_metadata" do
field :metadata_filepath, :string
belongs_to :source, Source
timestamps(type: :utc_datetime)
end
@doc false
def changeset(source_metadata, attrs) do
source_metadata
|> cast(attrs, @allowed_fields)
|> validate_required(@required_fields)
|> unique_constraint([:source_id])
end
@doc false
def filepath_attributes do
~w(metadata_filepath)a
end
end

View file

@ -0,0 +1,48 @@
defmodule Pinchflat.Metadata.SourceMetadataStorageWorker do
@moduledoc false
use Oban.Worker,
queue: :remote_metadata,
tags: ["media_source", "source_metadata", "remote_metadata"],
max_attempts: 1
alias __MODULE__
alias Pinchflat.Repo
alias Pinchflat.Tasks
alias Pinchflat.Sources
alias Pinchflat.YtDlp.MediaCollection
alias Pinchflat.Metadata.MetadataFileHelpers
@doc """
Starts the source metadata storage worker and creates a task for the source.
IDEA: testing out this method of handling job kickoff. I think I like it, so
I may use it in other places. Just testing it for now
Returns {:ok, %Task{}} | {:error, :duplicate_job} | {:error, %Ecto.Changeset{}}
"""
def kickoff_with_task(source) do
%{id: source.id}
|> SourceMetadataStorageWorker.new()
|> Tasks.create_job_with_task(source)
end
@impl Oban.Worker
@doc """
Fetches and stores metadata for a source in the secret metadata location.
Returns :ok
"""
def perform(%Oban.Job{args: %{"id" => source_id}}) do
source = Repo.preload(Sources.get_source!(source_id), :metadata)
{:ok, metadata} = MediaCollection.get_source_metadata(source.original_url)
Sources.update_source(source, %{
metadata: %{
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(source, metadata)
}
})
:ok
end
end

View file

@ -10,6 +10,7 @@ defmodule Pinchflat.Sources.Source do
alias Pinchflat.Tasks.Task alias Pinchflat.Tasks.Task
alias Pinchflat.Media.MediaItem alias Pinchflat.Media.MediaItem
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.Metadata.SourceMetadata
@allowed_fields ~w( @allowed_fields ~w(
collection_name collection_name
@ -28,7 +29,8 @@ defmodule Pinchflat.Sources.Source do
# Expensive API calls are made when a source is inserted/updated so # Expensive API calls are made when a source is inserted/updated so
# we want to ensure that the source is valid before making the call. # we want to ensure that the source is valid before making the call.
# This way, we check that the other attributes are valid before ensuring # This way, we check that the other attributes are valid before ensuring
# that all fields are valid. # that all fields are valid. This is still only one DB insert but it's
# a two-stage validation process to fail fast before the API call.
@initially_required_fields ~w( @initially_required_fields ~w(
index_frequency_minutes index_frequency_minutes
fast_index fast_index
@ -60,6 +62,8 @@ defmodule Pinchflat.Sources.Source do
belongs_to :media_profile, MediaProfile belongs_to :media_profile, MediaProfile
has_one :metadata, SourceMetadata, on_replace: :update
has_many :tasks, Task has_many :tasks, Task
has_many :media_items, MediaItem, foreign_key: :source_id has_many :media_items, MediaItem, foreign_key: :source_id
@ -80,6 +84,7 @@ defmodule Pinchflat.Sources.Source do
|> cast(attrs, @allowed_fields) |> cast(attrs, @allowed_fields)
|> dynamic_default(:custom_name, fn cs -> get_field(cs, :collection_name) end) |> dynamic_default(:custom_name, fn cs -> get_field(cs, :collection_name) end)
|> validate_required(required_fields) |> validate_required(required_fields)
|> cast_assoc(:metadata, with: &SourceMetadata.changeset/2, required: false)
|> unique_constraint([:collection_id, :media_profile_id]) |> unique_constraint([:collection_id, :media_profile_id])
end end

View file

@ -11,9 +11,11 @@ defmodule Pinchflat.Sources do
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.YtDlp.MediaCollection alias Pinchflat.YtDlp.MediaCollection
alias Pinchflat.Metadata.SourceMetadata
alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.FastIndexing.FastIndexingHelpers alias Pinchflat.FastIndexing.FastIndexingHelpers
alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers
alias Pinchflat.Metadata.SourceMetadataStorageWorker
@doc """ @doc """
Returns the list of sources. Returns [%Source{}, ...] Returns the list of sources. Returns [%Source{}, ...]
@ -54,7 +56,7 @@ defmodule Pinchflat.Sources do
case change_source(%Source{}, attrs, :initial) do case change_source(%Source{}, attrs, :initial) do
%Ecto.Changeset{valid?: true} -> %Ecto.Changeset{valid?: true} ->
%Source{} %Source{}
|> change_source_from_url(attrs) |> maybe_change_source_from_url(attrs)
|> maybe_change_indexing_frequency() |> maybe_change_indexing_frequency()
|> commit_and_handle_tasks() |> commit_and_handle_tasks()
@ -82,7 +84,7 @@ defmodule Pinchflat.Sources do
case change_source(source, attrs, :initial) do case change_source(source, attrs, :initial) do
%Ecto.Changeset{valid?: true} -> %Ecto.Changeset{valid?: true} ->
source source
|> change_source_from_url(attrs) |> maybe_change_source_from_url(attrs)
|> maybe_change_indexing_frequency() |> maybe_change_indexing_frequency()
|> commit_and_handle_tasks() |> commit_and_handle_tasks()
@ -107,6 +109,7 @@ defmodule Pinchflat.Sources do
end) end)
Tasks.delete_tasks_for(source) Tasks.delete_tasks_for(source)
delete_source_metadata_files(source)
Repo.delete(source) Repo.delete(source)
end end
@ -122,14 +125,12 @@ defmodule Pinchflat.Sources do
fetches source details from the original_url (if provided). If the source 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 source details as long as the `original_url` is present.
This means that it'll go for it even if a changeset is otherwise invalid. This
is pretty easy to change, but for MVP I'm not concerned.
NOTE: When operating in the ideal path, this effectively adds an API call NOTE: When operating in the ideal path, this effectively adds an API call
to the source creation/update process. Should be used only when needed. to the source creation/update process. Should be used only when needed.
NOTE: this can almost certainly be made private now
""" """
def change_source_from_url(%Source{} = source, attrs) do def maybe_change_source_from_url(%Source{} = source, attrs) do
case change_source(source, attrs) do case change_source(source, attrs) do
%Ecto.Changeset{changes: %{original_url: _}} = changeset -> %Ecto.Changeset{changes: %{original_url: _}} = changeset ->
add_source_details_to_changeset(source, changeset) add_source_details_to_changeset(source, changeset)
@ -139,6 +140,18 @@ defmodule Pinchflat.Sources do
end end
end end
defp delete_source_metadata_files(source) do
metadata = Repo.preload(source, :metadata).metadata || %SourceMetadata{}
mapped_struct = Map.from_struct(metadata)
filepaths =
SourceMetadata.filepath_attributes()
|> Enum.map(fn field -> mapped_struct[field] end)
|> Enum.filter(&is_binary/1)
Enum.each(filepaths, &File.rm/1)
end
defp add_source_details_to_changeset(source, changeset) do defp add_source_details_to_changeset(source, changeset) do
%Ecto.Changeset{changes: changes} = changeset %Ecto.Changeset{changes: changes} = changeset
@ -196,6 +209,9 @@ defmodule Pinchflat.Sources do
{:ok, %Source{} = source} -> {:ok, %Source{} = source} ->
maybe_handle_media_tasks(changeset, source) maybe_handle_media_tasks(changeset, source)
maybe_run_indexing_task(changeset, source) maybe_run_indexing_task(changeset, source)
run_metadata_storage_task(source)
{:ok, source}
err -> err ->
err err
@ -215,8 +231,6 @@ defmodule Pinchflat.Sources do
_ -> _ ->
:ok :ok
end end
{:ok, source}
end end
defp maybe_run_indexing_task(changeset, source) do defp maybe_run_indexing_task(changeset, source) do
@ -231,8 +245,11 @@ defmodule Pinchflat.Sources do
maybe_update_slow_indexing_task(changeset, source) maybe_update_slow_indexing_task(changeset, source)
maybe_update_fast_indexing_task(changeset, source) maybe_update_fast_indexing_task(changeset, source)
end end
end
{:ok, source} # This runs every time to pick up any changes to the metadata
defp run_metadata_storage_task(source) do
SourceMetadataStorageWorker.kickoff_with_task(source)
end end
defp maybe_update_slow_indexing_task(changeset, source) do defp maybe_update_slow_indexing_task(changeset, source) do

View file

@ -20,6 +20,8 @@ defmodule Pinchflat.Tasks do
Returns the list of tasks for a given record type and ID. Optionally allows you to specify Returns the list of tasks for a given record type and ID. Optionally allows you to specify
which worker or job states to include. which worker or job states to include.
IDEA: this should be updated to take a struct instead of a record type and ID
Returns [%Task{}, ...] Returns [%Task{}, ...]
""" """
def list_tasks_for(attached_record_type, attached_record_id, worker_name \\ nil, job_states \\ Oban.Job.states()) do def list_tasks_for(attached_record_type, attached_record_id, worker_name \\ nil, job_states \\ Oban.Job.states()) do

View file

@ -50,8 +50,8 @@ defmodule Pinchflat.YtDlp.MediaCollection do
@doc """ @doc """
Gets a source's ID and name from its URL. Gets a source's ID and name from its URL.
yt-dlp does not _really_ have source-specific functions, so yt-dlp does not _really_ have source-specific functions that return what
instead we're fetching just the first video (using playlist_end: 1) we need, so instead we're fetching just the first video (using playlist_end: 1)
and parsing the source ID and name from _its_ metadata and parsing the source ID and name from _its_ metadata
Returns {:ok, map()} | {:error, any, ...}. Returns {:ok, map()} | {:error, any, ...}.
@ -71,7 +71,35 @@ defmodule Pinchflat.YtDlp.MediaCollection do
end end
end end
@doc """
Gets a source's metadata from its URL.
This is mostly for things like getting the source's avatar and banner image
(if applicable). However, this yt-dlp call doesn't have enough overlap with
`get_source_details/1` to allow combining them - this one has _almost_ everything
we need, but it doesn't contain enough information to tell 100% if the url is a channel
or a playlist.
The main purpose of this (past using as a fetcher for _other_ metadata) is to live
as a compressed blob for possible future use. That's why it's not getting formatted like
`get_source_details/1`
Returns {:ok, map()} | {:error, any, ...}.
"""
def get_source_metadata(source_url) do
opts = [playlist_items: 0]
output_template = "playlist:%()j"
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
{:ok, parsed_json}
else
err -> err
end
end
defp format_source_details(response) do defp format_source_details(response) do
# NOTE: I should probably make this a struct some day
%{ %{
channel_id: response["channel_id"], channel_id: response["channel_id"],
channel_name: response["channel"], channel_name: response["channel"],

View file

@ -0,0 +1,14 @@
defmodule Pinchflat.Repo.Migrations.CreateSourceMetadata do
use Ecto.Migration
def change do
create table(:source_metadata) do
add :metadata_filepath, :string, null: false
add :source_id, references(:sources, on_delete: :delete_all), null: false
timestamps(type: :utc_datetime)
end
create unique_index(:source_metadata, [:source_id])
end
end

View file

@ -0,0 +1,55 @@
defmodule Pinchflat.Metadata.SourceMetadataStorageWorkerTest do
use Pinchflat.DataCase
import Mox
import Pinchflat.SourcesFixtures
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Metadata.SourceMetadataStorageWorker
setup :verify_on_exit!
describe "kickoff_with_task/1" do
test "enqueues a new worker for the source" do
source = source_fixture()
assert {:ok, _} = SourceMetadataStorageWorker.kickoff_with_task(source)
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
end
test "creates a new task for the source" do
source = source_fixture()
assert {:ok, task} = SourceMetadataStorageWorker.kickoff_with_task(source)
assert task.source_id == source.id
end
end
describe "perform/1" do
test "sets metadata location for source" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, "{}"} end)
source = Repo.preload(source_fixture(), :metadata)
refute source.metadata
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.preload(Repo.reload(source), :metadata)
assert source.metadata.metadata_filepath
File.rm!(source.metadata.metadata_filepath)
end
test "fetches and stores returned metadata for source" do
source = source_fixture()
file_contents = Phoenix.json_library().encode!(%{"title" => "test"})
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot -> {:ok, file_contents} end)
perform_job(SourceMetadataStorageWorker, %{id: source.id})
source = Repo.preload(Repo.reload(source), :metadata)
{:ok, metadata} = MetadataFileHelpers.read_compressed_metadata(source.metadata.metadata_filepath)
assert metadata == %{"title" => "test"}
end
end
end

View file

@ -101,7 +101,10 @@ defmodule Pinchflat.SlowIndexing.MediaCollectionIndexingWorkerTest do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end) expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts -> {:ok, ""} end)
source = source_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_for(:source_id, source.id, "MediaCollectionIndexingWorker"))
end
assert_changed([from: 0, to: 1], task_count_fetcher, fn -> assert_changed([from: 0, to: 1], task_count_fetcher, fn ->
perform_job(MediaCollectionIndexingWorker, %{id: source.id}) perform_job(MediaCollectionIndexingWorker, %{id: source.id})

View file

@ -8,16 +8,32 @@ defmodule Pinchflat.SourcesTest do
alias Pinchflat.Sources alias Pinchflat.Sources
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Metadata.MetadataFileHelpers
alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.FastIndexing.FastIndexingWorker alias Pinchflat.FastIndexing.FastIndexingWorker
alias Pinchflat.Downloading.MediaDownloadWorker alias Pinchflat.Downloading.MediaDownloadWorker
alias Pinchflat.FastIndexing.MediaIndexingWorker alias Pinchflat.FastIndexing.MediaIndexingWorker
alias Pinchflat.Metadata.SourceMetadataStorageWorker
alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker alias Pinchflat.SlowIndexing.MediaCollectionIndexingWorker
@invalid_source_attrs %{name: nil, collection_id: nil} @invalid_source_attrs %{name: nil, collection_id: nil}
setup :verify_on_exit! setup :verify_on_exit!
describe "schema" do
test "source_metadata is deleted when the source is deleted" do
source =
source_fixture(%{metadata: %{metadata_filepath: "/metadata.json.gz"}})
metadata = source.metadata
assert {:ok, %Source{}} = Sources.delete_source(source)
assert_raise Ecto.NoResultsError, fn ->
Repo.reload!(metadata)
end
end
end
describe "list_sources/0" do describe "list_sources/0" do
test "it returns all sources" do test "it returns all sources" do
source = source_fixture() source = source_fixture()
@ -220,6 +236,21 @@ defmodule Pinchflat.SourcesTest do
assert source.index_frequency_minutes == 0 assert source.index_frequency_minutes == 0
end end
test "creating will kickoff a metadata storage worker" do
expect(YtDlpRunnerMock, :run, &channel_mock/3)
valid_attrs = %{
media_profile_id: media_profile_fixture().id,
original_url: "https://www.youtube.com/channel/abc123",
fast_index: false,
index_frequency_minutes: 0
}
assert {:ok, %Source{} = source} = Sources.create_source(valid_attrs)
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
end
end end
describe "update_source/2" do describe "update_source/2" do
@ -384,6 +415,15 @@ defmodule Pinchflat.SourcesTest do
assert source.index_frequency_minutes == 0 assert source.index_frequency_minutes == 0
end end
test "updating will kickoff a metadata storage worker" do
source = source_fixture()
update_attrs = %{name: "some updated name"}
assert {:ok, %Source{} = source} = Sources.update_source(source, update_attrs)
assert_enqueued(worker: SourceMetadataStorageWorker, args: %{"id" => source.id})
end
end end
describe "delete_source/2" do describe "delete_source/2" do
@ -421,6 +461,22 @@ defmodule Pinchflat.SourcesTest do
assert {:ok, %Source{}} = Sources.delete_source(source) assert {:ok, %Source{}} = Sources.delete_source(source)
assert File.exists?(media_item.media_filepath) assert File.exists?(media_item.media_filepath)
end end
test "deletes the source's metadata files" do
stub(HTTPClientMock, :get, fn _url, _headers, _opts -> {:ok, ""} end)
source = Repo.preload(source_fixture(), :metadata)
update_attrs = %{
metadata: %{
metadata_filepath: MetadataFileHelpers.compress_and_store_metadata_for(source, %{})
}
}
{:ok, updated_source} = Sources.update_source(source, update_attrs)
assert {:ok, _} = Sources.delete_source(updated_source, delete_files: true)
refute File.exists?(updated_source.metadata.metadata_filepath)
end
end end
describe "delete_source/2 when deleting files" do describe "delete_source/2 when deleting files" do
@ -452,18 +508,18 @@ defmodule Pinchflat.SourcesTest do
end end
end end
describe "change_source_from_url/2" do describe "maybe_change_source_from_url/2" do
test "it returns a changeset" do test "it returns a changeset" do
stub(YtDlpRunnerMock, :run, &channel_mock/3) stub(YtDlpRunnerMock, :run, &channel_mock/3)
source = source_fixture() source = source_fixture()
assert %Ecto.Changeset{} = Sources.change_source_from_url(source, %{}) assert %Ecto.Changeset{} = Sources.maybe_change_source_from_url(source, %{})
end end
test "it does not fetch source 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, &channel_mock/3) expect(YtDlpRunnerMock, :run, 0, &channel_mock/3)
changeset = Sources.change_source_from_url(%Source{}, %{name: "some updated name"}) changeset = Sources.maybe_change_source_from_url(%Source{}, %{name: "some updated name"})
assert %Ecto.Changeset{} = changeset assert %Ecto.Changeset{} = changeset
end end
@ -472,7 +528,7 @@ defmodule Pinchflat.SourcesTest do
expect(YtDlpRunnerMock, :run, &channel_mock/3) expect(YtDlpRunnerMock, :run, &channel_mock/3)
changeset = changeset =
Sources.change_source_from_url(%Source{}, %{ Sources.maybe_change_source_from_url(%Source{}, %{
original_url: "https://www.youtube.com/channel/abc123" original_url: "https://www.youtube.com/channel/abc123"
}) })
@ -486,7 +542,7 @@ defmodule Pinchflat.SourcesTest do
media_profile_id = media_profile.id media_profile_id = media_profile.id
changeset = changeset =
Sources.change_source_from_url(%Source{}, %{ Sources.maybe_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
}) })
@ -507,7 +563,7 @@ defmodule Pinchflat.SourcesTest do
end) end)
changeset = changeset =
Sources.change_source_from_url(%Source{}, %{ Sources.maybe_change_source_from_url(%Source{}, %{
original_url: "https://www.youtube.com/channel/abc123" original_url: "https://www.youtube.com/channel/abc123"
}) })

View file

@ -108,4 +108,39 @@ defmodule Pinchflat.YtDlp.MediaCollectionTest do
assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_details(@channel_url) assert {:error, %Jason.DecodeError{}} = MediaCollection.get_source_details(@channel_url)
end end
end end
describe "get_source_metadata/1" do
test "it returns a map with data on success" do
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
Phoenix.json_library().encode(%{channel: "TheUselessTrials"})
end)
assert {:ok, res} = MediaCollection.get_source_metadata(@channel_url)
assert %{"channel" => "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_items: 0]
assert ot == "playlist:%()j"
{:ok, "{}"}
end)
assert {:ok, _} = MediaCollection.get_source_metadata(@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} = MediaCollection.get_source_metadata(@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{}} = MediaCollection.get_source_metadata(@channel_url)
end
end
end end

View file

@ -34,6 +34,20 @@ defmodule Pinchflat.SourcesFixtures do
source source
end end
@doc """
Generate a source with metadata.
"""
def source_with_metadata(attrs \\ %{}) do
merged_attrs =
Map.merge(attrs, %{
metadata: %{
metadata_filepath: Application.get_env(:pinchflat, :metadata_directory) <> "/metadata.json.gz"
}
})
source_fixture(merged_attrs)
end
def source_attributes_return_fixture do def source_attributes_return_fixture do
source_attributes = [ source_attributes = [
%{ %{