feat: multiple YouTube API keys

This commit is contained in:
rebel onion 2025-02-05 17:28:26 -06:00
parent b62d5c201b
commit cf87e6e047
3 changed files with 78 additions and 9 deletions

View file

@ -12,6 +12,8 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
@behaviour YoutubeBehaviour @behaviour YoutubeBehaviour
@agent_name {:global, __MODULE__.KeyIndex}
@doc """ @doc """
Determines if the YouTube API is enabled for fast indexing by checking Determines if the YouTube API is enabled for fast indexing by checking
if the user has an API key set if the user has an API key set
@ -19,7 +21,13 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
Returns boolean() Returns boolean()
""" """
@impl YoutubeBehaviour @impl YoutubeBehaviour
def enabled?(), do: is_binary(api_key()) def enabled?() do
try do
not Enum.empty?(api_keys())
rescue
_ -> false
end
end
@doc """ @doc """
Fetches the recent media IDs from the YouTube API for a given source. Fetches the recent media IDs from the YouTube API for a given source.
@ -74,8 +82,33 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
|> FunctionUtils.wrap_ok() |> FunctionUtils.wrap_ok()
end end
defp api_key do defp api_keys do
Settings.get!(:youtube_api_key) Settings.get!(:youtube_api_key)
|> String.trim()
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
# Gets the next API key in round-robin fashion
defp next_api_key do
keys = api_keys()
case keys do
[] -> nil
keys ->
case Agent.start(fn -> 0 end, name: @agent_name) do
{:ok, _pid} -> :ok
{:error, {:already_started, _pid}} -> :ok
end
current_index = Agent.get_and_update(@agent_name, fn current ->
next = rem(current + 1, length(keys))
{current, next}
end)
Logger.debug("Using YouTube API key: #{Enum.at(keys, current_index)}")
Enum.at(keys, current_index)
end
end end
defp construct_api_endpoint(playlist_id) do defp construct_api_endpoint(playlist_id) do
@ -83,7 +116,7 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
property_type = "contentDetails" property_type = "contentDetails"
max_results = 50 max_results = 50
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{api_key()}" "#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{next_api_key()}"
end end
defp http_client do defp http_client do

View file

@ -34,9 +34,9 @@
<.input <.input
field={f[:youtube_api_key]} field={f[:youtube_api_key]}
placeholder="ABC123" placeholder="ABC123,DEF456"
type="text" type="text"
label="YouTube API Key" label="YouTube API Key(s)"
help={youtube_api_help()} help={youtube_api_help()}
html_help={true} html_help={true}
inputclass="font-mono text-sm mr-4" inputclass="font-mono text-sm mr-4"

View file

@ -7,31 +7,67 @@ defmodule Pinchflat.FastIndexing.YoutubeApiTest do
alias Pinchflat.FastIndexing.YoutubeApi alias Pinchflat.FastIndexing.YoutubeApi
describe "enabled?/0" do describe "enabled?/0" do
test "returns true if the user has set a YouTube API key" do test "returns true if the user has set YouTube API keys" do
Settings.set(youtube_api_key: "key1, key2")
assert YoutubeApi.enabled?()
end
test "returns true with a single API key" do
Settings.set(youtube_api_key: "test_key") Settings.set(youtube_api_key: "test_key")
assert YoutubeApi.enabled?() assert YoutubeApi.enabled?()
end end
test "returns false if the user has not set an API key" do test "returns false if the user has not set any API keys" do
Settings.set(youtube_api_key: nil) Settings.set(youtube_api_key: nil)
refute YoutubeApi.enabled?()
end
test "returns false if only empty or whitespace keys are provided" do
Settings.set(youtube_api_key: " , ,")
refute YoutubeApi.enabled?() refute YoutubeApi.enabled?()
end end
end end
describe "get_recent_media_ids/1" do describe "get_recent_media_ids/1" do
setup do setup do
case :global.whereis_name(YoutubeApi.KeyIndex) do
:undefined -> :ok
pid -> Agent.stop(pid)
end
source = source_fixture() source = source_fixture()
Settings.set(youtube_api_key: "test_key") Settings.set(youtube_api_key: "key1, key2")
{:ok, source: source} {:ok, source: source}
end end
test "rotates through API keys", %{source: source} do
expect(HTTPClientMock, :get, fn url, _headers ->
assert url =~ "key=key1"
{:ok, "{}"}
end)
expect(HTTPClientMock, :get, fn url, _headers ->
assert url =~ "key=key2"
{:ok, "{}"}
end)
expect(HTTPClientMock, :get, fn url, _headers ->
assert url =~ "key=key1"
{:ok, "{}"}
end)
# three calls to verify rotation
YoutubeApi.get_recent_media_ids(source)
YoutubeApi.get_recent_media_ids(source)
YoutubeApi.get_recent_media_ids(source)
end
test "calls the expected URL", %{source: source} do test "calls the expected URL", %{source: source} do
expect(HTTPClientMock, :get, fn url, headers -> expect(HTTPClientMock, :get, fn url, headers ->
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems" api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=test_key" request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=key1"
assert url == request_url assert url == request_url
assert headers == [accept: "application/json"] assert headers == [accept: "application/json"]