[Enhancement] Support Multiple YouTube API Keys (#606)

* feat: multiple YouTube API keys

* fix: requested changes
This commit is contained in:
rebel onion
2025-02-10 13:30:28 -06:00
committed by GitHub
parent b62d5c201b
commit 28f0d8ca6e
3 changed files with 85 additions and 10 deletions
+43 -4
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,7 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
Returns boolean() Returns boolean()
""" """
@impl YoutubeBehaviour @impl YoutubeBehaviour
def enabled?(), do: is_binary(api_key()) def enabled?, do: Enum.any?(api_keys())
@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 +76,45 @@ 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) case Settings.get!(:youtube_api_key) do
nil ->
[]
keys ->
keys
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
end
defp get_or_start_api_key_agent do
case Agent.start(fn -> 0 end, name: @agent_name) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
end
# Gets the next API key in round-robin fashion
defp next_api_key do
keys = api_keys()
case keys do
[] ->
nil
keys ->
pid = get_or_start_api_key_agent()
current_index =
Agent.get_and_update(pid, fn current ->
{current, rem(current + 1, length(keys))}
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 +122,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
@@ -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"
@@ -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"]