fix: issue #7 rebuild media links to get gallery working again

This commit is contained in:
2026-07-05 14:44:47 +02:00
parent 8e60fe31a5
commit 7dfcd78abd
13 changed files with 272 additions and 49 deletions

View File

@@ -196,6 +196,14 @@ defmodule BDS.Desktop.ShellCommands do
end)
end
defp dispatch("rebuild_media_links", project, _params) do
queue_task(project, "rebuild_media_links", "Rebuild Media Links", "Maintenance", fn report ->
{:ok, result} = BDS.Media.rebuild_media_links(project.id, on_progress: report)
report.(1.0, "Media links rebuilt")
%{project_id: project.id, counts: %{media_links: result.links}}
end)
end
defp dispatch("regenerate_missing_thumbnails", project, _params) do
queue_task(
project,

View File

@@ -375,7 +375,8 @@
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_media_from_files"><%= dgettext("ui", "Rebuild Media From Files") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_scripts_from_files"><%= dgettext("ui", "Rebuild Scripts From Files") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_templates_from_files"><%= dgettext("ui", "Rebuild Templates From Files") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_post_links"><%= dgettext("ui", "Rebuild Links") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_post_links"><%= dgettext("ui", "Rebuild Post Links") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_media_links"><%= dgettext("ui", "Rebuild Media Links") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="regenerate_missing_thumbnails"><%= dgettext("ui", "Regenerate Missing Thumbnails") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="rebuild_embedding_index"><%= dgettext("ui", "Rebuild Embedding Index") %></button>
<button class="secondary ui-button ui-button-secondary" type="button" phx-click="settings_shell_command" phx-value-action="open_data_folder"><%= dgettext("ui", "Open Data Folder") %></button>

View File

@@ -66,6 +66,12 @@ defmodule BDS.Media do
defdelegate link_media_to_post(media_id, post_id), to: BDS.Media.Linking
defdelegate unlink_media_from_post(media_id, post_id), to: BDS.Media.Linking
defdelegate rebuild_media_links(project_id), to: BDS.Media.Linking,
as: :rebuild_links_from_sidecars
defdelegate rebuild_media_links(project_id, opts), to: BDS.Media.Linking,
as: :rebuild_links_from_sidecars
defdelegate rebuild_media_from_files(project_id), to: BDS.Media.Rebuilder
defdelegate rebuild_media_from_files(project_id, opts), to: BDS.Media.Rebuilder

View File

@@ -5,6 +5,13 @@ defmodule BDS.Media.Linking do
import Ecto.Query
import BDS.Media.FileOps,
only: [
progress_callback: 1,
report_rebuild_progress: 4,
report_rebuild_started: 3
]
alias BDS.Media.Media
alias BDS.Media.Sidecars
alias BDS.Persistence
@@ -134,6 +141,45 @@ defmodule BDS.Media.Linking do
def restore_post_links(_project_id, _media_id, _post_ids), do: :ok
@doc """
Rebuild the `post_media` junction for a project from every media sidecar's
`linkedPostIds`.
Media sidecars are the source of truth for gallery associations (matching the
old bDS `rebuildFromSidecars`). Existing project links are cleared and then
re-created from the on-disk sidecars, so galleries reflect the project's
metadata even if the junction table drifted or was never populated. Links to
posts that are not present in the database are skipped.
"""
@spec rebuild_links_from_sidecars(String.t(), keyword()) ::
{:ok, %{media: non_neg_integer(), links: non_neg_integer()}}
def rebuild_links_from_sidecars(project_id, opts \\ []) when is_binary(project_id) do
project = Projects.get_project!(project_id)
on_progress = progress_callback(opts)
media_list =
Repo.all(from m in Media, where: m.project_id == ^project_id, order_by: [asc: m.id])
total = length(media_list)
:ok = report_rebuild_started(on_progress, total, "media links")
{_cleared, _} =
Repo.delete_all(from pm in PostMedia, where: pm.project_id == ^project_id)
media_list
|> Enum.with_index(1)
|> Enum.each(fn {media, index} ->
post_ids = Sidecars.read_linked_post_ids(project, media)
restore_post_links(project_id, media.id, post_ids)
:ok = report_rebuild_progress(on_progress, index, total, "media links")
end)
links =
Repo.aggregate(from(pm in PostMedia, where: pm.project_id == ^project_id), :count)
{:ok, %{media: total, links: links}}
end
defp ensure_link(project_id, media_id, post_id) do
already_linked? =
Repo.exists?(

View File

@@ -349,6 +349,29 @@ defmodule BDS.Media.Sidecars do
|> File.exists?()
end
@doc """
Read the `linkedPostIds` list persisted in a media item's canonical sidecar.
The media sidecar is the source of truth for gallery associations (matching
the old bDS `linkedPostIds` field). Returns `[]` when the sidecar is missing
or has no linked posts.
"""
@spec read_linked_post_ids(BDS.Projects.Project.t(), Media.t()) :: [String.t()]
def read_linked_post_ids(project, %Media{} = media) do
sidecar_path = Path.join(Projects.project_data_dir(project), media.sidecar_path)
case parse_existing_canonical_sidecar(project, sidecar_path) do
{:ok, sidecar} ->
sidecar.fields
|> DocumentFields.get("linkedPostIds", [])
|> List.wrap()
|> Enum.filter(&is_binary/1)
{:error, _reason} ->
[]
end
end
defp parse_existing_canonical_sidecar(project, sidecar_path) do
if File.exists?(sidecar_path) do
parse_canonical_sidecar(project, sidecar_path)