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
@agent_name {:global, __MODULE__.KeyIndex}
@doc """
Determines if the YouTube API is enabled for fast indexing by checking
if the user has an API key set
@ -19,7 +21,13 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
Returns boolean()
"""
@impl YoutubeBehaviour
def enabled?(), do: is_binary(api_key())
def enabled?() do
try do
not Enum.empty?(api_keys())
rescue
_ -> false
end
end
@doc """
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()
end
defp api_key do
defp api_keys do
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
defp construct_api_endpoint(playlist_id) do
@ -83,7 +116,7 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
property_type = "contentDetails"
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
defp http_client do

View file

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

View file

@ -7,31 +7,67 @@ defmodule Pinchflat.FastIndexing.YoutubeApiTest do
alias Pinchflat.FastIndexing.YoutubeApi
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")
assert YoutubeApi.enabled?()
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)
refute YoutubeApi.enabled?()
end
test "returns false if only empty or whitespace keys are provided" do
Settings.set(youtube_api_key: " , ,")
refute YoutubeApi.enabled?()
end
end
describe "get_recent_media_ids/1" do
setup do
case :global.whereis_name(YoutubeApi.KeyIndex) do
:undefined -> :ok
pid -> Agent.stop(pid)
end
source = source_fixture()
Settings.set(youtube_api_key: "test_key")
Settings.set(youtube_api_key: "key1, key2")
{:ok, source: source}
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
expect(HTTPClientMock, :get, fn url, headers ->
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 headers == [accept: "application/json"]