[Enhancement] Support Multiple YouTube API Keys (#606)
* feat: multiple YouTube API keys * fix: requested changes
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user