diff --git a/lib/pinchflat/fast_indexing/youtube_api.ex b/lib/pinchflat/fast_indexing/youtube_api.ex index 04c7326..e6309f5 100644 --- a/lib/pinchflat/fast_indexing/youtube_api.ex +++ b/lib/pinchflat/fast_indexing/youtube_api.ex @@ -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. diff --git a/lib/pinchflat/fast_indexing/youtube_behaviour.ex b/lib/pinchflat/fast_indexing/youtube_behaviour.ex index 8a6c390..9258a4a 100644 --- a/lib/pinchflat/fast_indexing/youtube_behaviour.ex +++ b/lib/pinchflat/fast_indexing/youtube_behaviour.ex @@ -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 diff --git a/lib/pinchflat_web/controllers/settings/setting_html/setting_form.html.heex b/lib/pinchflat_web/controllers/settings/setting_html/setting_form.html.heex index c59031f..d9fd897 100644 --- a/lib/pinchflat_web/controllers/settings/setting_html/setting_form.html.heex +++ b/lib/pinchflat_web/controllers/settings/setting_html/setting_form.html.heex @@ -32,15 +32,11 @@ Extractor Settings - <.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]} diff --git a/lib/pinchflat_web/controllers/settings/setting_html/youtube_api_key_live.ex b/lib/pinchflat_web/controllers/settings/setting_html/youtube_api_key_live.ex new file mode 100644 index 0000000..35128b7 --- /dev/null +++ b/lib/pinchflat_web/controllers/settings/setting_html/youtube_api_key_live.ex @@ -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} /> + + + """ + 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