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:
8
app/controllers/media_metadata_controller.rb
Normal file
8
app/controllers/media_metadata_controller.rb
Normal 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
42
app/logical/exif_tool.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
22
app/models/media_metadata.rb
Normal file
22
app/models/media_metadata.rb
Normal 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
|
||||
@@ -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)
|
||||
|
||||
5
app/policies/media_metadata_policy.rb
Normal file
5
app/policies/media_metadata_policy.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class MediaMetadataPolicy < ApplicationPolicy
|
||||
def index?
|
||||
true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user