Add support for externally hosted images

This commit is contained in:
Donovan Daniels 2024-10-21 17:58:53 -05:00
parent e99bdc9906
commit 102158915a
Signed by: Donovan_DMC
GPG Key ID: 907D29CBFD6157BA
20 changed files with 614 additions and 71 deletions

View File

@ -6,10 +6,10 @@ module YiffRest
before_action :validate_discord before_action :validate_discord
before_action :prepare_user before_action :prepare_user
before_action :admin_only before_action :admin_only
before_action :load_image, only: %i[edit update destroy delete_with_reason toggle_viewable convert update_cache]
def index def index
@sp = search_images_params @sp = search_images_params
@sp[:id] ||= @sp[:md5]
@pagy, @images = pagy(APIImage.search(@sp).order(created_at: :desc), size: [1, 2, 2, 1]) @pagy, @images = pagy(APIImage.search(@sp).order(created_at: :desc), size: [1, 2, 2, 1])
end end
@ -57,6 +57,26 @@ module YiffRest
@image = APIImage.find(params[:id]) @image = APIImage.find(params[:id])
end end
def toggle_viewable
@image.update(is_viewable: !@image.is_viewable?)
redirect_back(fallback_location: yiff_rest_api_v2_manage_images_path, notice: "Image is #{@image.is_viewable? ? 'now' : 'no longer'} visible.")
end
def convert
return render_expected_error(400, "Image is already external") if @image.is_external?
return if request.get?
url = params.dig(:api_image, :external_url)
@image.convert_to_external(url)
redirect_to(yiff_rest_api_v2_manage_images_path(search: { id: @image.id }), notice: "Image concerted")
end
def update_cache
return render_expected_error(400, "Image is not external") unless @image.is_external?
@image.external_api_image.update_cache
redirect_back(fallback_location: yiff_rest_api_v2_manage_images_path, notice: "Cache will be updated soon")
end
def iqdb def iqdb
@sp = {} @sp = {}
end end
@ -84,12 +104,16 @@ module YiffRest
raise(ActiveRecord::RecordNotFound) if @category.blank? raise(ActiveRecord::RecordNotFound) if @category.blank?
end end
def load_image
@image = APIImage.find(params[:id])
end
def site_title def site_title
"YiffyAPI V2 - Manage Images" "YiffyAPI V2 - Manage Images"
end end
def search_images_params def search_images_params
permit_search_params(%i[category md5 original_url artist]) permit_search_params(%i[category md5 original_url artist viewable external])
end end
def search_iqdb_params def search_iqdb_params

View File

@ -83,7 +83,7 @@ module YiffRest
end end
def image def image
image = APIImage.find_by(id: params[:id].gsub("-", "")) image = APIImage.search({ md5: params[:id].gsub("-", "") }).first
return render_error(YiffyAPIErrorCodes::IMAGES_NOT_FOUND, error: "Image not found.") unless image return render_error(YiffyAPIErrorCodes::IMAGES_NOT_FOUND, error: "Image not found.") unless image
render(json: { render(json: {
success: true, success: true,

View File

@ -23,4 +23,12 @@ body.s-v2-yiff-rest {
} }
} }
} }
.red-text {
color: #fe6a64;
}
.green-text {
color: #4caf50;
}
} }

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class UpdateExternalCacheJob < ApplicationJob
def perform(image)
image.update_cache!
end
end

View File

@ -22,18 +22,31 @@ module Iqdb
raise(Error, "This service is temporarily unavailable. Please try again later.") raise(Error, "This service is temporarily unavailable. Please try again later.")
end end
def update_image(image) def get_image(image, &)
Websites.config.yiffy2_storage.get(image.path) do |file| Tempfile.open("yiffy2-#{image.md5}") do |tempfile|
Tempfile.open("yiffy2-#{image.md5}") do |tempfile| tempfile.binmode
file.binmode if image.is_external? && image.is_viewable?
tempfile.binmode HTTParty.get(image.external_api_image.file_url, Websites.config.httparty_options.merge(stream_body: true)) do |fragment|
tempfile.write(file.read) tempfile.write(fragment)
end
tempfile.rewind tempfile.rewind
thumb = generate_thumbnail(tempfile.path) else
raise(Error, "failed to generate thumb for #{image.iqdb_id}") unless thumb Websites.config.yiffy2_storage.get(image.path) do |file|
response = make_request("/images/#{image.iqdb_id}", :post, get_channels_data(thumb)) file.binmode
raise(Error, "iqdb request failed") if response.status != 200 tempfile.write(file.read)
tempfile.rewind
end
end end
yield(tempfile)
end
end
def update_image(image)
get_image(image) do |tempfile|
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 end
@ -48,8 +61,8 @@ module Iqdb
end end
def query_image(image, score_cutoff) def query_image(image, score_cutoff)
Websites.config.yiffy2_storage.get(image.path) do |file| get_image(image) do |tempfile|
query_file(file, score_cutoff) query_file(tempfile, score_cutoff)
end end
end end

