defmodule PinchflatWeb.Sources.SourceController do use PinchflatWeb, :controller use Pinchflat.Media.MediaQuery alias Pinchflat.Repo alias Pinchflat.Tasks alias Pinchflat.Sources alias Pinchflat.Sources.Source alias Pinchflat.Media.MediaItem alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Media.FileSyncingWorker alias Pinchflat.Sources.SourceDeletionWorker alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.Metadata.SourceMetadataStorageWorker def index(conn, _params) do source_query = from s in Source, as: :source, inner_join: mp in assoc(s, :media_profile), where: is_nil(s.marked_for_deletion_at) and is_nil(mp.marked_for_deletion_at), preload: [media_profile: mp], order_by: [asc: s.custom_name], select: map(s, ^Source.__schema__(:fields)), select_merge: %{ downloaded_count: subquery( from m in MediaItem, where: m.source_id == parent_as(:source).id, where: ^MediaQuery.downloaded(), select: count(m.id) ), pending_count: subquery( from m in MediaItem, join: s in assoc(m, :source), where: m.source_id == parent_as(:source).id, where: ^MediaQuery.pending(), select: count(m.id) ) } render(conn, :index, sources: Repo.all(source_query)) end def new(conn, params) do # This lets me preload the settings from another source for more efficient creation cs_struct = case to_string(params["template_id"]) do "" -> %Source{} template_id -> Repo.get(Source, template_id) || %Source{} end render(conn, :new, media_profiles: media_profiles(), layout: get_onboarding_layout(), # Most of these don't actually _need_ to be nullified at this point, # but if I don't do it now I know it'll bite me changeset: Sources.change_source(%Source{ cs_struct | id: nil, uuid: nil, custom_name: nil, description: nil, collection_name: nil, collection_id: nil, collection_type: nil, original_url: nil }) ) end def create(conn, %{"source" => source_params}) do case Sources.create_source(source_params) do {:ok, source} -> redirect_location = if Settings.get!(:onboarding), do: ~p"/?onboarding=1", else: ~p"/sources/#{source}" conn |> put_flash(:info, "Source created successfully.") |> redirect(to: redirect_location) {:error, %Ecto.Changeset{} = changeset} -> render(conn, :new, changeset: changeset, media_profiles: media_profiles(), layout: get_onboarding_layout() ) end end def show(conn, %{"id" => id}) do source = Repo.preload(Sources.get_source!(id), :media_profile) pending_tasks = source |> Tasks.list_tasks_for(nil, [:executing, :available, :scheduled, :retryable]) |> Repo.preload(:job) render(conn, :show, source: source, pending_tasks: pending_tasks) end def edit(conn, %{"id" => id}) do source = Sources.get_source!(id) changeset = Sources.change_source(source) render(conn, :edit, source: source, changeset: changeset, media_profiles: media_profiles()) end def update(conn, %{"id" => id, "source" => source_params}) do source = Sources.get_source!(id) case Sources.update_source(source, source_params) do {:ok, source} -> conn |> put_flash(:info, "Source updated successfully.") |> redirect(to: ~p"/sources/#{source}") {:error, %Ecto.Changeset{} = changeset} -> render(conn, :edit, source: source, changeset: changeset, media_profiles: media_profiles() ) end end def delete(conn, %{"id" => id} = params) do # This awkward comparison converts the string to a boolean delete_files = Map.get(params, "delete_files", "") == "true" source = Sources.get_source!(id) {:ok, _} = Sources.update_source(source, %{marked_for_deletion_at: DateTime.utc_now()}) SourceDeletionWorker.kickoff(source, %{delete_files: delete_files}) conn |> put_flash(:info, "Source deletion started. This may take a while to complete.") |> redirect(to: ~p"/sources") end def force_download_pending(conn, %{"source_id" => id}) do wrap_forced_action( conn, id, "Forcing download of pending media items.", &DownloadingHelpers.enqueue_pending_download_tasks/1 ) end def force_redownload(conn, %{"source_id" => id}) do wrap_forced_action( conn, id, "Forcing re-download of downloaded media items.", &DownloadingHelpers.kickoff_redownload_for_existing_media/1 ) end def force_index(conn, %{"source_id" => id}) do wrap_forced_action( conn, id, "Index enqueued.", &SlowIndexingHelpers.kickoff_indexing_task(&1, %{force: true}) ) end def force_metadata_refresh(conn, %{"source_id" => id}) do wrap_forced_action( conn, id, "Metadata refresh enqueued.", &SourceMetadataStorageWorker.kickoff_with_task/1 ) end def sync_files_on_disk(conn, %{"source_id" => id}) do wrap_forced_action( conn, id, "File sync enqueued.", &FileSyncingWorker.kickoff_with_task/1 ) end defp wrap_forced_action(conn, source_id, message, fun) do source = Sources.get_source!(source_id) fun.(source) conn |> put_flash(:info, message) |> redirect(to: ~p"/sources/#{source}") end defp media_profiles do MediaProfile |> order_by(asc: :name) |> Repo.all() end defp get_onboarding_layout do if Settings.get!(:onboarding) do {Layouts, :onboarding} else {Layouts, :app} end end end