kithe 2.3.0 → 2.6.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 +106 -0
- data/app/indexing/kithe/indexable/record_index_updater.rb +3 -0
- data/app/models/kithe/asset.rb +17 -39
- data/app/models/kithe/model.rb +11 -4
- data/app/models/kithe/parameters.rb +17 -4
- data/lib/kithe/indexable_settings.rb +8 -9
- data/lib/kithe/version.rb +1 -1
- data/lib/shrine/plugins/kithe_derivative_definitions.rb +104 -7
- data/lib/shrine/plugins/kithe_persisted_derivatives.rb +4 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0ca2d6002aec0accb7264e06f3abcb71f33cf5673fa3ef27bfa035cbca67195
|
4
|
+
data.tar.gz: 9c5ffce36b1ff7a318059babe307627e23ccc00f6997f22a1fdc0d547cfd8a0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42129dea71f6944d2a70ca69cde8c5f1802c8915ce32cf1b237dbc6223edfcd9cefe776255cb2321c1978da8906a322e88ca443240915b6b2ee1eee53063cd42
|
7
|
+
data.tar.gz: 44478b610e5eb8abda0b385932fb8528c3f6b962f77be9168391d940dac26b739c4c9ec81f1b0912123f178c19b2e2837b1cc6e68483b7c8c59d67e40f1bb8dc
|
@@ -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,106 @@
|
|
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,false,nil] 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
|
+
#
|
24
|
+
# NOTE: This can consume significant RAM depending on value and video resolution.
|
25
|
+
#
|
26
|
+
# [Default false, not operative]
|
27
|
+
#
|
28
|
+
# @width_pixels [Integer] output thumb at this width. aspect ratio will be
|
29
|
+
# maintained. Warning, if it's larger than video original, ffmpeg will
|
30
|
+
# upscale! If set to nil, thumb will be output at original video
|
31
|
+
# resolution. [Default nil]
|
32
|
+
def initialize(start_seconds: 0, frame_sample_size: false, width_pixels: nil)
|
33
|
+
@start_seconds = start_seconds
|
34
|
+
@frame_sample_size = frame_sample_size
|
35
|
+
@width_pixels = width_pixels
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
# @param input_arg [String,File,Shrine::UploadedFile] local File; String that
|
41
|
+
# can be URL or local file path; or Shrine::UploadedFile. If Shrine::UploadedFile,
|
42
|
+
# we'll try to get a URL from it if we can, otherwise use or make a local tempfile.
|
43
|
+
# Most efficient is if we have a remote URL to give ffmpeg, one way or another!
|
44
|
+
#
|
45
|
+
# @returns [Tempfile] jpg extracted from movie
|
46
|
+
def call(input_arg)
|
47
|
+
if input_arg.kind_of?(Shrine::UploadedFile)
|
48
|
+
if input_arg.respond_to?(:url) && input_arg.url&.start_with?(/https?\:/)
|
49
|
+
_call(input_arg.url)
|
50
|
+
else
|
51
|
+
Shrine.with_file(input_arg) do |local_file|
|
52
|
+
_call(local_file.path)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
elsif input_arg.respond_to?(:path)
|
56
|
+
_call(input_arg.path)
|
57
|
+
else
|
58
|
+
_call(input_arg.to_s)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Internal implementation, after input has already been normalized to an
|
65
|
+
# string that can be an ffmpeg arg.
|
66
|
+
#
|
67
|
+
# @param ffmpeg_source_arg [String] filepath or URL. ffmpeg can take urls, which
|
68
|
+
# can be very efficient.
|
69
|
+
#
|
70
|
+
# @returns Tempfile pointing to a thumbnail
|
71
|
+
def _call(ffmpeg_source_arg)
|
72
|
+
tempfile = Tempfile.new(['temp_deriv', ".jpg"])
|
73
|
+
|
74
|
+
ffmpeg_args = produce_ffmpeg_args(input_arg: ffmpeg_source_arg, output_path: tempfile.path)
|
75
|
+
|
76
|
+
TTY::Command.new(printer: :null).run(*ffmpeg_args)
|
77
|
+
|
78
|
+
return tempfile
|
79
|
+
rescue StandardError => e
|
80
|
+
tempfile.unlink if tempfile
|
81
|
+
raise e
|
82
|
+
end
|
83
|
+
|
84
|
+
def produce_ffmpeg_args(input_arg:, output_path:)
|
85
|
+
ffmpeg_args = [ffmpeg_command, "-y"]
|
86
|
+
|
87
|
+
if start_seconds && start_seconds > 0
|
88
|
+
ffmpeg_args.concat ["-ss", start_seconds.to_i]
|
89
|
+
end
|
90
|
+
|
91
|
+
ffmpeg_args.concat ["-i", input_arg]
|
92
|
+
|
93
|
+
video_filter_parts = []
|
94
|
+
video_filter_parts << "thumbnail=#{frame_sample_size}" if (frame_sample_size || 0) > 1
|
95
|
+
video_filter_parts << "scale=#{width_pixels}:-1" if width_pixels
|
96
|
+
|
97
|
+
if video_filter_parts.present?
|
98
|
+
ffmpeg_args.concat ["-vf", video_filter_parts.join(',')]
|
99
|
+
end
|
100
|
+
|
101
|
+
ffmpeg_args.concat ["-frames:v", "1"]
|
102
|
+
|
103
|
+
ffmpeg_args << output_path
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -24,6 +24,9 @@ module Kithe
|
|
24
24
|
# it's nil, meaning we'll find the indexer to use from current thread settings,
|
25
25
|
# or global settings.
|
26
26
|
#
|
27
|
+
# (note: this design means we can't currently use traject processing_thread_pool for
|
28
|
+
# concurrency, sorry.)
|
29
|
+
#
|
27
30
|
# @param writer [Traject::Writer] Can pass i a custom Traject::Writer which the
|
28
31
|
# index representation will be sent to. By default it's nil, meaning we'll find
|
29
32
|
# the writer to use from current thread settings or global settings.
|
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?
|
data/app/models/kithe/model.rb
CHANGED
@@ -32,8 +32,11 @@ class Kithe::Model < ActiveRecord::Base
|
|
32
32
|
# this should only apply to Works, but we define it here so we can preload it
|
33
33
|
# when fetching all Kithe::Model. And it's to Kithe::Model so it can include
|
34
34
|
# both Works and Assets. We do some app-level validation to try and make it used
|
35
|
-
# as intended.
|
36
|
-
has_many :members,
|
35
|
+
# as intended. Members are by default ordered by position, then created_at.
|
36
|
+
has_many :members, -> { order(position: :asc, created_at: :asc) },
|
37
|
+
class_name: "Kithe::Model", foreign_key: :parent_id,
|
38
|
+
inverse_of: :parent, dependent: :destroy
|
39
|
+
|
37
40
|
belongs_to :parent, class_name: "Kithe::Model", inverse_of: :members, optional: true
|
38
41
|
|
39
42
|
# a self-referential many-to-many is a bit confusing, but our "contains" relation
|
@@ -130,11 +133,15 @@ class Kithe::Model < ActiveRecord::Base
|
|
130
133
|
LIMIT 1;
|
131
134
|
EOS
|
132
135
|
|
133
|
-
# trying to use a prepared statement, hoping it means performance advantage
|
136
|
+
# trying to use a prepared statement, hoping it means performance advantage,
|
137
|
+
# this is super undocumented
|
138
|
+
|
139
|
+
bind = ActiveRecord::Relation::QueryAttribute.new("m.id", self.representative_id, ActiveRecord::Type::Value.new)
|
140
|
+
|
134
141
|
result = self.class.connection.select_all(
|
135
142
|
recursive_cte,
|
136
143
|
"set_leaf_representative",
|
137
|
-
[
|
144
|
+
[bind],
|
138
145
|
preparable: true
|
139
146
|
).first.try(:dig, "id")
|
140
147
|
|
@@ -63,11 +63,24 @@
|
|
63
63
|
class Kithe::Parameters < ActionController::Parameters
|
64
64
|
attr_reader :auto_allowed_keys
|
65
65
|
|
66
|
-
|
67
|
-
|
68
|
-
|
66
|
+
|
67
|
+
# Rails 7 adds another initializer method, annoyingly
|
68
|
+
if Rails.version.split.first.to_i >= 7
|
69
|
+
def initialize(hash = {}, logging_context = {})
|
70
|
+
if hash.respond_to?(:to_unsafe_h)
|
71
|
+
hash = hash.to_unsafe_h
|
72
|
+
end
|
73
|
+
|
74
|
+
super(hash, logging_context)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
def initialize(hash = {})
|
78
|
+
if hash.respond_to?(:to_unsafe_h)
|
79
|
+
hash = hash.to_unsafe_h
|
80
|
+
end
|
81
|
+
|
82
|
+
super(hash)
|
69
83
|
end
|
70
|
-
super(hash)
|
71
84
|
end
|
72
85
|
|
73
86
|
def permit_attr_json(klass, except:nil)
|
@@ -6,22 +6,21 @@ module Kithe
|
|
6
6
|
def initialize(solr_url:, writer_class_name:, writer_settings:,
|
7
7
|
model_name_solr_field:, solr_id_value_attribute:, disable_callbacks: false,
|
8
8
|
batching_mode_batch_size: 100)
|
9
|
-
@solr_url = solr_url
|
10
9
|
@writer_class_name = writer_class_name
|
11
10
|
@writer_settings = writer_settings
|
12
11
|
@model_name_solr_field = model_name_solr_field
|
13
12
|
@solr_id_value_attribute = solr_id_value_attribute || 'id'
|
14
13
|
@batching_mode_batch_size = batching_mode_batch_size
|
14
|
+
|
15
|
+
# use our local setter to set solr_url also in writer_settings
|
16
|
+
solr_url = solr_url
|
15
17
|
end
|
16
18
|
|
17
|
-
|
18
|
-
# writer_settings
|
19
|
-
def
|
20
|
-
|
21
|
-
|
22
|
-
else
|
23
|
-
@writer_settings
|
24
|
-
end
|
19
|
+
|
20
|
+
# set solr_url also in writer_settings, cause it's expected there.
|
21
|
+
def solr_url=(v)
|
22
|
+
@solr_url = v
|
23
|
+
writer_settings["solr.url"] = v if writer_settings
|
25
24
|
end
|
26
25
|
|
27
26
|
# Turn writer_class_name into an actual Class object.
|
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
|
@@ -134,6 +134,10 @@ class Shrine
|
|
134
134
|
def remove_persisted_derivatives(*paths, **options)
|
135
135
|
return if paths.empty?
|
136
136
|
|
137
|
+
# Shrine does weird things if we pass in Strings, let's save ourselves
|
138
|
+
# the terrible debugging on that mistake, and noramlize to symbols
|
139
|
+
paths = paths.collect(&:to_sym)
|
140
|
+
|
137
141
|
other_changes_allowed = !!options.delete(:allow_other_changes)
|
138
142
|
if record && !other_changes_allowed && record.changed?
|
139
143
|
raise TypeError.new("Can't safely add_persisted_derivatives on model with unsaved changes. Pass `allow_other_changes: true` to force.")
|
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.6.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-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: 5.2.1
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '7.1'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: 5.2.1
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '7.1'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: attr_json
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -345,6 +345,8 @@ files:
|
|
345
345
|
- README.md
|
346
346
|
- Rakefile
|
347
347
|
- app/assets/config/kithe_manifest.js
|
348
|
+
- app/characterization/kithe/ffprobe_characterization.rb
|
349
|
+
- app/derivative_transformers/kithe/ffmpeg_extract_jpg.rb
|
348
350
|
- app/derivative_transformers/kithe/ffmpeg_transformer.rb
|
349
351
|
- app/derivative_transformers/kithe/vips_cli_image_to_jpeg.rb
|
350
352
|
- app/helpers/kithe/form_helper.rb
|
@@ -424,7 +426,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
424
426
|
- !ruby/object:Gem::Version
|
425
427
|
version: '0'
|
426
428
|
requirements: []
|
427
|
-
rubygems_version: 3.
|
429
|
+
rubygems_version: 3.2.33
|
428
430
|
signing_key:
|
429
431
|
specification_version: 4
|
430
432
|
summary: Shareable tools/components for building a digital collections app in Rails.
|