View File

@ -0,0 +1,114 @@
# frozen_string_literal: true
module Requests
class FemboyFans
include HTTParty
base_uri "https://femboy.fan"
def options
opt = Websites.config.httparty_options
opt[:headers]["Authorization"] = "Basic #{Base64.strict_encode64("#{Websites.config.femboyfans_username}:#{Websites.config.femboyfans_apikey}")}" if Websites.config.femboyfans_username.present? && Websites.config.femboyfans_apikey.present?
opt
end
def get_post(id: nil, md5: nil)
raise(ArgumentError, "id or md5 must be given") if id.nil? && md5.nil?
path = "/"
path = "/posts/#{id}.json" if id
path = "/posts.json?md5=#{md5}" if md5
r = self.class.get(path, options)
return nil if r.code != 200
JSON.parse(r.body)
end
def get_posts_by_tags(tags:, limit: 100, page: 1)
path = "/posts.json?tags=#{tags}&limit=#{limit}&page=#{page}"
r = self.class.get(path, options)
JSON.parse(r.body) || []
end
def get_all_posts_by_tags(tags:)
posts = []
page = 1
loop do
posts += p = get_posts_by_tags(tags: tags, limit: 320, page: page)
break if p.length < 320
page += 1
end
posts
end
def get_posts(ids: nil, md5s: nil, status: nil)
raise(ArgumentError, "ids or md5s must be given") if ids.nil? && md5s.nil?
path = "/posts.json?"
path += "tags=id:#{ids.join(',')}%20limit:100" if ids
path += "tags=md5:#{md5s.join(',')}%20limit:100" if md5s
path += "%20status:#{status}" if path.include?("tags=") && !status.nil?
path += "tags=status:#{status}" if !path.include?("tags=") && !status.nil?
Rails.logger.info("Fetching posts from femboyfans: #{path}") if Rails.env.development?
r = self.class.get(path, options)
JSON.parse(r.body) || []
end
def get_all_posts(ids: [], md5s: [])
posts = []
ids.each_slice(100) do |slice|
posts += get_posts(ids: slice, status: "any")
end
md5s -= posts.map { |p| p["file"]["md5"] }
md5s.each_slice(100) do |slice|
posts += get_posts(md5s: slice, status: "any")
end
missing = {
ids: ids - posts.pluck("id"),
md5s: md5s - posts.map { |p| p["file"]["md5"] },
}
{ posts: posts, missing: missing }
end
def find_replacement(md5:)
r = self.class.get("/posts/replacements.json?search[md5]=#{md5}", options)
JSON.parse(r.body)&.first
end
def name_to_id(name)
Cache.fetch("name_to_id:#{name.downcase}", expires_in: 1.day) do
r = self.class.get("/users.json?search[name_matches]=#{name}", options)
JSON.parse(r.body)&.first&.dig("id")
end
end
def self.status
new.status
end
def self.get_post(...)
new.get_post(...)
end
def self.get_posts(...)
new.get_posts(...)
end
def self.get_all_posts(...)
new.get_all_posts(...)
end
def self.get_posts_by_tags(...)
new.get_posts_by_tags(...)
end
def self.get_all_posts_by_tags(...)
new.get_all_posts_by_tags(...)
end
def self.find_replacement(...)
new.find_replacement(...)
end
def self.name_to_id(...)
new.name_to_id(...)
end
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class APIImage < ApplicationRecord class APIImage < ApplicationRecord
class Error < StandardError; end
belongs_to_creator belongs_to_creator
attr_accessor :file, :exception, :deletion_reason, :no_webhook_messages attr_accessor :file, :exception, :deletion_reason, :no_webhook_messages
@ -25,19 +26,35 @@ class APIImage < ApplicationRecord
after_destroy :remove_iqdb after_destroy :remove_iqdb
after_destroy :send_deleted, unless: :no_webhook_messages after_destroy :send_deleted, unless: :no_webhook_messages
has_one :external_api_image, dependent: :destroy
scope :viewable, -> { where(is_viewable: true) }
scope :unviewable, -> { where(is_viewable: false) }
scope :external, -> { joins(:external_api_image).where.not("external_api_images.id": nil) }
scope :internal, -> { left_outer_joins(:external_api_image).where("external_api_images.id": nil) }
def is_external?
external_api_image.present?
end
def delete_files def delete_files
return if is_external?
delete_files!
end
def delete_files!
invalidate_cache
Websites.config.yiffy2_storage.delete(path) Websites.config.yiffy2_storage.delete(path)
Websites.config.yiffy2_backup_storage.delete(path) Websites.config.yiffy2_backup_storage.delete(path)
invalidate_cache
end end
def update_files def update_files
invalidate_cache
return if is_external?
file = Websites.config.yiffy2_storage.get(path_before_last_save) file = Websites.config.yiffy2_storage.get(path_before_last_save)
Websites.config.yiffy2_storage.put(path, file) Websites.config.yiffy2_storage.put(path, file)
Websites.config.yiffy2_storage.delete(path_before_last_save) Websites.config.yiffy2_storage.delete(path_before_last_save)
Websites.config.yiffy2_backup_storage.put(path, file) Websites.config.yiffy2_backup_storage.put(path, file)
Websites.config.yiffy2_backup_storage.delete(path_before_last_save) Websites.config.yiffy2_backup_storage.delete(path_before_last_save)
invalidate_cache
end end
def file_header_info(file_path) def file_header_info(file_path)
@ -64,7 +81,7 @@ class APIImage < ApplicationRecord
module SearchMethods module SearchMethods
def random(category, limit, size_limit = nil) def random(category, limit, size_limit = nil)
q = where(category: category) q = viewable.where(category: category)
q = q.where(file_size: ..size_limit) if size_limit q = q.where(file_size: ..size_limit) if size_limit
q.ids.sample(limit).map(&method(:find)) q.ids.sample(limit).map(&method(:find))
end end
@ -85,7 +102,15 @@ class APIImage < ApplicationRecord
q = super q = super
q = q.attribute_matches(:category, params[:category]) q = q.attribute_matches(:category, params[:category])
q = q.attribute_matches(:original_url, params[:original_url]) q = q.attribute_matches(:original_url, params[:original_url])
q = q.attribute_matches(:is_viewable, params[:viewable])
q = q.artists_like(params[:artist]) if params[:artist].present? q = q.artists_like(params[:artist]) if params[:artist].present?
if params[:md5].present? # rubocop:disable Style/IfUnlessModifier
q = q.attribute_matches(:id, params[:md5]).or(q.where(id: ExternalAPIImage.where("cached_data->'file'->>'md5' = ?", params[:md5]).select("api_image_id")))
end
if params[:external].present?
q = q.external if params[:external].truthy?
q = q.internal if params[:external].falsy?
end
q.order(created_at: :desc) q.order(created_at: :desc)
end end
end end
@ -134,7 +159,29 @@ class APIImage < ApplicationRecord
self.artists = str.split("\n").map(&:strip) self.artists = str.split("\n").map(&:strip)
end end
def format_external(*)
img = external_api_image
{
artists: img.artists,
sources: img.sources,
width: img.width,
height: img.height,
url: img.file_url,
type: Marcel::EXTENSIONS[img.ext],
name: "#{img.md5}.#{img.ext}",
id: md5,
md5: img.md5,
ext: img.ext,
size: img.size,
reportURL: img.report_url,
shortURL: short_url,
external: true,
viewable: is_viewable?,
}
end
def serializable_hash(*) def serializable_hash(*)
return format_external(*) if is_external?
{ {
artists: artists, artists: artists,
sources: sources, sources: sources,
@ -144,13 +191,44 @@ class APIImage < ApplicationRecord
type: mime_type, type: mime_type,
name: "#{md5}.#{file_ext}", name: "#{md5}.#{file_ext}",
id: md5, id: md5,
md5: md5,
ext: file_ext, ext: file_ext,
size: file_size, size: file_size,
reportURL: nil, reportURL: nil,
shortURL: short_url, shortURL: short_url,
external: false,
viewable: is_viewable?,
} }
end end
def convert_to_external(url)
transaction do
uri = URI.parse(url)
case uri.host
when "femboy.fan"
type = "femboyfans"
if %r{^/posts/(\d+)} =~ uri.path
external_id = $1.to_i
else
raise(Error, "Failed to parse external id from #{url}")
end
when "e621.net"
type = "e621"
if %r{^/posts/(\d+)} =~ uri.path
external_id = $1.to_i
else
raise(Error, "Failed to parse external id from #{url}")
end
else
raise(Error, "Attempted to convert to unknown external: #{url}")
end
ExternalAPIImage.create!(api_image: self, external_id: external_id, site: type)
update_columns(artists: [], sources: [], width: 0, height: 0, mime_type: "external", file_ext: "external", file_size: 0)
delete_files!
send_converted
end
end
def self.categories def self.categories
sfw = %w[animals.birb animals.blep animals.dikdik furry.boop furry.cuddle furry.flop furry.fursuit furry.hold furry.howl furry.hug furry.kiss furry.lick furry.propose] sfw = %w[animals.birb animals.blep animals.dikdik furry.boop furry.cuddle furry.flop furry.fursuit furry.hold furry.howl furry.hug furry.kiss furry.lick furry.propose]
nsfw = %w[furry.butts furry.bulge furry.yiff.andromorph furry.yiff.gay furry.yiff.gynomorph furry.yiff.lesbian furry.yiff.straight] nsfw = %w[furry.butts furry.bulge furry.yiff.andromorph furry.yiff.gay furry.yiff.gynomorph furry.yiff.lesbian furry.yiff.straight]
@ -231,6 +309,7 @@ class APIImage < ApplicationRecord
RED_TICK = "<:RedTick:1235058898549870724>" RED_TICK = "<:RedTick:1235058898549870724>"
def execute(content) def execute(content)
return unless Rails.env.production?
Websites.config.yiffyapi_image_logs_webhook.execute({ Websites.config.yiffyapi_image_logs_webhook.execute({
embeds: [content], embeds: [content],
}) })
@ -283,6 +362,7 @@ class APIImage < ApplicationRecord
changes = [] changes = []
check_change(:artists, changes) check_change(:artists, changes)
check_change(:category, changes) check_change(:category, changes)
check_change(:is_viewable, changes)
if sources != sources_before_last_save if sources != sources_before_last_save
diff = [] diff = []
@ -311,6 +391,19 @@ class APIImage < ApplicationRecord
}) })
end end
def send_converted
execute({
title: "Image Converted",
description: <<~DESC,
ID: `#{md5}`
Blame: #{blame}
Converted to external image: #{external_api_image.url}
DESC
color: YELLOW,
timestamp: Time.now.iso8601,
})
end
def blame def blame
if CurrentUser.user.present? if CurrentUser.user.present?
"<@#{CurrentUser.id}> (`#{CurrentUser.name}`)" "<@#{CurrentUser.id}> (`#{CurrentUser.name}`)"
@ -327,20 +420,28 @@ class APIImage < ApplicationRecord
include WebhookMethods include WebhookMethods
def is_image? def is_image?
%w[png jpg].include?(file_ext) ext = file_ext
ext = external_api_image.ext if is_external?
%w[png jpg].include?(ext)
end end
def is_gif? def is_gif?
file_ext == "gif" ext = file_ext
ext = external_api_image.ext if is_external?
ext == "gif"
end
def is_available?
!is_external? || !external_api_image.is_deleted?
end end
def update_iqdb def update_iqdb
return unless is_image? return unless is_available? && is_image?
IqdbUpdateJob.perform_later(id) IqdbUpdateJob.perform_later(id)
end end
def update_iqdb! def update_iqdb!
return unless is_image? return unless is_available? && is_image?
IqdbUpdateJob.perform_now(id) IqdbUpdateJob.perform_now(id)
end end

