philomena/lib/philomena_web/image_navigator.ex

175 lines
4.1 KiB
Elixir

defmodule PhilomenaWeb.ImageNavigator do
alias PhilomenaWeb.ImageSorter
alias Philomena.Images.{Image, ElasticsearchIndex}
alias Philomena.Elasticsearch
alias Philomena.Repo
import Ecto.Query
# We get consecutive images by finding all images greater than or less than
# the current image, and grabbing the FIRST one
@range_comparison_for_order %{
"asc" => :gt,
"desc" => :lt
}
# If we didn't reverse for prev, it would be the LAST image, which would
# make Elasticsearch choke on deep pagination
@order_for_dir %{
next: %{"asc" => "asc", "desc" => "desc"},
prev: %{"asc" => "desc", "desc" => "asc"}
}
@range_map %{
gt: :gte,
lt: :lte
}
def find_consecutive(image, rel, params, compiled_query, compiled_filter) do
image_index =
Image
|> where(id: ^image.id)
|> preload([:gallery_interactions, tags: :aliases])
|> Repo.one()
|> Map.merge(empty_fields())
|> ElasticsearchIndex.as_json()
sort_data = ImageSorter.parse_sort(params)
{sorts, filters} =
sort_data.sorts
|> Enum.map(&extract_filters(&1, image_index, rel))
|> Enum.unzip()
sorts = sortify(sorts, image_index)
filters = filterify(filters, image_index)
Elasticsearch.search_records(
Image,
%{
query: %{
bool: %{
must: List.flatten([compiled_query, sort_data.queries, filters]),
must_not: [
compiled_filter,
%{term: %{hidden_from_users: true}}
]
}
},
sort: List.flatten(sorts)
},
%{page_size: 1},
Image
)
|> Enum.to_list()
|> case do
[] -> image
[next_image] -> next_image
end
end
defp extract_filters(%{"galleries.position" => term} = sort, image, rel) do
# Extract gallery ID and current position
gid = term.nested.filter.term["galleries.id"]
pos = Enum.find(image[:galleries], &(&1.id == gid)).position
# Sort in the other direction if we are going backwards
sd = term.order
order = @order_for_dir[rel][to_string(sd)]
term = %{term | order: order}
sort = %{sort | "galleries.position" => term}
filter = gallery_range_filter(@range_comparison_for_order[order], pos)
{[sort], [filter]}
end
defp extract_filters(sort, image, rel) do
[{sf, sd}] = Enum.to_list(sort)
order = @order_for_dir[rel][sd]
sort = %{sort | sf => order}
field = String.to_existing_atom(sf)
filter = range_filter(sf, @range_comparison_for_order[order], image[field])
cond do
sf in [:_random, :_score] ->
{[sort], []}
true ->
{[sort], [filter]}
end
end
defp sortify(sorts, _image) do
List.flatten(sorts)
end
defp filterify(filters, image) do
filters = List.flatten(filters)
filters =
filters
|> Enum.with_index()
|> Enum.map(fn
{filter, 0} ->
filter.this
{filter, i} ->
filters_so_far =
filters
|> Enum.take(i)
|> Enum.map(& &1.for_next)
%{
bool: %{
must: [filter.this | filters_so_far]
}
}
end)
%{
bool: %{
should: filters,
must_not: %{term: %{id: image.id}}
}
}
end
defp range_filter(sf, dir, val) do
%{
this: %{range: %{sf => %{dir => parse_val(val)}}},
next: %{range: %{sf => %{@range_map[dir] => parse_val(val)}}}
}
end
defp gallery_range_filter(dir, val) do
%{
this: %{
nested: %{
path: :galleries,
query: %{range: %{"galleries.position" => %{dir => val}}}
}
},
next: %{
nested: %{
path: :galleries,
query: %{range: %{"galleries.position" => %{@range_map[dir] => val}}}
}
}
}
end
defp empty_fields do
%{
user: nil,
deleter: nil,
upvoters: [],
downvoters: [],
favers: [],
hiders: []
}
end
defp parse_val(%NaiveDateTime{} = value), do: NaiveDateTime.to_iso8601(value)
defp parse_val(value), do: value
end