Class: LatoCms::Media
- Inherits:
-
ApplicationRecord
- Object
- ApplicationRecord
- LatoCms::Media
- Includes:
- LatoSpaces::Associable, LatoSpaces::AssociableRequired, LatoSpaces::AssociableUnique
- Defined in:
- app/models/lato_cms/media.rb
Constant Summary collapse
- MEDIA_TYPES =
%w[image video document file].freeze
- ALT_TEXT_ACCESSOR =
alt_text is stored as a single JSON-serialized => text hash in the
alt_textcolumn (kept as one column rather than a translations table since it's the only translatable attribute Media has).alt_textitself is overridden below to read/write the current I18n.locale's entry;alt_text_en,alt_text_it, etc. (one per configured locale) are handled dynamically so forms can render/submit them like any other attribute without predefining a method per locale. /\Aalt_text_(?<locale>[a-z]{2}(?:_[A-Z]{2})?)(?<setter>=)?\z/
Instance Attribute Summary collapse
-
#actions ⇒ Object
Returns the value of attribute actions.
Class Method Summary collapse
Instance Method Summary collapse
- #alt_text(locale = I18n.locale) ⇒ Object
- #alt_text=(value) ⇒ Object
- #alt_text_translations ⇒ Object
- #alt_text_translations=(hash) ⇒ Object
- #as_json(_options = {}) ⇒ Object
- #filename ⇒ Object
-
#generate_alt_text!(raise_on_error: false) ⇒ Object
Asks the configured OpenAI-compatible LLM for alt text in every configured locale and merges it in (existing translations for locales the LLM didn't return, or that it's re-run for, are kept/replaced individually).
-
#generate_video_poster! ⇒ Object
Best effort: generates a poster from the video via Active Storage previews (ffmpeg).
- #image? ⇒ Boolean
- #method_missing(name, *args) ⇒ Object
- #poster_url ⇒ Object
- #respond_to_missing?(name, include_private = false) ⇒ Boolean
-
#thumbnail_url ⇒ Object
Fixed small variant used across admin UI (Media index, picker grid, field preview) regardless of a field's own
settings.sizes(used only by the public API, seevariant_urls). - #url ⇒ Object
- #usage_count ⇒ Object
-
#variant_urls(sizes_config) ⇒ Object
Builds a map of { size_name => variant_url } from a field's
settings.sizesconfig. - #video? ⇒ Boolean
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args) ⇒ Object
94 95 96 97 98 99 100 101 102 103 |
# File 'app/models/lato_cms/media.rb', line 94 def method_missing(name, *args) match = ALT_TEXT_ACCESSOR.match(name.to_s) return super unless match if match[:setter] self.alt_text_translations = alt_text_translations.merge(match[:locale] => args.first) else alt_text_translations[match[:locale]].presence end end |
Instance Attribute Details
#actions ⇒ Object
Returns the value of attribute actions.
5 6 7 |
# File 'app/models/lato_cms/media.rb', line 5 def actions @actions end |
Class Method Details
.infer_media_type(content_type) ⇒ Object
39 40 41 42 43 44 45 46 |
# File 'app/models/lato_cms/media.rb', line 39 def self.infer_media_type(content_type) content_type = content_type.to_s return 'image' if content_type.start_with?('image/') return 'video' if content_type.start_with?('video/') return 'document' if content_type == 'application/pdf' || content_type.start_with?('application/vnd') || content_type.start_with?('text/') 'file' end |
.variant_transformation(opts) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 |
# File 'app/models/lato_cms/media.rb', line 48 def self.variant_transformation(opts) opts = {} unless opts.respond_to?(:[]) dimensions = [opts['width'] || opts[:width], opts['height'] || opts[:height]] return nil if dimensions.compact.empty? case (opts['resize'] || opts[:resize] || 'limit').to_s when 'fill' then { resize_to_fill: dimensions } when 'fit' then { resize_to_fit: dimensions } else { resize_to_limit: dimensions } end end |
Instance Method Details
#alt_text(locale = I18n.locale) ⇒ Object
76 77 78 |
# File 'app/models/lato_cms/media.rb', line 76 def alt_text(locale = I18n.locale) alt_text_translations[locale.to_s].presence end |
#alt_text=(value) ⇒ Object
80 81 82 |
# File 'app/models/lato_cms/media.rb', line 80 def alt_text=(value) self.alt_text_translations = alt_text_translations.merge(I18n.locale.to_s => value) end |
#alt_text_translations ⇒ Object
84 85 86 87 88 |
# File 'app/models/lato_cms/media.rb', line 84 def alt_text_translations JSON.parse(self[:alt_text].presence || '{}') rescue JSON::ParserError {} end |
#alt_text_translations=(hash) ⇒ Object
90 91 92 |
# File 'app/models/lato_cms/media.rb', line 90 def alt_text_translations=(hash) self[:alt_text] = hash.stringify_keys.to_json end |
#as_json(_options = {}) ⇒ Object
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'app/models/lato_cms/media.rb', line 194 def as_json( = {}) { id: id, name: name, alt_text: alt_text, alt_text_translations: alt_text_translations, media_type: media_type, filename: filename, content_type: file.attached? ? file.content_type : nil, byte_size: file.attached? ? file.byte_size : nil, url: url, thumbnail_url: thumbnail_url, poster_url: poster_url, usage_count: usage_count, created_at: created_at, updated_at: updated_at } end |
#filename ⇒ Object
72 73 74 |
# File 'app/models/lato_cms/media.rb', line 72 def filename file.filename.to_s if file.attached? end |
#generate_alt_text!(raise_on_error: false) ⇒ Object
Asks the configured OpenAI-compatible LLM for alt text in every configured locale and merges it in (existing translations for locales the LLM didn't return, or that it's re-run for, are kept/replaced individually). No-op unless an LLM is configured (see LatoCms::Config#llm_configured?) and this media is an image.
Best effort by default (raise_on_error: false): any failure is logged
and swallowed, since the automatic post-upload call site (see
GenerateAltTextJob) must never break the upload over a flaky LLM. The
manual "Regenerate with AI" action runs as a Lato::Operation the admin
is actively watching, so it passes raise_on_error: true to have
failures surface there instead of disappearing silently.
148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'app/models/lato_cms/media.rb', line 148 def generate_alt_text!(raise_on_error: false) return unless image? && file.attached? && LatoCms.config.llm_configured? translations = fetch_alt_text_translations raise 'The LLM returned no usable alt text' if translations.blank? self.alt_text_translations = alt_text_translations.merge(translations) save! rescue StandardError => e Rails.logger.warn("LatoCms: failed to generate alt text for media #{id}: #{e.}") raise if raise_on_error end |
#generate_video_poster! ⇒ Object
Best effort: generates a poster from the video via Active Storage previews (ffmpeg). Any failure is logged, the video keeps working without a poster.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'app/models/lato_cms/media.rb', line 119 def generate_video_poster! return unless video? && file.attached? return if poster_file.attached? unless file.previewable? Rails.logger.warn("LatoCms: video preview unavailable (ffmpeg missing?) for media #{id}, skipping poster generation") return end preview = file.preview(resize_to_limit: [1280, 720]).processed preview.image.blob.open do |f| poster_file.attach(io: f, filename: "#{file.filename.base}_poster.jpg", content_type: preview.image.blob.content_type) end rescue StandardError => e Rails.logger.warn("LatoCms: failed to generate video poster for media #{id}: #{e.}") end |
#image? ⇒ Boolean
60 61 62 |
# File 'app/models/lato_cms/media.rb', line 60 def image? media_type == 'image' end |
#poster_url ⇒ Object
113 114 115 |
# File 'app/models/lato_cms/media.rb', line 113 def poster_url Rails.application.routes.url_helpers.rails_blob_path(poster_file, only_path: true) if poster_file.attached? end |
#respond_to_missing?(name, include_private = false) ⇒ Boolean
105 106 107 |
# File 'app/models/lato_cms/media.rb', line 105 def respond_to_missing?(name, include_private = false) ALT_TEXT_ACCESSOR.match?(name.to_s) || super end |
#thumbnail_url ⇒ Object
Fixed small variant used across admin UI (Media index, picker grid, field
preview) regardless of a field's own settings.sizes (used only by the
public API, see variant_urls).
164 165 166 167 168 169 170 171 172 173 |
# File 'app/models/lato_cms/media.rb', line 164 def thumbnail_url return nil unless image? && file.attached? && file.variable? Rails.application.routes.url_helpers.rails_representation_path( file.variant(resize_to_fill: [200, 200]), only_path: true ) rescue StandardError => e Rails.logger.error("LatoCms: Failed to build thumbnail for media #{id}: #{e.}") nil end |
#url ⇒ Object
109 110 111 |
# File 'app/models/lato_cms/media.rb', line 109 def url Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true) if file.attached? end |
#usage_count ⇒ Object
68 69 70 |
# File 'app/models/lato_cms/media.rb', line 68 def usage_count page_field_media.count end |
#variant_urls(sizes_config) ⇒ Object
Builds a map of { size_name => variant_url } from a field's settings.sizes
config. The config is field-owned (different fields can request different
crops of the same reused Media); the mechanics live here since Media owns
the attached blob.
179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'app/models/lato_cms/media.rb', line 179 def variant_urls(sizes_config) return {} if sizes_config.blank? || !sizes_config.respond_to?(:each_pair) || !file.attached? || !file.variable? url_helpers = Rails.application.routes.url_helpers sizes_config.each_with_object({}) do |(name, opts), acc| transformation = self.class.variant_transformation(opts) next if transformation.blank? acc[name] = url_helpers.rails_representation_path(file.variant(transformation), only_path: true) end rescue StandardError => e Rails.logger.error("LatoCms: Failed to build image variants for media #{id}: #{e.}") {} end |
#video? ⇒ Boolean
64 65 66 |
# File 'app/models/lato_cms/media.rb', line 64 def video? media_type == 'video' end |