# frozen_string_literal: true require 'json' require 'mimemagic' module Intranet module Pictures # Provides pictures data and pictures listings from a database file in JSON format. # # === Structure of the JSON database # See the example below. # # The title of the pictures gallery is indicated in +title+ # # Pictures are described individually as a hash in the +pictures+ array. The mandatory keys in # this hash are: # * +uri+ : location of the picture, relative to the JSON file # * +title+ : description of the picture # * +datetime+ : date and time of the picture, as a string in ISO8601 format # * +height+ and +width+ : size in pixels of the picture # # Additional keys may be added (for instance: event, city, region/country, ...); they will be # used to group pictures sharing the same value of a given key. # Some groups may be defined in the +groups+ hash to add a thumbnail picture and a brief text # description for them. The possible keys in this hash are: # * +id+ : unique identifier of the group, mandatory # * +brief+ : optional short text associated to the group name # * +uri+ : optional group thumbnail, relative to the JSON file # # @example Structure of the JSON database (not all mandatory are present for readability) # { # "title": "gallery title", # "groups": { # "event": [ # { "id": "Party", "brief": "...", "uri": "party.jpg", ... }, # { ... } # ], # "city": [ # { "id": "Houston", "uri": "houston.png", ... }, # { ... } # ] # }, # "pictures": [ # { "uri": "dir/pic0.jpg", "datetime": "...", "event": "Party", "city": "Houston", ... }, # { ... } # ] # } class JsonDbProvider # Initializes a new pictures data provider. # @param json_file [String] The path to the JSON database file. def initialize(json_file) @json_dir = File.dirname(json_file) # also works for URLs @json_file = json_file @json_time = Time.at(0) load_json end # Returns the pictures gallery title. # @return [String] The gallery title. # @raise KeyError If no title is defined in JSON file. def title load_json @json.fetch('title').to_s end # Returns the list of the pictures matching +selector+. # Results are returned ordered by +sort_by+ in ascending order if +asc+, and in descending # order otherwise. # @param selector [Hash] The pictures selector, interpreted as a logical AND # combination of all key/value pairs provided. # @param sort_by [String] The picture field to sort the results by, or nil if results should # be returned without particular sorting. # @param asc [Boolean] True to sort returned pictures in ascending order, False to sort them # in descending order. # @return [ArrayString, 'datetime'=>String, 'height'=>Integer, # 'width'=>Integer, ...}>] The selected pictures. # @raise KeyError If JSON file is malformed, or if +sort_by+ is not an existing picture field. def list_pictures(selector = {}, sort_by = nil, asc = true) load_json pics = select_pictures(selector).map { |p| p.except('uri') } pics.sort_by! { |p| p.fetch(sort_by) } unless sort_by.nil? pics.reverse! unless asc pics end # Returns the picture file matching +selector+. # @param selector [Hash] The picture selector, which should return exactly one # picture. # @return [String, Blob] The MIME type of the picture, and the picture file content. # @raise KeyError If JSON file is malformed, if selector does not match exactly one picture, # or if the picture +uri+ does not refer to an existing image file. def picture(selector = {}) load_json pic = select_pictures(selector) raise KeyError unless pic.size == 1 path = File.join(@json_dir, pic.first.fetch('uri')) read_image_file(path) end # Returns the thumbnail picture of the group whose type is +key+ and named +value+. # @param key [String] The group type. # @param value [String] The group name. # @return [String, Blob] The MIME type of the picture, and the picture file content. Nil may # be returned if no thumbnail is available for that group. # @raise KeyError If JSON file is malformed, if key/value do not designate exactly one group, # or if the group +uri+ does not refer to an existing image file. def group_thumbnail(key, value) load_json group = select_group(key, value) raise KeyError unless group.size == 1 return nil if group.first['uri'].nil? path = File.join(@json_dir, group.first.fetch('uri')) read_image_file(path) end # Returns the brief text of the group whose type is +key+ and named +value+. # @param key [String] The group type. # @param value [String] The group name. # @return [String] The brief text of the group, or an empty string if no brief is available. # @raise KeyError If JSON file is malformed or if key/value do not designate exactly one # group. def group_brief(key, value) load_json group = select_group(key, value) raise KeyError unless group.size == 1 return '' if group.first['brief'].nil? group.first.fetch('brief') end private # (Re)load the JSON file if modified on disk def load_json mtime = File.mtime(@json_file) return unless @json_time < File.mtime(@json_file) @json = JSON.parse(File.read(@json_file)) @json_time = mtime end def read_image_file(path) mime_type = MimeMagic.by_path(path).type raise KeyError unless mime_type.start_with?('image/') [MimeMagic.by_path(path).type, File.read(path)] rescue Errno::ENOENT raise KeyError end def picture_match?(picture, selector) selector.all? { |k, v| picture.fetch(k) == v } rescue KeyError false end def select_pictures(selector) @json.fetch('pictures').select { |p| picture_match?(p, selector) } end def select_group(type, id) @json.fetch('groups').fetch(type).select { |g| g.fetch('id') == id } end end end end