Skip to content

Commit ca01f17

Browse files
authored
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
1 parent bdef6c7 commit ca01f17

File tree

19 files changed

+291
-100
lines changed

19 files changed

+291
-100
lines changed

.iex.exs

+37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ alias Pinchflat.Repo
22

33
alias Pinchflat.Tasks.Task
44
alias Pinchflat.Media.MediaItem
5+
alias Pinchflat.Tasks.SourceTasks
56
alias Pinchflat.Media.MediaMetadata
67
alias Pinchflat.MediaSource.Source
78
alias Pinchflat.Profiles.MediaProfile
@@ -12,3 +13,39 @@ alias Pinchflat.Profiles
1213
alias Pinchflat.MediaSource
1314

1415
alias Pinchflat.MediaClient.{SourceDetails, VideoDownloader}
16+
17+
defmodule IexHelpers do
18+
def playlist_url do
19+
"https://www.youtube.com/playlist?list=PLmqC3wPkeL8kSlTCcSMDD63gmSi7evcXS"
20+
end
21+
22+
def channel_url do
23+
"https://www.youtube.com/c/TheUselessTrials"
24+
end
25+
26+
def video_url do
27+
"https://www.youtube.com/watch?v=bR52O78ZIUw"
28+
end
29+
30+
def details(type) do
31+
source =
32+
case type do
33+
:playlist -> playlist_url()
34+
:channel -> channel_url()
35+
end
36+
37+
SourceDetails.get_source_details(source)
38+
end
39+
40+
def ids(type) do
41+
source =
42+
case type do
43+
:playlist -> playlist_url()
44+
:channel -> channel_url()
45+
end
46+
47+
SourceDetails.get_video_ids(source)
48+
end
49+
end
50+
51+
import IexHelpers

lib/pinchflat/media_client/backends/yt_dlp/video_collection.ex

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollection do
44
videos (aka: a source [ie: channels, playlists]).
55
"""
66

7-
alias Pinchflat.MediaClient.SourceDetails
8-
97
@doc """
108
Returns a list of strings representing the video ids in the collection.
119
@@ -28,19 +26,29 @@ defmodule Pinchflat.MediaClient.Backends.YtDlp.VideoCollection do
2826
instead we're fetching just the first video (using playlist_end: 1)
2927
and parsing the source ID and name from _its_ metadata
3028
31-
Returns {:ok, %SourceDetails{}} | {:error, any, ...}.
29+
Returns {:ok, map()} | {:error, any, ...}.
3230
"""
3331
def get_source_details(source_url) do
34-
opts = [:skip_download, playlist_end: 1]
32+
opts = [:simulate, :skip_download, playlist_end: 1]
33+
output_template = "%(.{channel,channel_id,playlist_id,playlist_title})j"
3534

36-
with {:ok, output} <- backend_runner().run(source_url, opts, "%(.{channel,channel_id})j"),
35+
with {:ok, output} <- backend_runner().run(source_url, opts, output_template),
3736
{:ok, parsed_json} <- Phoenix.json_library().decode(output) do
38-
{:ok, SourceDetails.new(parsed_json["channel_id"], parsed_json["channel"])}
37+
{:ok, format_source_details(parsed_json)}
3938
else
4039
err -> err
4140
end
4241
end
4342

43+
defp format_source_details(response) do
44+
%{
45+
channel_id: response["channel_id"],
46+
channel_name: response["channel"],
47+
playlist_id: response["playlist_id"],
48+
playlist_name: response["playlist_title"]
49+
}
50+
end
51+
4452
defp backend_runner do
4553
Application.get_env(:pinchflat, :yt_dlp_runner)
4654
end

lib/pinchflat/media_client/source_details.ex

-7
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,9 @@ defmodule Pinchflat.MediaClient.SourceDetails do
55
Technically hardcodes the yt-dlp backend for now, but should leave
66
it open-ish for future expansion (just in case).
77
"""
8-
@enforce_keys [:id, :name]
9-
defstruct [:id, :name]
108

119
alias Pinchflat.MediaClient.Backends.YtDlp.VideoCollection, as: YtDlpSource
1210

13-
@doc false
14-
def new(id, name) do
15-
%__MODULE__{id: id, name: name}
16-
end
17-
1811
@doc """
1912
Gets a source's ID and name from its URL, using the given backend.
2013

lib/pinchflat/media_source.ex

+35-15
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ defmodule Pinchflat.MediaSource do
100100
Note that this fetches source details as long as the `original_url` is present.
101101
This means that it'll go for it even if a changeset is otherwise invalid. This
102102
is pretty easy to change, but for MVP I'm not concerned.
103+
104+
IDEA: Maybe I could discern `collection_type` based on the original URL?
105+
It also seems like it's a channel when the returned yt-dlp channel_id is the
106+
same as the playlist_id - maybe could use that?
103107
"""
104108
def change_source_from_url(%Source{} = source, attrs) do
105109
case change_source(source, attrs) do
@@ -115,14 +119,8 @@ defmodule Pinchflat.MediaSource do
115119
%Ecto.Changeset{changes: changes} = changeset
116120