View File

@ -0,0 +1,159 @@
# frozen_string_literal: true
class ExternalAPIImage < ApplicationRecord
class Error < StandardError; end
CACHE_TIME = 2.days
IGNORED_ARTISTS = %w[avoid_posting conditional_dnp sound_warning epilepsy_warning].freeze
belongs_to :api_image
validates :site, inclusion: { in: %w[e621 femboyfans] }
enum site: {
"e621" => "e621",
"femboyfans" => "femboyfans",
}
module DataMethods
def url
case site
when "e621"
"https://e621.net/posts/#{external_id}"
when "femboyfans"
"https://femboy.fan/posts/#{external_id}"
else
data_error("format url")
end
end
def file_url
return nil if is_deleted?
case site
when "e621", "femboyfans"
cache.dig("file", "url")
else
data_error("format file_url")
end
end
def width
case site
when "e621", "femboyfans"
cache.dig("file", "width")
else
data_error("format width")
end
end
def height
case site
when "e621", "femboyfans"
cache.dig("file", "height")
else
data_error("format height")
end
end
def size
case site
when "e621", "femboyfans"
cache.dig("file", "size")
else
data_error("format size")
end
end
def ext
case site
when "e621", "femboyfans"
cache.dig("file", "ext")
else
data_error("format ext")
end
end
def md5
case site
when "e621", "femboyfans"
cache.dig("file", "md5")
else
data_error("format md5")
end
end
def is_deleted?
case site
when "e621", "femboyfans"
cache.dig("flags", "deleted")
else
false
end
end
def artists
case site
when "e621", "femboyfans"
cache.dig("tags", "artist") - IGNORED_ARTISTS
else
data_error("format artists")
end
end
def sources
case site
when "e621", "femboyfans"
[url] + cache["sources"].select { |s| s.start_with?("http") }
else
data_error("format sources")
end
end
def report_url
case site
when "e621", "femboyfans"
url
end
end
def data_error(text)
raise(Error, "Don't know how to #{text} for ExternalApiImage #{id}: #{site}")
end
end
module CacheMethods
def cache
update_cache! if cached_data.blank?
update_cache if cache_expired?
cached_data
end
def cache_expired?
cache_last_updated_at < CACHE_TIME.ago
end
def update_cache
UpdateExternalCacheJob.perform_later(self)
end
def update_cache!
case site
when "e621"
data = Requests::E621.get_post(id: external_id)
when "femboyfans"
data = Requests::FemboyFans.get_post(id: external_id)
else
data_error("update cache")
end
update(cached_data: data, cache_last_updated_at: Time.now)
if is_deleted?
api_image.remove_iqdb
else
api_image.update_iqdb
end
api_image.update(is_viewable: !is_deleted?)
end
end
include CacheMethods
include DataMethods
end

