Skip to content

Commit 28f0d8c

Browse files
authored
[Enhancement] Support Multiple YouTube API Keys (#606)
* feat: multiple YouTube API keys * fix: requested changes
1 parent b62d5c2 commit 28f0d8c

File tree

3 files changed

+85
-10
lines changed

3 files changed

+85
-10
lines changed

lib/pinchflat/fast_indexing/youtube_api.ex

+43-4
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
1212

1313
@behaviour YoutubeBehaviour
1414

15+
@agent_name {:global, __MODULE__.KeyIndex}
16+
1517
@doc """
1618
Determines if the YouTube API is enabled for fast indexing by checking
1719
if the user has an API key set
1820
1921
Returns boolean()
2022
"""
2123
@impl YoutubeBehaviour
22-
def enabled?(), do: is_binary(api_key())
24+
def enabled?, do: Enum.any?(api_keys())
2325

2426
@doc """
2527
Fetches the recent media IDs from the YouTube API for a given source.
@@ -74,16 +76,53 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
7476
|> FunctionUtils.wrap_ok()
7577
end
7678

77-
defp api_key do
78-
Settings.get!(:youtube_api_key)
79+
defp api_keys do
80+
case Settings.get!(:youtube_api_key) do
81+
nil ->
82+
[]
83+
84+
keys ->
85+
keys
86+
|> String.split(",")
87+
|> Enum.map(&String.trim/1)
88+
|> Enum.reject(&(&1 == ""))
89+
end
90+
end
91+
92+
defp get_or_start_api_key_agent do
93+
case Agent.start(fn -> 0 end, name: @agent_name) do
94+
{:ok, pid} -> pid
95+
{:error, {:already_started, pid}} -> pid
96+
end
97+
end
98+
99+
# Gets the next API key in round-robin fashion
100+
defp next_api_key do
101+
keys = api_keys()
102+
103+
case keys do
104+
[] ->
105+
nil
106+
107+
keys ->
108+
pid = get_or_start_api_key_agent()
109+
110+
current_index =
111+
Agent.get_and_update(pid, fn current ->
112+
{current, rem(current + 1, length(keys))}
113+
end)
114+
115+
Logger.debug("Using YouTube API key: #{Enum.at(keys, current_index)}")
116+
Enum.at(keys, current_index)
117+
end
79118
end
80119

81120
defp construct_api_endpoint(playlist_id) do
82121
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
83122
property_type = "contentDetails"
84123
max_results = 50
85124

86-
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{api_key()}"
125+
"#{api_base}?part=#{property_type}&maxResults=#{max_results}&playlistId=#{playlist_id}&key=#{next_api_key()}"
87126
end
88127

89128
defp http_client do

lib/pinchflat_web/controllers/settings/setting_html/setting_form.html.heex

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434

3535
<.input
3636
field={f[:youtube_api_key]}
37-
placeholder="ABC123"
37+
placeholder="ABC123,DEF456"
3838
type="text"
39-
label="YouTube API Key"
39+
label="YouTube API Key(s)"
4040
help={youtube_api_help()}
4141
html_help={true}
4242
inputclass="font-mono text-sm mr-4"

test/pinchflat/fast_indexing/youtube_api_test.exs

+40-4
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,67 @@ defmodule Pinchflat.FastIndexing.YoutubeApiTest do
77
alias Pinchflat.FastIndexing.YoutubeApi
88

99
describe "enabled?/0" do
10-
test "returns true if the user has set a YouTube API key" do
10+
test "returns true if the user has set YouTube API keys" do
11+
Settings.set(youtube_api_key: "key1, key2")
12+
assert YoutubeApi.enabled?()
13+
end
14+
15+
test "returns true with a single API key" do
1116
Settings.set(youtube_api_key: "test_key")
1217

1318
assert YoutubeApi.enabled?()
1419
end
1520

16-
test "returns false if the user has not set an API key" do
21+
test "returns false if the user has not set any API keys" do
1722
Settings.set(youtube_api_key: nil)
23+
refute YoutubeApi.enabled?()
24+
end
1825

26+
test "returns false if only empty or whitespace keys are provided" do
27+
Settings.set(youtube_api_key: " , ,")
1928
refute YoutubeApi.enabled?()
2029
end
2130
end
2231

2332
describe "get_recent_media_ids/1" do
2433
setup do
34+
case :global.whereis_name(YoutubeApi.KeyIndex) do
35+
:undefined -> :ok
36+
pid -> Agent.stop(pid)
37+
end
38+
2539
source = source_fixture()
26-
Settings.set(youtube_api_key: "test_key")
40+
Settings.set(youtube_api_key: "key1, key2")
2741

2842
{:ok, source: source}
2943
end
3044

45+
test "rotates through API keys", %{source: source} do
46+
expect(HTTPClientMock, :get, fn url, _headers ->
47+
assert url =~ "key=key1"
48+
{:ok, "{}"}
49+
end)
50+
51+
expect(HTTPClientMock, :get, fn url, _headers ->
52+
assert url =~ "key=key2"
53+
{:ok, "{}"}
54+
end)
55+
56+
expect(HTTPClientMock, :get, fn url, _headers ->
57+
assert url =~ "key=key1"
58+
{:ok, "{}"}
59+
end)
60+
61+
# three calls to verify rotation
62+
YoutubeApi.get_recent_media_ids(source)
63+
YoutubeApi.get_recent_media_ids(source)
64+
YoutubeApi.get_recent_media_ids(source)
65+
end
66+
3167
test "calls the expected URL", %{source: source} do
3268
expect(HTTPClientMock, :get, fn url, headers ->
3369
api_base = "https://youtube.googleapis.com/youtube/v3/playlistItems"
34-
request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=test_key"
70+
request_url = "#{api_base}?part=contentDetails&maxResults=50&playlistId=#{source.collection_id}&key=key1"
3571

3672
assert url == request_url
3773
assert headers == [accept: "application/json"]

0 commit comments

Comments
 (0)