defmodule MusicLibraryWeb.MaintenanceLive.Index do use MusicLibraryWeb, :live_view require Logger alias MusicLibrary.Artists alias MusicLibrary.Assets.Cache alias MusicLibrary.Maintenance alias MusicLibrary.Records alias MusicLibrary.Secrets alias MusicLibrary.Worker.PruneAssets alias MusicLibraryWeb.ErrorMessages alias MusicLibraryWeb.RecordsOnThisDayEmail @poll_interval 2_000 @impl true def render(assigns) do ~H"""

{gettext("Records")}

{gettext( "Run operations on the entire record database. Monitor execution via the Oban dashboard." )}

{gettext("Artists")}

{gettext( "Run operations on the entire artist database. Monitor execution via the Oban dashboard." )}

{gettext("Database")}

{gettext("Run lower-level operations on the database")}

{gettext("Assets")}

{gettext("Manage cached and stored assets.")}

{gettext("Emails")}

{gettext("Manually trigger email notifications.")}

{gettext("Last.fm")}

{gettext("Manage your Last.fm connection.")} <.async_result :let={status} assign={@lastfm_status}> <:loading> {gettext("Checking...")} <:failed :let={_failure}> {gettext("Not connected")} {gettext("Not connected")} {gettext("Outdated token")} {gettext("Connected as %{username}", username: elem(status, 1))}

""" end @impl true def mount(_params, _session, socket) do if connected?(socket) do Process.send_after(self(), :update_job_counts, @poll_interval) end {:ok, socket |> assign( page_title: gettext("Maintenance"), current_section: :maintenance ) |> assign_job_counts() |> assign_async(:lastfm_status, fn -> status = case Secrets.get("last_fm_session_key") do nil -> :not_connected %{value: value} -> case LastFm.get_profile(value) do {:ok, username} -> {:connected, username} {:error, _} -> :outdated_token end end {:ok, %{lastfm_status: status}} end)} end @impl true def handle_info(:update_job_counts, socket) do Process.send_after(self(), :update_job_counts, @poll_interval) {:noreply, assign_job_counts(socket)} end defp assign_job_counts(socket) do socket |> assign( :refresh_records_musicbrainz_jobs, Maintenance.count_active_jobs("MusicLibrary.Worker.RecordRefreshMusicBrainzData") ) |> assign( :generate_record_embeddings_jobs, Maintenance.count_active_jobs("MusicLibrary.Worker.GenerateRecordEmbedding") ) |> assign( :refresh_artists_musicbrainz_jobs, Maintenance.count_active_jobs("MusicLibrary.Worker.ArtistRefreshMusicBrainzData") ) |> assign( :refresh_artists_discogs_jobs, Maintenance.count_active_jobs("MusicLibrary.Worker.ArtistRefreshDiscogsData") ) |> assign( :refresh_artists_wikipedia_jobs, Maintenance.count_active_jobs("MusicLibrary.Worker.ArtistRefreshWikipediaData") ) |> assign( :refresh_artists_lastfm_jobs, Maintenance.count_active_jobs("MusicLibrary.Worker.FetchArtistLastFmData") ) end @impl true def handle_event("refresh_records_musicbrainz_data", _params, socket) do Records.Batch.refresh_musicbrainz_data() {:noreply, socket |> put_toast(:info, gettext("Operation started in the background."))} end def handle_event("generate_record_embeddings", _params, socket) do Records.Batch.generate_embeddings() {:noreply, socket |> put_toast(:info, gettext("Operation started in the background."))} end def handle_event("refresh_artists_musicbrainz_data", _params, socket) do Artists.Batch.refresh_musicbrainz_data() {:noreply, socket |> put_toast(:info, gettext("Operation started in the background."))} end def handle_event("refresh_artists_discogs_data", _params, socket) do Artists.Batch.refresh_discogs_data() {:noreply, socket |> put_toast(:info, gettext("Operation started in the background."))} end def handle_event("refresh_artists_wikipedia_data", _params, socket) do Artists.Batch.refresh_wikipedia_data() {:noreply, socket |> put_toast(:info, gettext("Operation started in the background."))} end def handle_event("refresh_artists_lastfm_data", _params, socket) do Artists.Batch.refresh_lastfm_data() {:noreply, socket |> put_toast(:info, gettext("Operation started in the background."))} end def handle_event("prune_asset_cache", _params, socket) do prune_count = Cache.prune() {:noreply, put_toast(socket, :info, gettext("Pruned %{count} cached assets.", count: prune_count))} end def handle_event("prune_assets", _params, socket) do %{} |> PruneAssets.new() |> Oban.insert() {:noreply, put_toast(socket, :info, gettext("Asset pruning started in the background."))} end def handle_event("send_records_on_this_day_email", _params, socket) do today = DateTime.now!(MusicLibrary.default_timezone()) |> DateTime.to_date() socket = case RecordsOnThisDayEmail.send(today) do {:ok, :sent} -> put_toast(socket, :info, gettext("Email sent successfully.")) {:ok, :no_records} -> put_toast(socket, :info, gettext("No records on this day.")) {:error, reason} -> Logger.error("Failed to send records on this day email: #{inspect(reason)}.") put_toast( socket, :error, gettext("Failed to send email") <> ": " <> ErrorMessages.friendly_message(reason) ) end {:noreply, socket} end def handle_event("db_vacuum", _params, socket) do case Maintenance.vacuum() do {:ok, _result} -> {:noreply, socket |> put_toast(:info, gettext("Database vacuumed successfully."))} {:error, reason} -> Logger.error("Database vacuum failed: #{inspect(reason)}.") {:noreply, socket |> put_toast( :error, gettext("Database vacuum failed") <> ": " <> ErrorMessages.friendly_message(reason) )} end end def handle_event("db_optimize", _params, socket) do case Maintenance.optimize() do {:ok, _result} -> {:noreply, socket |> put_toast(:info, gettext("Database optimized successfully."))} {:error, reason} -> Logger.error("Database optimize failed: #{inspect(reason)}.") {:noreply, socket |> put_toast( :error, gettext("Database optimize failed") <> ": " <> ErrorMessages.friendly_message(reason) )} end end def handle_event("reconnect_lastfm", _params, socket) do Secrets.delete("last_fm_session_key") {:noreply, redirect(socket, external: LastFm.auth_url())} end end