View File

@ -0,0 +1,46 @@
<%# locals: (image:, external:) -%>
<td style="width: 12%">
<% if external.is_deleted? %>
(deleted)
<% else %>
<%= link_to(external.file_url, target: "_blank", rel: "noopener") do %>
<%= image_tag(external.file_url, class: "img-fluid img-thumbnail") %>
<% end %>
<% end %>
</td>
<td><%= external.artists.join(", ") %></td>
<td>
<ul class="details">
<li>Resolution: <b><%= external.width %>x<%= external.height %></b></li>
<% if image.md5 != external.md5 %>
<li>ID: <b><%= image.md5 %></li>
<% end %>
<li>MD5: <b><%= external.md5 %></b></li>
<li>Type: <b><%= external.ext %></b></li>
<li>Size: <b><%= number_to_human_size(external.size) %></b></li>
<li>Creator: <b><%= image.creator.name %></b></li>
<li>Created At: <b><%= compact_time(image.created_at) %></b></li>
<li>Updated At: <b><%= compact_time(image.updated_at) %></b></li>
<% unless @sp[:category].present? %>
<li>Category: <b><%= image.category %></b></li>
<% end %>
<li>
Upload Type: <b>external</b>
</li>
<li>Viewable: <b class="<%= image.is_viewable? ? "green-text" : "red-text" %>"><%= image.is_viewable? ? "Yes" : "No" %></b></li>
</ul>
</td>
<td>
<% external.sources.each do |source| %>
<%= link_to(source, source) %><br>
<% end %>
</td>
<td>
<%= link_to("Edit", edit_yiff_rest_api_v2_manage_image_path(image)) %> |
<%= link_to("Delete", yiff_rest_api_v2_manage_image_path(image), method: :delete, data: { confirm: "Are you sure? This action is not reversible." }) %><br>
<%= link_to("Delete W/R", delete_with_reason_yiff_rest_api_v2_manage_image_path(image)) %><br>
<%= link_to("Toggle Visibility", toggle_viewable_yiff_rest_api_v2_manage_image_path(image), method: :put) %><br>
<%= link_to("Update Cache", update_cache_yiff_rest_api_v2_manage_image_path(image), method: :put) %><br>
<%= link_to("Find Similar", iqdb_query_yiff_rest_api_v2_manage_images_path(search: { image_id: image.id })) %>
</td>

