Add MediaMetadata model.

Add a model for storing image and video metadata for uploaded files.

Metadata is extracted using ExifTool. You will need to install ExifTool
after this commit. ExifTool 12.22 is the minimum required version
because we use the `--binary` option, which was added in this release.

The MediaMetadata model is separate from the MediaAsset model because
some files contain tons of metadata, and most of it is non-essential.
The MediaAsset model represents an uploaded file and contains essential
metadata, like the file's size and type, while the MediaMetadata model
represents all the other non-essential metadata associated with a file.

Metadata is stored as a JSON column in the database.

ExifTool returns all the file's metadata, not just the EXIF metadata.
EXIF is one of several types of image metadata, hence why we call
it MediaMetadata instead of EXIFMetadata.
This commit is contained in:
evazion
2021-09-07 18:22:34 -05:00
parent 291758ddb7
commit 3d660953d4
20 changed files with 235 additions and 21 deletions

View File

@@ -0,0 +1,8 @@
class MediaMetadataController < ApplicationController
respond_to :json, :xml
def index
@media_metadata = authorize MediaMetadata.visible(CurrentUser.user).paginated_search(params, count_pages: true)
respond_with(@media_metadata)
end
end

42
app/logical/exif_tool.rb Normal file
View File

@@ -0,0 +1,42 @@
require "shellwords"
# A wrapper for the exiftool command.
class ExifTool
extend Memoist
class Error < StandardError; end
# @see https://exiftool.org/exiftool_pod.html#OPTIONS
DEFAULT_OPTIONS = %q(
-G1 -duplicates -unknown -struct --binary
-x 'System:*' -x ExifToolVersion -x FileType -x FileTypeExtension
-x MIMEType -x ImageWidth -x ImageHeight -x ImageSize -x MegaPixels
).squish
attr_reader :file
# Open a file with ExifTool.
# @param file [File, String] an image or video file
def initialize(file)
@file = file.is_a?(String) ? File.open(file) : file
end
# Get the file's metadata.
# @see https://exiftool.org/TagNames/index.html
# @param options [String] the options to pass to exiftool
# @return [Hash] the file's metadata
def metadata(options: DEFAULT_OPTIONS)
output = shell!("exiftool #{options} -json #{file.path.shellescape}")
json = JSON.parse(output).first
json = json.except("SourceFile")
json.with_indifferent_access
end
def shell!(command)
output, status = Open3.capture2e(command)
raise Error, "#{command}` failed: #{output}" if !status.success?
output
end
memoize :metadata
end

View File

@@ -102,6 +102,10 @@ class MediaFile
file.size
end
def metadata
ExifTool.new(file).metadata
end
# @return [Boolean] true if the file is an image
def is_image?
file_ext.in?([:jpg, :png, :gif])
@@ -164,5 +168,5 @@ class MediaFile
nil
end
memoize :file_ext, :file_size, :md5
memoize :file_ext, :file_size, :md5, :metadata
end

View File

@@ -104,13 +104,6 @@ class UploadService
p.uploader_id = upload.uploader_id
p.uploader_ip_addr = upload.uploader_ip_addr
p.parent_id = upload.parent_id
p.media_asset = MediaAsset.new(
md5: upload.md5,
file_ext: upload.file_ext,
file_size: upload.file_size,
image_width: upload.image_width,
image_height: upload.image_height,
)
if !upload.uploader.can_upload_free? || upload.upload_as_pending?
p.is_pending = true

View File

@@ -62,6 +62,8 @@ class UploadService
upload.tag_string = "#{upload.tag_string} #{Utils.automatic_tags(media_file)}"
process_resizes(upload, file, original_post_id)
MediaAsset.create_from_media_file!(media_file)
end
def automatic_tags(media_file)

View File

@@ -1,4 +1,17 @@
class MediaAsset < ApplicationRecord
has_one :media_metadata, dependent: :destroy
def self.create_from_media_file!(media_file)
create!(
md5: media_file.md5,
file_ext: media_file.file_ext,
file_size: media_file.file_size,
image_width: media_file.width,
image_height: media_file.height,
media_metadata: MediaMetadata.new(metadata: media_file.metadata),
)
end
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :md5, :file_ext, :file_size, :image_width, :image_height)
q = q.apply_default_order(params)

View File

@@ -0,0 +1,22 @@
# MediaMetadata represents the EXIF and other metadata associated with a
# MediaAsset (an uploaded image or video file). The `metadata` field contains a
# JSON hash of the file's metadata as returned by ExifTool.
#
# @see ExifTool
# @see https://exiftool.org/TagNames/index.html
class MediaMetadata < ApplicationRecord
self.table_name = "media_metadata"
attribute :id
attribute :created_at
attribute :updated_at
attribute :media_asset_id
attribute :metadata
belongs_to :media_asset
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :media_asset_id)
q = q.apply_default_order(params)
q
end
end

View File

@@ -65,6 +65,7 @@ class Upload < ApplicationRecord
belongs_to :uploader, :class_name => "User"
belongs_to :post, optional: true
has_one :media_asset, foreign_key: :md5, primary_key: :md5
before_validation :initialize_attributes, on: :create
before_validation :assign_rating_from_tags
@@ -114,6 +115,7 @@ class Upload < ApplicationRecord
return
end
media_asset.destroy!
DanbooruLogger.info("Uploads: Deleting files for upload md5=#{md5}")
Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :original)
Danbooru.config.storage_manager.delete_file(nil, md5, file_ext, :large)

View File

@@ -0,0 +1,5 @@
class MediaMetadataPolicy < ApplicationPolicy
def index?
true
end
end