uploads: merge tags when a duplicate is uploaded (fix #3130).

Automatically merge tags when uploading a duplicate.

There are two cases:

* You try to upload an image, but it's already on Danbooru. In this case
  you'll be immediately redirected to the original post, before you
  can start tagging the upload.

* You're uploading an image, it wasn't a dupe when you first opened the
  upload page, but you got sniped while tagging it. In this case your tags
  will be merged with the original post, and you will be redirected to the
  original post.

There are a few corner cases:

* If you don't have permission to edit the original post, for example
  because it's banned or has a censored tag, then your tags won't be
  merged and will be silently ignored.

* Only the tags, rating, and parent ID will be merged. The source and
  artist commentary won't be merged. This is so that if an artist uploads
  the exact same file to multiple sites, the new source won't override
  the original source.

* Some tags might be contradictory. For example, the new post might
  be tagged translation_request, but the original post might already be
  translated. It's up to the user to fix these things afterwards.
This commit is contained in:
evazion
2022-01-30 02:31:22 -06:00
parent dadd6aed47
commit 43c4158d36
6 changed files with 41 additions and 27 deletions

View File

@@ -67,12 +67,17 @@ class PostsController < ApplicationController
@post = authorize Post.new_from_upload(permitted_attributes(Post))
@post.save
if @post.errors.any?
if @post.errors.none?
respond_with(@post)
elsif @post.errors.of_kind?(:md5, :taken)
@original_post = Post.find_by!(md5: @post.md5)
@original_post.update(rating: @post.rating, parent_id: @post.parent_id, tag_string: "#{@original_post.tag_string} #{@post.tag_string}")
flash[:notice] = "Duplicate of post ##{@original_post.id}; merging tags"
redirect_to @original_post
else
@upload = UploadMediaAsset.find(params[:post][:upload_media_asset_id]).upload
flash[:notice] = @post.errors.full_messages.join("; ")
respond_with(@post, render: { template: "uploads/show" })
else
respond_with(@post)
end
end

View File

@@ -38,6 +38,12 @@ class UploadsController < ApplicationController
def show
@upload = authorize Upload.find(params[:id])
@post = Post.new(uploader: @upload.uploader, uploader_ip_addr: @upload.uploader_ip_addr, source: @upload.source, rating: nil, **permitted_attributes(Post))
respond_with(@upload)
if request.format.html? && @upload.is_completed? && @upload.media_assets.first&.post.present?
flash[:notice] = "Duplicate of post ##{@upload.media_assets.first.post.id}"
redirect_to @upload.media_assets.first.post
else
respond_with(@upload)
end
end
end

View File

@@ -12,6 +12,7 @@ class MediaAsset < ApplicationRecord
LARGE_IMAGE_WIDTH = Danbooru.config.large_image_width
STORAGE_SERVICE = Danbooru.config.storage_manager
has_one :post, foreign_key: :md5, primary_key: :md5
has_one :media_metadata, dependent: :destroy
has_one :pixiv_ugoira_frame_data, class_name: "PixivUgoiraFrameData", foreign_key: :md5, primary_key: :md5

View File

@@ -5,9 +5,9 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
assert_equal(expected, response.parsed_body.css("link[rel=canonical]").attribute("href").value)
end
def create_post!(user: create(:user), rating: "q", tag_string: "tagme", **params)
def create_post!(user: create(:user), media_asset: build(:media_asset), rating: "q", tag_string: "tagme", **params)
upload = build(:upload, uploader: user)
asset = create(:upload_media_asset, upload: upload)
asset = create(:upload_media_asset, upload: upload, media_asset: media_asset)
post_auth posts_path, user, params: { post: { upload_media_asset_id: asset.id, rating: rating, tag_string: tag_string, **params }}
Post.last
@@ -655,6 +655,21 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
assert_equal("test", @post.tag_string)
end
should "re-render the upload page if the upload fails" do
@post = create_post!(rating: "z", tag_string: "tagme")
assert_response :success
end
should "merge the tags and redirect to the original post if the upload is a duplicate of an existing post" do
media_asset = create(:media_asset)
post1 = create_post!(rating: "s", tag_string: "post1", media_asset: media_asset)
post2 = create_post!(rating: "e", tag_string: "post2", media_asset: media_asset)
assert_redirected_to post1
assert_equal("post1 post2", post1.reload.tag_string)
assert_equal("e", post1.rating)
end
should "apply the rating from the tags" do
@post = create_post!(rating: nil, tag_string: "rating:s")

View File

@@ -130,6 +130,14 @@ class UploadsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
should "redirect a completed upload to the original post if it's a duplicate of an existing post" do
@upload = create(:completed_file_upload, uploader: @user)
@post = create(:post, md5: @upload.media_assets.first.md5, media_asset: @upload.media_assets.first)
get_auth upload_path(@upload), @user
assert_redirected_to @post
end
end
context "create action" do

View File

@@ -311,27 +311,6 @@ class UploadServiceTest < ActiveSupport::TestCase
CurrentUser.ip_addr = nil
end
context "with a preprocessed predecessor" do
setup do
@predecessor = FactoryBot.create(:source_upload, status: "preprocessed", source: @source, image_height: 0, image_width: 0, file_size: 1, md5: 'd34e4cf0a437a5d65f8e82b7bcd02606', file_ext: "jpg")
@tags = 'hello world'
end
context "when the file has already been uploaded" do
setup do
@asset = MediaAsset.find_by_md5("d34e4cf0a437a5d65f8e82b7bcd02606")
@post = create(:post, md5: "d34e4cf0a437a5d65f8e82b7bcd02606", media_asset: @asset)
@service = subject.new(source: @source)
end
should "point to the dup post in the upload" do
@upload = subject.new(source: @source, tag_string: @tags).start!
@predecessor.reload
assert_equal("error: ActiveRecord::RecordInvalid - Validation failed: Md5 duplicate: #{@post.id}", @predecessor.status)
end
end
end
context "with a source containing unicode characters" do
should "normalize unicode characters in the source field" do
source1 = "poke\u0301mon" # pokémon (nfd form)