kithe 2.2.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.
|