Add YouTube API key testing in settings

Allow users to test their YouTube API key directly from the settings page
by clicking a button. Uses a LiveView component that provides visual
feedback on whether the key is valid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Da Cunha 2026-01-18 21:02:03 +08:00
parent 67d8bd5598
commit dcee984567
4 changed files with 120 additions and 9 deletions

View file

@ -23,6 +23,39 @@ defmodule Pinchflat.FastIndexing.YoutubeApi do
@impl YoutubeBehaviour
def enabled?, do: Enum.any?(api_keys())
@doc """
Tests if a YouTube API key is valid by making a simple API request.
Returns :ok | {:error, binary()}
"""
@impl YoutubeBehaviour
def test_api_key(api_key) when is_binary(api_key) do
# Use a well-known public playlist (YouTube's "Popular Right Now" uploads playlist)
# to test if the API key is valid
test_playlist_id = "PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf"
url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=id&maxResults=1&playlistId=#{test_playlist_id}&key=#{api_key}"
case http_client().get(url, accept: "application/json") do
{:ok, response} ->
case Phoenix.json_library().decode(response) do
{:ok, %{"error" => %{"message" => message}}} ->
{:error, message}
{:ok, %{"items" => _}} ->
:ok
{:ok, _} ->
:ok
{:error, _} ->
{:error, "Invalid JSON response"}
end
{:error, reason} ->
{:error, "Request failed: #{inspect(reason)}"}
end
end
@doc """
Fetches the recent media IDs from the YouTube API for a given source.

View file

@ -8,4 +8,5 @@ defmodule Pinchflat.FastIndexing.YoutubeBehaviour do
@callback enabled?() :: boolean()
@callback get_recent_media_ids(%Source{}) :: {:ok, [String.t()]} | {:error, String.t()}
@callback test_api_key(String.t()) :: :ok | {:error, String.t()}
end

View file

@ -32,15 +32,11 @@
Extractor Settings
</h3>
<.input
field={f[:youtube_api_key]}
placeholder="ABC123,DEF456"
type="text"
label="YouTube API Key(s)"
help={youtube_api_help()}
html_help={true}
inputclass="font-mono text-sm mr-4"
/>
{live_render(
@conn,
Pinchflat.Settings.YoutubeApiKeyLive,
session: %{"value" => f[:youtube_api_key].value}
)}
<.input
field={f[:extractor_sleep_interval_seconds]}

View file

@ -0,0 +1,81 @@
defmodule Pinchflat.Settings.YoutubeApiKeyLive do
use PinchflatWeb, :live_view
alias PinchflatWeb.Settings.SettingHTML
def render(assigns) do
~H"""
<.input
type="text"
id="setting_youtube_api_key"
name="setting[youtube_api_key]"
value={@value}
label="YouTube API Key(s)"
help={SettingHTML.youtube_api_help()}
html_help={true}
inputclass="font-mono text-sm mr-4"
placeholder="ABC123,DEF456"
phx-change="youtube_api_key_changed"
>
<:input_append>
<.icon_button icon_name={@icon_name} class="h-12 w-12" phx-click="test_youtube_api_key" tooltip={@tooltip} />
</:input_append>
</.input>
"""
end
def mount(_params, session, socket) do
new_assigns = %{
value: session["value"],
icon_name: "hero-play",
tooltip: "Test API Key"
}
{:ok, assign(socket, new_assigns)}
end
def handle_event("test_youtube_api_key", _params, %{assigns: assigns} = socket) do
case test_api_key(assigns.value) do
:ok ->
Process.send_after(self(), :reset_button_icon, 4_000)
{:noreply, assign(socket, %{icon_name: "hero-check", tooltip: "Success!"})}
{:error, reason} ->
Process.send_after(self(), :reset_button_icon, 4_000)
{:noreply, assign(socket, %{icon_name: "hero-x-mark", tooltip: reason})}
end
end
def handle_event("youtube_api_key_changed", %{"setting" => setting}, socket) do
{:noreply, assign(socket, %{value: setting["youtube_api_key"]})}
end
def handle_info(:reset_button_icon, socket) do
{:noreply, assign(socket, %{icon_name: "hero-play", tooltip: "Test API Key"})}
end
defp test_api_key(nil), do: {:error, "No API key provided"}
defp test_api_key(""), do: {:error, "No API key provided"}
defp test_api_key(keys_string) do
# Test the first API key provided
first_key =
keys_string
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> List.first()
case first_key do
nil ->
{:error, "No API key provided"}
key ->
youtube_api().test_api_key(key)
end
end
defp youtube_api do
Application.get_env(:pinchflat, :youtube_api, Pinchflat.FastIndexing.YoutubeApi)
end
end