philomena/lib/philomena/image_navigator.ex
2019-11-29 21:33:15 -05:00

171 lines
No EOL
4 KiB
Elixir

defmodule Philomena.ImageNavigator do
alias Philomena.ImageSorter
alias Philomena.Images.{Image, 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)
|> Repo.one()
|> Map.merge(empty_fields())
|> Elasticsearch.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)
Image.search_records(
%{
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}
)
|> 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][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: [],
tags: []
}
end
defp parse_val(%NaiveDateTime{} = value), do: NaiveDateTime.to_iso8601(value)
defp parse_val(value), do: value
end