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:
@@ -28,6 +28,7 @@ defmodule PinchflatWeb.ConnCase do
|
||||
import Plug.Conn
|
||||
import Phoenix.ConnTest
|
||||
import PinchflatWeb.ConnCase
|
||||
import Pinchflat.TestingHelperMethods
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -20,10 +20,13 @@ defmodule Pinchflat.DataCase do
|
||||
quote do
|
||||
alias Pinchflat.Repo
|
||||
|
||||
use Oban.Testing, repo: Repo
|
||||
|
||||
import Ecto
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Pinchflat.DataCase
|
||||
import Pinchflat.TestingHelperMethods
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
defmodule Pinchflat.JobFixtures do
|
||||
@moduledoc false
|
||||
|
||||
defmodule TestJobWorker do
|
||||
@moduledoc false
|
||||
use Oban.Worker, queue: :default
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Oban.Job{}) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def job_fixture() do
|
||||
{:ok, job} = Oban.insert(TestJobWorker.new(%{}))
|
||||
|
||||
job
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
defmodule Pinchflat.MediaFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.Media` context.
|
||||
"""
|
||||
|
||||
alias Pinchflat.MediaSourceFixtures
|
||||
|
||||
@doc """
|
||||
Generate a media_item.
|
||||
"""
|
||||
def media_item_fixture(attrs \\ %{}) do
|
||||
{:ok, media_item} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
media_id: Faker.String.base64(12),
|
||||
title: Faker.Commerce.product_name(),
|
||||
video_filepath: "/video/#{Faker.File.file_name(:video)}",
|
||||
channel_id: MediaSourceFixtures.channel_fixture().id
|
||||
})
|
||||
|> Pinchflat.Media.create_media_item()
|
||||
|
||||
media_item
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate a media_item with metadata.
|
||||
"""
|
||||
def media_item_with_metadata(attrs \\ %{}) do
|
||||
json_filepath =
|
||||
Path.join([
|
||||
Path.dirname(__ENV__.file),
|
||||
"support",
|
||||
"fixtures",
|
||||
"files",
|
||||
"media_metadata.json"
|
||||
])
|
||||
|
||||
{:ok, file_body} = File.read(json_filepath)
|
||||
{:ok, parsed_json} = Phoenix.json_library().decode(file_body)
|
||||
merged_attrs = Map.merge(attrs, %{metadata: %{client_respinse: parsed_json}})
|
||||
|
||||
media_item_fixture(merged_attrs)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
defmodule Pinchflat.MediaSourceFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.MediaSource` context.
|
||||
"""
|
||||
|
||||
alias Pinchflat.Repo
|
||||
alias Pinchflat.ProfilesFixtures
|
||||
alias Pinchflat.MediaSource.Channel
|
||||
|
||||
@doc """
|
||||
Generate a channel.
|
||||
"""
|
||||
def channel_fixture(attrs \\ %{}) do
|
||||
{:ok, channel} =
|
||||
%Channel{}
|
||||
|> Channel.changeset(
|
||||
Enum.into(attrs, %{
|
||||
name: "Channel ##{:rand.uniform(1_000_000)}",
|
||||
channel_id: Base.encode16(:crypto.hash(:md5, "#{:rand.uniform(1_000_000)}")),
|
||||
original_url: "https://www.youtube.com/channel/#{Faker.String.base64(12)}",
|
||||
media_profile_id: ProfilesFixtures.media_profile_fixture().id,
|
||||
index_frequency_minutes: 60
|
||||
})
|
||||
)
|
||||
|> Repo.insert()
|
||||
|
||||
channel
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule Pinchflat.ProfilesFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.Profiles` context.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generate a media_profile.
|
||||
"""
|
||||
def media_profile_fixture(attrs \\ %{}) do
|
||||
{:ok, media_profile} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
name: "Media Profile ##{:rand.uniform(1_000_000)}",
|
||||
output_path_template: "{{title}}.{{ext}}"
|
||||
})
|
||||
|> Pinchflat.Profiles.create_media_profile()
|
||||
|
||||
media_profile
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
defmodule Pinchflat.TasksFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Pinchflat.Tasks` context.
|
||||
"""
|
||||
|
||||
alias Pinchflat.JobFixtures
|
||||
alias Pinchflat.MediaSourceFixtures
|
||||
|
||||
@doc """
|
||||
Generate a task.
|
||||
"""
|
||||
def task_fixture(attrs \\ %{}) do
|
||||
{:ok, task} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
channel_id: MediaSourceFixtures.channel_fixture().id,
|
||||
job_id: JobFixtures.job_fixture().id
|
||||
})
|
||||
|> Pinchflat.Tasks.create_task()
|
||||
|
||||
task
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$@" == *"--dump-json"* ]]; then
|
||||
echo '{ "args": "'$@'"}'
|
||||
else
|
||||
echo $@
|
||||
fi
|
||||
# Args come in the format of "<unknown number of args> --print-to-file <output template> <file location> <unknown number of args>".
|
||||
# I need to extract <file location> and write all args to it.
|
||||
|
||||
# Extract the file location (in an unknown position BUT it's 2 args after --print-to-file).
|
||||
for ((i = 1; i <= $#; i++)); do
|
||||
if [ "${!i}" == "--print-to-file" ]; then
|
||||
# Extract the file location.
|
||||
file_location="${@:i+2:1}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Write all args to the file
|
||||
echo "$@" >"$file_location"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
defmodule Pinchflat.TestingHelperMethods do
|
||||
@moduledoc false
|
||||
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
def now do
|
||||
DateTime.utc_now()
|
||||
end
|
||||
|
||||
def now_plus(offset, unit) when unit in [:minute, :minutes] do
|
||||
DateTime.add(now(), offset, :minute)
|
||||
end
|
||||
|
||||
def assert_changed(checker_fun, action_fn) do
|
||||
before_res = checker_fun.()
|
||||
action_fn.()
|
||||
after_res = checker_fun.()
|
||||
|
||||
assert before_res != after_res
|
||||
end
|
||||
|
||||
def assert_changed([from: from, to: to], checker_fun, action_fn) do
|
||||
before_res = checker_fun.()
|
||||
action_fn.()
|
||||
after_res = checker_fun.()
|
||||
|
||||
assert before_res == from
|
||||
assert after_res == to
|
||||
end
|
||||
|
||||
def render_metadata(metadata_name) do
|
||||
json_filepath =
|
||||
Path.join([
|
||||
File.cwd!(),
|
||||
"test",
|
||||
"support",
|
||||
"files",
|
||||
"#{metadata_name}.json"
|
||||
])
|
||||
|
||||
File.read!(json_filepath)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user