View File

@ -0,0 +1,44 @@
<%# locals: (image:) -%>
<td style="width: 12%">
<%= link_to(image.url, target: "_blank", rel: "noopener") do %>
<%= image_tag(image.url, class: "img-fluid img-thumbnail") %>
<% end %>
</td>
<td><%= image.artists.join(", ") %></td>
<td>
<ul class="details">
<li>Resolution: <b><%= image.width %>x<%= image.height %></b></li>
<li>MD5: <b><%= image.md5 %></b></li>
<li>Type: <b><%= image.file_ext %></b></li>
<li>Size: <b><%= number_to_human_size(image.file_size) %></b></li>
<li>Creator: <b><%= image.creator.name %></b></li>
<li>Created At: <b><%= compact_time(image.created_at) %></b></li>
<li>Updated At: <b><%= compact_time(image.updated_at) %></b></li>
<% unless @sp[:category].present? %>
<li>Category: <b><%= image.category %></b></li>
<% end %>
<li>
Upload Type:
<% if image.original_url.present? %>
<%= link_to "<b>url</b>".html_safe, image.original_url %>
<% else %>
<b>file</b>
<% end %>
</li>
<li>Viewable: <b class="<%= image.is_viewable? ? "green-text" : "red-text" %>"><%= image.is_viewable? ? "Yes" : "No" %></b></li>
</ul>
</td>
<td>
<% image.sources.each do |source| %>
<%= link_to(source, source) %><br>
<% end %>
</td>
<td>
<%= link_to("Edit", edit_yiff_rest_api_v2_manage_image_path(image)) %> |
<%= link_to("Delete", yiff_rest_api_v2_manage_image_path(image), method: :delete, data: { confirm: "Are you sure? This action is not reversible." }) %><br>
<%= link_to("Delete W/R", delete_with_reason_yiff_rest_api_v2_manage_image_path(image)) %><br>
<%= link_to("Toggle Visibility", toggle_viewable_yiff_rest_api_v2_manage_image_path(image), method: :put) %><br>
<%= link_to("Find Similar", iqdb_query_yiff_rest_api_v2_manage_images_path(search: { image_id: image.id })) %><br>
<%= link_to("Convert", convert_yiff_rest_api_v2_manage_image_path(image)) %>
</td>

