Skip to content

Commit

Permalink
Iiif endpoint (#2781)
Browse files Browse the repository at this point in the history
* Adds a catalog endpoint for IIIF Presentation manifests

To use, add '.iiif' to a catalog URL to get the IIIF presentation manifest.

Also:
* Updates Dockerfile to multi-stage build for 'development' and 'production'.
* Move RedirectMiddleware to production environment only.
* Use relative path for log location instaed of hard coded path.
* Removes erroneously committed sprockets manifest.
* Updates gitignore to include .vscode.

Closes #2752.

* Rearranges titles be arrays of internationalized titles rather than internationaled arrays of titles.

Also adds IIIF JSON schems and json-schema gem to do validation soon.
  • Loading branch information
afred authored Jul 24, 2024
1 parent fb25428 commit 80ae15e
Show file tree
Hide file tree
Showing 14 changed files with 1,334 additions and 21 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ jetty
# Coverage
/coverage



# ignore link check report
/spec/support/link-check-report.txt

Expand All @@ -42,6 +40,11 @@ jetty
*.csv
/*.xml
/*.pbcore

# Local Gemfiles
Gemfile.me
Gemfile.lock.me

# Local VSCode settings
.vscode

57 changes: 49 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,66 @@ WORKDIR /usr/src/app

RUN /bin/echo -e "deb http://archive.debian.org/debian stretch main\ndeb http://archive.debian.org/debian-security stretch/updates main\n" > /etc/apt/sources.list

# Install non-ruby dependencies
RUN apt update && apt install -y nodejs curl libcurl3 libcurl3-openssl-dev openjdk-8-jdk

COPY Gemfile Gemfile.lock ./
# Copy source code to container
COPY . .

RUN bundle install

############################
# Development Build Stage
############################
FROM base as development

# Set the RAILS_ENV to production. This affects several things in Rails.
ENV RAILS_ENV=development

# Update the bundle from Gemfile to pull in any newer versions not committed to
# Gemfile.lock yet.
RUN bundle update

# Install fresh jetty instance
RUN bundle exec rake jetty:clean

EXPOSE 3000

CMD bundle exec rake jetty:clean && bundle exec rake jetty:config && bundle exec rake jetty:start && bundle exec bundle exec rake db:migrate RAILS_ENV=development && bundle exec rails s -b 0.0.0.0
# Run several commmands to start the development server:
# 1. bundle exec rake jetty:config
# Copies jetty configuration from config/jetty.yml to jetty instance,
# which is installed in the 'base' build stage.
# 2. bundle exec rake jetty:start
# Starts jetty server
# 3. bundle exec rake db:migrate
# Runs databae migrations, if any need to be run.
# 4. bundle exec rails s -b 0.0.0.0
# Starts the Rails server.
CMD bundle exec rake jetty:config \
bundle exec rake jetty:start && \
bundle exec rake db:migrate && \
bundle exec rails s -b 0.0.0.0


############################
# Production Build Stage
############################
FROM base as production

# Set the RAILS_ENV to production. This affects several things in Rails.
ENV RAILS_ENV=production

# TODO: is this needed?
RUN apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*

# Update the bundle from Gemfile.lock (don't update the Bundle in production)
RUN bundle install

COPY . .

RUN bundle exec rake jetty:clean && bundle exec rake jetty:config

CMD bundle exec rake jetty:start && bundle exec rake db:migrate RAILS_ENV=development && bundle exec rails s -b 0.0.0.0
# Run commands atomically to start production AAPB web application:
# 1. bundle exec rake jetty:start
# Starts the jetty server
# 2. bundle exec rails s -b 0.0.0.0
# Starts the Rails server.
CMD bundle exec rake jetty:start && \
bundle exec rails s -b 0.0.0.0
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,4 @@ gem 'bigdecimal', '1.4.4'
gem 'httparty'

gem 'pbcore', '~> 0.2.0'
gem 'json-schema', '~> 2.8.0'
5 changes: 4 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ GEM
jquery-ui-rails (6.0.1)
railties (>= 3.2.16)
json (1.8.6)
json-schema (2.8.1)
addressable (>= 2.4)
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
Expand Down Expand Up @@ -371,6 +373,7 @@ DEPENDENCIES
jettywrapper
jquery-rails
jquery-ui-rails
json-schema (~> 2.8.0)
launchy
libv8 (~> 3.16.14.13)
maxminddb
Expand Down Expand Up @@ -401,4 +404,4 @@ RUBY VERSION
ruby 2.4.4p296

BUNDLED WITH
1.17.3
1.17.3
3 changes: 3 additions & 0 deletions app/controllers/catalog_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ def show
format.mods do
render text: PBCorePresenter.new(xml).to_mods
end
format.iiif do
render json: PBCorePresenter.new(xml).iiif_manifest
end
end
end

Expand Down
86 changes: 86 additions & 0 deletions app/models/iiif_manifest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module IIIFManifest
def iiif_manifest
{
"@context" => "http://iiif.io/api/presentation/3/context.json",
"id" => "#{aapb_host}/#{id}.iiif",
"type" => "Manifest",
"label" => i18n_titles,
"metadata" => i18n_metadata,
"homepage" => [{
"id" => "#{aapb_host}/catalog/#{id}",
"type" => "Text",
"label" => i18n_titles,
"format" => "text/html" }],
"summary" => i18n_descriptions,
"items" => [
{
"id" => "#{aapb_host}/iiif/#{id}/canvas",
"type" => "Canvas",
"duration" => duration_seconds,
# height and witdth would be required for video content
"items" => [
{
"id" => "#{aapb_host}/iiif/#{id}/annotationpage/1",
"type" => "AnnotationPage",
"items" => [
{
"id" => "#{aapb_host}/iiif/#{id}/annotation/1",
"type" => "Annotation",
"motivation" => "painting",
"body" => {
"id" => location, # TODO: URI for media file, player will consume this and expect bits. Redirects ok.
"type" => media_type, # TODO: map to "Sound" or "Video"
"format" => media_format,
"duration" => duration_seconds # TODO: just ensure it's in seconds
},
"target" => "#{aapb_host}/iiif/canvas/1" # IMPORTANT: this has to be the ame as the 'id' property of the parent canvas
}
]
}
]
}
]
}.to_json
end

def i18n_titles
titles.map { |title| { "en" => [title] } }
end

def i18n_descriptions
descriptions.map { |description| { "en" => [description] } }
end

def i18n_metadata
metadata.map do |key, value|
{
"label" => {
"en" => [key]
},
"value" => {
"en" => [value]
}
}
end
end

def metadata
{ 'id' => id }
end

def duration_seconds
duration.to_s.split(":").map(&:to_f).inject(0) { |a, e| a * 60 + e }.round(3)
end

def location
URI.join(aapb_host, 'media', id).to_s
end

def aapb_host
'https://americanarchive.org'
end

def media_format
digital_instantiations.map(&:format).compact.first
end
end
16 changes: 9 additions & 7 deletions app/models/pb_core_instantiation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def colors
end

def format
@format ||= read_format
@format ||= (digital || physical)
end

def annotations
Expand All @@ -71,6 +71,14 @@ def display_text_fields
@display_text ||= { identifier: identifier_display, format: format, generation: generations, color: colors, duration: duration }.compact
end

def digital
optional('instantiationDigital')
end

def physical
optional('instantiationPhysical')
end

private

def optional(xpath)
Expand All @@ -91,10 +99,4 @@ def optional_element_attribute(xpath, attribute)
match = REXML::XPath.match(@rexml, xpath).first.attributes[attribute.to_s]
match ? match : nil
end

def read_format
return optional('instantiationDigital') unless optional('instantiationDigital').nil?
return optional('instantiationPhysical') unless optional('instantiationPhysical').nil?
nil
end
end
13 changes: 12 additions & 1 deletion app/models/pb_core_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require_relative '../../lib/html_scrubber'
require_relative 'xml_backed'
require_relative 'to_mods'
require_relative 'iiif_manifest'
require_relative 'pb_core_instantiation'
require_relative 'pb_core_name_role_affiliation'
require_relative 'organization'
Expand All @@ -25,6 +26,7 @@ class PBCorePresenter
# rubocop:disable Style/EmptyLineBetweenDefs
include XmlBacked
include ToMods
include IIIFManifest
include ApplicationHelper
include IdHelper

Expand Down Expand Up @@ -92,6 +94,15 @@ def instantiations
PBCoreInstantiation.new(rexml)
end
end

def digital_instantiations
instantiations.select(&:digital)
end

def physical_instantiations
instantiations.select(&:physical)
end

def instantiations_display
@instantiations_display ||= instantiations.reject { |ins| ins.organization == 'American Archive of Public Broadcasting' }
end
Expand Down Expand Up @@ -555,7 +566,7 @@ def text
:text, :to_solr, :contribs, :img_src, :media_srcs,
:captions_src, :transcript_src, :rights_code,
:access_level, :access_types, :title, :ci_ids, :display_ids,
:instantiations, :outside_urls,
:instantiations, :digital_instantiations, :physical_instantiations, :digital, :physical, :outside_urls,
:reference_urls, :exhibits, :top_exhibits, :special_collections, :access_level_description,
:img_height, :img_width, :player_aspect_ratio, :seconds,
:player_specs, :transcript_status, :transcript_content, :constructed_transcript_src, :verify_transcript_src,
Expand Down
1 change: 0 additions & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class Application < Rails::Application

config.autoload_paths << Rails.root.join('lib')
config.autoload_paths << Rails.root.join('lib', 'middleware')
config.middleware.use('RedirectMiddleware')
config.middleware.insert_before(0, 'Rack::Cors') do
allow do
origins '*'
Expand Down
4 changes: 3 additions & 1 deletion config/environments/production.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
# config.log_tags = [ :subdomain, :uuid ]

# Only keep up to 10 log files of ~ 1MB each.
config.logger = ActiveSupport::Logger.new('/var/www/aapb/current/log/production.log', 10, 1.megabytes)
config.logger = ActiveSupport::Logger.new(File.expand_path('../../../log/production.log', __FILE__), 10, 1.megabytes)

# Use a different cache store in production.
# config.cache_store = :mem_cache_store
Expand Down Expand Up @@ -82,4 +82,6 @@

# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false

config.middleware.use('RedirectMiddleware')
end
1 change: 1 addition & 0 deletions config/initializers/mime_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
Mime::Type.register 'text/plain', :txt
Mime::Type.register 'text/plain', :srt
Mime::Type.register 'text/vtt', :vtt
Mime::Type.register 'application/json', :iiif
Loading

0 comments on commit 80ae15e

Please sign in to comment.