Renamed Channel struct to Source

This commit is contained in:
Kieran Eglin 2024-02-02 10:10:07 -08:00
parent ed4ebccd69
commit 2377371d8c
No known key found for this signature in database
GPG key ID: 193984967FCF432D
12 changed files with 57 additions and 57 deletions

View file

@ -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

View file

@ -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{}, ...].
@ -23,7 +23,7 @@ defmodule Pinchflat.Media do
Returns [%MediaItem{}, ...]. Returns [%MediaItem{}, ...].
""" """
def list_pending_media_items_for(%Channel{} = source) do def list_pending_media_items_for(%Source{} = source) do
from( from(
m in MediaItem, m in MediaItem,
where: m.source_id == ^source.id and is_nil(m.media_filepath) where: m.source_id == ^source.id and is_nil(m.media_filepath)

View file

@ -7,7 +7,7 @@ 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 source_id)a @required_fields ~w(media_id source_id)a
@ -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 :source, Channel belongs_to :source, Source
has_one :metadata, MediaMetadata, on_replace: :update has_one :metadata, MediaMetadata, on_replace: :update

View file

@ -9,32 +9,32 @@ defmodule Pinchflat.MediaSource do
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Media alias Pinchflat.Media
alias Pinchflat.Tasks.ChannelTasks alias Pinchflat.Tasks.ChannelTasks
alias Pinchflat.MediaSource.Channel alias Pinchflat.MediaSource.Source
alias Pinchflat.MediaClient.ChannelDetails alias Pinchflat.MediaClient.ChannelDetails
@doc """ @doc """
Returns the list of channels. Returns [%Channel{}, ...] Returns the list of channels. Returns [%Source{}, ...]
""" """
def list_sources do def list_sources do
Repo.all(Channel) Repo.all(Source)
end end
@doc """ @doc """
Gets a single channel. Gets a single channel.
Returns %Channel{}. Raises `Ecto.NoResultsError` if the Channel does not exist. Returns %Source{}. Raises `Ecto.NoResultsError` if the Channel does not exist.
""" """
def get_source!(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 channel. May attempt to pull additional channel 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 channel's
media if successfully inserted. media if successfully inserted.
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}} Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
""" """
def create_source(attrs) do def create_source(attrs) do
%Channel{} %Source{}
|> change_source_from_url(attrs) |> change_source_from_url(attrs)
|> commit_and_start_indexing() |> commit_and_start_indexing()
end end
@ -45,7 +45,7 @@ defmodule Pinchflat.MediaSource do
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...] Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
""" """
def index_media_items(%Channel{} = source) do def index_media_items(%Source{} = source) do
{:ok, media_ids} = ChannelDetails.get_video_ids(source.original_url) {:ok, media_ids} = ChannelDetails.get_video_ids(source.original_url)
media_ids media_ids
@ -67,9 +67,9 @@ defmodule Pinchflat.MediaSource do
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 `ChannelTasks.kickoff_indexing_task`)
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}} Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
""" """
def update_source(%Channel{} = channel, attrs) do def update_source(%Source{} = channel, attrs) do
channel channel
|> change_source_from_url(attrs) |> change_source_from_url(attrs)
|> commit_and_start_indexing() |> commit_and_start_indexing()
@ -78,9 +78,9 @@ defmodule Pinchflat.MediaSource do
@doc """ @doc """
Deletes a channel and it's associated tasks (of any state). Deletes a channel and it's associated tasks (of any state).
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}} Returns {:ok, %Source{}} | {:error, %Ecto.Changeset{}}
""" """
def delete_source(%Channel{} = channel) do def delete_source(%Source{} = channel) do
Tasks.delete_tasks_for(channel) Tasks.delete_tasks_for(channel)
Repo.delete(channel) Repo.delete(channel)
end end
@ -88,8 +88,8 @@ defmodule Pinchflat.MediaSource do
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking channel changes. Returns an `%Ecto.Changeset{}` for tracking channel changes.
""" """
def change_source(%Channel{} = channel, attrs \\ %{}) do def change_source(%Source{} = channel, attrs \\ %{}) do
Channel.changeset(channel, attrs) Source.changeset(channel, attrs)
end end
@doc """ @doc """
@ -101,7 +101,7 @@ defmodule Pinchflat.MediaSource do
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_source_from_url(%Channel{} = channel, attrs) do def change_source_from_url(%Source{} = channel, attrs) do
case change_source(channel, attrs) do case change_source(channel, attrs) do
%Ecto.Changeset{changes: %{original_url: _}} = changeset -> %Ecto.Changeset{changes: %{original_url: _}} = changeset ->
add_source_details_to_changeset(channel, changeset) add_source_details_to_changeset(channel, changeset)
@ -136,7 +136,7 @@ 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{} = channel} ->
maybe_run_indexing_task(changeset, channel) maybe_run_indexing_task(changeset, channel)
{:ok, channel} {:ok, channel}