View File

@ -0,0 +1,14 @@
<% content_for(:page_title) do %>
YiffyAPI - Convert Image
<% end %>
<%= render "yiff_rest/api_v2/manage/nav" %>
<div class="w-100">
<div style="width: 40%; margin-left: 30%;">
<%= simple_form_for(:api_image, url: convert_yiff_rest_api_v2_manage_image_path(@image), method: :post) do |f| %>
<%= f.input(:external_url) %>
<%= f.button(:submit, "Convert Image", name: nil) %>
<% end %>
</div>
</div>

View File

@ -6,7 +6,7 @@
<div class="w-100"> <div class="w-100">
<div style="width: 40%; margin-left: 30%;"> <div style="width: 40%; margin-left: 30%;">
<%= simple_form_for(@image, url: yiff_rest_api_v2_manage_image_path(manage_id: params[:manage_id], id: params[:id]), method: :delete) do |f| %> <%= simple_form_for(@image, url: yiff_rest_api_v2_manage_image_path(@image), method: :delete) do |f| %>
<%= f.input(:deletion_reason) %> <%= f.input(:deletion_reason) %>
<%= f.button(:submit, "Delete Image", name: nil) %> <%= f.button(:submit, "Delete Image", name: nil) %>
<% end %> <% end %>

View File

