Download video(s) (first iteration) (#5)

* Updated package.json (and made an excuse to make a branch)

* Video filepath parser (#6)

* Restructured files; Added parser placeholder

* More restructuring

* Added basic parser for hydrating template strings

* Improved docs

* More docs

* Initial implementation of media profiles (#7)

* [WIP] Added basic video download method

* [WIP] Very-WIP first steps at parsing options and downloading

* Made my options safe by default and removed special safe versions

* Ran html generator for mediaprofile model - leaving as-is for now

* Addressed a bunch of TODO comments

* Add "channel" type Media Source (#8)

* [WIP] Working on fetching channel metadata in yt-dlp backend

* Finished first draft of methods to do with querying channels

* Renamed CommandRunnerMock to have a more descriptive name

* Ran the phx generator for the channel model

* Renamed Downloader namespace to MediaClient

* [WIP] saving before attempting LiveView

* LiveView did not work out but here's a working controller how about

* Index a channel (#9)

* Ran a MediaItem generator; Reformatted to my liking

* [WIP] added basic index function

* setup oban

* Added basic Oban job for indexing

* Added in workers for indexing; hooked them into record creation flow

* Added a task model with a phx generator

* Tied together tasks with jobs and channels

* Download indexed videos (#10)

* Clarified documentation

* more comments

* [WIP] hooked up basic video downloading; starting work on metadata

* Added metadata model and parsing

Adding the metadata model made me realize that, in many cases, yt-dlp
returns undesired input in stdout, breaking parsing. In order to get
the metadata model working, I had to change the way in which the app
interacts with yt-dlp. Now, output is written as a file to disk which
is immediately re-read and returned.

* Added tests for video download worker

* Hooked up video downloading to the channel indexing pipeline

* Adds tasks for media items

* Updated video metadata parser to extract the title

* Ran linting
This commit is contained in:
Kieran
2024-01-30 20:15:59 -08:00
committed by GitHub
parent fee39151a8
commit a5e7c48f70
96 changed files with 9358 additions and 158 deletions
+163
View File
@@ -0,0 +1,163 @@
defmodule Pinchflat.MediaSource do
@moduledoc """
The MediaSource context.
"""
import Ecto.Query, warn: false
alias Pinchflat.Repo
alias Pinchflat.Tasks
alias Pinchflat.Media
alias Pinchflat.Tasks.ChannelTasks
alias Pinchflat.MediaSource.Channel
alias Pinchflat.MediaClient.ChannelDetails
@doc """
Returns the list of channels. Returns [%Channel{}, ...]
"""
def list_channels do
Repo.all(Channel)
end
@doc """
Gets a single channel.
Returns %Channel{}. Raises `Ecto.NoResultsError` if the Channel does not exist.
"""
def get_channel!(id), do: Repo.get!(Channel, id)
@doc """
Creates a channel. May attempt to pull additional channel details from the
original_url (if provided). Will attempt to start indexing the channel's
media if successfully inserted.
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
"""
def create_channel(attrs) do
%Channel{}
|> change_channel_from_url(attrs)
|> commit_and_start_indexing()
end
@doc """
Given a media source, creates (indexes) the media by creating media_items for each
media ID in the source.
Returns [%MediaItem{}, ...] | [%Ecto.Changeset{}, ...]
"""
def index_media_items(%Channel{} = channel) do
{:ok, media_ids} = ChannelDetails.get_video_ids(channel.original_url)
media_ids
|> Enum.map(fn media_id ->
attrs = %{channel_id: channel.id, media_id: media_id}
case Media.create_media_item(attrs) do
{:ok, media_item} -> media_item
{:error, changeset} -> changeset
end
end)
end
@doc """
Updates a channel. May attempt to pull additional channel details from the
original_url (if changed). May attempt to start indexing the channel's
media if the indexing frequency has been changed.
Existing indexing tasks will be cancelled if the indexing frequency has been
changed (logic in `ChannelTasks.kickoff_indexing_task`)
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
"""
def update_channel(%Channel{} = channel, attrs) do
channel
|> change_channel_from_url(attrs)
|> commit_and_start_indexing()
end
@doc """
Deletes a channel and it's associated tasks (of any state).
Returns {:ok, %Channel{}} | {:error, %Ecto.Changeset{}}
"""
def delete_channel(%Channel{} = channel) do
Tasks.delete_tasks_for(channel)
Repo.delete(channel)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking channel changes.
"""
def change_channel(%Channel{} = channel, attrs \\ %{}) do
Channel.changeset(channel, attrs)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking channel changes and additionally
fetches channel details from the original_url (if provided). If the channel
details cannot be fetched, an error is added to the changeset.
Note that this fetches channel 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.
"""
def change_channel_from_url(%Channel{} = channel, attrs) do
case change_channel(channel, attrs) do
%Ecto.Changeset{changes: %{original_url: _}} = changeset ->
add_channel_details_to_changeset(channel, changeset)
changeset ->
changeset
end
end
defp add_channel_details_to_changeset(channel, changeset) do
%Ecto.Changeset{changes: changes} = changeset
case ChannelDetails.get_channel_details(changes.original_url) do
{:ok, %ChannelDetails{} = channel_details} ->
change_channel(
channel,
Map.merge(changes, %{
name: channel_details.name,
channel_id: channel_details.id
})
)
{:error, runner_error, _status_code} ->
Ecto.Changeset.add_error(
changeset,
:original_url,
"could not fetch channel details from URL",
error: runner_error
)
end
end
defp commit_and_start_indexing(changeset) do
case Repo.insert_or_update(changeset) do
{:ok, %Channel{} = channel} ->
maybe_run_indexing_task(changeset, channel)
{:ok, channel}
err ->
err
end
end
defp maybe_run_indexing_task(changeset, channel) do
case changeset.data do
# If the changeset is new (not persisted), attempt indexing no matter what
%{__meta__: %{state: :built}} ->
ChannelTasks.kickoff_indexing_task(channel)
# If the record has been persisted, only attempt indexing if the
# indexing frequency has been changed
%{__meta__: %{state: :loaded}} ->
if Map.has_key?(changeset.changes, :index_frequency_minutes) do
ChannelTasks.kickoff_indexing_task(channel)
end
end
end
end