Add IQDB for yiffy2
This commit is contained in:
parent
da068e44d0
commit
624ac2f738
2
Gemfile
2
Gemfile
@ -101,3 +101,5 @@ gem "aws-sdk-s3", "~> 1.149"
|
|||||||
gem "opensearch-ruby", "~> 3.3"
|
gem "opensearch-ruby", "~> 3.3"
|
||||||
|
|
||||||
gem "good_job", "~> 3.99"
|
gem "good_job", "~> 3.99"
|
||||||
|
|
||||||
|
gem "faraday", "~> 2.9"
|
||||||
|
@ -357,6 +357,7 @@ DEPENDENCIES
|
|||||||
debug
|
debug
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
ed25519 (~> 1.3)
|
ed25519 (~> 1.3)
|
||||||
|
faraday (~> 2.9)
|
||||||
filesize (~> 0.2.0)
|
filesize (~> 0.2.0)
|
||||||
github_webhook (~> 1.4)
|
github_webhook (~> 1.4)
|
||||||
good_job (~> 3.99)
|
good_job (~> 3.99)
|
||||||
|
@ -57,6 +57,24 @@ module YiffRest
|
|||||||
@image = APIImage.find(params[:id])
|
@image = APIImage.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def iqdb
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_iqdb
|
||||||
|
@sp = search_iqdb_params
|
||||||
|
@sc = (@sp[:score_cutoff].presence || 60).to_i
|
||||||
|
if @sp[:file].present?
|
||||||
|
@results = Iqdb.query_file(@sp[:file], @sc)
|
||||||
|
elsif @sp[:url].present?
|
||||||
|
@results = Iqdb.query_url(@sp[:url], @sc)
|
||||||
|
elsif @sp[:image_id].present?
|
||||||
|
@results = Iqdb.query_image(APIImage.find(@sp[:image_id]), @sc)
|
||||||
|
end
|
||||||
|
@results ||= []
|
||||||
|
@pagy, @images = pagy_array(@results.pluck("image"), size: [1, 2, 2, 1])
|
||||||
|
render(:index)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_category
|
def load_category
|
||||||
@ -72,6 +90,10 @@ module YiffRest
|
|||||||
permit_search_params(%i[category md5 original_url artist])
|
permit_search_params(%i[category md5 original_url artist])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def search_iqdb_params
|
||||||
|
permit_search_params(%i[file url image_id score_cutoff])
|
||||||
|
end
|
||||||
|
|
||||||
def create_params
|
def create_params
|
||||||
params.fetch(:api_image, {}).permit(:category, :original_url, :sources_string, :artists_string, :file)
|
params.fetch(:api_image, {}).permit(:category, :original_url, :sources_string, :artists_string, :file)
|
||||||
end
|
end
|
||||||
|
9
app/jobs/iqdb_remove_job.rb
Normal file
9
app/jobs/iqdb_remove_job.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IqdbRemoveJob < ApplicationJob
|
||||||
|
queue_as :iqdb
|
||||||
|
|
||||||
|
def perform(id)
|
||||||
|
Iqdb.remove_image(id)
|
||||||
|
end
|
||||||
|
end
|
11
app/jobs/iqdb_update_job.rb
Normal file
11
app/jobs/iqdb_update_job.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IqdbUpdateJob < ApplicationJob
|
||||||
|
queue_as :iqdb
|
||||||
|
|
||||||
|
def perform(id)
|
||||||
|
image = APIImage.find(id)
|
||||||
|
return if image.blank?
|
||||||
|
Iqdb.update_image(image)
|
||||||
|
end
|
||||||
|
end
|
107
app/logical/iqdb.rb
Normal file
107
app/logical/iqdb.rb
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Iqdb
|
||||||
|
class Error < StandardError; end
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
IQDB_NUM_PIXELS = 128
|
||||||
|
|
||||||
|
def endpoint
|
||||||
|
Websites.config.iqdb_server
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled?
|
||||||
|
endpoint.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_request(path, request_type, body = nil)
|
||||||
|
conn = Faraday.new(Websites.config.faraday_options)
|
||||||
|
conn.send(request_type, endpoint + path, body&.to_json, { content_type: "application/json" })
|
||||||
|
rescue Faraday::Error
|
||||||
|
raise(Error, "This service is temporarily unavailable. Please try again later.")
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_image(image)
|
||||||
|
Websites.config.yiffy2_storage.get(image.path) do |file|
|
||||||
|
Tempfile.open("yiffy2-#{image.md5}") do |tempfile|
|
||||||
|
file.binmode
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(file.read)
|
||||||
|
tempfile.rewind
|
||||||
|
thumb = generate_thumbnail(tempfile.path)
|
||||||
|
raise(Error, "failed to generate thumb for #{image.iqdb_id}") unless thumb
|
||||||
|
response = make_request("/images/#{image.iqdb_id}", :post, get_channels_data(thumb))
|
||||||
|
raise(Error, "iqdb request failed") if response.status != 200
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_image(id)
|
||||||
|
response = make_request("/images/#{id}", :delete)
|
||||||
|
raise(Error, "iqdb request failed") if response.status != 200
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_url(url, score_cutoff)
|
||||||
|
file = FileDownload.new(url).download!
|
||||||
|
query_file(file, score_cutoff)
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_image(image, score_cutoff)
|
||||||
|
Websites.config.yiffy2_storage.get(image.path) do |file|
|
||||||
|
query_file(file, score_cutoff)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_file(file, score_cutoff)
|
||||||
|
thumb = generate_thumbnail(file.path)
|
||||||
|
return [] unless thumb
|
||||||
|
|
||||||
|
response = make_request("/query", :post, get_channels_data(thumb))
|
||||||
|
return [] if response.status != 200
|
||||||
|
|
||||||
|
process_iqdb_result(JSON.parse(response.body), score_cutoff)
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_hash(hash, score_cutoff)
|
||||||
|
response = make_request("/query", :post, { hash: hash })
|
||||||
|
return [] if response.status != 200
|
||||||
|
|
||||||
|
process_iqdb_result(JSON.parse(response.body), score_cutoff)
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_iqdb_result(json, score_cutoff)
|
||||||
|
raise(Error, "Server returned an error. Most likely the url is not found.") unless json.is_a?(Array)
|
||||||
|
|
||||||
|
json.filter! { |entry| (entry["score"] || 0) >= (score_cutoff.presence || 60).to_i }
|
||||||
|
json.map do |x|
|
||||||
|
x["image"] = APIImage.find_by!(iqdb_id: x["post_id"])
|
||||||
|
x
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
nil
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_thumbnail(file_path)
|
||||||
|
Vips::Image.thumbnail(file_path, IQDB_NUM_PIXELS, height: IQDB_NUM_PIXELS, size: :force)
|
||||||
|
rescue Vips::Error => e
|
||||||
|
Rails.logger.error("failed to generate thumbnail for #{file_path}")
|
||||||
|
Rails.logger.error(e)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_channels_data(thumbnail)
|
||||||
|
r = []
|
||||||
|
g = []
|
||||||
|
b = []
|
||||||
|
is_grayscale = thumbnail.bands == 1
|
||||||
|
thumbnail.to_a.each do |data|
|
||||||
|
data.each do |rgb|
|
||||||
|
r << rgb[0]
|
||||||
|
g << (is_grayscale ? rgb[0] : rgb[1])
|
||||||
|
b << (is_grayscale ? rgb[0] : rgb[2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{ channels: { r: r, g: g, b: b } }
|
||||||
|
end
|
||||||
|
end
|
@ -23,9 +23,11 @@ module Requests
|
|||||||
!get(path).nil?
|
!get(path).nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(path)
|
def get(path, &block)
|
||||||
r = self.class.get("/#{storage_zone_name}/#{path}", headers: { "AccessKey" => access_key })
|
r = self.class.get("/#{storage_zone_name}/#{path}", headers: { "AccessKey" => access_key })
|
||||||
r.success? ? r.body : nil
|
file = r.success? ? r.body : nil
|
||||||
|
block&.call(file)
|
||||||
|
file
|
||||||
end
|
end
|
||||||
|
|
||||||
def put(path, body)
|
def put(path, body)
|
||||||
|
@ -19,8 +19,8 @@ module StorageManager
|
|||||||
File.exist?("#{base_path}#{path}")
|
File.exist?("#{base_path}#{path}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(path)
|
def get(path, &)
|
||||||
File.open("#{base_path}#{path}", "r", binmode: true)
|
File.open("#{base_path}#{path}", "r", binmode: true, &)
|
||||||
end
|
end
|
||||||
|
|
||||||
def put(path, io)
|
def put(path, io)
|
||||||
|
@ -22,8 +22,10 @@ module StorageManager
|
|||||||
@s3.object(trim(path)).exists?
|
@s3.object(trim(path)).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(path)
|
def get(path, &block)
|
||||||
@s3.object(trim(path)).get.body
|
file = @s3.object(trim(path)).get.body
|
||||||
|
block&.call(file)
|
||||||
|
file
|
||||||
end
|
end
|
||||||
|
|
||||||
def put(path, io)
|
def put(path, io)
|
||||||
|
@ -18,9 +18,11 @@ class APIImage < ApplicationRecord
|
|||||||
after_create :invalidate_cache
|
after_create :invalidate_cache
|
||||||
after_create :send_created, unless: :no_webhook_messages
|
after_create :send_created, unless: :no_webhook_messages
|
||||||
after_create :initialize_short_url
|
after_create :initialize_short_url
|
||||||
|
after_create :update_iqdb
|
||||||
after_update :update_files, if: :saved_change_to_category?
|
after_update :update_files, if: :saved_change_to_category?
|
||||||
after_update :send_updated, unless: :no_webhook_messages
|
after_update :send_updated, unless: :no_webhook_messages
|
||||||
after_destroy :delete_files
|
after_destroy :delete_files
|
||||||
|
after_destroy :remove_iqdb
|
||||||
after_destroy :send_deleted, unless: :no_webhook_messages
|
after_destroy :send_deleted, unless: :no_webhook_messages
|
||||||
|
|
||||||
def delete_files
|
def delete_files
|
||||||
@ -323,4 +325,32 @@ class APIImage < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
include WebhookMethods
|
include WebhookMethods
|
||||||
|
|
||||||
|
def is_image?
|
||||||
|
%w[png jpg].include?(file_ext)
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_gif?
|
||||||
|
file_ext == "gif"
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_iqdb
|
||||||
|
return unless is_image?
|
||||||
|
IqdbUpdateJob.perform_later(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_iqdb!
|
||||||
|
return unless is_image?
|
||||||
|
IqdbUpdateJob.perform_now(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_iqdb
|
||||||
|
return unless is_image?
|
||||||
|
IqdbRemoveJob.perform_later(iqdb_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_iqdb!
|
||||||
|
return unless is_image?
|
||||||
|
IqdbRemoveJob.perform_now(iqdb_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
<%= link_to "Edit", edit_yiff_rest_api_v2_manage_image_path(manage_id: params[:id], id: img.id) %> |
|
<%= link_to "Edit", edit_yiff_rest_api_v2_manage_image_path(manage_id: params[:id], id: img.id) %> |
|
||||||
<%= link_to "Delete", yiff_rest_api_v2_manage_image_path(manage_id: params[:id], id: img.id), method: :delete %><br>
|
<%= link_to "Delete", yiff_rest_api_v2_manage_image_path(manage_id: params[:id], id: img.id), method: :delete %><br>
|
||||||
<%= link_to "Delete W/R", delete_with_reason_yiff_rest_api_v2_manage_image_path(manage_id: params[:id], id: img.id) %>
|
<%= link_to "Delete W/R", delete_with_reason_yiff_rest_api_v2_manage_image_path(manage_id: params[:id], id: img.id) %>
|
||||||
|
<%= link_to("Find Similar", iqdb_query_yiff_rest_api_v2_manage_images_path(search: { image_id: img.id })) %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
16
app/views/yiff_rest/api_v2/images/iqdb.html.erb
Normal file
16
app/views/yiff_rest/api_v2/images/iqdb.html.erb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<% content_for(:page_title) do %>
|
||||||
|
YiffyAPI - IQDB
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= render "yiff_rest/api_v2/manage/nav" %>
|
||||||
|
|
||||||
|
<div class="w-100">
|
||||||
|
<div style="width: 40%; margin-left: 30%;">
|
||||||
|
<%= simple_form_for(:search, url: iqdb_query_yiff_rest_api_v2_manage_images_path, method: :post) do |f| %>
|
||||||
|
<%= f.input(:file, as: :file) %>
|
||||||
|
<%= f.input(:url, label: "File URL") %>
|
||||||
|
<%= f.input(:image_id, label: "Image ID") %>
|
||||||
|
<%= f.button(:submit, "Query", name: nil) %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -27,6 +27,9 @@
|
|||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<li class="nav-item">
|
||||||
|
<%= link_to("IQDB", iqdb_query_yiff_rest_api_v2_manage_images_path, class: "nav-link") %>
|
||||||
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
|
@ -56,6 +56,10 @@ module Websites
|
|||||||
"opensearch.websites4.containers.local"
|
"opensearch.websites4.containers.local"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def iqdb_server
|
||||||
|
"http://iqdb.websites4.containers.local:5588"
|
||||||
|
end
|
||||||
|
|
||||||
def github_webhook_secret
|
def github_webhook_secret
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -231,6 +235,17 @@ module Websites
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# https://lostisland.github.io/faraday/#/customization/connection-options
|
||||||
|
def faraday_options
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
timeout: 10,
|
||||||
|
open_timeout: 5,
|
||||||
|
},
|
||||||
|
headers: http_headers,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def yiffy2_cdn_url
|
def yiffy2_cdn_url
|
||||||
return "http://yiffy2.local" if Rails.env.development?
|
return "http://yiffy2.local" if Rails.env.development?
|
||||||
"https://v2.yiff.media"
|
"https://v2.yiff.media"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "pagy/extras/bootstrap"
|
require "pagy/extras/bootstrap"
|
||||||
|
require "pagy/extras/array"
|
||||||
Pagy::DEFAULT[:items] = 20
|
Pagy::DEFAULT[:items] = 20
|
||||||
|
@ -54,6 +54,11 @@ module YiffRestRoutes
|
|||||||
put(:sync)
|
put(:sync)
|
||||||
resources(:images, as: "manage_images") do
|
resources(:images, as: "manage_images") do
|
||||||
get(:delete_with_reason, on: :member)
|
get(:delete_with_reason, on: :member)
|
||||||
|
collection do
|
||||||
|
get(:iqdb)
|
||||||
|
get("/iqdb/query", action: :query_iqdb)
|
||||||
|
post("/iqdb/query", action: :query_iqdb)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
6
db/fixes/001_iqdb_import.rb
Executable file
6
db/fixes/001_iqdb_import.rb
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "config", "environment"))
|
||||||
|
|
||||||
|
APIImage.find_each(&:update_iqdb)
|
7
db/migrate/20241013040658_add_iqdb_id_to_api_images.rb
Normal file
7
db/migrate/20241013040658_add_iqdb_id_to_api_images.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddIqdbIdToAPIImages < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column(:api_images, :iqdb_id, :bigserial, null: false) # rubocop:disable Rails/NotNullColumn
|
||||||
|
end
|
||||||
|
end
|
3
db/schema.rb
generated
3
db/schema.rb
generated
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_08_06_104838) do
|
ActiveRecord::Schema[7.1].define(version: 2024_10_13_040658) do
|
||||||
create_schema "e621"
|
create_schema "e621"
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
@ -58,6 +58,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_06_104838) do
|
|||||||
t.integer "file_size", null: false
|
t.integer "file_size", null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.bigserial "iqdb_id", null: false
|
||||||
t.index ["creator_id"], name: "index_api_images_on_creator_id"
|
t.index ["creator_id"], name: "index_api_images_on_creator_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -123,8 +123,20 @@ services:
|
|||||||
hostname: rethinkdb.websites4.containers.local
|
hostname: rethinkdb.websites4.containers.local
|
||||||
labels:
|
labels:
|
||||||
- "hostname=rethinkdb.websites4.containers.local"
|
- "hostname=rethinkdb.websites4.containers.local"
|
||||||
networks:
|
|
||||||
- default
|
iqdb:
|
||||||
|
image: ghcr.io/e621ng/iqdb:ad5e363879d4e4b80a0821253ae610b7859c5d32
|
||||||
|
command: iqdb http 0.0.0.0 5588 /iqdb/iqdb.db
|
||||||
|
healthcheck:
|
||||||
|
test: wget --no-verbose --tries=1 --spider http://127.0.0.1:5588/status || exit 1
|
||||||
|
interval: 30s
|
||||||
|
timeout: 2s
|
||||||
|
retries: 5
|
||||||
|
volumes:
|
||||||
|
- iqdb_data:/iqdb
|
||||||
|
hostname: iqdb.websites4.containers.local
|
||||||
|
labels:
|
||||||
|
- "hostname=iqdb.websites4.containers.local"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
@ -145,3 +157,4 @@ volumes:
|
|||||||
yiffy2_image_data:
|
yiffy2_image_data:
|
||||||
oceanic_docs_data:
|
oceanic_docs_data:
|
||||||
rethinkdb_data:
|
rethinkdb_data:
|
||||||
|
iqdb_data:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Middleware
|
module Middleware
|
||||||
class DevHost
|
class DevHost
|
||||||
DEFAULT_HOST = "admin.furry.computer"
|
DEFAULT_HOST = "v2.yiff.rest"
|
||||||
def initialize(app)
|
def initialize(app)
|
||||||
@app = app
|
@app = app
|
||||||
end
|
end
|
||||||
@ -10,6 +10,7 @@ module Middleware
|
|||||||
def call(env)
|
def call(env)
|
||||||
request = Rack::Request.new(env)
|
request = Rack::Request.new(env)
|
||||||
domain = request.params["domain"].presence || DEFAULT_HOST
|
domain = request.params["domain"].presence || DEFAULT_HOST
|
||||||
|
domain = OtherRoutes::ADMIN_DOMAIN if %w[/jobs /exceptions].any? { |p| request.path.start_with?(p) }
|
||||||
|
|
||||||
if Rails.env.development? && domain
|
if Rails.env.development? && domain
|
||||||
env["websites.dev_host"] = domain
|
env["websites.dev_host"] = domain
|
||||||
|
Loading…
Reference in New Issue
Block a user