@ -6,7 +6,7 @@
<div class="w-100"> <div class="w-100">
<div style="width: 40%; margin-left: 30%;"> <div style="width: 40%; margin-left: 30%;">
<%= simple_form_for(@image, url: yiff_rest_api_v2_manage_image_path(manage_id: params[:manage_id], id: params[:id]), method: :patch) do |f| %> <%= simple_form_for(@image, url: yiff_rest_api_v2_manage_image_path(@image), method: :patch) do |f| %>
<%= f.input(:sources_string, as: :text, label: "Sources") %> <%= f.input(:sources_string, as: :text, label: "Sources") %>
<%= f.input(:artists_string, as: :text, label: "Artists") %> <%= f.input(:artists_string, as: :text, label: "Artists") %>
<%= f.input(:category, as: :select, collection: @categories.map { |cat| [cat.name, cat.db] }) %> <%= f.input(:category, as: :select, collection: @categories.map { |cat| [cat.name, cat.db] }) %>

View File

@ -7,52 +7,18 @@
<th style="width: 12%;"></th> <th style="width: 12%;"></th>
<th style="width: 10%;">Artists</th> <th style="width: 10%;">Artists</th>
<th style="width: 25%;">Details</th> <th style="width: 25%;">Details</th>
<th style="width: 45%;">Sources</th> <th style="width: 43%;">Sources</th>
<th style="width: 8%"></th> <th style="width: 10%"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @images.each do |img| %> <% @images.each do |img| %>
<tr> <tr>
<td style="width: 12%"> <% if img.is_external? %>
<%= link_to(img.url, target: "_blank", rel: "noopener") do %> <%= render(partial: "yiff_rest/api_v2/images/external_image", locals: { image: img, external: img.external_api_image }) %>
<%= image_tag(img.url, class: "img-fluid img-thumbnail") %> <% else %>
<% end %> <%= render(partial: "yiff_rest/api_v2/images/image", locals: { image: img }) %>
</td> <% end %>
<td><%= img.artists.join(", ") %></td>
<td>
<ul class="details">
<li>Resolution: <b><%= img.width %>x<%= img.height %></b></li>
<li>MD5: <b><%= img.md5 %></b></li>
<li>Type: <b><%= img.file_ext %></b></li>
<li>Size: <b><%= number_to_human_size(img.file_size) %></b></li>
<li>Creator: <b><%= img.creator.name %></b></li>
<li>Created At: <b><%= compact_time(img.created_at) %></b></li>
<li>Updated At: <b><%= compact_time(img.updated_at) %></b></li>
<% unless @sp[:category].present? %>
<li>Category: <b><%= img.category %></b></li>
<% end %>
<li>
Upload Type:
<% if img.original_url.present? %>
<%= link_to "<b>url</b>".html_safe, img.original_url %>
<% else %>
<b>file</b>
<% end %>
</li>
</ul>
</td>
<td>
<% img.sources.each do |source| %>
<%= link_to source, source %><br>
<% end %>
</td>
<td>
<%= 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 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>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>

View File

