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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df697bf6cd0128be5ee217fb3eb191af55994b1284e1e05f75da73667e92d8f3
4
- data.tar.gz: f388992232e60a99f7a6f7a2743e47a1690152665b9c85adbdf34457ce822104
3
+ metadata.gz: 4a51da44ba82df89dd84f14a9559de8032d2e026ee666d9dba4e9fb41d05b1b0
4
+ data.tar.gz: feb995557174e9ef7c2fb6689bb352ec2dd227dc4e98a282d1b639e1d8d11393
5
5
  SHA512:
6
- metadata.gz: '079a440ca120cc56e4a624b335cab990b2dc0f8a7df73821f251ecae4bf17f91000e9f57bc59c01d6a378577c871d1bc5d8a31ccee3a49a8402459edb532c7c0'
7
- data.tar.gz: 73ba63882d5df383011c354c648b82f422891018679c8397a13143a937f9f9a12142eac18ea22d3162bb0f3f127b7ba9234a7309a18bd6329013c598ad17737d
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 = !!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" => 100)
85
+ Kithe.indexable_settings.writer_instance!("solr_writer.batch_size" => batch_size)
85
86
  end
86
87
  end
87
88
  end
@@ -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
- # A convenience to call file_attacher.create_persisted_derivatives (from :kithe_derivatives)
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
- # Will normally re-create derivatives (per existing definitions) even if they already exist,
63
- # but pass `lazy: false` to skip creating if a derivative with a given key already exists.
64
- # This will use the asset `derivatives` association, so if you are doing this in bulk for several
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
- # ## Avoiding eager-download
52
+ # This is safe for concurrent updates to the underlying model, the derivatives will
53
+ # be consistent and not left orphaned.
68
54
  #
69
- # Additionally, this has a feature to 'trick' shrine into not eager downloading
70
- # the original before our kithe_derivatives processor has a chance to decide if it
71
- # needs to create any derivatives (based on `lazy` arg), making no-op
72
- # create_derivatives so much faster and more efficient, which matters when doing
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
- source = file
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
- return unless self.file_attacher.kithe_derivative_definitions.present? # no need to schedule if we don't have any
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
@@ -1,3 +1,3 @@
1
1
  module Kithe
2
- VERSION = '2.2.0'
2
+ VERSION = '2.5.0'
3
3
  end
@@ -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 a custom Asset
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 Asset < Kithe::Asset
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.2.0
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: 2021-11-15 00:00:00.000000000 Z
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.1.6
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.