kithe 2.2.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/characterization/kithe/ffprobe_characterization.rb +186 -0
- data/app/derivative_transformers/kithe/ffmpeg_extract_jpg.rb +97 -0
- data/app/indexing/kithe/indexable/thread_settings.rb +3 -2
- data/app/models/kithe/asset.rb +17 -39
- data/app/models/kithe/validators/model_parent.rb +3 -2
- data/lib/kithe/indexable_settings.rb +5 -2
- data/lib/kithe/version.rb +1 -1
- data/lib/shrine/plugins/kithe_derivative_definitions.rb +104 -7
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a51da44ba82df89dd84f14a9559de8032d2e026ee666d9dba4e9fb41d05b1b0
|
4
|
+
data.tar.gz: feb995557174e9ef7c2fb6689bb352ec2dd227dc4e98a282d1b639e1d8d11393
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6de9ece27273199672bc007b229b63707fb4470456091a5f166b1913d6affad22d16ecb2c1a32e5a053459b408a227149ff3b2947ed06e00e2e1408ca6cd9cce
|
7
|
+
data.tar.gz: 1004d59f35ce5a4abdd958fa87891b3e51c6da3e8e65f078a16e74cb99b601b471aaacf895fb5f4f2249cb8a728d78542b66fe656865ae235c8fbd7a8aa2ce27
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'tty/command'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Kithe
|
5
|
+
# Characterizes Audio or Video files using `ffprobe`, a tool that comes with `ffmpeg`.
|
6
|
+
#
|
7
|
+
# You can pass in a local File object (with a pathname), a local String pathname, or
|
8
|
+
# a remote URL. (Remote URLs will be passed directy to ffprobe, which can efficiently
|
9
|
+
# fetch just the bytes it needs)
|
10
|
+
#
|
11
|
+
# You can get back normalized A/V metadata:
|
12
|
+
#
|
13
|
+
# metadata = FfprobeCharacterization.new(url).normalized_metadata
|
14
|
+
#
|
15
|
+
# Normalized metadata is a *flat* hash of typed JSON-able values. It uses
|
16
|
+
# keys based on what the ActiveEncode gem seems to use, but adds some extras
|
17
|
+
# and makes a few tweaks. See the #normalized_metadata method source for
|
18
|
+
# keys supplied.
|
19
|
+
#
|
20
|
+
# Or the complete FFprobe response as JSON. (We try to use ffprobe options that
|
21
|
+
# are exhausitive as to what is returned, including ffprobe version(s))
|
22
|
+
#
|
23
|
+
# ffprobe_results = FfprobeCharacterization.new(url).ffprobe_hash
|
24
|
+
#
|
25
|
+
# The class method .characterize_from_uploader can usefully extract a URL if possible
|
26
|
+
# or else execute with a file, such as from a shrine `add_metadata` block.
|
27
|
+
#
|
28
|
+
# add_metadata do |source_io, **context|
|
29
|
+
# Kithe::FfprobeCharacterization.characterize_from_uploader(source_io, context)
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
class FfprobeCharacterization
|
33
|
+
class_attribute :ffprobe_command, default: "ffprobe"
|
34
|
+
class_attribute :ffprobe_timeout, default: 10
|
35
|
+
|
36
|
+
attr_reader :input_arg
|
37
|
+
|
38
|
+
# @param input [String,File] local File OR local filepath as String, OR remote URL as string
|
39
|
+
# If you have a remote url, just passing hte remote url is way more performant than
|
40
|
+
# downloading it yourself locally -- ffprobe will just fetch the bytes it needs.
|
41
|
+
def initialize(input)
|
42
|
+
if input.respond_to?(:path)
|
43
|
+
input = input.path
|
44
|
+
end
|
45
|
+
@input_arg = input
|
46
|
+
end
|
47
|
+
|
48
|
+
# a helper for creating a block for shrine uploader, you can always use
|
49
|
+
# FFprobeCharecterization.new directly too!
|
50
|
+
#
|
51
|
+
# * Does not run on "cache" action, only on promotion (or manual execution).
|
52
|
+
#
|
53
|
+
# * Will run only on items with "audio/" or "video/" content-type.
|
54
|
+
#
|
55
|
+
# * By default only on main original, not derivatives, although
|
56
|
+
# you can pass `run_on_derivatives: true` if desired.
|
57
|
+
#
|
58
|
+
# Will use ffprobe with direct URL if possible based on source_io (ffprobe
|
59
|
+
# can very efficiently access only bytes needed from URL), otherwise will
|
60
|
+
# download local temp copy if necessary.
|
61
|
+
#
|
62
|
+
# class AssetUploader < Kithe::AssetUploader
|
63
|
+
# add_metadata do |source_io, **context|
|
64
|
+
# Kithe::FfprobeCharacterization.characterize_from_uploader(source_io, context)
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# #...
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
def self.characterize_from_uploader(source_io, add_metadata_context, run_on_derivatives: false)
|
71
|
+
# only for A/V please
|
72
|
+
return {} unless add_metadata_context.dig(:metadata, "mime_type")&.start_with?(%r{\A(audio|video)/})
|
73
|
+
|
74
|
+
# don't run on cache, only on promotion or manual trigger
|
75
|
+
return {} unless add_metadata_context[:action] != :cache
|
76
|
+
|
77
|
+
# don't run on derivatives unless option given
|
78
|
+
return {} unless add_metadata_context[:derivative].nil? || run_on_derivatives
|
79
|
+
|
80
|
+
# ffprobe can use a URL and very efficiently only retrieve what bytes it needs...
|
81
|
+
if source_io.respond_to?(:url) && source_io.url.start_with?(/\Ahttps?:/)
|
82
|
+
Kithe::FfprobeCharacterization.new(source_io.url).normalized_metadata
|
83
|
+
else
|
84
|
+
# if not already a file, will download, possibly slow, but gets us to go.
|
85
|
+
Shrine.with_file(source_io) do |file|
|
86
|
+
Kithe::FfprobeCharacterization.new(file.path).normalized_metadata
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# ffprobe args come from this suggestion:
|
92
|
+
#
|
93
|
+
# https://gist.github.com/nrk/2286511?permalink_comment_id=2593200#gistcomment-2593200
|
94
|
+
#
|
95
|
+
# We also add in various current version tags! If we're going to record all ffprobe
|
96
|
+
# output, we'll want that too!
|
97
|
+
def ffprobe_options
|
98
|
+
[
|
99
|
+
"-hide_banner",
|
100
|
+
"-loglevel", "fatal",
|
101
|
+
"-show_error", "-show_format", "-show_streams", "-show_programs",
|
102
|
+
"-show_chapters", "-show_private_data", "-show_versions",
|
103
|
+
"-print_format", "json",
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
# ffprobe output parsed as JSON...
|
108
|
+
def ffprobe_hash
|
109
|
+
@ffprobe_hash ||= JSON.parse(ffprobe_stdout).merge(
|
110
|
+
"ffprobe_options_used" => ffprobe_options.join(" ")
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a FLAT JSON-able hash of normalized a/v metadata.
|
115
|
+
#
|
116
|
+
# Tries to standardize to what ActiveEncode uses, with some changes and additions.
|
117
|
+
# https://github.com/samvera-labs/active_encode/blob/42f5ed5427a39e56093a5e82123918c4b2619a47/lib/active_encode/technical_metadata.rb
|
118
|
+
#
|
119
|
+
# A video file or other container can have more than one audio or video stream in it, although
|
120
|
+
# this is somewhat unusual for our domain. For the stream-specific audio_ and video_ metadata
|
121
|
+
# returned, we just choose the *first* returned audio or video stream (which may be more or
|
122
|
+
# less arbitrary)
|
123
|
+
#
|
124
|
+
# See also #ffprobe_hash for complete ffprobe results
|
125
|
+
def normalized_metadata
|
126
|
+
# overall audio_sample_rate are null, audio codec is wrong
|
127
|
+
@normalized_metadata ||= {
|
128
|
+
"width" => first_video_stream_json&.dig("width"),
|
129
|
+
"height" => first_video_stream_json&.dig("height"),
|
130
|
+
"frame_rate" => video_frame_rate_as_float, # frames per second
|
131
|
+
"duration_seconds" => ffprobe_hash&.dig("format", "duration")&.to_f&.round(3),
|
132
|
+
"audio_codec" => first_audio_stream_json&.dig("codec_name"),
|
133
|
+
"video_codec" => first_video_stream_json&.dig("codec_name"),
|
134
|
+
"audio_bitrate" => first_audio_stream_json&.dig("bit_rate")&.to_i, # in bps
|
135
|
+
"video_bitrate" => first_video_stream_json&.dig("bit_rate")&.to_i, # in bps
|
136
|
+
# extra ones not ActiveEncode
|
137
|
+
"bitrate" => ffprobe_hash.dig("format", "bit_rate")&.to_i, # overall bitrate of whole file in bps
|
138
|
+
"audio_sample_rate" => first_audio_stream_json&.dig("sample_rate")&.to_i, # in Hz
|
139
|
+
"audio_channels" => first_audio_stream_json&.dig("channels")&.to_i, # usually 1 or 2 (for stereo)
|
140
|
+
"audio_channel_layout" => first_audio_stream_json&.dig("channel_layout"), # stereo or mono or (dolby) 2.1, or something else.
|
141
|
+
}.compact
|
142
|
+
end
|
143
|
+
|
144
|
+
# just the ffprobe version please. This is also available
|
145
|
+
# in ffprobe_hash
|
146
|
+
def ffprobe_version
|
147
|
+
ffprobe_hash.dig("program_version", "version")
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def ffprobe_stdout
|
153
|
+
@ffprobe_output ||= TTY::Command.new(printer: :null).run(
|
154
|
+
ffprobe_command,
|
155
|
+
*ffprobe_options,
|
156
|
+
input_arg,
|
157
|
+
timeout: ffprobe_timeout).out
|
158
|
+
end
|
159
|
+
|
160
|
+
def first_video_stream_json
|
161
|
+
@first_video_stream_json ||= ffprobe_hash["streams"].find { |stream| stream["codec_type"] == "video" }
|
162
|
+
end
|
163
|
+
|
164
|
+
def first_audio_stream_json
|
165
|
+
@first_audio_stream_json ||= ffprobe_hash["streams"].find { |stream| stream["codec_type"] == "audio" }
|
166
|
+
end
|
167
|
+
|
168
|
+
# There are a few different values we could choose here. We're going to choose
|
169
|
+
# `avg_frame_rate` == total duration / number of frames,
|
170
|
+
# vs (not chosen) `r_frame_rate ` "the lowest framerate with which all timestamps can be represented accurately (it is the least common multiple of all framerates in the stream)"
|
171
|
+
#
|
172
|
+
# (note this sometimes gets us not what we expected, like it gets us 29.78 fps instead of 29.97)
|
173
|
+
#
|
174
|
+
# Then we have to change it from numerator/denomominator to float truncated to two decimal places,
|
175
|
+
# which we let ruby rational do for us.
|
176
|
+
def video_frame_rate_as_float
|
177
|
+
avg_frame_rate = first_video_stream_json&.dig("avg_frame_rate")
|
178
|
+
|
179
|
+
return nil unless avg_frame_rate
|
180
|
+
|
181
|
+
return nil if avg_frame_rate.split("/")[1] == "0" # sometimes it returns '0/0', don't know why.
|
182
|
+
|
183
|
+
Rational(avg_frame_rate).to_f.round(2)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'tty/command'
|
2
|
+
|
3
|
+
module Kithe
|
4
|
+
# Creates a JPG screen capture using ffmpeg, by default with the `thumbnail`
|
5
|
+
# filter to choose a representative frame from the first minute or so.
|
6
|
+
#
|
7
|
+
# @example tempfile = FfmpegExtractJpg.new.call(shrine_uploaded_file)
|
8
|
+
# @example tempfile = FfmpegExtractJpg.new.call(url)
|
9
|
+
# @example tempfile = FfmpegExtractJpg.new(start_seconds: 60).call(shrine_uploaded_file)
|
10
|
+
# @example tempfile = FfmpegExtractJpg.new(start_seconds: 10, width_pixels: 420).call(shrine_uploaded_file)
|
11
|
+
class FfmpegExtractJpg
|
12
|
+
class_attribute :ffmpeg_command, default: "ffmpeg"
|
13
|
+
attr_reader :start_seconds, :frame_sample_size, :width_pixels
|
14
|
+
|
15
|
+
# @param start_seconds [Integer] seek to this point to find thumbnail. If it's
|
16
|
+
# after the end of the video, you won't get a thumb back though! [Default 0]
|
17
|
+
#
|
18
|
+
# @param frame_sample_size [Integer] argument passed to ffmpeg thumbnail filter,
|
19
|
+
# how many frames to sample, starting at start_seconds, to choose representative
|
20
|
+
# thumbnail. If set to false, thumbnail filter won't be used. If this one
|
21
|
+
# goes past the end of the video, ffmpeg is fine with it. Set to `false` to
|
22
|
+
# disable use of ffmpeg sample feature, and just use exact frame at start_seconds.
|
23
|
+
# [Default 900, around 30 seconds at 30 fps]
|
24
|
+
#
|
25
|
+
# @width_pixels [Integer] output thumb at this width. aspect ratio will be
|
26
|
+
# maintained. Warning, if it's larger than video original, ffmpeg will
|
27
|
+
# upscale! If set to nil, thumb will be output at original video
|
28
|
+
# resolution. [Default nil]
|
29
|
+
def initialize(start_seconds: 0, frame_sample_size: 900, width_pixels: nil)
|
30
|
+
@start_seconds = start_seconds
|
31
|
+
@frame_sample_size = frame_sample_size
|
32
|
+
@width_pixels = width_pixels
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
# @param input_arg [String,File,Shrine::UploadedFile] local File; String that
|
38
|
+
# can be URL or local file path; or Shrine::UploadedFile. If Shrine::UploadedFile,
|
39
|
+
# we'll try to get a URL from it if we can, otherwise use or make a local tempfile.
|
40
|
+
# Most efficient is if we have a remote URL to give ffmpeg, one way or another!
|
41
|
+
#
|
42
|
+
# @returns [Tempfile] jpg extracted from movie
|
43
|
+
def call(input_arg)
|
44
|
+
if input_arg.kind_of?(Shrine::UploadedFile)
|
45
|
+
if input_arg.respond_to?(:url) && input_arg.url&.start_with?(/https?\:/)
|
46
|
+
_call(input_arg.url)
|
47
|
+
else
|
48
|
+
Shrine.with_file(input_arg) do |local_file|
|
49
|
+
_call(local_file.path)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
elsif input_arg.respond_to?(:path)
|
53
|
+
_call(input_arg.path)
|
54
|
+
else
|
55
|
+
_call(input_arg.to_s)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Internal implementation, after input has already been normalized to an
|
62
|
+
# string that can be an ffmpeg arg.
|
63
|
+
#
|
64
|
+
# @param ffmpeg_source_arg [String] filepath or URL. ffmpeg can take urls, which
|
65
|
+
# can be very efficient.
|
66
|
+
#
|
67
|
+
# @returns Tempfile pointing to a thumbnail
|
68
|
+
def _call(ffmpeg_source_arg)
|
69
|
+
tempfile = Tempfile.new(['temp_deriv', ".jpg"])
|
70
|
+
|
71
|
+
ffmpeg_args = [ffmpeg_command, "-y"]
|
72
|
+
if start_seconds && start_seconds > 0
|
73
|
+
ffmpeg_args.concat ["-ss", start_seconds.to_i]
|
74
|
+
end
|
75
|
+
|
76
|
+
ffmpeg_args.concat ["-i", ffmpeg_source_arg]
|
77
|
+
|
78
|
+
video_filter_parts = []
|
79
|
+
video_filter_parts << "thumbnail=#{frame_sample_size}" if frame_sample_size
|
80
|
+
video_filter_parts << "scale=#{width_pixels}:-1" if width_pixels
|
81
|
+
if video_filter_parts
|
82
|
+
ffmpeg_args.concat ["-vf", video_filter_parts.join(',')]
|
83
|
+
end
|
84
|
+
|
85
|
+
ffmpeg_args.concat ["-frames:v", "1"]
|
86
|
+
|
87
|
+
ffmpeg_args << tempfile.path
|
88
|
+
|
89
|
+
TTY::Command.new(printer: :null).run(*ffmpeg_args)
|
90
|
+
|
91
|
+
return tempfile
|
92
|
+
rescue StandardError => e
|
93
|
+
tempfile.unlink if tempfile
|
94
|
+
raise e
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -57,7 +57,7 @@ module Kithe
|
|
57
57
|
def initialize(batching:, disable_callbacks:, original_settings:,
|
58
58
|
writer:, on_finish:)
|
59
59
|
@original_settings = original_settings
|
60
|
-
@batching =
|
60
|
+
@batching = batching
|
61
61
|
@disable_callbacks = disable_callbacks
|
62
62
|
@on_finish = on_finish
|
63
63
|
|
@@ -80,8 +80,9 @@ module Kithe
|
|
80
80
|
def writer
|
81
81
|
@writer ||= begin
|
82
82
|
if @batching
|
83
|
+
batch_size = (@batching == true) ? Kithe.indexable_settings.batching_mode_batch_size : @batching
|
83
84
|
@local_writer = true
|
84
|
-
Kithe.indexable_settings.writer_instance!("solr_writer.batch_size" =>
|
85
|
+
Kithe.indexable_settings.writer_instance!("solr_writer.batch_size" => batch_size)
|
85
86
|
end
|
86
87
|
end
|
87
88
|
end
|
data/app/models/kithe/asset.rb
CHANGED
@@ -27,7 +27,8 @@ class Kithe::Asset < Kithe::Model
|
|
27
27
|
to: :file, allow_nil: true
|
28
28
|
delegate :stored?, to: :file_attacher
|
29
29
|
delegate :set_promotion_directives, :promotion_directives, to: :file_attacher
|
30
|
-
|
30
|
+
# delegate "metadata" as #file_metadata
|
31
|
+
delegate :metadata, to: :file, prefix: true, allow_nil: true
|
31
32
|
|
32
33
|
# will be sent to file_attacher.set_promotion_directives, provided by our
|
33
34
|
# kithe_promotion_hooks shrine plugin.
|
@@ -42,48 +43,24 @@ class Kithe::Asset < Kithe::Model
|
|
42
43
|
before_promotion :refresh_metadata_before_promotion
|
43
44
|
after_promotion :schedule_derivatives
|
44
45
|
|
45
|
-
#
|
46
|
-
# to create derivatives with conncurrent access safety, with the :kithe_derivatives
|
47
|
-
# processor argument, to create derivatives defined using kithe_derivative_definitions.
|
48
|
-
#
|
49
|
-
# This is designed for use with kithe_derivatives processor, and has options only relevant
|
50
|
-
# to it, although could be expanded to take a processor argument in the future if needed.
|
51
|
-
#
|
52
|
-
# Create derivatives for every definition added to uploader/attacher with kithe_derivatives
|
53
|
-
# `define_derivative`. Ordinarily will create a definition for every definition
|
54
|
-
# that has not been marked `default_create: false`.
|
55
|
-
#
|
56
|
-
# But you can also pass `only` and/or `except` to customize the list of definitions to be created,
|
57
|
-
# possibly including some that are `default_create: false`.
|
58
|
-
#
|
59
|
-
# create_derivatives should be idempotent. If it has failed having only created some derivatives,
|
60
|
-
# you can always just run it again.
|
46
|
+
# Proxies to file_attacher.kithe_create_derivatives to create kithe managed derivatives.
|
61
47
|
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# assets, you should eager-load the derivatives association for efficiency.
|
48
|
+
# This will include any derivatives you created with in the uploader with kithe
|
49
|
+
# Attacher.define_derivative, as well as any derivative processors you opted into
|
50
|
+
# kithe management with `kithe_include_derivatives_processors` in uploader.
|
66
51
|
#
|
67
|
-
#
|
52
|
+
# This is safe for concurrent updates to the underlying model, the derivatives will
|
53
|
+
# be consistent and not left orphaned.
|
68
54
|
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
# bulk operations say with our rake task.
|
74
|
-
#
|
75
|
-
# kithe_derivatives processor must then do a Shrine.with_file to make sure
|
76
|
-
# it's a local file, when needed.
|
77
|
-
#
|
78
|
-
# See https://github.com/shrinerb/shrine/issues/470
|
55
|
+
# By default it will create all derivatives configured for default generation,
|
56
|
+
# but you can customize by listing derivative keys with :only and :except options,
|
57
|
+
# and with :lazy option which, if true, only executes derivative creation if
|
58
|
+
# a given derivative doesn't already exist.
|
79
59
|
#
|
60
|
+
# See more in docs for kithe at
|
61
|
+
# Shrine::Plugins::KitheDerivativeDefinitions::AttacherMethods#kithe_create_derivatives
|
80
62
|
def create_derivatives(only: nil, except: nil, lazy: false)
|
81
|
-
|
82
|
-
return false unless source
|
83
|
-
|
84
|
-
local_files = file_attacher.process_derivatives(:kithe_derivatives, only: only, except: except, lazy: lazy)
|
85
|
-
|
86
|
-
file_attacher.add_persisted_derivatives(local_files)
|
63
|
+
file_attacher.kithe_create_derivatives(only: only, except: except, lazy: lazy)
|
87
64
|
end
|
88
65
|
|
89
66
|
|
@@ -185,7 +162,8 @@ class Kithe::Asset < Kithe::Model
|
|
185
162
|
|
186
163
|
# called by after_promotion hook
|
187
164
|
def schedule_derivatives
|
188
|
-
|
165
|
+
# no need to schedule if we don't have any
|
166
|
+
return unless self.file_attacher.kithe_derivative_definitions.present? || self.file_attacher.kithe_include_derivatives_processors.present?
|
189
167
|
|
190
168
|
Kithe::TimingPromotionDirective.new(key: :create_derivatives, directives: file_attacher.promotion_directives) do |directive|
|
191
169
|
if directive.inline?
|
@@ -1,5 +1,8 @@
|
|
1
1
|
class Kithe::Validators::ModelParent < ActiveModel::Validator
|
2
2
|
def validate(record)
|
3
|
+
# don't load the parent just to validate it if it hasn't even changed.
|
4
|
+
return unless record.parent_id_changed?
|
5
|
+
|
3
6
|
if record.parent.present? && (record.parent.class <= Kithe::Asset)
|
4
7
|
record.errors.add(:parent, 'can not be an Asset instance')
|
5
8
|
end
|
@@ -7,7 +10,5 @@ class Kithe::Validators::ModelParent < ActiveModel::Validator
|
|
7
10
|
if record.parent.present? && record.class <= Kithe::Collection
|
8
11
|
record.errors.add(:parent, 'is invalid for Collection instances')
|
9
12
|
end
|
10
|
-
|
11
|
-
# TODO avoid recursive parents, maybe using a postgres CTE for efficiency?
|
12
13
|
end
|
13
14
|
end
|
@@ -1,14 +1,17 @@
|
|
1
1
|
module Kithe
|
2
2
|
class IndexableSettings
|
3
3
|
attr_accessor :solr_url, :writer_class_name, :writer_settings,
|
4
|
-
:model_name_solr_field, :solr_id_value_attribute, :disable_callbacks
|
4
|
+
:model_name_solr_field, :solr_id_value_attribute, :disable_callbacks,
|
5
|
+
:batching_mode_batch_size
|
5
6
|
def initialize(solr_url:, writer_class_name:, writer_settings:,
|
6
|
-
model_name_solr_field:, solr_id_value_attribute:, disable_callbacks: false
|
7
|
+
model_name_solr_field:, solr_id_value_attribute:, disable_callbacks: false,
|
8
|
+
batching_mode_batch_size: 100)
|
7
9
|
@solr_url = solr_url
|
8
10
|
@writer_class_name = writer_class_name
|
9
11
|
@writer_settings = writer_settings
|
10
12
|
@model_name_solr_field = model_name_solr_field
|
11
13
|
@solr_id_value_attribute = solr_id_value_attribute || 'id'
|
14
|
+
@batching_mode_batch_size = batching_mode_batch_size
|
12
15
|
end
|
13
16
|
|
14
17
|
# Use configured solr_url, and merge together with configured
|
data/lib/kithe/version.rb
CHANGED
@@ -5,9 +5,25 @@ class Shrine
|
|
5
5
|
# use Rails class_attribute to conveniently have a class-level place
|
6
6
|
# to store our derivative definitions that are inheritable and overrideable.
|
7
7
|
# We store it on the Attacher class, because that's where shrine
|
8
|
-
# puts derivative processor definitions, so seems appropriate.
|
8
|
+
# puts derivative processor definitions, so seems appropriate. Normally
|
9
|
+
# not touched directly by non-kithe code.
|
9
10
|
uploader::Attacher.class_attribute :kithe_derivative_definitions, instance_writer: false, default: []
|
10
11
|
|
12
|
+
# Kithe exersizes lifecycle control over derivatives, normally just the
|
13
|
+
# shrine processor labelled :kithe_derivatives. But you can opt additional shrine
|
14
|
+
# derivative processors into kithe control by listing their labels in this attribute.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
#
|
18
|
+
# class AssetUploader < Kithe::AssetUploader
|
19
|
+
# Attacher.kithe_include_derivatives_processors += [:my_processor]
|
20
|
+
# Attacher.derivatives(:my_processor) do |original|
|
21
|
+
# derivatives
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
uploader::Attacher.class_attribute :kithe_include_derivatives_processors, instance_writer: false, default: []
|
26
|
+
|
11
27
|
# Register our derivative processor, that will create our registered derivatives,
|
12
28
|
# with our custom options.
|
13
29
|
#
|
@@ -31,11 +47,12 @@ class Shrine
|
|
31
47
|
#
|
32
48
|
# The most basic definition consists of a derivative key, and a ruby block that
|
33
49
|
# takes the original file, transforms it, and returns a ruby File or other
|
34
|
-
# (shrine-compatible) IO-like object. It will usually be done inside
|
35
|
-
# class definition.
|
50
|
+
# (shrine-compatible) IO-like object. It will usually be done inside your custom
|
51
|
+
# AssetUploader class definition.
|
36
52
|
#
|
37
|
-
# class
|
38
|
-
# define_derivative :thumbnail do |original_file|
|
53
|
+
# class AssetUploader < Kithe::AssetUploader
|
54
|
+
# Attacher.define_derivative :thumbnail do |original_file|
|
55
|
+
# someTempFileOrOtherIo
|
39
56
|
# end
|
40
57
|
# end
|
41
58
|
#
|
@@ -52,7 +69,7 @@ class Shrine
|
|
52
69
|
# will be passed in. You can then get the model object from `attacher.record`, or the
|
53
70
|
# original file as a `Shrine::UploadedFile` object with `attacher.file`.
|
54
71
|
#
|
55
|
-
# define_derivative :thumbnail do |original_file, attacher:|
|
72
|
+
# Attacher.define_derivative :thumbnail do |original_file, attacher:|
|
56
73
|
# attacher.record.title, attacher.file.width, attacher.file.content_type # etc
|
57
74
|
# end
|
58
75
|
#
|
@@ -62,7 +79,7 @@ class Shrine
|
|
62
79
|
# remain, and be accessible, where they were created; there is no built-in solution at present
|
63
80
|
# for moving them).
|
64
81
|
#
|
65
|
-
# define_derivative :thumbnail, storage_key: :my_thumb_storage do |original| # ...
|
82
|
+
# Attacher.define_derivative :thumbnail, storage_key: :my_thumb_storage do |original| # ...
|
66
83
|
#
|
67
84
|
# You can also set `default_create: false` if you want a particular definition not to be
|
68
85
|
# included in a no-arg `asset.create_derivatives` that is normally triggered on asset creation.
|
@@ -101,6 +118,86 @@ class Shrine
|
|
101
118
|
end.freeze
|
102
119
|
end
|
103
120
|
end
|
121
|
+
|
122
|
+
module AttacherMethods
|
123
|
+
|
124
|
+
|
125
|
+
# Similar to shrine create_derivatives, but with kithe standards:
|
126
|
+
#
|
127
|
+
# * Will call the :kithe_derivatives processor (that handles any define_derivative definitions),
|
128
|
+
# plus any processors you've configured with kithe_include_derivatives_processors
|
129
|
+
#
|
130
|
+
# * Uses the methods added by :kithe_persisted_derivatives to add derivatives completely
|
131
|
+
# concurrency-safely, if the model had it's attachment changed concurrently, you
|
132
|
+
# won't get derivatives attached that belong to old version of original attachment,
|
133
|
+
# and won't get any leftover "orphaned" derivatives either.
|
134
|
+
#
|
135
|
+
# The :kithe_derivatives processor has additional logic and options for determining
|
136
|
+
# *which* derivative definitions -- created with `define_deriative` will be executed:
|
137
|
+
#
|
138
|
+
# * Ordinarily will create a definition for every definition that has not been marked
|
139
|
+
# `default_create: false`.
|
140
|
+
#
|
141
|
+
# * But you can also pass `only` and/or `except` to customize the list of definitions to be created,
|
142
|
+
# possibly including some that are `default_create: false`.
|
143
|
+
#
|
144
|
+
# * Will normally re-create derivatives (per existing definitions) even if they already exist,
|
145
|
+
# but pass `lazy: false` to skip creating if a derivative with a given key already exists.
|
146
|
+
# This will use the asset `derivatives` association, so if you are doing this in bulk for several
|
147
|
+
# assets, you should eager-load the derivatives association for efficiency.
|
148
|
+
#
|
149
|
+
# If you've added any custom processors with `kithe_include_derivatives_processors`, it's
|
150
|
+
# your responsibility to make them respect those options. See #process_kithe_derivative?
|
151
|
+
# helper method.
|
152
|
+
#
|
153
|
+
# create_derivatives should be idempotent. If it has failed having only created some derivatives,
|
154
|
+
# you can always just run it again.
|
155
|
+
#
|
156
|
+
def kithe_create_derivatives(only: nil, except: nil, lazy: false)
|
157
|
+
return false unless file
|
158
|
+
|
159
|
+
local_files = self.process_derivatives(:kithe_derivatives, only: only, except: except, lazy: lazy)
|
160
|
+
|
161
|
+
# include any other configured processors
|
162
|
+
self.kithe_include_derivatives_processors.each do |processor|
|
163
|
+
local_files.merge!(
|
164
|
+
self.process_derivatives(processor.to_sym, only: only, except: except, lazy: lazy)
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
self.add_persisted_derivatives(local_files)
|
169
|
+
end
|
170
|
+
|
171
|
+
# a helper method that you can use in your own shrine processors to
|
172
|
+
# handle only/except/lazy guarding logic.
|
173
|
+
#
|
174
|
+
# @return [Boolean] should the `key` be processed based on only/except/lazy conditions?
|
175
|
+
#
|
176
|
+
# @param key [Symbol] derivative key to check for guarded processing
|
177
|
+
# @param only [Array<Symbol>] If present, method will only return true if `key` is included in `only`
|
178
|
+
# @param except [Array<Symbol] If present, method will only return true if `key` is NOT included in `except`
|
179
|
+
# @param lazy [Boolean] If true, method will only return true if derivative key is not already present
|
180
|
+
# in attacher with a value.
|
181
|
+
#
|
182
|
+
def process_kithe_derivative?(key, **options)
|
183
|
+
key = key.to_sym
|
184
|
+
only = options[:only] && Array(options[:only]).map(&:to_sym)
|
185
|
+
except = options[:except] && Array(options[:except]).map(&:to_sym)
|
186
|
+
lazy = !! options[:lazy]
|
187
|
+
|
188
|
+
(only.nil? ? true : only.include?(key)) &&
|
189
|
+
(except.nil? || ! except.include?(key)) &&
|
190
|
+
(!lazy || !derivatives.keys.include?(key))
|
191
|
+
end
|
192
|
+
|
193
|
+
# Convenience to check #process_kithe_derivative? for multiple keys at once,
|
194
|
+
# @return true if any key returns true
|
195
|
+
#
|
196
|
+
# @example process_any_kithe_derivative?([:thumb_mini, :thumb_large], only: [:thumb_large, :thumb_mega], lazy: true)
|
197
|
+
def process_any_kithe_derivative?(keys, **options)
|
198
|
+
keys.any? { |k| process_kithe_derivative?(k, **options) }
|
199
|
+
end
|
200
|
+
end
|
104
201
|
end
|
105
202
|
register_plugin(:kithe_derivative_definitions, KitheDerivativeDefinitions)
|
106
203
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kithe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Rochkind
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -264,6 +264,20 @@ dependencies:
|
|
264
264
|
- - ">="
|
265
265
|
- !ruby/object:Gem::Version
|
266
266
|
version: '0'
|
267
|
+
- !ruby/object:Gem::Dependency
|
268
|
+
name: db-query-matchers
|
269
|
+
requirement: !ruby/object:Gem::Requirement
|
270
|
+
requirements:
|
271
|
+
- - "<"
|
272
|
+
- !ruby/object:Gem::Version
|
273
|
+
version: '1'
|
274
|
+
type: :development
|
275
|
+
prerelease: false
|
276
|
+
version_requirements: !ruby/object:Gem::Requirement
|
277
|
+
requirements:
|
278
|
+
- - "<"
|
279
|
+
- !ruby/object:Gem::Version
|
280
|
+
version: '1'
|
267
281
|
- !ruby/object:Gem::Dependency
|
268
282
|
name: pg
|
269
283
|
requirement: !ruby/object:Gem::Requirement
|
@@ -331,6 +345,8 @@ files:
|
|
331
345
|
- README.md
|
332
346
|
- Rakefile
|
333
347
|
- app/assets/config/kithe_manifest.js
|
348
|
+
- app/characterization/kithe/ffprobe_characterization.rb
|
349
|
+
- app/derivative_transformers/kithe/ffmpeg_extract_jpg.rb
|
334
350
|
- app/derivative_transformers/kithe/ffmpeg_transformer.rb
|
335
351
|
- app/derivative_transformers/kithe/vips_cli_image_to_jpeg.rb
|
336
352
|
- app/helpers/kithe/form_helper.rb
|
@@ -410,7 +426,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
410
426
|
- !ruby/object:Gem::Version
|
411
427
|
version: '0'
|
412
428
|
requirements: []
|
413
|
-
rubygems_version: 3.
|
429
|
+
rubygems_version: 3.2.32
|
414
430
|
signing_key:
|
415
431
|
specification_version: 4
|
416
432
|
summary: Shareable tools/components for building a digital collections app in Rails.
|