117121
case SourceDetails.get_source_details(changes.original_url) do
118-
{:ok, %SourceDetails{} = source_details} ->
119-
change_source(
120-
source,
121-
Map.merge(changes, %{
122-
name: source_details.name,
123-
collection_id: source_details.id
124-
})
125-
)
122+
{:ok, source_details} ->
123+
add_source_details_by_collection_type(source, changeset, source_details)
126124

127125
{:error, runner_error, _status_code} ->
128126
Ecto.Changeset.add_error(
@@ -134,15 +132,35 @@ defmodule Pinchflat.MediaSource do
134132
end
135133
end
136134

137-
defp commit_and_start_indexing(changeset) do
138-
case Repo.insert_or_update(changeset) do
139-
{:ok, %Source{} = source} ->
140-
maybe_run_indexing_task(changeset, source)
135+
defp add_source_details_by_collection_type(source, changeset, source_details) do
136+
%Ecto.Changeset{changes: changes} = changeset
137+
collection_type = source.collection_type || changes[:collection_type]
138+
139+
collection_changes =
140+
case collection_type do
141+
:channel ->
142+
%{
143+
collection_id: source_details.channel_id,
144+
collection_name: source_details.channel_name
145+
}
146+
147+
:playlist ->
148+
%{
149+
collection_id: source_details.playlist_id,
150+
collection_name: source_details.playlist_name
151+
}
152+
153+
_ ->
154+
%{}
155+
end
141156

142-
{:ok, source}
157+
change_source(source, Map.merge(changes, collection_changes))
158+
end
143159

144-
err ->
145-
err
160+
defp commit_and_start_indexing(changeset) do
161+
case Repo.insert_or_update(changeset) do
162+
{:ok, %Source{} = source} -> maybe_run_indexing_task(changeset, source)
163+
err -> err
146164
end
147165
end
148166

@@ -159,5 +177,7 @@ defmodule Pinchflat.MediaSource do
159177
SourceTasks.kickoff_indexing_task(source)
160178
end
161179
end
180+
181+
{:ok, source}
162182
end
163183
end

lib/pinchflat/media_source/source.ex

+14-4
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,24 @@ defmodule Pinchflat.MediaSource.Source do
99
alias Pinchflat.Media.MediaItem
1010
alias Pinchflat.Profiles.MediaProfile
1111

12-
@allowed_fields ~w(name collection_id collection_type index_frequency_minutes original_url media_profile_id)a
13-
@required_fields @allowed_fields -- ~w(index_frequency_minutes)a
12+
@allowed_fields ~w(
13+
collection_name
14+
collection_id
15+
collection_type
16+
friendly_name
17+
index_frequency_minutes
18+
original_url
19+
media_profile_id
20+
)a
21+
22+
@required_fields @allowed_fields -- ~w(index_frequency_minutes friendly_name)a
1423

1524
schema "sources" do
16-
field :name, :string
25+
field :friendly_name, :string
26+
field :collection_name, :string
1727
field :collection_id, :string
1828
field :collection_type, Ecto.Enum, values: [:channel, :playlist]
19-
field :index_frequency_minutes, :integer
29+
field :index_frequency_minutes, :integer, default: 60 * 24
2030
# This should only be used for user reference going forward
2131
# as the collection_id should be used for all API calls
2232
field :original_url, :string

lib/pinchflat/tasks/source_tasks.ex

+25
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ defmodule Pinchflat.Tasks.SourceTasks do
33
This module contains methods for managing tasks (workers) related to sources.
44
"""
55

6+
alias Pinchflat.Media
67
alias Pinchflat.Tasks
78
alias Pinchflat.MediaSource.Source
89
alias Pinchflat.Workers.MediaIndexingWorker
10+
alias Pinchflat.Workers.VideoDownloadWorker
911

1012
@doc """
1113
Starts tasks for indexing a source's media.
@@ -30,4 +32,27 @@ defmodule Pinchflat.Tasks.SourceTasks do
3032
end
3133
end
3234
end
35+
36+
@doc """
37+
Starts tasks for downloading videos for any of a sources _pending_ media items.
38+
39+
NOTE: this starts a download for each media item that is pending,
40+
not just the ones that were indexed in this job run. This should ensure
41+
that any stragglers are caught if, for some reason, they weren't enqueued
42+
or somehow got de-queued.
43+
44+
I'm not sure of a case where this would happen, but it's cheap insurance.
45+
46+
Returns :ok
47+
"""
48+
def enqueue_pending_media_downloads(%Source{} = source) do
49+
source
50+
|> Media.list_pending_media_items_for()
51+
|> Enum.each(fn media_item ->
52+
media_item
53+
|> Map.take([:id])
54+
|> VideoDownloadWorker.new()
55+
|> Tasks.create_job_with_task(media_item)
56+
end)
57+
end
3358
end

lib/pinchflat/workers/media_indexing_worker.ex

+2-20
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
77
tags: ["media_source", "media_indexing"]
88

99
alias __MODULE__
10-
alias Pinchflat.Media
1110
alias Pinchflat.Tasks
1211
alias Pinchflat.MediaSource
13-
alias Pinchflat.Workers.VideoDownloadWorker
12+
alias Pinchflat.Tasks.SourceTasks
1413

1514
@impl Oban.Worker
1615
@doc """
@@ -49,7 +48,7 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
4948

5049
defp index_media_and_reschedule(source) do
5150
MediaSource.index_media_items(source)
52-
enqueue_video_downloads(source)
51+
SourceTasks.enqueue_pending_media_downloads(source)
5352

5453
source
5554
|> Map.take([:id])
@@ -60,21 +59,4 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
6059
{:error, :duplicate_job} -> {:ok, :job_exists}
6160
end
6261
end
63-
64-
# NOTE: this starts a download for each media item that is pending,
65-
# not just the ones that were indexed in this job run. This should ensure
66-
# that any stragglers are caught if, for some reason, they weren't enqueued
67-
# or somehow got de-queued.
68-
#
69-
# I'm not sure of a case where this would happen, but it's cheap insurance.
70-
defp enqueue_video_downloads(source) do
71-
source
72-
|> Media.list_pending_media_items_for()
73-
|> Enum.each(fn media_item ->
74-
media_item
75-
|> Map.take([:id])
76-
|> VideoDownloadWorker.new()
77-
|> Tasks.create_job_with_task(media_item)
78-
end)
79-
end
8062
end

lib/pinchflat_web/controllers/media_sources/source_controller.ex

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule PinchflatWeb.MediaSources.SourceController do
22
use PinchflatWeb, :controller
33

4+
alias Pinchflat.Repo
45
alias Pinchflat.Profiles
56
alias Pinchflat.MediaSource
67
alias Pinchflat.MediaSource.Source
@@ -30,7 +31,10 @@ defmodule PinchflatWeb.MediaSources.SourceController do
3031
end
3132

3233
def show(conn, %{"id" => id}) do
33-
source = MediaSource.get_source!(id)
34+
source =
35+
id
36+
|> MediaSource.get_source!()
37+
|> Repo.preload(:media_profile)
3438

3539
render(conn, :show, source: source)
3640
end

lib/pinchflat_web/controllers/media_sources/source_html.ex

+7
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,11 @@ defmodule PinchflatWeb.MediaSources.SourceHTML do
2424
{"Monthly", 30 * 24 * 60}
2525
]
2626
end
27+
28+
def friendly_collection_types do
29+
[
30+
{"Channel", "channel"},
31+
{"Playlist", "playlist"}
32+
]
33+
end
2734
end

lib/pinchflat_web/controllers/media_sources/source_html/index.html.heex

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
</.header>
99

1010
<.table id="sources" rows={@sources} row_click={&JS.navigate(~p"/media_sources/sources/#{&1}")}>
11-
<:col :let={source} label="Name"><%= source.name %></:col>
12-
<:col :let={source} label="Source"><%= source.collection_id %></:col>
11+
<:col :let={source} label="Collection Name"><%= source.collection_name %></:col>
12+
<:col :let={source} label="Collection ID"><%= source.collection_id %></:col>
1313
<:action :let={source}>
1414
<div class="sr-only">
1515
<.link navigate={~p"/media_sources/sources/#{source}"}>Show</.link>

lib/pinchflat_web/controllers/media_sources/source_html/show.html.heex

+12-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@
99
</.header>
1010

1111
<.list>
12-
<:item title="Source Name"><%= @source.name %></:item>
13-
<:item title="Source ID"><%= @source.collection_id %></:item>
14-
<:item title="Original URL"><%= @source.original_url %></:item>
12+
<:item title="media_profile">
13+
<.link href={~p"/media_profiles/#{@source.media_profile}"}>
14+
<%= @source.media_profile.name %>
15+
</.link>
16+
</:item>
17+
18+
<:item
19+
:for={attr <- ~w(collection_type collection_name collection_id original_url friendly_name)a}
20+
title={attr}
21+
>
22+
<%= Map.get(@source, attr) %>
23+
</:item>
1524
</.list>
1625

1726
<.back navigate={~p"/media_sources/sources"}>Back to sources</.back>

lib/pinchflat_web/controllers/media_sources/source_html/source_form.html.heex

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
Oops, something went wrong! Please check the errors below.
44
</.error>
55

6+
<.input field={f[:friendly_name]} type="text" label="Friendly Name" />
7+
68
<.input
79
field={f[:media_profile_id]}
810
options={Enum.map(@media_profiles, &{&1.name, &1.id})}
911
type="select"
1012
label="Media Profile"
1113
/>
1214

13-
<.input field={f[:collection_type]} type="text" label="Collection Type" />
15+
<.input
16+
field={f[:collection_type]}
17+
options={friendly_collection_types()}
18+
type="select"
19+
label="Collection Type"
20+
/>
21+
1422
<.input field={f[:original_url]} type="text" label="Source URL" />
1523

1624
<.input
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Pinchflat.Repo.Migrations.RenameSourceNameToCollectionName do
2+
use Ecto.Migration
3+
4+
def change do
5+
rename table(:sources), :name, to: :collection_name
6+
7+
alter table(:sources) do
8+
add :friendly_name, :string
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)