Added methods to kick off indexing workers from RSS response
This commit is contained in:
parent
4b63a39883
commit
57ca216442
8 changed files with 165 additions and 6 deletions
|
|
@ -23,6 +23,7 @@ defmodule Pinchflat.Api.YoutubeRss do
|
||||||
|> Regex.scan(response)
|
|> Regex.scan(response)
|
||||||
|> Enum.map(fn [_, id] -> String.trim(id) end)
|
|> Enum.map(fn [_, id] -> String.trim(id) end)
|
||||||
|> Enum.filter(&(String.length(&1) > 0))
|
|> Enum.filter(&(String.length(&1) > 0))
|
||||||
|
|> Enum.uniq()
|
||||||
|
|
||||||
{:ok, media_ids}
|
{:ok, media_ids}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,22 @@ defmodule Pinchflat.Media do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetches all media items belonging to a given source that have a media_id in the given list.
|
||||||
|
Useful for determining the what media items we DON'T already have for fast indexing.
|
||||||
|
|
||||||
|
NOTE: These queries are getting a little tedious. When I have the time, I should see about
|
||||||
|
implementing a query pattern and having these compose queries from a common base. This would
|
||||||
|
also let me compose simple queries in the module using them for one-off methods
|
||||||
|
|
||||||
|
Returns [%MediaItem{}, ...].
|
||||||
|
"""
|
||||||
|
def list_media_items_by_media_id_for(%Source{} = source, media_ids) do
|
||||||
|
MediaItem
|
||||||
|
|> where([mi], mi.source_id == ^source.id and mi.media_id in ^media_ids)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of pending media_items for a given source, where
|
Returns a list of pending media_items for a given source, where
|
||||||
pending means the `media_filepath` is `nil` AND the media_item
|
pending means the `media_filepath` is `nil` AND the media_item
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ defmodule Pinchflat.Tasks.SourceTasks do
|
||||||
alias Pinchflat.Tasks
|
alias Pinchflat.Tasks
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
alias Pinchflat.Sources.Source
|
alias Pinchflat.Sources.Source
|
||||||
|
alias Pinchflat.Api.YoutubeRss
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Workers.MediaDownloadWorker
|
alias Pinchflat.Workers.MediaDownloadWorker
|
||||||
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
alias Pinchflat.YtDlp.Backend.MediaCollection
|
alias Pinchflat.YtDlp.Backend.MediaCollection
|
||||||
alias Pinchflat.Workers.MediaCollectionIndexingWorker
|
alias Pinchflat.Workers.MediaCollectionIndexingWorker
|
||||||
alias Pinchflat.Utils.FilesystemUtils.FileFollowerServer
|
alias Pinchflat.Utils.FilesystemUtils.FileFollowerServer
|
||||||
|
|
@ -38,6 +40,27 @@ defmodule Pinchflat.Tasks.SourceTasks do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetches new media IDs from a source's YouTube RSS feed and kicks off indexing tasks
|
||||||
|
for any new media items. See comments in `MediaIndexingWorker` for more info on the
|
||||||
|
order of operations and how this fits into the indexing process.
|
||||||
|
|
||||||
|
Returns :ok
|
||||||
|
"""
|
||||||
|
def kickoff_indexing_tasks_from_youtube_rss_feed(%Source{} = source) do
|
||||||
|
{:ok, media_ids} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
|
existing_media_items = Media.list_media_items_by_media_id_for(source, media_ids)
|
||||||
|
new_media_ids = media_ids -- Enum.map(existing_media_items, & &1.media_id)
|
||||||
|
|
||||||
|
Enum.each(new_media_ids, fn media_id ->
|
||||||
|
url = "https://www.youtube.com/watch?v=#{media_id}"
|
||||||
|
|
||||||
|
%{id: source.id, media_url: url}
|
||||||
|
|> MediaIndexingWorker.new()
|
||||||
|
|> Tasks.create_job_with_task(source)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Given a media source, creates (indexes) the media by creating media_items for each
|
Given a media source, creates (indexes) the media by creating media_items for each
|
||||||
media ID in the source. Afterward, kicks off a download task for each pending media
|
media ID in the source. Afterward, kicks off a download task for each pending media
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
|
||||||
use Oban.Worker,
|
use Oban.Worker,
|
||||||
queue: :media_indexing,
|
queue: :media_indexing,
|
||||||
unique: [period: :infinity, states: [:available, :scheduled, :retryable]],
|
unique: [period: :infinity, states: [:available, :scheduled, :retryable]],
|
||||||
tags: ["media_source", "media_collection_indexing"]
|
tags: ["media_source", "media_indexing"]
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
alias Pinchflat.Sources
|
alias Pinchflat.Sources
|
||||||
|
alias Pinchflat.Tasks.MediaItemTasks
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -17,13 +20,34 @@ defmodule Pinchflat.Workers.MediaIndexingWorker do
|
||||||
useful for downloading one-off media items based on a URL (like for fast indexing).
|
useful for downloading one-off media items based on a URL (like for fast indexing).
|
||||||
|
|
||||||
Only downloads media that _should_ be downloaded (ie: the source is set to download
|
Only downloads media that _should_ be downloaded (ie: the source is set to download
|
||||||
and the media matches the profile's format preferences). Splits downloading into
|
and the media matches the profile's format preferences)
|
||||||
another job to keep the indexing queue moving quickly.
|
|
||||||
|
Order of operations:
|
||||||
|
1. SourceTasks.kickoff_indexing_tasks_from_youtube_rss_feed/1 (which is running
|
||||||
|
in its own worker) periodically checks the YouTube RSS feed for new media
|
||||||
|
2. If new media is found, it enqueues a MediaIndexingWorker (this module) for each new media
|
||||||
|
item
|
||||||
|
3. This worker fetches the media metadata and uses that to determine if it should be
|
||||||
|
downloaded. If so, it enqueues a MediaDownloadWorker
|
||||||
|
|
||||||
|
Each is a worker because they all either need to be scheduled periodically or call out to
|
||||||
|
an external service and will be long-running. They're split into different jobs to separate
|
||||||
|
retry logic for each step and allow us to better optimize various queues (eg: the indexing
|
||||||
|
steps can keep running while the slow download steps are worked through).
|
||||||
|
|
||||||
Returns :ok
|
Returns :ok
|
||||||
"""
|
"""
|
||||||
|
def perform(%Oban.Job{args: %{"id" => source_id, "media_url" => media_url}}) do
|
||||||
|
source = Sources.get_source!(source_id)
|
||||||
|
|
||||||
def perform(%Oban.Job{args: %{"id" => source_id, "media_url" => _media_url}}) do
|
case MediaItemTasks.index_and_enqueue_download_for_media_item(source, media_url) do
|
||||||
_source = Sources.get_source!(source_id)
|
{:ok, media_item} ->
|
||||||
|
Logger.debug("Indexed and enqueued download for url: #{media_url} (media item: #{media_item.id})")
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.debug("Failed to index and enqueue download for url: #{media_url} (reason: #{inspect(reason)})")
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -67,5 +67,13 @@ defmodule Pinchflat.Api.YoutubeRssTest do
|
||||||
|
|
||||||
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "removes duplicate media IDs", %{source: source} do
|
||||||
|
expect(HTTPClientMock, :get, fn _url ->
|
||||||
|
{:ok, "<yt:videoId>test_1</yt:videoId><yt:videoId>test_1</yt:videoId>"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:ok, ["test_1"]} = YoutubeRss.get_recent_media_ids_from_rss(source)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,23 @@ defmodule Pinchflat.MediaTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "list_media_items_by_media_id_for/2" do
|
||||||
|
test "returns media_items for a given source and media_ids" do
|
||||||
|
source = source_fixture()
|
||||||
|
media_item = media_item_fixture(%{source_id: source.id, media_id: "123"})
|
||||||
|
|
||||||
|
assert Media.list_media_items_by_media_id_for(source, ["123"]) == [media_item]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not return matching media_ids for a different source" do
|
||||||
|
source = source_fixture()
|
||||||
|
other_source = source_fixture()
|
||||||
|
_media_item = media_item_fixture(%{source_id: other_source.id, media_id: "123"})
|
||||||
|
|
||||||
|
assert Media.list_media_items_by_media_id_for(source, ["123"]) == []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "list_pending_media_items_for/1" do
|
describe "list_pending_media_items_for/1" do
|
||||||
test "it returns pending without a filepath for a given source" do
|
test "it returns pending without a filepath for a given source" do
|
||||||
source = source_fixture()
|
source = source_fixture()
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
|
||||||
alias Pinchflat.Tasks.Task
|
alias Pinchflat.Tasks.Task
|
||||||
alias Pinchflat.Tasks.SourceTasks
|
alias Pinchflat.Tasks.SourceTasks
|
||||||
alias Pinchflat.Media.MediaItem
|
alias Pinchflat.Media.MediaItem
|
||||||
alias Pinchflat.Workers.MediaCollectionIndexingWorker
|
|
||||||
alias Pinchflat.Workers.MediaDownloadWorker
|
alias Pinchflat.Workers.MediaDownloadWorker
|
||||||
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
alias Pinchflat.Workers.MediaCollectionIndexingWorker
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
|
@ -44,6 +45,31 @@ defmodule Pinchflat.Tasks.SourceTasksTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "kickoff_indexing_tasks_from_youtube_rss_feed/1" do
|
||||||
|
setup do
|
||||||
|
{:ok, [source: source_fixture()]}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enqueues a new worker for each new media_id in the source's RSS feed", %{source: source} do
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
|
||||||
|
assert :ok = SourceTasks.kickoff_indexing_tasks_from_youtube_rss_feed(source)
|
||||||
|
|
||||||
|
assert [worker] = all_enqueued(worker: MediaIndexingWorker)
|
||||||
|
assert worker.args["id"] == source.id
|
||||||
|
assert worker.args["media_url"] == "https://www.youtube.com/watch?v=test_1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "does not enqueue a new worker for the source's media IDs we already know about", %{source: source} do
|
||||||
|
expect(HTTPClientMock, :get, fn _url -> {:ok, "<yt:videoId>test_1</yt:videoId>"} end)
|
||||||
|
media_item_fixture(source_id: source.id, media_id: "test_1")
|
||||||
|
|
||||||
|
assert :ok = SourceTasks.kickoff_indexing_tasks_from_youtube_rss_feed(source)
|
||||||
|
|
||||||
|
refute_enqueued(worker: MediaIndexingWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "index_and_enqueue_download_for_media_items/1" do
|
describe "index_and_enqueue_download_for_media_items/1" do
|
||||||
setup do
|
setup do
|
||||||
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
stub(YtDlpRunnerMock, :run, fn _url, _opts, _ot, _addl_opts ->
|
||||||
|
|
|
||||||
44
test/pinchflat/workers/media_indexing_worker_test.exs
Normal file
44
test/pinchflat/workers/media_indexing_worker_test.exs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule Pinchflat.Workers.MediaIndexingWorkerTest do
|
||||||
|
use Pinchflat.DataCase
|
||||||
|
|
||||||
|
import Mox
|
||||||
|
import Pinchflat.MediaFixtures
|
||||||
|
import Pinchflat.SourcesFixtures
|
||||||
|
|
||||||
|
alias Pinchflat.Media.MediaItem
|
||||||
|
alias Pinchflat.Workers.MediaIndexingWorker
|
||||||
|
alias Pinchflat.Workers.MediaDownloadWorker
|
||||||
|
|
||||||
|
@media_url "https://www.youtube.com/watch?v=1234567890"
|
||||||
|
|
||||||
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
setup do
|
||||||
|
source = source_fixture()
|
||||||
|
|
||||||
|
{:ok, source: source}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "perform/1" do
|
||||||
|
test "indexes the media item and saves it to the database", %{source: source} do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
|
{:ok, media_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
before = Repo.aggregate(MediaItem, :count, :id)
|
||||||
|
perform_job(MediaIndexingWorker, %{id: source.id, media_url: @media_url})
|
||||||
|
|
||||||
|
assert Repo.aggregate(MediaItem, :count, :id) == before + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enqueues a download job for the media item", %{source: source} do
|
||||||
|
expect(YtDlpRunnerMock, :run, fn _url, _opts, _ot ->
|
||||||
|
{:ok, media_attributes_return_fixture()}
|
||||||
|
end)
|
||||||
|
|
||||||
|
perform_job(MediaIndexingWorker, %{id: source.id, media_url: @media_url})
|
||||||
|
|
||||||
|
assert [_] = all_enqueued(worker: MediaDownloadWorker)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue