diff --git a/lib/pinchflat/downloading/download_option_builder.ex b/lib/pinchflat/downloading/download_option_builder.ex index f1817f4..3bd8830 100644 --- a/lib/pinchflat/downloading/download_option_builder.ex +++ b/lib/pinchflat/downloading/download_option_builder.ex @@ -203,6 +203,7 @@ defmodule Pinchflat.Downloading.DownloadOptionBuilder do "source_collection_id" => source.collection_id, "source_collection_name" => source.collection_name, "source_collection_type" => to_string(source.collection_type), + "media_playlist_index" => to_string(media_item_with_preloads.playlist_index), "media_upload_date_index" => media_item_with_preloads.upload_date_index |> to_string() diff --git a/lib/pinchflat/media/media.ex b/lib/pinchflat/media/media.ex index 191b28f..159c8b7 100644 --- a/lib/pinchflat/media/media.ex +++ b/lib/pinchflat/media/media.ex @@ -142,12 +142,17 @@ defmodule Pinchflat.Media do """ def create_media_item_from_backend_attrs(source, media_attrs_struct) do attrs = Map.merge(%{source_id: source.id}, Map.from_struct(media_attrs_struct)) + # Some fields should only be set on insert and not on update. + fields_to_drop_on_update = [:playlist_index] %MediaItem{} |> MediaItem.changeset(attrs) |> Repo.insert( on_conflict: [ - set: Map.to_list(attrs) + set: + attrs + |> Map.drop(fields_to_drop_on_update) + |> Map.to_list() ], conflict_target: [:source_id, :media_id] ) diff --git a/lib/pinchflat/media/media_item.ex b/lib/pinchflat/media/media_item.ex index 99a53c0..f85fcfb 100644 --- a/lib/pinchflat/media/media_item.ex +++ b/lib/pinchflat/media/media_item.ex @@ -18,6 +18,8 @@ defmodule Pinchflat.Media.MediaItem do alias Pinchflat.Media.MediaItemsSearchIndex @allowed_fields [ + # these fields are only captured on index + :playlist_index, # these fields are captured on indexing (and again on download) :title, :media_id, @@ -72,6 +74,7 @@ defmodule Pinchflat.Media.MediaItem do field :uploaded_at, :utc_datetime field :upload_date_index, :integer, default: 0 field :duration_seconds, :integer + field :playlist_index, :integer, default: 0 field :media_filepath, :string field :media_size_bytes, :integer diff --git a/lib/pinchflat/yt_dlp/media.ex b/lib/pinchflat/yt_dlp/media.ex index d6879e3..cecf08a 100644 --- a/lib/pinchflat/yt_dlp/media.ex +++ b/lib/pinchflat/yt_dlp/media.ex @@ -22,7 +22,8 @@ defmodule Pinchflat.YtDlp.Media do :livestream, :short_form_content, :uploaded_at, - :duration_seconds + :duration_seconds, + :playlist_index ] alias __MODULE__ @@ -63,7 +64,7 @@ defmodule Pinchflat.YtDlp.Media do @doc """ Returns a map representing the media at the given URL. - Returns {:ok, [map()]} | {:error, any, ...}. + Returns {:ok, %Media{}} | {:error, any, ...}. """ def get_media_attributes(url) do runner = Application.get_env(:pinchflat, :yt_dlp_runner) @@ -84,9 +85,11 @@ defmodule Pinchflat.YtDlp.Media do @doc """ Returns the output template for yt-dlp's indexing command. + + NOTE: playlist_index is really only useful for playlists that will never change their order. """ def indexing_output_template do - "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j" + "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp,playlist_index})j" end @doc """ @@ -104,7 +107,8 @@ defmodule Pinchflat.YtDlp.Media do livestream: !!response["was_live"], duration_seconds: response["duration"] && round(response["duration"]), short_form_content: response["webpage_url"] && short_form_content?(response), - uploaded_at: response["upload_date"] && parse_uploaded_at(response) + uploaded_at: response["upload_date"] && parse_uploaded_at(response), + playlist_index: response["playlist_index"] || 0 } end diff --git a/lib/pinchflat_web/controllers/media_profiles/media_profile_html.ex b/lib/pinchflat_web/controllers/media_profiles/media_profile_html.ex index 44ed8da..b54061d 100644 --- a/lib/pinchflat_web/controllers/media_profiles/media_profile_html.ex +++ b/lib/pinchflat_web/controllers/media_profiles/media_profile_html.ex @@ -68,7 +68,9 @@ defmodule PinchflatWeb.MediaProfiles.MediaProfileHTML do season_from_date: "alias for upload_year", season_episode_from_date: "the upload date formatted as sYYYYeMMDD", season_episode_index_from_date: - "the upload date formatted as sYYYYeMMDDII where II is an index to prevent date collisions" + "the upload date formatted as sYYYYeMMDDII where II is an index to prevent date collisions", + media_playlist_index: + "the place of the media item in the playlist. Do not use with channels. May not work if the playlist is updated" } end diff --git a/priv/repo/erd.png b/priv/repo/erd.png index e88df38..50e0ec4 100644 Binary files a/priv/repo/erd.png and b/priv/repo/erd.png differ diff --git a/priv/repo/migrations/20240715212133_add_playlist_index_to_media_items.exs b/priv/repo/migrations/20240715212133_add_playlist_index_to_media_items.exs new file mode 100644 index 0000000..e629efd --- /dev/null +++ b/priv/repo/migrations/20240715212133_add_playlist_index_to_media_items.exs @@ -0,0 +1,9 @@ +defmodule Pinchflat.Repo.Migrations.AddPlaylistIndexToMediaItems do + use Ecto.Migration + + def change do + alter table(:media_items) do + add :playlist_index, :integer, null: false, default: 0 + end + end +end diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index 13bfb8b..c95b4d0 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -689,7 +689,24 @@ defmodule Pinchflat.MediaTest do assert {:ok, %MediaItem{} = media_item_2} = Media.create_media_item_from_backend_attrs(source, different_attrs) assert media_item_1.id == media_item_2.id - assert media_item_2.title == different_attrs.title + assert Repo.reload(media_item_2).title == different_attrs.title + end + + test "doesn't update fields like playlist_index" do + source = source_fixture() + + media_attrs = + media_attributes_return_fixture() + |> Phoenix.json_library().decode!() + |> Map.put("playlist_index", 1) + |> YtDlpMedia.response_to_struct() + + different_attrs = %YtDlpMedia{media_attrs | playlist_index: 9999} + + assert {:ok, %MediaItem{} = _media_item_1} = Media.create_media_item_from_backend_attrs(source, media_attrs) + assert {:ok, %MediaItem{} = media_item_2} = Media.create_media_item_from_backend_attrs(source, different_attrs) + + assert Repo.reload(media_item_2).playlist_index == media_attrs.playlist_index end end diff --git a/test/pinchflat/yt_dlp/media_test.exs b/test/pinchflat/yt_dlp/media_test.exs index 937d2ca..1e8b110 100644 --- a/test/pinchflat/yt_dlp/media_test.exs +++ b/test/pinchflat/yt_dlp/media_test.exs @@ -110,8 +110,10 @@ defmodule Pinchflat.YtDlp.MediaTest do describe "indexing_output_template/0" do test "contains all the greatest hits" do - assert "%(.{id,title,was_live,webpage_url,description,aspect_ratio,duration,upload_date,timestamp})j" == - Media.indexing_output_template() + attrs = ~w(id title was_live webpage_url description aspect_ratio duration upload_date timestamp playlist_index)a + formatted_attrs = "%(.{#{Enum.join(attrs, ",")}})j" + + assert formatted_attrs == Media.indexing_output_template() end end @@ -126,7 +128,8 @@ defmodule Pinchflat.YtDlp.MediaTest do "aspect_ratio" => 1.0, "duration" => 60, "upload_date" => "20210101", - "timestamp" => 1_600_000_000 + "timestamp" => 1_600_000_000, + "playlist_index" => 1 } assert %Media{ @@ -137,7 +140,8 @@ defmodule Pinchflat.YtDlp.MediaTest do livestream: false, short_form_content: false, uploaded_at: ~U[2020-09-13 12:26:40Z], - duration_seconds: 60 + duration_seconds: 60, + playlist_index: 1 } == Media.response_to_struct(response) end @@ -217,6 +221,17 @@ defmodule Pinchflat.YtDlp.MediaTest do assert %Media{livestream: false} = Media.response_to_struct(response) end + + test "doesn't blow up if playlist_index is missing" do + response = %{ + "webpage_url" => "https://www.youtube.com/watch?v=TiZPUDkDYbk", + "aspect_ratio" => 1.0, + "duration" => nil, + "upload_date" => "20210101" + } + + assert %Media{playlist_index: 0} = Media.response_to_struct(response) + end end describe "response_to_struct/1 when testing uploaded_at" do