View file

@ -1,4 +1,4 @@
defmodule Pinchflat.MediaSource.Channel do defmodule Pinchflat.MediaSource.Source do
@moduledoc """ @moduledoc """
The Channel schema. The Channel schema.
""" """

View file

@ -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

View file

@ -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 -> %{source_id: channel.id} %Source{} = channel -> %{source_id: channel.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{} = source -> list_tasks_for(:source_id, source.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{} = source -> list_pending_tasks_for(:source_id, source.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

View file

@ -4,7 +4,7 @@ defmodule Pinchflat.Tasks.ChannelTasks do
""" """
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.MediaSource.Channel alias Pinchflat.MediaSource.Source
alias Pinchflat.Workers.MediaIndexingWorker alias Pinchflat.Workers.MediaIndexingWorker
@doc """ @doc """
@ -12,7 +12,7 @@ defmodule Pinchflat.Tasks.ChannelTasks do
Returns {:ok, :should_not_index} | {:ok, %Task{}}. Returns {:ok, :should_not_index} | {:ok, %Task{}}.
""" """
def kickoff_indexing_task(%Channel{} = source) do def kickoff_indexing_task(%Source{} = source) do
Tasks.delete_pending_tasks_for(source) Tasks.delete_pending_tasks_for(source)
if source.index_frequency_minutes <= 0 do if source.index_frequency_minutes <= 0 do

View file

@ -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 :source, Channel belongs_to :source, Source
belongs_to :media_item, MediaItem belongs_to :media_item, MediaItem
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)

View file

@ -3,7 +3,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
alias Pinchflat.Profiles alias Pinchflat.Profiles
alias Pinchflat.MediaSource alias Pinchflat.MediaSource
alias Pinchflat.MediaSource.Channel alias Pinchflat.MediaSource.Source
def index(conn, _params) do def index(conn, _params) do
sources = MediaSource.list_sources() sources = MediaSource.list_sources()
@ -12,7 +12,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
end end
def new(conn, _params) do def new(conn, _params) do
changeset = MediaSource.change_source(%Channel{}) changeset = MediaSource.change_source(%Source{})
render(conn, :new, changeset: changeset, media_profiles: media_profiles()) render(conn, :new, changeset: changeset, media_profiles: media_profiles())
end end
@ -48,7 +48,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
case MediaSource.update_source(source, source_params) do case MediaSource.update_source(source, source_params) do
{:ok, source} -> {:ok, source} ->
conn conn
|> put_flash(:info, "Channel updated successfully.") |> put_flash(:info, "Source updated successfully.")
|> redirect(to: ~p"/media_sources/sources/#{source}") |> redirect(to: ~p"/media_sources/sources/#{source}")
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
@ -65,7 +65,7 @@ defmodule PinchflatWeb.MediaSources.SourceController do
{:ok, _source} = MediaSource.delete_source(source) {:ok, _source} = MediaSource.delete_source(source)
conn conn
|> put_flash(:info, "Channel deleted successfully.") |> put_flash(:info, "Source deleted successfully.")
|> redirect(to: ~p"/media_sources/sources") |> redirect(to: ~p"/media_sources/sources")
end end

View file

@ -7,7 +7,7 @@ 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_source_attrs %{name: nil, collection_id: nil} @invalid_source_attrs %{name: nil, collection_id: nil}
@ -38,7 +38,7 @@ defmodule Pinchflat.MediaSourceTest do
collection_type: "channel" collection_type: "channel"
} }
assert {:ok, %Channel{} = source} = MediaSource.create_source(valid_attrs) assert {:ok, %Source{} = source} = MediaSource.create_source(valid_attrs)
assert source.name == "some name" assert source.name == "some name"
assert String.starts_with?(source.collection_id, "some_source_id_") assert String.starts_with?(source.collection_id, "some_source_id_")
end end
@ -62,7 +62,7 @@ defmodule Pinchflat.MediaSourceTest do
collection_type: "channel" collection_type: "channel"
} }
assert {:ok, %Channel{}} = MediaSource.create_source(valid_once_attrs) assert {:ok, %Source{}} = MediaSource.create_source(valid_once_attrs)
assert {:error, %Ecto.Changeset{}} = MediaSource.create_source(valid_once_attrs) assert {:error, %Ecto.Changeset{}} = MediaSource.create_source(valid_once_attrs)
end end
@ -84,8 +84,8 @@ defmodule Pinchflat.MediaSourceTest do
source_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})
source_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_source(source_1_attrs) assert {:ok, %Source{}} = MediaSource.create_source(source_1_attrs)
assert {:ok, %Channel{}} = MediaSource.create_source(source_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
@ -97,7 +97,7 @@ defmodule Pinchflat.MediaSourceTest do
collection_type: "channel" collection_type: "channel"
} }
assert {:ok, %Channel{} = source} = MediaSource.create_source(valid_attrs) assert {:ok, %Source{} = source} = MediaSource.create_source(valid_attrs)
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id}) assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end end
@ -161,7 +161,7 @@ defmodule Pinchflat.MediaSourceTest do
source = source_fixture() source = source_fixture()
update_attrs = %{name: "some updated name"} update_attrs = %{name: "some updated name"}
assert {:ok, %Channel{} = source} = MediaSource.update_source(source, update_attrs) assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
assert source.name == "some updated name" assert source.name == "some updated name"
end end
@ -171,7 +171,7 @@ defmodule Pinchflat.MediaSourceTest do
source = source_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{} = source} = MediaSource.update_source(source, update_attrs) assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
assert source.name == "some name" assert source.name == "some name"
assert String.starts_with?(source.collection_id, "some_source_id_") assert String.starts_with?(source.collection_id, "some_source_id_")
end end
@ -182,14 +182,14 @@ defmodule Pinchflat.MediaSourceTest do
source = source_fixture() source = source_fixture()
update_attrs = %{name: "some updated name"} update_attrs = %{name: "some updated name"}
assert {:ok, %Channel{}} = MediaSource.update_source(source, 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
source = source_fixture() source = source_fixture()
update_attrs = %{index_frequency_minutes: 123} update_attrs = %{index_frequency_minutes: 123}
assert {:ok, %Channel{} = source} = MediaSource.update_source(source, update_attrs) assert {:ok, %Source{} = source} = MediaSource.update_source(source, update_attrs)
assert source.index_frequency_minutes == 123 assert source.index_frequency_minutes == 123
assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id}) assert_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end end
@ -198,7 +198,7 @@ defmodule Pinchflat.MediaSourceTest do
source = source_fixture() source = source_fixture()
update_attrs = %{name: "some updated name"} update_attrs = %{name: "some updated name"}
assert {:ok, %Channel{}} = MediaSource.update_source(source, update_attrs) assert {:ok, %Source{}} = MediaSource.update_source(source, update_attrs)
refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id}) refute_enqueued(worker: MediaIndexingWorker, args: %{"id" => source.id})
end end
@ -215,7 +215,7 @@ defmodule Pinchflat.MediaSourceTest do
describe "delete_source/1" do describe "delete_source/1" do
test "it deletes the source" do test "it deletes the source" do
source = source_fixture() source = source_fixture()
assert {:ok, %Channel{}} = MediaSource.delete_source(source) assert {:ok, %Source{}} = MediaSource.delete_source(source)
assert_raise Ecto.NoResultsError, fn -> MediaSource.get_source!(source.id) end assert_raise Ecto.NoResultsError, fn -> MediaSource.get_source!(source.id) end
end end
@ -228,7 +228,7 @@ defmodule Pinchflat.MediaSourceTest do
source = source_fixture() source = source_fixture()
task = task_fixture(source_id: source.id) task = task_fixture(source_id: source.id)
assert {:ok, %Channel{}} = MediaSource.delete_source(source) 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
@ -252,7 +252,7 @@ defmodule Pinchflat.MediaSourceTest do
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, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, 0, &runner_function_mock/3)
changeset = MediaSource.change_source_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
@ -261,7 +261,7 @@ defmodule Pinchflat.MediaSourceTest do
expect(YtDlpRunnerMock, :run, &runner_function_mock/3) expect(YtDlpRunnerMock, :run, &runner_function_mock/3)
changeset = changeset =
MediaSource.change_source_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"
}) })
@ -275,7 +275,7 @@ defmodule Pinchflat.MediaSourceTest do
media_profile_id = media_profile.id media_profile_id = media_profile.id
changeset = changeset =
MediaSource.change_source_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
}) })
@ -296,7 +296,7 @@ defmodule Pinchflat.MediaSourceTest do
end) end)
changeset = changeset =
MediaSource.change_source_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"
}) })

View file

@ -6,15 +6,15 @@ 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 source. Generate a source.
""" """
def source_fixture(attrs \\ %{}) do def source_fixture(attrs \\ %{}) do
{:ok, channel} = {:ok, channel} =
%Channel{} %Source{}
|> Channel.changeset( |> Source.changeset(
Enum.into(attrs, %{ Enum.into(attrs, %{
name: "Channel ##{:rand.uniform(1_000_000)}", name: "Channel ##{:rand.uniform(1_000_000)}",
collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")), collection_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),