[Enhancement] Optionally use the YouTube API for improved fast indexing (#282)

* Started adding youtube API for fast indexing

* Hooked youtube API into fast indexing

* Added youtube_api_key to settings

* Added youtube api key to settings UI

* Added tests

* Refactored the youtube api module

* More refactor

* Changed editing mode name from basic to standard

* [WIP] started on copy changes

* Updated copy
This commit is contained in:
Kieran
2024-06-10 11:45:41 -07:00
committed by GitHub
parent 582eb53698
commit f6708a327c
17 changed files with 389 additions and 35 deletions
@@ -13,12 +13,13 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
alias Pinchflat.Media
alias Pinchflat.Sources.Source
alias Pinchflat.FastIndexing.YoutubeRss
alias Pinchflat.FastIndexing.YoutubeApi
alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.YtDlp.Media, as: YtDlpMedia
@doc """
Fetches new media IDs from a source's YouTube RSS feed, indexes them, and kicks off downloading
Fetches new media IDs for a source from YT's API or RSS, indexes them, and kicks off downloading
tasks for any pending media items. See comments in `FastIndexingWorker` for more info on the
order of operations and how this fits into the indexing process.
@@ -26,7 +27,7 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
downloaded_.
"""
def kickoff_download_tasks_from_youtube_rss_feed(%Source{} = source) do
{:ok, media_ids} = YoutubeRss.get_recent_media_ids_from_rss(source)
{:ok, media_ids} = get_recent_media_ids(source)
existing_media_items = list_media_items_by_media_id_for(source, media_ids)
new_media_ids = media_ids -- Enum.map(existing_media_items, & &1.media_id)
@@ -47,6 +48,17 @@ defmodule Pinchflat.FastIndexing.FastIndexingHelpers do
Enum.filter(maybe_new_media_items, & &1)
end
# If possible, use the YouTube API to fetch media IDs. If that fails, fall back to the RSS feed.
# If the YouTube API isn't set up, just use the RSS feed.
defp get_recent_media_ids(source) do
with true <- YoutubeApi.enabled?(),
{:ok, media_ids} <- YoutubeApi.get_recent_media_ids(source) do
{:ok, media_ids}
else
_ -> YoutubeRss.get_recent_media_ids(source)
end
end
defp list_media_items_by_media_id_for(source, media_ids) do
MediaQuery.new()
|> where(^dynamic([mi], ^MediaQuery.for_source(source) and mi.media_id in ^media_ids))
@@ -0,0 +1,92 @@
defmodule Pinchflat.FastIndexing.YoutubeApi do
@moduledoc """
Methods for interacting with the YouTube API for fast indexing
"""
require Logger
alias Pinchflat.Settings
alias Pinchflat.Sources.Source
alias Pinchflat.Utils.FunctionUtils
alias Pinchflat.FastIndexing.YoutubeBehaviour
@behaviour YoutubeBehaviour
@doc """
Determines if the YouTube API is enabled for fast indexing by checking
if the user has an API key set
Returns boolean()
"""
@impl YoutubeBehaviour
def enabled?(), do: is_binary(api_key())
@doc """
Fetches the recent media IDs from the YouTube API for a given source.
Returns {:ok, [binary()]} | {:error, binary()}
"""
@impl YoutubeBehaviour
def get_recent_media_ids(%Source{} = source) do
api_response =
source
|> determine_playlist_id()
|> do_api_request()
case api_response do
{:ok, parsed_json} -> get_media_ids_from_response(parsed_json)
{:error, reason} -> {:error, reason}
end
end
# The UC prefix is for channels which won't work with this API endpoint. Swapping
# the prefix to UU will get us the playlist that represents the channel's uploads
defp determine_playlist_id(%{collection_id: c_id}) do
String.replace_prefix(c_id, "UC", "UU")
end
defp do_api_request(playlist_id) do
Logger.debug("Fetching recent media IDs from YouTube API for playlist: #{playlist_id}")
playlist_id
|> construct_api_endpoint()
|> http_client().get(accept: "application/json")
|> case do
{:ok, response} ->
Phoenix.json_library().decode(response)
{:error, reason} ->
Logger.error("Failed to fetch YouTube API: #{inspect(reason)}")
{:error, reason}
end
end
defp get_media_ids_from_response(parsed_json) do
parsed_json
|> Map.get("items", [])
|> Enum.map(fn item ->
item
|> Map.get("contentDetails", %{})
|> Map.get("videoId", nil)
end)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
|> FunctionUtils.wrap_ok()
end
defp api_key do
Settings.get!(:youtube_api_key)
end
defp construct_api_endpoint(playlist_id) do
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
property_type = "contentDetails"
max_results = 50
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{api_key()}"
end
defp http_client do
Application.get_env(:pinchflat, :http_client, Pinchflat.HTTP.HTTPClient)
end
end
@@ -0,0 +1,11 @@
defmodule Pinchflat.FastIndexing.YoutubeBehaviour do
@moduledoc """
This module defines the behaviour for clients that interface with YouTube
for the purpose of fast indexing.
"""
alias Pinchflat.Sources.Source
@callback enabled?() :: boolean()
@callback get_recent_media_ids(%Source{}) :: {:ok, [String.t()]} | {:error, String.t()}
end
+15 -2
View File
@@ -1,18 +1,31 @@
defmodule Pinchflat.FastIndexing.YoutubeRss do
@moduledoc """
Methods for interacting with YouTube RSS feeds
Methods for interacting with YouTube RSS feeds for fast indexing
"""
require Logger
alias Pinchflat.Sources.Source
alias Pinchflat.FastIndexing.YoutubeBehaviour
@behaviour YoutubeBehaviour
@doc """
Determines if the YouTube RSS feed is enabled for fast indexing. Used to satisfy
the `YoutubeBehaviour` behaviour.
Returns true
"""
@impl YoutubeBehaviour
def enabled?(), do: true
@doc """
Fetches the recent media IDs from a YouTube RSS feed for a given source.
Returns {:ok, [binary()]} | {:error, binary()}
"""
def get_recent_media_ids_from_rss(%Source{} = source) do
@impl YoutubeBehaviour
def get_recent_media_ids(%Source{} = source) do
Logger.debug("Fetching recent media IDs from YouTube RSS feed for source: #{source.collection_id}")
case http_client().get(rss_url_for_source(source)) do
+7 -1
View File
@@ -21,9 +21,11 @@ defmodule Pinchflat.HTTP.HTTPClient do
"""
@impl HTTPBehaviour
def get(url, headers \\ [], opts \\ []) do
headers = parse_headers(headers)
case :httpc.request(:get, {url, headers}, [], opts) do
{:ok, {{_version, 200, _reason_phrase}, _headers, body}} ->
{:ok, body}
{:ok, to_string(body)}
{:ok, {{_version, status_code, reason_phrase}, _headers, _body}} ->
{:error, "HTTP request failed with status code #{status_code}: #{reason_phrase}"}
@@ -32,4 +34,8 @@ defmodule Pinchflat.HTTP.HTTPClient do
{:error, "HTTP request failed: #{reason}"}
end
end
defp parse_headers(headers) do
Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
end
end
+3 -1
View File
@@ -13,7 +13,8 @@ defmodule Pinchflat.Settings.Setting do
:apprise_version,
:apprise_server,
:video_codec_preference,
:audio_codec_preference
:audio_codec_preference,
:youtube_api_key
]
@required_fields ~w(
@@ -29,6 +30,7 @@ defmodule Pinchflat.Settings.Setting do
field :yt_dlp_version, :string
field :apprise_version, :string
field :apprise_server, :string
field :youtube_api_key, :string
field :video_codec_preference, :string
field :audio_codec_preference, :string
+1 -1
View File
@@ -129,7 +129,7 @@ defmodule Pinchflat.Sources.Source do
@doc false
def fast_index_frequency do
# minutes
15
10
end
@doc false