Allow searching the /uploads and /media_assets pages by the following metatags: * id: * md5: * width: * height: * duration: * mpixels: * ratio: * filesize: * filetype: * date: * age: * status:<processing|active|deleted|expunged|failed> (for /media_assets) * status:<pending|processing|active|failed> (for /uploads) * is:<filetype>, is:<status> * exif: Examples: * https://betabooru.donmai.us/media_assets?search[ai_tags_match]=filetype:png * https://betabooru.donmai.us/uploads?search[ai_tags_match]=filetype:png Note that in /uploads search, the id:, date:, and age: metatags refer to the upload media asset, not the upload itself. Note also that uploads may contain multiple assets, so for example searching uploads by `filetype:png` will return all uploads containing at least one PNG file, even if they contain other non-PNG files.
90 lines
2.9 KiB
Ruby
90 lines
2.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# A MediaAssetQuery is a tag search performed on media assets (or upload media assets) using AI tags.
|
|
class MediaAssetQuery
|
|
extend Memoist
|
|
|
|
attr_reader :search_string
|
|
delegate :to_infix, :to_pretty_string, to: :ast
|
|
alias_method :to_s, :to_infix
|
|
|
|
def initialize(search_string)
|
|
@search_string = search_string.to_s.strip
|
|
end
|
|
|
|
def self.search(search_string, **options)
|
|
new(search_string).search(**options)
|
|
end
|
|
|
|
def ast
|
|
@ast ||= PostQuery::Parser.parse(search_string)
|
|
end
|
|
|
|
def normalized_ast
|
|
@normalized_ast ||= ast.to_cnf.rewrite_opts.trim
|
|
end
|
|
|
|
def search(relation: MediaAsset.all, foreign_key: :id, score_range: (50..))
|
|
normalized_ast.visit do |node, *children|
|
|
case node.type
|
|
in :all
|
|
relation.all
|
|
in :none
|
|
relation.none
|
|
in :tag
|
|
ai_tag = AITag.named(node.name).where(score: score_range)
|
|
relation.where(ai_tag.where(AITag.arel_table[:media_asset_id].eq(relation.arel_table[foreign_key])).arel.exists)
|
|
in :metatag
|
|
metatag_matches(node.name, node.value, relation)
|
|
in :wildcard
|
|
relation.none
|
|
in :not
|
|
children.first.negate_relation
|
|
in :and
|
|
joins = children.flat_map(&:joins_values)
|
|
orders = children.flat_map(&:order_values)
|
|
nodes = children.map { |child| child.joins(joins).order(orders) }
|
|
nodes.reduce(&:and)
|
|
in :or
|
|
joins = children.flat_map(&:joins_values)
|
|
orders = children.flat_map(&:order_values)
|
|
nodes = children.map { |child| child.joins(joins).order(orders) }
|
|
nodes.reduce(&:or)
|
|
end
|
|
end
|
|
end
|
|
|
|
def metatag_matches(name, value, relation)
|
|
case name
|
|
when "id"
|
|
relation.attribute_matches(value, :id)
|
|
when "md5"
|
|
relation.attribute_matches(value, "media_assets.md5", :md5)
|
|
when "width"
|
|
relation.attribute_matches(value, "media_assets.image_width")
|
|
when "height"
|
|
relation.attribute_matches(value, "media_assets.image_height")
|
|
when "duration"
|
|
relation.attribute_matches(value, "media_assets.duration", :float)
|
|
when "mpixels"
|
|
relation.attribute_matches(value, "(media_assets.image_width * media_assets.image_height) / 1000000.0", :float)
|
|
when "ratio"
|
|
relation.attribute_matches(value, "ROUND(media_assets.image_width::numeric / media_assets.image_height::numeric, 2)", :ratio)
|
|
when "filesize"
|
|
relation.attribute_matches(value, "media_assets.file_size", :filesize)
|
|
when "filetype"
|
|
relation.attribute_matches(value, "media_assets.file_ext", :enum)
|
|
when "date"
|
|
relation.attribute_matches(value, :created_at, :date)
|
|
when "age"
|
|
relation.attribute_matches(value, :created_at, :age)
|
|
when "status"
|
|
relation.attribute_matches(value, :status, :enum)
|
|
when "is"
|
|
relation.is_matches(value)
|
|
when "exif"
|
|
relation.exif_matches(value)
|
|
end
|
|
end
|
|
end
|