[Enhancement] Improve handling of large media collections (#272)
* Added compound index for updated media item table for performance * Improved large number display on homepage * Improved UI around large numbers * Centered the content of homepage data cards * Renamed migration
This commit is contained in:
parent
22439e0273
commit
e751c65424
12 changed files with 146 additions and 42 deletions
|
|
@ -2,7 +2,8 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||
@moduledoc false
|
||||
use Phoenix.Component
|
||||
|
||||
alias PinchflatWeb.CoreComponents
|
||||
import PinchflatWeb.CoreComponents
|
||||
import PinchflatWeb.CustomComponents.TextComponents
|
||||
|
||||
@doc """
|
||||
Renders a table component with the given rows and columns.
|
||||
|
|
@ -76,11 +77,13 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||
phx-click={@page_number != 1 && "page_change"}
|
||||
phx-value-direction="dec"
|
||||
>
|
||||
<CoreComponents.icon name="hero-chevron-left" />
|
||||
<.icon name="hero-chevron-left" />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="mx-2">Page <%= @page_number %> of <%= @total_pages %></span>
|
||||
<span class="mx-2">
|
||||
Page <.localized_number number={@page_number} /> of <.localized_number number={@total_pages} />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span
|
||||
|
|
@ -92,7 +95,7 @@ defmodule PinchflatWeb.CustomComponents.TableComponents do
|
|||
phx-click={@page_number != @total_pages && "page_change"}
|
||||
phx-value-direction="inc"
|
||||
>
|
||||
<CoreComponents.icon name="hero-chevron-right" />
|
||||
<.icon name="hero-chevron-right" />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -94,4 +94,28 @@ defmodule PinchflatWeb.CustomComponents.TextComponents do
|
|||
<time><%= Calendar.strftime(Timex.Timezone.convert(@datetime, @timezone), @format) %></time>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a localized number using the Intl.NumberFormat API, falling back to the raw number if needed
|
||||
"""
|
||||
attr :number, :any, required: true
|
||||
|
||||
def localized_number(assigns) do
|
||||
~H"""
|
||||
<span x-data x-text={"Intl.NumberFormat().format(#{@number})"}><%= @number %></span>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a word with a suffix if the count is not 1
|
||||
"""
|
||||
attr :word, :string, required: true
|
||||
attr :count, :integer, required: true
|
||||
attr :suffix, :string, default: "s"
|
||||
|
||||
def pluralize(assigns) do
|
||||
~H"""
|
||||
<%= @word %><%= if @count == 1, do: "", else: @suffix %>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,9 +5,19 @@ defmodule PinchflatWeb.Pages.PageHTML do
|
|||
|
||||
embed_templates "page_html/*"
|
||||
|
||||
def readable_media_filesize(media_filesize) do
|
||||
{num, suffix} = NumberUtils.human_byte_size(media_filesize, precision: 1)
|
||||
attr :media_filesize, :integer, required: true
|
||||
|
||||
"#{Float.round(num)} #{suffix}"
|
||||
def readable_media_filesize(assigns) do
|
||||
{num, suffix} = NumberUtils.human_byte_size(assigns.media_filesize, precision: 2)
|
||||
|
||||
assigns =
|
||||
Map.merge(assigns, %{
|
||||
num: num,
|
||||
suffix: suffix
|
||||
})
|
||||
|
||||
~H"""
|
||||
<.localized_number number={@num} /> <%= @suffix %>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ defmodule Pinchflat.Pages.HistoryTableLive do
|
|||
<div>
|
||||
<span class="mb-4 flex items-center">
|
||||
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
|
||||
<span class="ml-2">Showing <%= length(@records) %> of <%= @total_record_count %></span>
|
||||
<span class="ml-2">
|
||||
Showing <.localized_number number={length(@records)} /> of <.localized_number number={@total_record_count} />
|
||||
</span>
|
||||
</span>
|
||||
<div class="max-w-full overflow-x-auto">
|
||||
<.table rows={@records} table_class="text-white">
|
||||
|
|
|
|||
|
|
@ -1,33 +1,39 @@
|
|||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<a href={~p"/media_profiles"} class="mt-4 flex flex-col items-center justify-center">
|
||||
<span class="text-md font-medium">Media Profile(s)</span>
|
||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<a href={~p"/media_profiles"} class="flex flex-col items-center py-2">
|
||||
<span class="text-md font-medium">
|
||||
Media <.pluralize count={@media_profile_count} word="Profile" />
|
||||
</span>
|
||||
<h4 class="text-title-md font-bold text-white">
|
||||
<%= @media_profile_count %>
|
||||
<.localized_number number={@media_profile_count} />
|
||||
</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<a href={~p"/sources"} class="mt-4 flex flex-col items-center justify-center">
|
||||
<span class="text-md font-medium">Source(s)</span>
|
||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<a href={~p"/sources"} class="flex flex-col items-center py-2">
|
||||
<span class="text-md font-medium">
|
||||
<.pluralize count={@source_count} word="Source" />
|
||||
</span>
|
||||
<h4 class="text-title-md font-bold text-white">
|
||||
<%= @source_count %>
|
||||
<.localized_number number={@source_count} />
|
||||
</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<span class="mt-4 flex flex-col items-center justify-center">
|
||||
<span class="text-md font-medium">Downloaded Media</span>
|
||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<span class="flex flex-col items-center py-2">
|
||||
<span class="text-md font-medium">
|
||||
<.pluralize count={@media_item_count} word="Download" />
|
||||
</span>
|
||||
<h4 class="text-title-md font-bold text-white">
|
||||
<%= @media_item_count %>
|
||||
<.localized_number number={@media_item_count} />
|
||||
</h4>
|
||||
</span>
|
||||
</div>
|
||||
<div class="rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<span class="mt-4 flex flex-col items-center justify-center">
|
||||
<div class="flex flex-col justify-center rounded-sm border px-7.5 py-6 shadow-default border-strokedark bg-boxdark">
|
||||
<span class="flex flex-col items-center py-2">
|
||||
<span class="text-md font-medium">Library Size</span>
|
||||
<h4 class="text-title-md font-bold text-white">
|
||||
<%= readable_media_filesize(@media_item_size) %>
|
||||
<.readable_media_filesize media_filesize={@media_item_size} />
|
||||
</h4>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@
|
|||
</.subtle_link>
|
||||
</:col>
|
||||
<:col :let={source} label="Type"><%= source.collection_type %></:col>
|
||||
<:col :let={source} label="Pending"><%= source.pending_count %></:col>
|
||||
<:col :let={source} label="Downloaded"><%= source.downloaded_count %></:col>
|
||||
<:col :let={source} label="Pending"><.localized_number number={source.pending_count} /></:col>
|
||||
<:col :let={source} label="Downloaded"><.localized_number number={source.downloaded_count} /></:col>
|
||||
<:col :let={source} label="Retention">
|
||||
<%= if source.retention_period_days && source.retention_period_days > 0 do %>
|
||||
<%= source.retention_period_days %> day(s)
|
||||
<.localized_number number={source.retention_period_days} />
|
||||
<.pluralize count={source.retention_period_days} word="day" />
|
||||
<% else %>
|
||||
<span class="text-lg">∞</span>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||
<header class="flex justify-between items-center mb-4">
|
||||
<span class="flex items-center">
|
||||
<.icon_button icon_name="hero-arrow-path" class="h-10 w-10" phx-click="reload_page" tooltip="Refresh" />
|
||||
<span class="ml-2">Showing <%= length(@records) %> of <%= @filtered_record_count %></span>
|
||||
<span class="ml-2">
|
||||
Showing <.localized_number number={length(@records)} /> of <.localized_number number={@filtered_record_count} />
|
||||
</span>
|
||||
</span>
|
||||
<div class="bg-meta-4 rounded-md">
|
||||
<div class="relative">
|
||||
|
|
@ -49,12 +51,12 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||
<%= StringUtils.truncate(media_item.title, 50) %>
|
||||
</.subtle_link>
|
||||
</:col>
|
||||
<:col :let={media_item} label="Upload Date">
|
||||
<%= DateTime.to_date(media_item.uploaded_at) %>
|
||||
</:col>
|
||||
<:col :let={media_item} :if={@media_state == "other"} label="Manually Ignored?">
|
||||
<.icon name={if media_item.prevent_download, do: "hero-check", else: "hero-x-mark"} />
|
||||
</:col>
|
||||
<:col :let={media_item} label="Upload Date">
|
||||
<%= DateTime.to_date(media_item.uploaded_at) %>
|
||||
</:col>
|
||||
<:col :let={media_item} label="" class="flex justify-end">
|
||||
<.icon_link href={~p"/sources/#{@source.id}/media/#{media_item.id}/edit"} icon="hero-pencil-square" class="mr-4" />
|
||||
</:col>
|
||||
|
|
@ -117,14 +119,40 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||
{:noreply, assign(socket, new_assigns)}
|
||||
end
|
||||
|
||||
defp fetch_pagination_attributes(base_query, page, ""), do: fetch_pagination_attributes(base_query, page, nil)
|
||||
|
||||
defp fetch_pagination_attributes(base_query, page, nil) do
|
||||
total_record_count = Repo.aggregate(base_query, :count, :id)
|
||||
total_pages = max(ceil(total_record_count / @limit), 1)
|
||||
page = NumberUtils.clamp(page, 1, total_pages)
|
||||
|
||||
records =
|
||||
fetch_records(base_query, page)
|
||||
|> order_by(desc: :uploaded_at)
|
||||
|> Repo.all()
|
||||
|
||||
%{
|
||||
page: page,
|
||||
total_pages: total_pages,
|
||||
records: records,
|
||||
search_term: nil,
|
||||
total_record_count: total_record_count,
|
||||
filtered_record_count: total_record_count
|
||||
}
|
||||
end
|
||||
|
||||
defp fetch_pagination_attributes(base_query, page, search_term) do
|
||||
filtered_base_query = filter_base_query(base_query, search_term)
|
||||
filtered_base_query = filtered_base_query(base_query, search_term)
|
||||
|
||||
total_record_count = Repo.aggregate(base_query, :count, :id)
|
||||
filtered_record_count = Repo.aggregate(filtered_base_query, :count, :id)
|
||||
total_pages = max(ceil(filtered_record_count / @limit), 1)
|
||||
page = NumberUtils.clamp(page, 1, total_pages)
|
||||
records = fetch_records(filtered_base_query, page)
|
||||
|
||||
records =
|
||||
fetch_records(filtered_base_query, page)
|
||||
|> order_by(desc: fragment("rank"), desc: :uploaded_at)
|
||||
|> Repo.all()
|
||||
|
||||
%{
|
||||
page: page,
|
||||
|
|
@ -142,39 +170,41 @@ defmodule Pinchflat.Sources.MediaItemTableLive do
|
|||
base_query
|
||||
|> limit(^@limit)
|
||||
|> offset(^offset)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp generate_base_query(source, "pending") do
|
||||
MediaQuery.new()
|
||||
|> select(^select_fields())
|
||||
|> MediaQuery.require_assoc(:media_profile)
|
||||
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.pending()))
|
||||
|> order_by(desc: fragment("rank"), desc: :uploaded_at)
|
||||
end
|
||||
|
||||
defp generate_base_query(source, "downloaded") do
|
||||
MediaQuery.new()
|
||||
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||
|> select(^select_fields())
|
||||
|> where(^dynamic(^MediaQuery.for_source(source) and ^MediaQuery.downloaded()))
|
||||
|> order_by(desc: fragment("rank"), desc: :uploaded_at)
|
||||
end
|
||||
|
||||
defp generate_base_query(source, "other") do
|
||||
MediaQuery.new()
|
||||
|> select(^select_fields())
|
||||
|> MediaQuery.require_assoc(:media_profile)
|
||||
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||
|> where(
|
||||
^dynamic(
|
||||
^MediaQuery.for_source(source) and
|
||||
(not (^MediaQuery.downloaded()) and not (^MediaQuery.pending()))
|
||||
)
|
||||
)
|
||||
|> order_by(desc: fragment("rank"), desc: :uploaded_at)
|
||||
end
|
||||
|
||||
defp filter_base_query(base_query, search_term) do
|
||||
defp filtered_base_query(base_query, search_term) do
|
||||
base_query
|
||||
|> MediaQuery.require_assoc(:media_items_search_index)
|
||||
|> where(^MediaQuery.matches_search_term(search_term))
|
||||
end
|
||||
|
||||
# Selecting only what we need GREATLY speeds up queries on large tables
|
||||
defp select_fields do
|
||||
[:id, :title, :uploaded_at, :prevent_download]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@
|
|||
type="number"
|
||||
label="Retention Period (days)"
|
||||
min="0"
|
||||
help="Days between when media is *downloaded* and when it's deleted. Leave blank to keep media indefinitely"
|
||||
help="Days between when media is <em>downloaded</em> and when it's deleted. Leave blank to keep media indefinitely"
|
||||
html_help={true}
|
||||
/>
|
||||
|
||||
<section x-show="advancedMode">
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 448 KiB After Width: | Height: | Size: 471 KiB |
|
|
@ -0,0 +1,8 @@
|
|||
defmodule Pinchflat.Repo.Migrations.ModifyUploadDateIndex do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
drop index("media_items", [:upload_date])
|
||||
create index("media_items", [:uploaded_at])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
defmodule Pinchflat.Repo.Migrations.AddIndexesForLargeCollections do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create index(
|
||||
"media_items",
|
||||
[
|
||||
:source_id,
|
||||
:media_filepath,
|
||||
:uploaded_at,
|
||||
:prevent_download,
|
||||
:livestream,
|
||||
:short_form_content,
|
||||
:title
|
||||
],
|
||||
name: "media_items_pending_and_downloaded_index"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -27,7 +27,7 @@ defmodule PinchflatWeb.Sources.MediaItemTableLiveTest do
|
|||
|
||||
{:ok, _view, html} = live_isolated(conn, MediaItemTableLive, session: create_session(source))
|
||||
|
||||
assert html =~ "Showing 1 of 1"
|
||||
assert html =~ "Showing"
|
||||
assert html =~ "Title"
|
||||
assert html =~ media_item.title
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue