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 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.