@ -11,11 +11,14 @@
<li class="nav-item"> <li class="nav-item">
<%= link_to("Home", yiff_rest_api_v2_manage_index_path, class: "nav-link#{if_active(' active', path: yiff_rest_api_v2_manage_index_path)}") %> <%= link_to("Home", yiff_rest_api_v2_manage_index_path, class: "nav-link#{if_active(' active', path: yiff_rest_api_v2_manage_index_path)}") %>
</li> </li>
<li class="nav-item">
<%= link_to("Image List", yiff_rest_api_v2_manage_images_path, class: "nav-link") %>
</li>
<% if params[:controller] == "yiff_rest/api_v2/manage" %> <% if params[:controller] == "yiff_rest/api_v2/manage" %>
<% elsif params[:controller] == "yiff_rest/api_v2/images" %> <% elsif params[:controller] == "yiff_rest/api_v2/images" %>
<% if %w[index iqdb query_iqdb].include?(params[:action]) %> <% if %w[index iqdb query_iqdb].include?(params[:action]) %>
<li class="nav-item"> <li class="nav-item">
<%= link_to("Upload Image", new_yiff_rest_api_v2_manage_image_path(api_image: ({ category: @sp[:category] } if @sp[:category])), class: "nav-link#{if_active(' active', path: new_yiff_rest_api_v2_manage_image_path(manage_id: params[:id]))}") %> <%= link_to("Upload Image", new_yiff_rest_api_v2_manage_image_path(api_image: ({ category: @sp[:category] } if @sp[:category])), class: "nav-link#{if_active(' active', path: new_yiff_rest_api_v2_manage_image_path)}") %>
</li> </li>
<% else %> <% else %>
<% category = params.dig(:api_image, :category) if params[:action] == "new" %> <% category = params.dig(:api_image, :category) if params[:action] == "new" %>

View File

@ -293,6 +293,12 @@ module Websites
def e621_apikey def e621_apikey
end end
def femboyfans_username
end
def femboyfans_apikey
end
def admin_domain def admin_domain
end end

View File

@ -53,11 +53,15 @@ module YiffRestRoutes
get(:logout) get(:logout)
put(:sync) put(:sync)
resources(:images, as: "manage_images") do resources(:images, as: "manage_images") do
get(:delete_with_reason, on: :member) member do
get(:delete_with_reason)
match(:convert, via: %i[get post])
put(:toggle_viewable)
put(:update_cache)
end
collection do collection do
get(:iqdb) get(:iqdb)
get("/iqdb/query", action: :query_iqdb) match("/iqdb/query", via: %i[get post], action: :query_iqdb)
post("/iqdb/query", action: :query_iqdb)
end end
end end
end end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateExternalAPIImages < ActiveRecord::Migration[7.1]
def change
create_table(:external_api_images) do |t|
t.references(:api_image, foreign_key: true, null: false, type: :uuid)
t.string(:site, null: false)
t.jsonb(:cached_data, null: false, default: {})
t.integer(:external_id, null: false)
t.datetime(:cache_last_updated_at, null: false, default: -> { "CURRENT_TIMESTAMP(3)" })
t.timestamps
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddIsViewableToAPIImages < ActiveRecord::Migration[7.1]
def change
add_column(:api_images, :is_viewable, :boolean, default: true, null: false)
end
end

17
db/schema.rb generated
View File

@ -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_10_13_040658) do ActiveRecord::Schema[7.1].define(version: 2024_10_21_212746) 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
@ -59,6 +59,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_13_040658) do
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.bigserial "iqdb_id", null: false
t.boolean "is_viewable", default: true, 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
@ -140,7 +141,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_13_040658) do
t.string "class_name" t.string "class_name"
t.inet "ip_addr" t.inet "ip_addr"
t.string "version" t.string "version"
t.text "extra_params" t.string "extra_params"
t.text "message" t.text "message"
t.text "trace" t.text "trace"
t.uuid "code" t.uuid "code"
@ -148,6 +149,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_13_040658) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "external_api_images", force: :cascade do |t|
t.uuid "api_image_id", null: false
t.string "site", null: false
t.jsonb "cached_data", default: {}, null: false
t.integer "external_id", null: false
t.datetime "cache_last_updated_at", default: -> { "CURRENT_TIMESTAMP(3)" }, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["api_image_id"], name: "index_external_api_images_on_api_image_id"
end
create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
@ -259,5 +271,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_10_13_040658) do
add_foreign_key "api_keys", "api_users", column: "owner_id" add_foreign_key "api_keys", "api_users", column: "owner_id"
add_foreign_key "api_usages", "api_users", column: "user_id" add_foreign_key "api_usages", "api_users", column: "user_id"
add_foreign_key "e621_thumbnails", "api_users", column: "creator_id" add_foreign_key "e621_thumbnails", "api_users", column: "creator_id"
add_foreign_key "external_api_images", "api_images"
add_foreign_key "short_urls", "api_users", column: "creator_id" add_foreign_key "short_urls", "api_users", column: "creator_id"
end end