diff --git a/Gemfile b/Gemfile index 72503fe418..a6274460e6 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ gem 'bootsnap', require: false gem 'listen' gem 'net-smtp', require: false gem 'psych', '< 4' -gem 'rails', '=7.0.4.3' +gem 'rails', '~>7.0.8' gem 'sprockets', '~>3.7.2' #gem 'sprockets-rails', require: 'sprockets/railtie' gem 'sqlite3' @@ -53,7 +53,7 @@ gem 'avalon-about', git: 'https://github.com/avalonmediasystem/avalon-about.git' #gem 'bootstrap-sass', '< 3.4.1' # Pin to less than 3.4.1 due to change in behavior with popovers gem 'bootstrap-toggle-rails' gem 'bootstrap_form' -gem 'iiif_manifest', '>= 1.4.0' +gem 'iiif_manifest', '~> 1.5' gem 'rack-cors', require: 'rack/cors' gem 'rails_same_site_cookie' gem 'recaptcha', require: 'recaptcha/rails' @@ -155,7 +155,7 @@ group :production do gem 'google-analytics-rails', '1.1.0' gem 'lograge' gem 'okcomputer' - gem 'puma', '>= 4.3.8' + gem 'puma', '>= 6.4.2' end # Install the bundle --with aws when running on Amazon Elastic Beanstalk diff --git a/Gemfile.lock b/Gemfile.lock index fc57de7f85..2871b823f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,47 +68,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.4.3) - actionpack (= 7.0.4.3) - activesupport (= 7.0.4.3) + actioncable (7.0.8.1) + actionpack (= 7.0.8.1) + activesupport (= 7.0.8.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.4.3) - actionpack (= 7.0.4.3) - activejob (= 7.0.4.3) - activerecord (= 7.0.4.3) - activestorage (= 7.0.4.3) - activesupport (= 7.0.4.3) + actionmailbox (7.0.8.1) + actionpack (= 7.0.8.1) + activejob (= 7.0.8.1) + activerecord (= 7.0.8.1) + activestorage (= 7.0.8.1) + activesupport (= 7.0.8.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.4.3) - actionpack (= 7.0.4.3) - actionview (= 7.0.4.3) - activejob (= 7.0.4.3) - activesupport (= 7.0.4.3) + actionmailer (7.0.8.1) + actionpack (= 7.0.8.1) + actionview (= 7.0.8.1) + activejob (= 7.0.8.1) + activesupport (= 7.0.8.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.4.3) - actionview (= 7.0.4.3) - activesupport (= 7.0.4.3) - rack (~> 2.0, >= 2.2.0) + actionpack (7.0.8.1) + actionview (= 7.0.8.1) + activesupport (= 7.0.8.1) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.4.3) - actionpack (= 7.0.4.3) - activerecord (= 7.0.4.3) - activestorage (= 7.0.4.3) - activesupport (= 7.0.4.3) + actiontext (7.0.8.1) + actionpack (= 7.0.8.1) + activerecord (= 7.0.8.1) + activestorage (= 7.0.8.1) + activesupport (= 7.0.8.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.4.3) - activesupport (= 7.0.4.3) + actionview (7.0.8.1) + activesupport (= 7.0.8.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -145,8 +145,8 @@ GEM om (~> 3.1) rdf (~> 3.2) rdf-rdfxml (~> 3.2) - activejob (7.0.4.3) - activesupport (= 7.0.4.3) + activejob (7.0.8.1) + activesupport (= 7.0.8.1) globalid (>= 0.3.6) activejob-traffic_control (0.1.3) activejob (>= 4.2) @@ -155,25 +155,25 @@ GEM activejob-uniqueness (0.2.5) activejob (>= 4.2, < 7.1) redlock (>= 1.2, < 2) - activemodel (7.0.4.3) - activesupport (= 7.0.4.3) - activerecord (7.0.4.3) - activemodel (= 7.0.4.3) - activesupport (= 7.0.4.3) + activemodel (7.0.8.1) + activesupport (= 7.0.8.1) + activerecord (7.0.8.1) + activemodel (= 7.0.8.1) + activesupport (= 7.0.8.1) activerecord-session_store (2.0.0) actionpack (>= 5.2.4.1) activerecord (>= 5.2.4.1) multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 3) railties (>= 5.2.4.1) - activestorage (7.0.4.3) - actionpack (= 7.0.4.3) - activejob (= 7.0.4.3) - activerecord (= 7.0.4.3) - activesupport (= 7.0.4.3) + activestorage (7.0.8.1) + actionpack (= 7.0.8.1) + activejob (= 7.0.8.1) + activerecord (= 7.0.8.1) + activesupport (= 7.0.8.1) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.4.3) + activesupport (7.0.8.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -335,7 +335,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.3) + date (3.3.4) declarative (0.0.20) deep_merge (1.2.2) deprecation (1.1.0) @@ -346,7 +346,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise_invitable (2.0.8) + devise_invitable (2.0.9) actionmailer (>= 5.0) devise (>= 4.6) diff-lcs (1.5.0) @@ -438,8 +438,8 @@ GEM fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) google-analytics-rails (1.1.0) google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) @@ -491,10 +491,10 @@ GEM hydra-access-controls (= 12.1.0) hydra-core (= 12.1.0) rails (>= 5.2, < 7.1) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) iconv (1.0.8) - iiif_manifest (1.4.0) + iiif_manifest (1.5.0) activesupport (>= 4) ims-lti (1.1.13) builder @@ -557,7 +557,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.3) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -569,42 +569,42 @@ GEM rexml scrub_rb (>= 1.0.1, < 2) unf - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) memoist (0.16.2) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) - mini_mime (1.1.2) - mini_portile2 (2.8.4) + mini_mime (1.1.5) + mini_portile2 (2.8.5) minitar (0.9) - minitest (5.21.2) + minitest (5.22.3) msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.3.0) mysql2 (0.5.5) - net-imap (0.3.4) + net-imap (0.4.10) date net-protocol net-ldap (0.18.0) net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) - net-smtp (0.3.3) + net-smtp (0.4.0.1) net-protocol net-ssh (7.0.1) netrc (0.11.0) - nio4r (2.5.8) + nio4r (2.7.1) noid (0.9.0) noid-rails (3.1.0) actionpack (>= 5.0.0, < 7.1) noid (~> 0.9) - nokogiri (1.15.3) + nokogiri (1.16.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) nom-xml (1.2.0) @@ -650,12 +650,12 @@ GEM pry (>= 0.10.4) psych (3.3.4) public_suffix (5.0.1) - puma (6.3.0) + puma (6.4.2) nio4r (~> 2.0) raabro (1.4.0) - racc (1.7.1) - rack (2.2.8) - rack-cors (2.0.1) + racc (1.7.3) + rack (2.2.9) + rack-cors (2.0.2) rack (>= 2.0.0) rack-protection (3.0.5) rack @@ -663,20 +663,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.4.3) - actioncable (= 7.0.4.3) - actionmailbox (= 7.0.4.3) - actionmailer (= 7.0.4.3) - actionpack (= 7.0.4.3) - actiontext (= 7.0.4.3) - actionview (= 7.0.4.3) - activejob (= 7.0.4.3) - activemodel (= 7.0.4.3) - activerecord (= 7.0.4.3) - activestorage (= 7.0.4.3) - activesupport (= 7.0.4.3) + rails (7.0.8.1) + actioncable (= 7.0.8.1) + actionmailbox (= 7.0.8.1) + actionmailer (= 7.0.8.1) + actionpack (= 7.0.8.1) + actiontext (= 7.0.8.1) + actionview (= 7.0.8.1) + activejob (= 7.0.8.1) + activemodel (= 7.0.8.1) + activerecord (= 7.0.8.1) + activestorage (= 7.0.8.1) + activesupport (= 7.0.8.1) bundler (>= 1.15.0) - railties (= 7.0.4.3) + railties (= 7.0.8.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -691,15 +691,15 @@ GEM rails_same_site_cookie (0.1.9) rack (>= 1.5) user_agent_parser (~> 2.6) - railties (7.0.4.3) - actionpack (= 7.0.4.3) - activesupport (= 7.0.4.3) + railties (7.0.8.1) + actionpack (= 7.0.8.1) + activesupport (= 7.0.8.1) method_source rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -858,7 +858,7 @@ GEM semantic_range (>= 2.3.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (6.5.9) + sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -918,9 +918,9 @@ GEM rdf (~> 3.2) terser (1.2.0) execjs (>= 0.3.0, < 3) - thor (1.2.2) + thor (1.3.1) tilt (2.0.11) - timeout (0.3.2) + timeout (0.4.1) trailblazer-option (0.1.2) twitter-typeahead-rails (0.11.1.pre.corejavascript) actionpack (>= 3.1) @@ -937,7 +937,7 @@ GEM unicode-display_width (2.4.2) unicode-types (1.8.0) user_agent_parser (2.14.0) - view_component (2.82.0) + view_component (2.83.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) @@ -958,7 +958,7 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) webrick (1.8.1) - websocket-driver (0.7.5) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) with_locking (1.0.2) @@ -966,7 +966,7 @@ GEM rexml xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.11) + zeitwerk (2.6.13) zk (1.10.0) zookeeper (~> 1.5.0) zookeeper (1.5.5) @@ -1037,7 +1037,7 @@ DEPENDENCIES httpx hydra-head (~> 12.0) iconv (~> 1.0.6) - iiif_manifest (>= 1.4.0) + iiif_manifest (~> 1.5) ims-lti (~> 1.1.13) jbuilder (~> 2.0) jquery-datatables @@ -1064,9 +1064,9 @@ DEPENDENCIES pry-byebug pry-rails psych (< 4) - puma (>= 4.3.8) + puma (>= 6.4.2) rack-cors - rails (= 7.0.4.3) + rails (~> 7.0.8) rails-controller-testing rails_same_site_cookie rb-readline diff --git a/app/assets/stylesheets/nestable.scss b/app/assets/stylesheets/nestable.scss index 915400514f..af88ba01ba 100644 --- a/app/assets/stylesheets/nestable.scss +++ b/app/assets/stylesheets/nestable.scss @@ -62,9 +62,9 @@ tr.dd-item { font-weight: bold; border: 1px solid #ccc; background: #fafafa; - background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); - background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); - background: linear-gradient(top, #fafafa 0%, #eee 100%); + background: -webkit-linear-gradient(to bottom, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(to bottom, #fafafa 0%, #eee 100%); + background: linear-gradient(to bottom, #fafafa 0%, #eee 100%); -webkit-border-radius: 3px; border-radius: 3px; box-sizing: border-box; -moz-box-sizing: border-box; @@ -114,9 +114,9 @@ tr.dd-item { .dd3-content { display: block; margin: 5px 0; padding: 0 0 0 30px; background: #fafafa; - background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); - background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); - background: linear-gradient(top, #fafafa 0%, #eee 100%); + background: -webkit-linear-gradient(to bottom, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(to bottom, #fafafa 0%, #eee 100%); + background: linear-gradient(to bottom, #fafafa 0%, #eee 100%); -webkit-border-radius: 3px; border-radius: 3px; box-sizing: border-box; -moz-box-sizing: border-box; @@ -135,9 +135,9 @@ tr.dd-item { white-space: nowrap; overflow: hidden; border: 1px solid #aaa; background: #ddd; - background: -webkit-linear-gradient(top, #ddd 0%, #bbb 100%); - background: -moz-linear-gradient(top, #ddd 0%, #bbb 100%); - background: linear-gradient(top, #ddd 0%, #bbb 100%); + background: -webkit-linear-gradient(to bottom, #ddd 0%, #bbb 100%); + background: -moz-linear-gradient(to bottom, #ddd 0%, #bbb 100%); + background: linear-gradient(to bottom, #ddd 0%, #bbb 100%); border-top-right-radius: 0; border-bottom-right-radius: 0; } diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 5d7cfa02c9..aa8f49d93f 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -36,7 +36,22 @@ def load_and_authorize_collections builder.user = user end response = repository.search(builder) - @collections = response.documents.collect { |doc| ::Admin::CollectionPresenter.new(doc) }.sort_by { |c| c.name.downcase } + + # Query solr for facet values for collection media object counts and pass into presenter to avoid making 2 solr queries per collection + count_query = "has_model_ssim:MediaObject" + count_response = ActiveFedora::SolrService.get(count_query, { rows: 0, facet: true, 'facet.field': "isMemberOfCollection_ssim", 'facet.limit': -1 }) + counts_array = count_response["facet_counts"]["facet_fields"]["isMemberOfCollection_ssim"] rescue [] + counts = counts_array.each_slice(2).to_h + unpublished_query = count_query + " AND workflow_published_sim:Unpublished" + unpublished_count_response = ActiveFedora::SolrService.get(unpublished_query, { rows: 0, facet: true, 'facet.field': "isMemberOfCollection_ssim", 'facet.limit': -1 }) + unpublished_counts_array = unpublished_count_response["facet_counts"]["facet_fields"]["isMemberOfCollection_ssim"] rescue [] + unpublished_counts = unpublished_counts_array.each_slice(2).to_h + + @collections = response.documents.collect do |doc| + ::Admin::CollectionPresenter.new(doc, + media_object_count: (counts[doc.id] || 0), + unpublished_media_object_count: (unpublished_counts[doc.id] || 0)) + end.sort_by { |c| c.name.downcase } end # GET /collections diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index f88a5392a6..31f2011ffe 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -21,8 +21,8 @@ class MasterFilesController < ApplicationController include NoidValidator before_action :authenticate_user!, :only => [:create] - before_action :set_masterfile_proxy, except: [:create, :oembed, :attach_structure, :attach_captions, :delete_structure, :delete_captions, :destroy, :update, :set_structure] - before_action :set_masterfile, only: [:attach_structure, :attach_captions, :delete_structure, :delete_captions, :destroy, :update, :set_structure] + before_action :set_masterfile_proxy, except: [:create, :oembed, :attach_structure, :delete_structure, :destroy, :update, :set_structure] + before_action :set_masterfile, only: [:attach_structure, :delete_structure, :destroy, :update, :set_structure] before_action :ensure_readable_filedata, :only => [:create] skip_before_action :verify_authenticity_token, only: [:set_structure, :delete_structure] @@ -280,16 +280,6 @@ def hls_manifest end end - def caption_manifest - return head :unauthorized if cannot?(:read, @master_file) - caption_id = params[:c_id] - @caption_url = if caption_id == 'master_file_caption' - captions_master_file_path - else - master_file_supplemental_file_path(master_file_id: @master_file.id, id: caption_id) - end - end - def structure authorize! :read, @master_file, message: "You do not have sufficient privileges" render json: @master_file.structuralMetadata.as_json diff --git a/app/controllers/playlists_controller.rb b/app/controllers/playlists_controller.rb index 6b1baed16c..6084394829 100644 --- a/app/controllers/playlists_controller.rb +++ b/app/controllers/playlists_controller.rb @@ -260,10 +260,11 @@ def manifest # Condense secure_streams into single call using master_files stream_info_hash = secure_stream_infos(master_files, media_objects) - canvas_presenters = @playlist.items.collect do |item| + canvas_presenters = @playlist.items.collect.with_index do |item, i| master_file = master_files.find { |mf| mf.id == item.clip.master_file_id } cannot_read_item = master_file.nil? || cannot_read_hash[master_file.media_object_id] - IiifPlaylistCanvasPresenter.new(playlist_item: item, stream_info: stream_info_hash[master_file&.id], cannot_read_item: cannot_read_item, master_file: master_file) + position = i + 1 + IiifPlaylistCanvasPresenter.new(playlist_item: item, stream_info: stream_info_hash[master_file&.id], cannot_read_item: cannot_read_item, position: position, master_file: master_file) end can_edit_playlist = can? :edit, @playlist diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index c9dbef8ccc..298289a855 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -58,12 +58,7 @@ def create end def show - # TODO: Use a master file presenter which reads from solr instead of loading the masterfile from fedora - # FIXME: authorize supplemental file directly (needs supplemental file to have reference to masterfile) - raise Avalon::NotFound, "Supplemental file: #{params[:id]} not found" unless SupplementalFile.exists? params[:id].to_s - - @supplemental_file = SupplementalFile.find(params[:id]) - raise Avalon::NotFound, "Supplemental file: #{@supplemental_file.id} not found" unless @object.supplemental_files.any? { |f| f.id == @supplemental_file.id } + find_supplemental_file # Redirect or proxy the content if Settings.supplemental_files.proxy @@ -107,6 +102,15 @@ def destroy end end + def captions + find_supplemental_file + + file_content = @supplemental_file.file.download + content = @supplemental_file.file.content_type == 'text/srt' ? SupplementalFile.convert_from_srt(file_content) : file_content + + send_data content, filename: @supplemental_file.file.filename.to_s, type: 'text/vtt', disposition: 'attachment' + end + private def set_object @@ -118,6 +122,16 @@ def supplemental_file_params params.fetch(:supplemental_file, {}).permit(:label, :language, :file, tags: []) end + def find_supplemental_file + # TODO: Use a master file presenter which reads from solr instead of loading the masterfile from fedora + # FIXME: authorize supplemental file directly (needs supplemental file to have reference to masterfile) + raise Avalon::NotFound, "Supplemental file: #{params[:id]} not found" unless SupplementalFile.exists? params[:id].to_s + + @supplemental_file = SupplementalFile.find(params[:id]) + raise Avalon::NotFound, "Supplemental file: #{@supplemental_file.id} not found" unless @object.supplemental_files.any? { |f| f.id == @supplemental_file.id } + end + + def handle_error(message:, status:) if request.format == :json render json: { errors: message }, status: status @@ -158,7 +172,7 @@ def object_supplemental_file_path end def authorize_object - action = action_name.to_sym == :show ? :show : :edit + action = [:show, :captions].include?(action_name.to_sym) ? :show : :edit authorize! action, @object, message: "You do not have sufficient privileges to #{action_name} this supplemental file" end end diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index 7ba3870a35..78fe26ebe6 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -39,8 +39,9 @@ const ExpandCollapseArrow = () => { }; const Ramp = ({ - base_url, + urls, playlist_id, + playlist_item_ids, token, share, comment_tag @@ -48,6 +49,7 @@ const Ramp = ({ const [manifestUrl, setManifestUrl] = React.useState(''); const [activeItemTitle, setActiveItemTitle] = React.useState(); const [activeItemSummary, setActiveItemSummary] = React.useState(); + const [startCanvasId, setStartCanvasId] = React.useState(); let interval; @@ -55,8 +57,17 @@ const Ramp = ({ const IS_MOBILE = (/Mobi/i).test(USER_AGENT); React.useEffect(() => { + const { base_url, fullpath_url } = urls; let url = `${base_url}/playlists/${playlist_id}/manifest.json`; if (token) url += `?token=${token}`; + + let [fullpath, position] = fullpath_url.split('?position='); + let start_canvas = playlist_item_ids[position - 1] + setStartCanvasId( + start_canvas && start_canvas != undefined + ? `${base_url}/playlists/${playlist_id}/manifest/canvas/${start_canvas}` + : undefined + ); setManifestUrl(url); interval = setInterval(addPlayerEventListeners, 500); @@ -84,7 +95,9 @@ const Ramp = ({ }; return ( - + diff --git a/app/javascript/components/Ramp.scss b/app/javascript/components/Ramp.scss index 2a072b9139..fb47b8101e 100644 --- a/app/javascript/components/Ramp.scss +++ b/app/javascript/components/Ramp.scss @@ -27,6 +27,16 @@ .video-js .vjs-big-play-button { scale: 1.5; } + + .video-js .vjs-control-bar { + font-size: 90% !important; + } + + // reduce player height to match with adjusted font-size + // for smaller screens + .video-js.vjs-audio { + min-height: 2.9em; + } } } diff --git a/app/models/concerns/iiif_supplemental_file_behavior.rb b/app/models/concerns/iiif_supplemental_file_behavior.rb index d8f355d38b..7e43a9b61d 100644 --- a/app/models/concerns/iiif_supplemental_file_behavior.rb +++ b/app/models/concerns/iiif_supplemental_file_behavior.rb @@ -36,7 +36,7 @@ def object_supplemental_file_url(object, supplemental_file) def determine_rendering_type(mime) case mime - when 'application/pdf', 'application/msword', 'application/vnd.oasis.opendocument.text', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/html', 'text/plain', 'text/vtt' + when 'application/pdf', 'application/msword', 'application/vnd.oasis.opendocument.text', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/html', 'text/plain', 'text/srt', 'text/vtt' 'Text' when /image\/.+/ 'Image' diff --git a/app/models/concerns/media_object_behavior.rb b/app/models/concerns/media_object_behavior.rb index 26d03f907f..498522fbae 100644 --- a/app/models/concerns/media_object_behavior.rb +++ b/app/models/concerns/media_object_behavior.rb @@ -19,6 +19,7 @@ def as_json(options={}) id: id, title: title, collection: collection.name, + collection_id: collection.id, unit: collection.unit, main_contributors: creator, publication_date: date_created, diff --git a/app/models/iiif_canvas_presenter.rb b/app/models/iiif_canvas_presenter.rb index fc2b6620d5..7b650248a5 100644 --- a/app/models/iiif_canvas_presenter.rb +++ b/app/models/iiif_canvas_presenter.rb @@ -209,7 +209,11 @@ def manifest_attributes(quality, media_type) def supplemental_attributes(file) if file.is_a?(SupplementalFile) label = file.tags.include?('machine_generated') ? file.label + ' (machine generated)' : file.label - format = file.file.content_type + format = if file.file.content_type == 'text/srt' && file.tags.include?('caption') + 'text/vtt' + else + file.file.content_type + end language = file.language || 'en' filename = file.file.filename.to_s else diff --git a/app/models/iiif_playlist_canvas_presenter.rb b/app/models/iiif_playlist_canvas_presenter.rb index e37cb37b93..e4bc565a49 100644 --- a/app/models/iiif_playlist_canvas_presenter.rb +++ b/app/models/iiif_playlist_canvas_presenter.rb @@ -13,13 +13,14 @@ # --- END LICENSE_HEADER BLOCK --- class IiifPlaylistCanvasPresenter - attr_reader :playlist_item, :stream_info, :cannot_read_item + attr_reader :playlist_item, :stream_info, :cannot_read_item, :position attr_accessor :media_fragment - def initialize(playlist_item:, stream_info:, cannot_read_item: false, media_fragment: nil, master_file: nil) + def initialize(playlist_item:, stream_info:, cannot_read_item: false, position: nil, media_fragment: nil, master_file: nil) @playlist_item = playlist_item @stream_info = stream_info @cannot_read_item = cannot_read_item + @position = position @media_fragment = media_fragment @master_file = master_file end @@ -97,6 +98,14 @@ def description playlist_item.comment end + def homepage + [{ + "@id" => "#{Rails.application.routes.url_helpers.playlist_url(playlist_item.playlist_id).to_s}?position=#{position}", + "type" => "Text", + "label" => "Playlist Item #{position}" + }] + end + private def playlist_source_link diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 9ffe260089..6ae184a631 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -294,9 +294,13 @@ def update_progress_on_success!(encode) #TODO pull this from the encode self.date_digitized ||= Time.now.utc.iso8601 - # Set duration after transcode if mediainfo fails to find. - # e.x. WebM files missing technical metadata - self.duration ||= encode.input.duration + # Update the duration detected by ActiveEncode + # because it has higher precision than mediainfo + # Set for the first time for files without duration + # e.g. WebM files missing technical metadata + # ActiveEncode returns duration in milliseconds which + # is stored as an integer string + self.duration = encode.input.duration.to_i.to_s if encode.input.duration.present? outputs = Array(encode.output).collect do |output| { diff --git a/app/models/playlist.rb b/app/models/playlist.rb index 6d90ee0a64..56b58c7a25 100644 --- a/app/models/playlist.rb +++ b/app/models/playlist.rb @@ -15,7 +15,11 @@ class Playlist < ActiveRecord::Base belongs_to :user scope :by_user, ->(user) { where(user_id: user.id) } - scope :title_like, ->(title_filter) { where("title LIKE ?", "%#{title_filter}%")} + scope :title_like, ->(title_filter) do + term_array = title_filter.split.map { |term| "%#{sanitize_sql_like(term).downcase}%" } + query = Array.new(term_array.size, "LOWER(title) LIKE ?").join(" AND ") + where(query, *term_array) + end scope :with_tag, ->(tag_filter) { where("tags LIKE ?", "%\n- #{tag_filter}\n%") } validates :user, presence: true diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index ec22663acf..84722d1fa5 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -28,6 +28,8 @@ def validate_file_type def attach_file(new_file) file.attach(new_file) + extension = File.extname(new_file.original_filename) + self.file.content_type = Mime::Type.lookup_by_extension(extension.slice(1..-1)).to_s if extension == '.srt' self.label = file.filename.to_s if label.blank? self.language = tags.include?('caption') ? Settings.caption_default.language : 'eng' end @@ -43,4 +45,20 @@ def caption? def machine_generated? tags.include?('machine_generated') end + + # Adapted from https://github.com/opencoconut/webvtt-ruby/blob/e07d59220260fce33ba5a0c3b355e3ae88b99457/lib/webvtt/parser.rb#L11-L30 + def self.convert_from_srt(srt) + # normalize timestamps in srt + # This Regex looks for malformed time stamp pieces such as '00:1:00,000', '0:01:00,000', etc. + # When it finds a match it prepends a 0 to the capture group so both of the above examples + # would return '00:01:00,000' + conversion = srt.gsub(/(:|^)(\d)(,|:)/, '\10\2\3') + # convert timestamps and save the file + # VTT uses '.' as its decimal separator, SRT uses ',' so we convert the punctuation + conversion.gsub!(/([0-9]{2}:[0-9]{2}:[0-9]{2})([,])([0-9]{3})/, '\1.\3') + # normalize new line character + conversion.gsub!("\r\n", "\n") + + "WEBVTT\n\n#{conversion}".strip + end end diff --git a/app/presenters/admin/collection_presenter.rb b/app/presenters/admin/collection_presenter.rb index 9bdadcd387..c4b07064bb 100644 --- a/app/presenters/admin/collection_presenter.rb +++ b/app/presenters/admin/collection_presenter.rb @@ -15,8 +15,10 @@ class Admin::CollectionPresenter attr_reader :document - def initialize(solr_doc) + def initialize(solr_doc, media_object_count: nil, unpublished_media_object_count: nil) @document = solr_doc + @media_object_count = media_object_count + @unpublished_media_object_count = unpublished_media_object_count end delegate :id, to: :document @@ -33,7 +35,6 @@ def description document["description_tesim"]&.first end - # TODO: do these counts in one large query for all collections on the index page to avoid having to do them here def media_object_count @media_object_count ||= MediaObject.where("collection_ssim" => name).count end diff --git a/app/presenters/collection_presenter.rb b/app/presenters/collection_presenter.rb index b4322315fe..f4a1032bb8 100644 --- a/app/presenters/collection_presenter.rb +++ b/app/presenters/collection_presenter.rb @@ -52,7 +52,8 @@ def contact_email end def website_link - view_context.link_to document["website_label_ssi"], document["website_url_ssi"] if document["website_url_ssi"].present? + label = document["website_label_ssi"].presence || document["website_url_ssi"] + view_context.link_to label, document["website_url_ssi"] if document["website_url_ssi"].present? end def as_json(_) diff --git a/app/views/master_files/caption_manifest.m3u8.erb b/app/views/master_files/caption_manifest.m3u8.erb deleted file mode 100644 index c2101f5e9b..0000000000 --- a/app/views/master_files/caption_manifest.m3u8.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%# -Copyright 2011-2024, The Trustees of Indiana University and Northwestern - University. Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed - under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. See the License for the - specific language governing permissions and limitations under the License. ---- END LICENSE_HEADER BLOCK --- -%> -#EXTM3U -#EXT-X-TARGETDURATION:<%= @master_file.duration %> -#EXT-X-VERSION:3 -#EXT-X-MEDIA-SEQUENCE:0 -#EXT-X-PLAYLIST-TYPE:VOD -#EXTINF:<%= @master_file.duration %> -<%= @caption_url %> -#EXT-X-ENDLIST diff --git a/app/views/master_files/hls_manifest.m3u8.erb b/app/views/master_files/hls_manifest.m3u8.erb index 5a6d528854..7b2db72b83 100644 --- a/app/views/master_files/hls_manifest.m3u8.erb +++ b/app/views/master_files/hls_manifest.m3u8.erb @@ -14,22 +14,7 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> #EXTM3U -<% if @master_file.has_captions? %> -<% captions_list = @master_file.supplemental_file_captions %> -<% captions_list.append(@master_file.captions) if @master_file.captions&.content %> -<% captions_list.each_with_index do |caption, index| %> -<% label = caption.is_a?(SupplementalFile) ? caption.label : 'English' %> -<% language = caption.is_a?(SupplementalFile) ? caption.language : 'en' %> -<% id = caption.is_a?(SupplementalFile) ? caption.id : 'master_file_caption' %> -#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="<%= language %>",NAME="<%= label %>",DEFAULT=<%= index == 0 ? "YES" : "NO" %>,AUTOSELECT=YES,URI="<%= caption_manifest_master_file_path(c_id: id) %>" -<% end %> -<% @hls_streams.each do |hls| %> -#EXT-X-STREAM-INF:BANDWIDTH=<%= hls[:bitrate] %>,SUBTITLES="subs" -<%= hls[:url] %> -<% end %> -<% else %> <% @hls_streams.each do |hls| %> #EXT-X-STREAM-INF:BANDWIDTH=<%= hls[:bitrate] %> <%= hls[:url] %> -<% end %> -<% end %> +<% end %> \ No newline at end of file diff --git a/app/views/media_objects/_add_to_playlist.html.erb b/app/views/media_objects/_add_to_playlist.html.erb index 4266775673..a08c60a1f6 100644 --- a/app/views/media_objects/_add_to_playlist.html.erb +++ b/app/views/media_objects/_add_to_playlist.html.erb @@ -177,7 +177,7 @@ Unless required by applicable law or agreed to in writing, software distributed // This timeout enables the add to playlist button, when this happens. It checks the button's state and enables it as needed. setTimeout(() => { enableAddToPlaylist(); - }, 500); + }, 100); player.on('seeked', () => { if(getActiveItem() != undefined) { activeTrack = getActiveItem(false); @@ -200,11 +200,15 @@ Unless required by applicable law or agreed to in writing, software distributed } function enableAddToPlaylist() { + let player = document.getElementById('iiif-media-player'); let addToPlaylistBtn = document.getElementById('addToPlaylistBtn'); - if(addToPlaylistBtn && addToPlaylistBtn.disabled) { + if(addToPlaylistBtn && addToPlaylistBtn.disabled && player?.player.readyState() === 4) { addToPlaylistBtn.disabled = false; } if(!listenersAdded) { + // Add 'Add new playlist' option to dropdown + window.add_new_playlist_option(); + addListeners(); } } @@ -228,8 +232,6 @@ Unless required by applicable law or agreed to in writing, software distributed currentTime = currentPlayer.player.currentTime(); duration = currentPlayer.player.duration(); } - // Add 'Add new playlist' option to dropdown - window.add_new_playlist_option(); let canvasIndex = parseInt(currentPlayer.dataset.canvasindex); mediaObjectTitle = playlistForm.dataset.title; diff --git a/app/views/media_objects/_thumbnail.html.erb b/app/views/media_objects/_thumbnail.html.erb index 2310c1a2b7..e071081bf1 100644 --- a/app/views/media_objects/_thumbnail.html.erb +++ b/app/views/media_objects/_thumbnail.html.erb @@ -56,22 +56,34 @@ Unless required by applicable law or agreed to in writing, software distributed if (player && player.player != undefined) { player.player.on('loadedmetadata', () => { let thumbnailBtn = document.getElementById('create-thumbnail-btn'); - if (thumbnailBtn) { + // Leave 'Create Thumbnail' button disabled when item is audio + if (thumbnailBtn && !player.player.isAudio()) { thumbnailBtn.disabled = false; } - clearInterval(timeCheck); }); /* Browsers on MacOS sometimes miss the 'loadedmetadata' event resulting in a disabled add to playlist button indefinitely. This timeout enables the add to playlist button, when this happens. It checks the button's state and enables it as needed. + Additional check for player's readyState ensures the button is enabled only when player is ready after the timeout. */ setTimeout(() => { let thumbnailBtn = document.getElementById('create-thumbnail-btn'); - if (thumbnailBtn && thumbnailBtn.disabled) { - thumbnailBtn.disabled = false; + // Leave 'Create Thumbnail' button disabled when item is audio + if (thumbnailBtn && thumbnailBtn.disabled && player.player?.readyState() === 4 && !player.player.isAudio()) { + thumbnailBtn.disabled = false; + } + }, 100); + + /* + Disable 'Create Thumbnail' button on player dispose, so that it can be enabled again or keep disabled on the next section load + based on the player status. + */ + player.player.on('dispose', () => { + let thumbnailBtn = document.getElementById('create-thumbnail-btn'); + if (thumbnailBtn) { + thumbnailBtn.disabled = true; } - clearInterval(timeCheck); - }, 500); + }); } $('#thumbnailModal').on('show.bs.modal', function(e) { diff --git a/app/views/media_objects/_timeline.html.erb b/app/views/media_objects/_timeline.html.erb index f5f3f22f27..c857e4344b 100644 --- a/app/views/media_objects/_timeline.html.erb +++ b/app/views/media_objects/_timeline.html.erb @@ -54,19 +54,29 @@ $(document).ready(function() { if (timelineBtn) { timelineBtn.disabled = false; } - clearInterval(timeCheck); }); /* Browsers on MacOS sometimes miss the 'loadedmetadata' event resulting in a disabled add to playlist button indefinitely. This timeout enables the add to playlist button, when this happens. It checks the button's state and enables it as needed. + Additional check for player's readyState ensures the button is enabled only when player is ready after the timeout. */ setTimeout(() => { let timelineBtn = document.getElementById('timelineBtn'); - if (timelineBtn && timelineBtn.disabled) { + if (timelineBtn && timelineBtn.disabled && player.player?.readyState() === 4) { timelineBtn.disabled = false; } - clearInterval(timeCheck); - }, 500); + }, 100); + + /* + Disable 'Create Timeline' button on player dispose, so that it can be enabled again or keep disabled on the next section load + based on the player status. + */ + player.player.on('dispose', () => { + let timelineBtn = document.getElementById('timelineBtn'); + if (timelineBtn) { + timelineBtn.disabled = true; + } + }); } $('#timelineModal').on('shown.bs.modal', function (e) { diff --git a/app/views/modules/player/_video_js_element.html.erb b/app/views/modules/player/_video_js_element.html.erb index 24c8afe746..38e869a58b 100644 --- a/app/views/modules/player/_video_js_element.html.erb +++ b/app/views/modules/player/_video_js_element.html.erb @@ -64,10 +64,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% section_info[:stream_hls].each do |hls| %> <% end %> - <%# Captions are contained in the HLS manifest and so we do not need to manually provide them to VideoJS here %> - <%# TODO: Reenable if/when we remove captions from HLS %> - <% skip_captions = true %> - <% if section_info[:caption_paths].present? && !skip_captions %> + <% if section_info[:caption_paths].present? %> <% section_info[:caption_paths].each do |c| %> label="<%= c[:label] %>" <% end %> srclang="<%= c[:language] %>" kind="subtitles" type="<%= c[:mime_type] %>" src="<%= c[:path] %>"> <% end %> diff --git a/app/views/playlists/show.html.erb b/app/views/playlists/show.html.erb index 49ba451f6d..5b72257141 100644 --- a/app/views/playlists/show.html.erb +++ b/app/views/playlists/show.html.erb @@ -30,8 +30,9 @@ Unless required by applicable law or agreed to in writing, software distributed
<%= react_component("PlaylistRamp", { - base_url: request.protocol+request.host_with_port, + urls: { base_url: request.protocol+request.host_with_port, fullpath_url: request.fullpath }, playlist_id: @playlist.id, + playlist_item_ids: @playlist.item_ids, token: @playlist_token, share: { canShare: (will_partial_list_render? :share), content: render('share') }, comment_tag: { content: render('comments_and_tags') } diff --git a/config/application.rb b/config/application.rb index 032119dc35..9f78a4087f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,7 +8,7 @@ Bundler.require(*Rails.groups) module Avalon - VERSION = '7.7.0' + VERSION = '7.7.1' class Application < Rails::Application require 'avalon/configuration' @@ -48,7 +48,6 @@ class Application < Rails::Application resource '/master_files/*/structure.json', headers: :any, methods: [:get, :post, :delete] resource '/master_files/*/waveform.json', headers: :any, methods: [:get] resource '/master_files/*/*.m3u8', headers: :any, credentials: true, methods: [:get, :head] - resource '/master_files/*/caption_manifest/*', headers: :any, methods: [:get] resource '/master_files/*/captions', headers: :any, methods: [:get] resource '/master_files/*/supplemental_files/*', headers: :any, methods: [:get] resource '/playlists/*/manifest.json', headers: :any, credentials: true, methods: [:get] diff --git a/config/routes.rb b/config/routes.rb index fdc4c039fd..3f803d67d1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -155,7 +155,6 @@ get :captions get :waveform match ':quality.m3u8', to: 'master_files#hls_manifest', via: [:get], as: :hls_manifest - get 'caption_manifest/:c_id', to: 'master_files#caption_manifest', as: :caption_manifest get 'structure', to: 'master_files#structure', constraints: { format: 'json' } post 'structure', to: 'master_files#set_structure', constraints: { format: 'json' } delete 'structure', to: 'master_files#delete_structure', constraints: { format: 'json' } @@ -166,7 +165,7 @@ # Supplemental Files resources :supplemental_files, except: [:new, :index, :edit] do member do - get 'captions', :to => redirect('/master_files/%{master_file_id}/supplemental_files/%{id}') + get 'captions' get 'transcripts', :to => redirect('/master_files/%{master_file_id}/supplemental_files/%{id}') end end diff --git a/lib/tasks/avalon_migrations.rake b/lib/tasks/avalon_migrations.rake index ee0cc4a8a7..0ce4d442cc 100644 --- a/lib/tasks/avalon_migrations.rake +++ b/lib/tasks/avalon_migrations.rake @@ -26,6 +26,8 @@ namespace :avalon do desc "Migrate legacy IndexedFile captions to ActiveStorage as part of supporting upload of multiple captions files" task caption_files: :environment do count = 0 + error = [] + logger = Logger.new(File.join(Rails.root, 'log/caption_type_errors.log')) # Iterate through all caption IndexedFiles IndexedFile.where("id: */captions").each do |caption_file| # Retrieve parent master file @@ -36,9 +38,16 @@ namespace :avalon do filename = caption_file.original_name content_type = caption_file.mime_type # Create and populate new SupplementalFile record using original metadata - supplemental_file = SupplementalFile.new(label: filename, tags: ['caption'], language: 'eng') + supplemental_file = SupplementalFile.new(label: filename, tags: ['caption'], language: Settings.caption_default.language) supplemental_file.file.attach(io: ActiveFedora::FileIO.new(caption_file), filename: filename, content_type: content_type, identify: false) - supplemental_file.save + # Skip validation so that incorrect mimetypes do not bomb the entire task + supplemental_file.save(validate: false) + # Log when a file has an incorrect mimetype for later manual remediation + if !['text/vtt', 'text/srt'].include?(content_type) + message = "File with unsupported mime type: #{supplemental_file.id}" + puts(message) + error += [supplemental_file.id] + end # Link new SupplementalFile to the MasterFile master_file.supplemental_files += [supplemental_file] # Delete legacy caption file @@ -50,6 +59,10 @@ namespace :avalon do end count > 0 ? puts("Successfully updated #{count} records") : puts("All files are already up to date. No records updated.") + if error.present? + logger.info("Files with unsupported mime types: #{error}") + puts("#{error.length} files migrated with unsupported mime types. Refer to caption_type_errors.log for full list of SupplementalFile IDs.") + end end end end diff --git a/package.json b/package.json index 10efefdb09..f2a9cc741c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "^3.0.0", + "@samvera/ramp": "^3.1.0", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", @@ -22,7 +22,7 @@ "react": "^17.0.1", "react-bootstrap": "^1.0.0", "react-dom": "^17.0.1", - "react-structural-metadata-editor": "https://github.com/avalonmediasystem/react-structural-metadata-editor#avalon-7.7", + "react-structural-metadata-editor": "https://github.com/avalonmediasystem/react-structural-metadata-editor#avalon-7.7.1", "react_ujs": "^2.4.4", "sass": "^1.65.1", "sass-loader": "^13.3.2", diff --git a/script/reindex.rb b/script/reindex.rb index f4a8e57c54..75110dbc50 100644 --- a/script/reindex.rb +++ b/script/reindex.rb @@ -66,8 +66,8 @@ # already processed minus one day. This should run quickly (< 1 hour). # # This script was written for migrating from solr 6 to solr 8/9 with the following process in mind: -# 1. Reindex from solr 6 into new solr 8 instance -# 2. Configure avalon to use new solr 8 instance and restart +# 1. Reindex from solr 6 into new solr 9 instance +# 2. Configure avalon to use new solr 9 instance and restart # 3. Run reindex delta to read from solr 6 and catch any items that have changed since step 1 @@ -114,15 +114,27 @@ end parser.on("--reindex-limit REINDEX_LIMIT", "Limit reindexing to a set number of items") do |rl| - options[:reindex_limit] = rl + options[:reindex_limit] = rl.to_i end parser.on("--parallel-indexing", "Reindex using paralellism") do |p| options[:parallel_indexing] = p end - parser.on("--batch-size", "Size of batches for indexing (default: 50)") do |bs| - options[:batch_size] = bs + parser.on("--parallel-threads THREADS", "Number of parallel threads to use for reindexing") do |pt| + options[:parallel_threads] = pt.to_i + end + + parser.on("--batch-size SIZE", "Size of batches for indexing (default: 50)") do |bs| + options[:batch_size] = bs.to_i + end + + parser.on("--only-models MODELS", "Only index certain models in the order specified by comma-separated list") do |om| + options[:only_models] = om.split(',').map(&:strip) + end + + parser.on("--file-fedora-url URL", "Replace fedora url in IndexedFile uri_ss fields (This is not validated so be careful!)") do |ffu| + options[:file_fedora_url] = ffu end parser.on("--delta", "Only find changes since last reindexing") do |d| @@ -154,7 +166,7 @@ else database_url = options[:database_url] database_url ||= ENV['DATABASE_URL'] - DB = Sequel.connect(database_url) + DB = Sequel.connect(database_url, max_connections: 20, pool_timeout: 10) end if options[:prune] @@ -186,16 +198,23 @@ last_updated_at = items.order(:updated_at).last[:updated_at] query += " AND timestamp:[#{(last_updated_at - 1.day).utc.iso8601} TO *]" end - docs = read_solr.conn.get("select", params: { q: query, qt: 'standard', fl: ["id", "timestamp"], rows: 1_000_000_000 })["response"]["docs"] - # Need to transform ids into uris to match what we get from crawling fedora - docs.map { |doc| doc["id"] = ActiveFedora::Base.id_to_uri(doc["id"]) } - # Need to transform timestamps into DateTime objects - docs.map { |doc| doc["timestamp"] = DateTime.parse(doc["timestamp"]) } - # Skip those that are already waiting reindex - docs.reject! { |doc| items.where(uri: doc["id"], state: "waiting reindex").any? } - # Skip those which haven't changed - docs.reject! { |doc| items.where(uri: doc["id"]).where(Sequel.lit('updated_at >= ?', doc["timestamp"])).any? } - items.import([:uri, :updated_at, :state, :state_changed_at], docs.map(&:values).product([["waiting reindex", DateTime.now]]).map(&:flatten), commit_every: 10_000) + docs = read_solr.conn.get("select", params: { q: query, qt: 'standard', fl: ["id", "timestamp", "has_model_ssim"], rows: 1_000_000_000 })["response"]["docs"] + docs.map do |doc| + # Need to transform ids into uris to match what we get from crawling fedora + doc["id"] = ActiveFedora::Base.id_to_uri(doc["id"]) + # Need to transform timestamps into DateTime objects + doc["timestamp"] = DateTime.parse(doc["timestamp"]) + model = doc["has_model_ssim"]&.first + doc["model"] = model if model + doc.delete("has_model_ssim") + end + docs.reject! do |doc| + # Skip those that are already waiting reindex + items.where(uri: doc["id"], state: "waiting reindex").any? || + # Skip those which haven't changed + items.where(uri: doc["id"]).where(Sequel.lit('updated_at >= ?', doc["timestamp"])).any? + end + items.import([:uri, :updated_at, :model, :state, :state_changed_at], docs.map(&:values).product([["waiting reindex", DateTime.now]]).map(&:flatten), commit_every: 10_000) if options[:delta] already_deleted_uris = items.where(state: ["waiting deletion", "deleted"]).order(:uri).distinct(:uri).select(:uri).pluck(:uri) @@ -266,101 +285,114 @@ # Re-index unless options[:skip_reindexing] reindex_limit = options[:reindex_limit] || nil - if reindex_limit - puts "#{DateTime.now} Attempting reindex of #{reindex_limit} nodes out of #{items.where(state:"waiting reindex").count}." if options[:verbose] - else - puts "#{DateTime.now} Attempting reindex of #{items.where(state:"waiting reindex").count} nodes." if options[:verbose] - end - batch_size = options[:batch_size] || 50 softCommit = true uris_to_skip = [/\/poster$/, /\/thumbnail$/, /\/waveform$/, /\/captions$/, /\/structuralMetadata$/] - if options[:parallel_indexing] - require 'parallel' - require 'ruby-progressbar' + models_for_all = DB[:reindexing_nodes].map(:model).uniq - ["Hydra::AccessControl", "Hydra::AccessControls::Permission", "Admin::Collection"] + model_prioritization = options[:only_models] || ["Hydra::AccessControl", "Hydra::AccessControls::Permission", "Admin::Collection", models_for_all] + + model_prioritization.each do |model| + items_for_reindexing_relation = items.where(state: "waiting reindex", model: model).limit(reindex_limit).map(:uri) + + if reindex_limit + puts "#{DateTime.now} Attempting reindex of #{reindex_limit} #{model} nodes out of #{items.where(state:"waiting reindex", model: model).count}." if options[:verbose] + reindex_limit = reindex_limit - items_for_reindexing_relation.count + else + puts "#{DateTime.now} Attempting reindex of #{items.where(state:"waiting reindex", model: model).count} #{model} nodes." if options[:verbose] + end + + if options[:parallel_indexing] + require 'parallel' + require 'ruby-progressbar' - Parallel.each(items.where(state: "waiting reindex").limit(reindex_limit).map(:uri).each_slice(batch_size), in_threads: 10, progress: "Reindexing") do |uris| + Parallel.each(items_for_reindexing_relation.each_slice(batch_size), in_threads: options[:parallel_threads] || 10, progress: "Reindexing") do |uris| + batch = [] + batch_uris = [] + + uris.each do |uri| + begin + if uris_to_skip.any? { |pattern| uri =~ pattern } + items.where(uri: uri, state: "waiting reindex").update(state: "skipped", state_changed_at: DateTime.now) + next + end + obj = ActiveFedora::Base.find(ActiveFedora::Base.uri_to_id(uri)) + batch << (obj.is_a?(MediaObject) ? obj.to_solr(include_child_fields: true) : obj.to_solr) + batch_uris << uri + # Handle speedy_af indexing + if obj.is_a?(MasterFile) || obj.is_a?(Admin::Collection) + obj.declared_attached_files.each_pair do |name, file| + next unless file.present? + file_doc = file.to_solr({}, external_index: true) if file.respond_to?(:update_external_index) + file_doc[:uri_ss] = file_doc[:uri_ss].sub(ActiveFedora.fedora_config.credentials[:url], options[:file_fedora_url]) if options[:file_fedora_url].present? && file.is_a?(IndexedFile) + batch << file_doc + end + end + rescue Exception => e + puts "#{DateTime.now} Error adding #{uri} to batch: #{e.message}" + puts e.backtrace if options[:verbose] + batch_uris -= [uri] + batch.compact! + batch.delete_if { |doc| ActiveFedora::Base.uri_to_id(uri) == doc[:id] } + # Need to worry about removing masterfile attached files from the batch as well? + items.where(uri: uri, state: "waiting reindex").update(state: "errored", state_changed_at: DateTime.now) + next + end + end + + begin + solr.conn.add(batch, params: { softCommit: softCommit }) unless options[:dry_run] + items.where(uri: batch_uris, state: "waiting reindex").update(state: "processed", state_changed_at: DateTime.now) + rescue Exception => e + puts "#{DateTime.now} Error persisting batch to solr: #{e.message}" + puts e.backtrace if options[:verbose] + items.where(uri: batch_uris, state: "waiting reindex").update(state: "errored", state_changed_at: DateTime.now) + end + end + else batch = [] batch_uris = [] - - uris.each do |uri| + items_for_reindexing_relation.each do |uri| begin - if uris_to_skip.any? { |pattern| uri =~ pattern } - items.where(uri: uri, state: "waiting reindex").update(state: "skipped", state_changed_at: DateTime.now) - next - end obj = ActiveFedora::Base.find(ActiveFedora::Base.uri_to_id(uri)) - batch << (obj.is_a?(MediaObject) ? obj.to_solr(include_child_fields: true) : obj.to_solr) - batch_uris << uri + batch << (obj.is_a?(MediaObject) ? obj.to_solr(include_child_fields: true) : obj.to_solr) + batch_uris << uri # Handle speedy_af indexing - if obj.is_a?(MasterFile) || obj.is_a?(Admin::Collection) + if obj.is_a?(MasterFile) || obj.is_a?(Admin::Collection) obj.declared_attached_files.each_pair do |name, file| - batch << file.to_solr({}, external_index: true) if file.present? && file.respond_to?(:update_external_index) + next unless file.present? + file_doc = file.to_solr({}, external_index: true) if file.respond_to?(:update_external_index) + file_doc[:uri_ss] = file_doc[:uri_ss].sub(ActiveFedora.fedora_config.credentials[:url], options[:file_fedora_url]) if options[:file_fedora_url].present? && file.is_a?(IndexedFile) + batch << file_doc end end rescue Exception => e - puts "#{DateTime.now} Error adding #{uri} to batch: #{e.message}" - puts e.backtrace if options[:verbose] - batch_uris -= [uri] - batch.delete_if { |doc| ActiveFedora::Base.uri_to_id(uri) == doc[:id] } - # Need to worry about removing masterfile attached files from the batch as well? - items.where(uri: uri, state: "waiting reindex").update(state: "errored", state_changed_at: DateTime.now) + puts "#{DateTime.now} Error adding #{uri} to batch: #{e.message}" + puts e.backtrace if options[:verbose] + batch_uris -= [uri] + batch.compact! + batch.delete_if { |doc| ActiveFedora::Base.uri_to_id(uri) == doc[:id] } + # Need to worry about removing masterfile attached files from the batch as well? + items.where(uri: uri, state: "waiting reindex").update(state: "errored", state_changed_at: DateTime.now) next end - end - begin - solr.conn.add(batch, params: { softCommit: softCommit }) unless options[:dry_run] - items.where(uri: batch_uris, state: "waiting reindex").update(state: "processed", state_changed_at: DateTime.now) - rescue Exception => e - puts "#{DateTime.now} Error persisting batch to solr: #{e.message}" - puts e.backtrace if options[:verbose] - items.where(uri: batch_uris, state: "waiting reindex").update(state: "errored", state_changed_at: DateTime.now) - end - end - else - batch = [] - batch_uris = [] - # TODO: take batch_size of uris and pass to background job and remove rescue so it will surface - # This could also obviate the need for the final batch processing - # Should this actually be a cron-type job to wake up and look for items needing reindexing? - items.where(state: "waiting reindex").limit(reindex_limit).map(:uri).each do |uri| - begin - obj = ActiveFedora::Base.find(ActiveFedora::Base.uri_to_id(uri)) - batch << (obj.is_a?(MediaObject) ? obj.to_solr(include_child_fields: true) : obj.to_solr) - batch_uris << uri - # Handle speedy_af indexing - if obj.is_a?(MasterFile) || obj.is_a?(Admin::Collection) - obj.declared_attached_files.each_pair do |name, file| - batch << file.to_solr({}, external_index: true) if file.present? && file.respond_to?(:update_external_index) - end - end - rescue Exception => e - puts "#{DateTime.now} Error adding #{uri} to batch: #{e.message}" - puts e.backtrace if options[:verbose] - batch_uris -= [uri] - batch.delete_if { |doc| ActiveFedora::Base.uri_to_id(uri) == doc[:id] } - # Need to worry about removing masterfile attached files from the batch as well? - items.where(uri: uri, state: "waiting reindex").update(state: "errored", state_changed_at: DateTime.now) - next + if (batch.count % batch_size).zero? + solr.conn.add(batch, params: { softCommit: softCommit }) unless options[:dry_run] + items.where(uri: batch_uris, state: "waiting reindex").update(state: "processed", state_changed_at: DateTime.now) + batch.clear + batch_uris.clear + puts "#{DateTime.now} #{items.where(state: "processed").count} processed" if options[:verbose] + end end - if (batch.count % batch_size).zero? - solr.conn.add(batch, params: { softCommit: softCommit }) unless options[:dry_run] - items.where(uri: batch_uris, state: "waiting reindex").update(state: "processed", state_changed_at: DateTime.now) - batch.clear - batch_uris.clear - puts "#{DateTime.now} #{items.where(state: "processed").count} processed" if options[:verbose] + if batch.present? + solr.conn.add(batch, params: { softCommit: softCommit }) unless options[:dry_run] + items.where(uri: batch_uris, state: "waiting reindex").update(state: "processed", state_changed_at: DateTime.now) + batch.clear + batch_uris.clear end end - - if batch.present? - solr.conn.add(batch, params: { softCommit: softCommit }) unless options[:dry_run] - items.where(uri: batch_uris, state: "waiting reindex").update(state: "processed", state_changed_at: DateTime.now) - batch.clear - batch_uris.clear - end end if options[:delta] @@ -378,6 +410,10 @@ items.where(state: "waiting deletion").update(state: "errored", state_changed_at: DateTime.now) end end + + # Do a final hard commit and optimize + solr.conn.commit + solr.conn.optimize end puts "#{DateTime.now} Completed" if options[:verbose] diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index 6ac3583201..f092311be4 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -154,7 +154,8 @@ end describe "#index" do - let!(:collection) { FactoryBot.create(:collection) } + let!(:collection) { FactoryBot.create(:collection, items: 1) } + let!(:collection2) { FactoryBot.create(:collection, items: 1) } subject(:json) { JSON.parse(response.body) } let(:administrator) { FactoryBot.create(:administrator) } @@ -165,7 +166,7 @@ end it "should return list of collections" do get 'index', params: { format:'json' } - expect(json.count).to eq(1) + expect(json.count).to eq(2) expect(json.first['id']).to eq(collection.id) expect(json.first['name']).to eq(collection.name) expect(json.first['unit']).to eq(collection.unit) diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index fbe2f17e94..0f2fa35d41 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -647,51 +647,6 @@ class << file end end - describe '#caption_manifest' do - let(:media_object) { FactoryBot.create(:published_media_object) } - let(:master_file) { FactoryBot.create(:master_file, :with_captions, media_object: media_object) } - let(:public_media_object) { FactoryBot.create(:published_media_object, visibility: 'public') } - let(:public_master_file) { FactoryBot.create(:master_file, :with_captions, media_object: public_media_object) } - - context 'master file has been deleted' do - before do - master_file.destroy - end - - it 'returns unauthorized (401)' do - expect(get('caption_manifest', params: { id: master_file.id, c_id: 1 }, xhr: true)).to have_http_status(:unauthorized) - end - end - - it 'returns unauthorized (401) if cannot read the master file' do - expect(get('caption_manifest', params: { id: master_file.id, c_id: 1 }, xhr: true)).to have_http_status(:unauthorized) - end - - it 'returns the caption manifest' do - login_as :administrator - expect(get('caption_manifest', params: { id: master_file.id, c_id: 1 }, xhr: true)).to have_http_status(:ok) - expect(response.content_type).to eq 'application/x-mpegURL; charset=utf-8' - end - - it 'returns a manifest if public' do - expect(get('caption_manifest', params: { id: public_master_file.id, c_id: 1 }, xhr: true)).to have_http_status(:ok) - expect(response.content_type).to eq 'application/x-mpegURL; charset=utf-8' - expect(get('caption_manifest', params: { id: public_master_file.id, c_id: 'master_file_caption' }, xhr: true)).to have_http_status(:ok) - expect(response.content_type).to eq 'application/x-mpegURL; charset=utf-8' - end - - context 'read from solr' do - it 'should not read from fedora' do - public_master_file - perform_enqueued_jobs(only: MediaObjectIndexingJob) - WebMock.reset_executed_requests! - login_as :administrator - get('caption_manifest', params: { id: public_master_file.id, c_id: 1 }, xhr: true) - expect(a_request(:any, /#{ActiveFedora.fedora.base_uri}/)).not_to have_been_made - end - end - end - describe '#move' do let(:master_file) { FactoryBot.create(:master_file, :with_media_object) } let(:target_media_object) { FactoryBot.create(:media_object) } diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 92d03f4069..74ee109890 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1193,6 +1193,7 @@ expect(json['id']).to eq(media_object.id) expect(json['title']).to eq(media_object.title) expect(json['collection']).to eq(media_object.collection.name) + expect(json['collection_id']).to eq(media_object.collection.id) expect(json['main_contributors']).to eq(media_object.creator) expect(json['publication_date']).to eq(media_object.date_created) expect(json['published_by']).to eq(media_object.avalon_publisher) diff --git a/spec/controllers/playlists_controller_spec.rb b/spec/controllers/playlists_controller_spec.rb index fee3be1139..64e47df3fb 100644 --- a/spec/controllers/playlists_controller_spec.rb +++ b/spec/controllers/playlists_controller_spec.rb @@ -558,8 +558,9 @@ end describe "GET #manifest" do - let(:playlist) { FactoryBot.create(:playlist, items: [playlist_item], visibility: Playlist::PUBLIC) } + let(:playlist) { FactoryBot.create(:playlist, items: [playlist_item, playlist_item_2], visibility: Playlist::PUBLIC) } let(:playlist_item) { FactoryBot.create(:playlist_item, clip: clip) } + let(:playlist_item_2) { FactoryBot.create(:playlist_item, clip: clip) } let(:clip) { FactoryBot.create(:avalon_clip, master_file: master_file) } let(:master_file) { FactoryBot.create(:master_file, :with_derivative, media_object: media_object) } let(:media_object) { FactoryBot.create(:published_media_object, visibility: 'public') } @@ -573,10 +574,48 @@ expect(parsed_response['items']).not_to be_empty end - it "contains metadata about the playlist item's parent media obejct" do - get :manifest, format: 'json', params: { id: playlist.id}, session: valid_session - parsed_response = JSON.parse(response.body) - expect(parsed_response['items'][0]['metadata']).to be_present + context "playlist item" do + it "contains metadata about the playlist item's parent media obejct" do + get :manifest, format: 'json', params: { id: playlist.id }, session: valid_session + parsed_response = JSON.parse(response.body) + expect(parsed_response['items'][0]['metadata']).to be_present + end + + it "contains a homepage with the playlist item's positional URL" do + get :manifest, format: 'json', params:{ id: playlist.id }, session: valid_session + parsed_response = JSON.parse(response.body) + expect(parsed_response['items'][0]['homepage']).to be_present + expect(parsed_response['items'][0]['homepage'][0]['id']).to eq "#{Rails.application.routes.url_helpers.playlist_url(playlist.id)}?position=1" + expect(parsed_response['items'][1]['homepage']).to be_present + expect(parsed_response['items'][1]['homepage'][0]['id']).to eq "#{Rails.application.routes.url_helpers.playlist_url(playlist.id)}?position=2" + end + + context "with deleted source" do + before do + master_file.delete + end + + it "returns a blank canvas" do + get :manifest, format: 'json', params: { id: playlist.id }, session: valid_session + parsed_response = JSON.parse(response.body) + expect(parsed_response['items'][0]['items'][0].keys).to_not include 'items' + end + end + end + + context "playlist item auth" do + let(:playlist_item_2) { FactoryBot.create(:playlist_item, clip: clip_2) } + let(:clip_2) { FactoryBot.create(:avalon_clip, master_file: master_file_2) } + let(:master_file_2) { FactoryBot.create(:master_file, :with_derivative, media_object: media_object_2) } + let(:media_object_2) { FactoryBot.create(:published_media_object, visibility: 'restricted') } + + it "returns populated canvas for public item and blank canvas for restricted item" do + get :manifest, format: 'json', params: { id: playlist.id }, session: valid_session + parsed_response = JSON.parse(response.body) + expect(parsed_response['items'].length).to eq 2 + expect(parsed_response['items'][0]['items'][0].keys).to include 'items' + expect(parsed_response['items'][1]['items'][0].keys).to_not include 'items' + end end context "when playlist is empty" do @@ -611,33 +650,5 @@ expect(parsed_response["service"]).not_to be_present end end - - context "playlist item auth" do - let(:playlist) { FactoryBot.create(:playlist, items: [playlist_item, playlist_item_2], visibility: Playlist::PUBLIC) } - let(:playlist_item_2) { FactoryBot.create(:playlist_item, clip: clip_2) } - let(:clip_2) { FactoryBot.create(:avalon_clip, master_file: master_file_2) } - let(:master_file_2) { FactoryBot.create(:master_file, :with_derivative, media_object: media_object_2) } - let(:media_object_2) { FactoryBot.create(:published_media_object, visibility: 'restricted') } - - it "returns populated canvas for public item and blank canvas for restricted item" do - get :manifest, format: 'json', params: { id: playlist.id }, session: valid_session - parsed_response = JSON.parse(response.body) - expect(parsed_response['items'].length).to eq 2 - expect(parsed_response['items'][0]['items'][0].keys).to include 'items' - expect(parsed_response['items'][1]['items'][0].keys).to_not include 'items' - end - end - - context "playlist item with deleted source" do - before do - master_file.delete - end - - it "returns a blank canvas" do - get :manifest, format: 'json', params: { id: playlist.id }, session: valid_session - parsed_response = JSON.parse(response.body) - expect(parsed_response['items'][0]['items'][0].keys).to_not include 'items' - end - end end end diff --git a/spec/controllers/supplemental_files_controller_spec.rb b/spec/controllers/supplemental_files_controller_spec.rb index 5da8ad72a2..52ea93b79c 100644 --- a/spec/controllers/supplemental_files_controller_spec.rb +++ b/spec/controllers/supplemental_files_controller_spec.rb @@ -17,4 +17,55 @@ RSpec.describe SupplementalFilesController, type: :controller do it_behaves_like "a nested controller for", MasterFile it_behaves_like "a nested controller for", MediaObject + + describe 'captions endpoint for MasterFile' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } + # This should return the minimal set of values that should be in the session + # in order to pass any filters (e.g. authentication) defined in + # SupplementalFilesController. Be sure to keep this updated too. + let(:valid_session) { {} } + + + describe 'security' do + let(:master_file) { FactoryBot.create(:master_file, :with_media_object, supplemental_files: [supplemental_file]) } + + context 'with unauthenticated user' do + it 'should return 401' do + expect(get :captions, params: { master_file_id: master_file.id, id: supplemental_file.id }).to have_http_status(401) + end + end + context 'with end-user without permissions' do + before do + login_as :user + end + it 'should return 401' do + expect(get :captions, params: { master_file_id: master_file.id, id: supplemental_file.id }).to have_http_status(401) + end + end + end + + describe "GET #captions" do + let(:public_media_object) { FactoryBot.create(:fully_searchable_media_object) } + let(:master_file) { FactoryBot.create(:master_file, media_object: public_media_object, supplemental_files: [supplemental_file]) } + before { allow(Settings.supplemental_files).to receive(:proxy).and_return(true) } + + it "returns the caption file content" do + get :captions, params: { master_file_id: master_file.id, id: supplemental_file.id }, session: valid_session + expect(response).to have_http_status(200) + expect(response.header["Content-Type"]).to eq 'text/vtt' + expect(response.body).to eq supplemental_file.file.download + end + + context 'with SRT caption' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_tag, :with_caption_srt_file) } + let(:file) { Rails.root.join('spec', 'fixtures', 'captions.srt')} + it 'returns the caption file content in VTT format' do + get :captions, params: { master_file_id: master_file.id, id: supplemental_file.id }, session: valid_session + expect(response).to have_http_status(200) + expect(response.header["Content-Type"]).to eq 'text/vtt' + expect(response.body).to eq SupplementalFile.convert_from_srt(File.read(file)) + end + end + end + end end diff --git a/spec/factories/encode.rb b/spec/factories/encode.rb index ba2c5d2590..0a4b5770a7 100644 --- a/spec/factories/encode.rb +++ b/spec/factories/encode.rb @@ -26,12 +26,14 @@ state { :running } percent_complete { 50.5 } current_operations { ['encoding'] } + input { FactoryBot.build(:encode_output) } end trait :succeeded do state { :completed } percent_complete { 100 } current_operations { ['DONE'] } + input { FactoryBot.build(:encode_output) } output { [ FactoryBot.build(:encode_output) ] } end @@ -39,15 +41,29 @@ state { :failed } percent_complete { 50.5 } current_operations { ['FAILED'] } + input { FactoryBot.build(:encode_output) } errors { ['Out of disk space.'] } end end + factory :encode_input, class: ActiveEncode::Input do + id { SecureRandom.uuid } + label { 'quality-high' } + url { 'file://path/to/output.mp4' } + duration { '21575.0' } + audio_bitrate { '163842.0' } + audio_codec { 'AAC' } + video_bitrate { '4000000.0' } + video_codec { 'AVC' } + width { '1024' } + height { '768' } + end + factory :encode_output, class: ActiveEncode::Output do id { SecureRandom.uuid } label { 'quality-high' } url { 'file://path/to/output.mp4' } - duration { '21575' } + duration { '21575.0' } audio_bitrate { '163842.0' } audio_codec { 'AAC' } video_bitrate { '4000000.0' } diff --git a/spec/factories/supplemental_file.rb b/spec/factories/supplemental_file.rb index 7e76b26213..066476f77a 100644 --- a/spec/factories/supplemental_file.rb +++ b/spec/factories/supplemental_file.rb @@ -29,6 +29,10 @@ file { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.vtt'), 'text/vtt') } end + trait :with_caption_srt_file do + file { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.srt'), 'text/srt')} + end + trait :with_transcript_tag do tags { ['transcript'] } end diff --git a/spec/fixtures/captions.srt b/spec/fixtures/captions.srt new file mode 100644 index 0000000000..71ea3d55b2 --- /dev/null +++ b/spec/fixtures/captions.srt @@ -0,0 +1,3 @@ +1 +00:00:03,498 --> 00:00:05,000 +- Example Captions diff --git a/spec/models/iiif_canvas_presenter_spec.rb b/spec/models/iiif_canvas_presenter_spec.rb index 345870020e..051d320fa0 100644 --- a/spec/models/iiif_canvas_presenter_spec.rb +++ b/spec/models/iiif_canvas_presenter_spec.rb @@ -269,6 +269,16 @@ expect(subject.any? { |content| content.body_id =~ /master_files\/#{master_file.id}\/captions/ }).to eq false end + context 'srt captions' do + let(:srt_caption_file) { FactoryBot.create(:supplemental_file, :with_caption_srt_file, :with_caption_tag) } + let(:supplemental_files) { [supplemental_file, transcript_file, caption_file, srt_caption_file] } + it 'sets format to "text/vtt"' do + captions = subject.select { |s| s.body_id.include?('captions') } + expect(captions.none? { |content| content.format == 'text/srt' }).to eq true + expect(captions.all? { |content| content.format == 'text/vtt' }).to eq true + end + end + context 'legacy master file captions' do let(:master_file) { FactoryBot.create(:master_file, :with_waveform, :with_captions, supplemental_files_json: supplemental_files_json, media_object: media_object, derivatives: [derivative]) } diff --git a/spec/models/iiif_playlist_canvas_presenter_spec.rb b/spec/models/iiif_playlist_canvas_presenter_spec.rb index 4a09934629..a4aaeb4698 100644 --- a/spec/models/iiif_playlist_canvas_presenter_spec.rb +++ b/spec/models/iiif_playlist_canvas_presenter_spec.rb @@ -21,7 +21,7 @@ let(:playlist_item) { FactoryBot.build(:playlist_item, clip: playlist_clip) } let(:playlist_clip) { FactoryBot.build(:avalon_clip, master_file: master_file) } let(:stream_info) { master_file.stream_details } - let(:presenter) { described_class.new(playlist_item: playlist_item, stream_info: stream_info) } + let(:presenter) { described_class.new(playlist_item: playlist_item, stream_info: stream_info, position: 1) } describe '#to_s' do it 'returns the playlist_item label' do @@ -292,4 +292,16 @@ expect(subject).to eq playlist_item.comment end end + + describe "#homepage" do + subject { presenter.homepage } + let(:playlist_item) { FactoryBot.create(:playlist_item, clip: playlist_clip) } + let(:playlist_clip) { FactoryBot.create(:avalon_clip, master_file: master_file) } + + it "it references the item's position within the playlist" do + expect(subject.first['@id']).to eq "#{Rails.application.routes.url_helpers.playlist_url(playlist_item.playlist_id)}?position=1" + expect(subject.first['label']).to be_present + expect(subject.first['type']).to be_present + end + end end diff --git a/spec/models/master_file_spec.rb b/spec/models/master_file_spec.rb index 488e950a4d..70ffa27e91 100644 --- a/spec/models/master_file_spec.rb +++ b/spec/models/master_file_spec.rb @@ -539,21 +539,6 @@ end end - describe '#update_progress_on_success!' do - subject(:master_file) { FactoryBot.create(:master_file) } - let(:encode) { double("encode", :output => []) } - before do - allow(master_file).to receive(:update_ingest_batch).and_return(true) - end - - it 'should set the digitized date' do - master_file.update_progress_on_success!(encode) - master_file.reload - expect(master_file.date_digitized).to_not be_empty - end - - end - describe "#structural_metadata_labels" do subject(:master_file) { FactoryBot.create(:master_file, :with_structure) } it 'should return correct list of labels' do @@ -816,11 +801,25 @@ let(:master_file) { FactoryBot.build(:master_file) } let(:encode_succeeded) { FactoryBot.build(:encode, :succeeded) } + before do + allow(master_file).to receive(:update_derivatives) + allow(master_file).to receive(:run_hook) + end + it 'calls update_derivatives' do expect(master_file).to receive(:update_derivatives).with(array_including(hash_including(label: 'quality-high'))) expect(master_file).to receive(:run_hook).with(:after_transcoding) master_file.update_progress_on_success!(encode_succeeded) end + + it 'updates duration' do + expect { master_file.update_progress_on_success!(encode_succeeded) }.to change { master_file.duration }.from("200000").to("21575") + end + + it 'should set the digitized date' do + master_file.update_progress_on_success!(encode_succeeded) + expect(master_file.date_digitized).to_not be_empty + end end describe 'update_derivatives' do diff --git a/spec/models/playlist_spec.rb b/spec/models/playlist_spec.rb index 23f40fe9a8..4a0ea7bde6 100644 --- a/spec/models/playlist_spec.rb +++ b/spec/models/playlist_spec.rb @@ -157,13 +157,20 @@ let(:playlist3) { FactoryBot.create(:playlist, title: 'Favorites') } let(:title_filter) { 'moose' } it 'returns playlists with matching titles' do - # Commented out since case insensitivity is default for mysql but not postgres - # expect(Playlist.title_like(title_filter)).to include(playlist1) + expect(Playlist.title_like(title_filter)).to include(playlist1) expect(Playlist.title_like(title_filter)).to include(playlist2) end it 'does not return playlists without matching titles' do expect(Playlist.title_like(title_filter)).not_to include(playlist3) end + it 'searches on multiple terms' do + expect(Playlist.title_like('Fav Moose')).to include(playlist2) + expect(Playlist.title_like('Fav Moose')).not_to include(playlist1) + expect(Playlist.title_like('Fav Moose')).not_to include(playlist3) + expect(Playlist.title_like('Moose Fav')).to include(playlist2) + expect(Playlist.title_like('Moose Fav')).not_to include(playlist1) + expect(Playlist.title_like('Moose Fav')).not_to include(playlist3) + end end describe 'with_tag' do let(:playlist1) { FactoryBot.create(:playlist, tags: ['Moose']) } diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index 21897768dd..da66391a37 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -25,12 +25,18 @@ expect(subject.valid?).to be_truthy end end - context 'VTT/SRT caption file' do + context 'VTT caption file' do let(:subject) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } it 'should validate' do expect(subject.valid?).to be_truthy end end + context 'SRT caption file' do + let(:subject) { FactoryBot.create(:supplemental_file, :with_caption_srt_file, :with_caption_tag) } + it 'should validate' do + expect(subject.valid?).to be_truthy + end + end context 'non-VTT/non-SRT caption file' do let(:subject) { FactoryBot.build(:supplemental_file, :with_attached_file, :with_caption_tag) } it 'should not validate' do @@ -84,4 +90,12 @@ expect(subject.reload.language).to eq "ger" end end + + describe '.convert_from_srt' do + let(:input) { "1\n00:00:03,498 --> 00:00:05,000\n- Example Captions\n" } + let(:output) { "WEBVTT\n\n1\n00:00:03.498 --> 00:00:05.000\n- Example Captions" } + it 'converts SRT format captions into VTT captions' do + expect(SupplementalFile.convert_from_srt(input)).to eq output + end + end end diff --git a/spec/presenters/collection_presenter_spec.rb b/spec/presenters/collection_presenter_spec.rb index 6d677c6a95..8d2c71c0a4 100644 --- a/spec/presenters/collection_presenter_spec.rb +++ b/spec/presenters/collection_presenter_spec.rb @@ -23,7 +23,7 @@ let(:website_label) { Faker::Lorem.words.join(' ') } let(:website_url) { Faker::Internet.url } let(:contact_email) { Faker::Internet.email } - let(:website_link) { "#{website_url}" } + let(:website_link) { "#{website_label.presence || website_url}" } let(:solr_doc) do SolrDocument.new( id: id, @@ -41,6 +41,7 @@ before do allow(view_context).to receive(:link_to).with(website_label, website_url).and_return(website_link) + allow(view_context).to receive(:link_to).with(website_url, website_url).and_return(website_link) end it 'provides getters for descriptive fields' do @@ -94,6 +95,19 @@ expect(presenter.website_link).to be_nil end end + + context 'with a blank website label' do + let(:website_label) { '' } + + it 'provides a link tag' do + expect(presenter.website_link).to eq website_link + end + + it 'sets label to the website url' do + expect(view_context).to receive(:link_to).with(website_url, website_url) + presenter.website_link + end + end end describe '#as_json' do diff --git a/spec/routing/master_files_routing_spec.rb b/spec/routing/master_files_routing_spec.rb index 8fb7bf4c94..8be9b032c2 100644 --- a/spec/routing/master_files_routing_spec.rb +++ b/spec/routing/master_files_routing_spec.rb @@ -19,8 +19,5 @@ it "routes to #move" do expect(:post => "/master_files/abc1234/move").to route_to("master_files#move", id: 'abc1234') end - it "routes to #caption_manifest" do - expect(:get => "/master_files/abc1234/caption_manifest/def5678").to route_to("master_files#caption_manifest", id: 'abc1234', c_id: 'def5678') - end end end diff --git a/spec/routing/supplemental_files_routing_spec.rb b/spec/routing/supplemental_files_routing_spec.rb index bee254f4b5..0349520e97 100644 --- a/spec/routing/supplemental_files_routing_spec.rb +++ b/spec/routing/supplemental_files_routing_spec.rb @@ -28,11 +28,12 @@ it "routes to #update" do expect(:put => "/master_files/abc1234/supplemental_files/edf567").to route_to("supplemental_files#update", master_file_id: 'abc1234', id: 'edf567') end + it "routes to #captions" do + expect(:get => "/master_files/abc1234/supplemental_files/edf567/captions").to route_to("supplemental_files#captions", master_file_id: 'abc1234', id: 'edf567') + end # Redirects are not testable from the routing spec out of the box. # Forcing the tests to `type: :request` to keep routing tests in one place. it "redirects to supplemental_files#show", type: :request do - get "/master_files/abc1234/supplemental_files/edf567/captions" - expect(response).to redirect_to("/master_files/abc1234/supplemental_files/edf567") get "/master_files/abc1234/supplemental_files/edf567/transcripts" expect(response).to redirect_to("/master_files/abc1234/supplemental_files/edf567") end diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index 51b5c08a9f..c79f1a44ad 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -134,6 +134,25 @@ expect(object.supplemental_files.first.tags).to eq tags expect(object.supplemental_files.first.file).to be_attached end + + context 'with mime type that does not match extension' do + let(:tags) { ['caption'] } + let(:extension) { 'srt' } + let(:uploaded_file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.srt'), 'text/plain') } + it "creates a SupplementalFile with correct content_type" do + expect{ + post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes_with_tags, format: :json}, session: valid_session + }.to change { object.reload.supplemental_files.size }.by(1) + expect(response).to have_http_status(:created) + expect(response.location).to eq "/#{object_class.model_name.plural}/#{object.id}/supplemental_files/#{assigns(:supplemental_file).id}" + + expect(object.supplemental_files.first.id).to eq 1 + expect(object.supplemental_files.first.label).to eq 'label' + expect(object.supplemental_files.first.tags).to eq tags + expect(object.supplemental_files.first.file).to be_attached + expect(object.supplemental_files.first.file.content_type).to eq Mime::Type.lookup_by_extension(extension) + end + end end context "with invalid params" do diff --git a/yarn.lock b/yarn.lock index a5a9623882..7516286e5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,6 +25,14 @@ "@babel/highlight" "^7.22.10" chalk "^2.4.2" +"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== + dependencies: + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" + "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" @@ -61,6 +69,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.1.tgz#e67e06f68568a4ebf194d1c6014235344f0476d0" + integrity sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A== + dependencies: + "@babel/types" "^7.24.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.14.5": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz#3d0e43b00c5e49fdb6c57e421601a7a658d5f835" @@ -128,6 +146,11 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-environment-visitor@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" @@ -141,6 +164,14 @@ "@babel/template" "^7.22.5" "@babel/types" "^7.22.5" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -241,11 +272,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-string-parser@^7.23.4": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== + "@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9": version "7.15.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -297,11 +338,26 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" + integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.22.10", "@babel/parser@^7.22.5": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== +"@babel/parser@^7.24.0", "@babel/parser@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.1.tgz#1e416d3627393fab1cb5b0f2f1796a100ae9133a" + integrity sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" @@ -1028,10 +1084,10 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.3", "@babel/runtime@^7.9.2": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" - integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== +"@babel/runtime@^7.1.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.3", "@babel/runtime@^7.9.2": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57" + integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ== dependencies: regenerator-runtime "^0.14.0" @@ -1042,6 +1098,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.12.5": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" + integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" @@ -1049,6 +1112,15 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/template@^7.22.15": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" + integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" + "@babel/template@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -1059,19 +1131,19 @@ "@babel/types" "^7.22.5" "@babel/traverse@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" - integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== - dependencies: - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" + integrity sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ== + dependencies: + "@babel/code-frame" "^7.24.1" + "@babel/generator" "^7.24.1" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.10" - "@babel/types" "^7.22.10" - debug "^4.1.0" + "@babel/parser" "^7.24.1" + "@babel/types" "^7.24.0" + debug "^4.3.1" globals "^11.1.0" "@babel/types@^7.14.9", "@babel/types@^7.15.4", "@babel/types@^7.4.4": @@ -1091,6 +1163,15 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@babel/types@^7.23.0", "@babel/types@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" + integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== + dependencies: + "@babel/helper-string-parser" "^7.23.4" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -1179,6 +1260,15 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" @@ -1194,6 +1284,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.3": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" @@ -1220,6 +1315,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -1337,10 +1440,10 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@samvera/ramp/-/ramp-3.0.0.tgz#41251e0fb30ceb86d5d3905d27d9fa8089c193fe" - integrity sha512-Hapi2XJhNUHdye0XyRryBU8DlA51jvE7uDUy+cRflnjxDKMZuO6AqgqrZG0SRraS1ziXa1o5t2awAqzEzRi/KQ== +"@samvera/ramp@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@samvera/ramp/-/ramp-3.1.0.tgz#6254646f34b9a434ff6e6046f1e013130d036e28" + integrity sha512-B5UiU0DDxi7Ub+DfbKC7j80zjMCmkuLnmDJNUqnftjO1L2gocdMOuUfYZ30pAh9SO9HbMCEtAFtLUHPlizPz3A== dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.2.4" @@ -1985,11 +2088,11 @@ available-typed-arrays@^1.0.5: integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== axios@^1.6.0: - version "1.6.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" - integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== + version "1.6.8" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" + integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== dependencies: - follow-redirects "^1.15.4" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -2628,6 +2731,13 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" +debug@^4.3.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + deepmerge@^4.0, deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -3049,15 +3159,10 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" -follow-redirects@^1.0.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -follow-redirects@^1.15.4: - version "1.15.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" - integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== +follow-redirects@^1.0.0, follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-each@^0.3.3: version "0.3.3" @@ -3277,9 +3382,9 @@ he@^1.2.0: integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== hls.js@^1.1.2: - version "1.5.5" - resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.5.tgz#8791a1d9e88d0d3f0bb023b573cfd54dea68bff7" - integrity sha512-IMio4uloZNDoBLXUMup2dW0/wOSe+0lHL9qYfRV4oUcPQ/nkTBQibG5h4OPTXOTXSI3iUwcon7TFgNb6uEoKIQ== + version "1.5.7" + resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.5.7.tgz#e069e78fe962a422d16aa17a2bfc2f1e2321089d" + integrity sha512-Hnyf7ojTBtXHeOW1/t6wCBJSiK1WpoKF9yg7juxldDx8u3iswrkPt2wbOA/1NiwU4j27DSIVoIEJRAhcdMef/A== "hls.js@https://github.com/avalonmediasystem/hls.js#stricter_ts_probing": version "0.13.1" @@ -3814,9 +3919,9 @@ kind-of@^6.0.2, kind-of@^6.0.3: integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== konva@^9.0.0: - version "9.3.3" - resolved "https://registry.yarnpkg.com/konva/-/konva-9.3.3.tgz#b4e719c287fd8ffa88360e25e2fd3201d6896f7a" - integrity sha512-cg/AHxnfawZ1rKxygCnzx0TZY7hQiQiAKgAHPinEwMn49MVrBkeKLj2d0EaleoFG/0y0XhEKTD0dFZiPPdWlCQ== + version "9.3.6" + resolved "https://registry.yarnpkg.com/konva/-/konva-9.3.6.tgz#62b36292dbe06c56eb161d5ead221c2b5c5a8926" + integrity sha512-dqR8EbcM0hjuilZCBP6xauQ5V3kH3m9kBcsDkqPypQuRgsXbcXUrxqYxhNbdvKZpYNW8Amq94jAD/C0NY3qfBQ== launch-editor@^2.6.0: version "2.6.0" @@ -3955,9 +4060,9 @@ make-dir@^3.0.2, make-dir@^3.1.0: semver "^6.0.0" mammoth@^1.4.19: - version "1.7.0" - resolved "https://registry.yarnpkg.com/mammoth/-/mammoth-1.7.0.tgz#e59b2d89a4907595906429c9d3313729d67afca7" - integrity sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw== + version "1.7.1" + resolved "https://registry.yarnpkg.com/mammoth/-/mammoth-1.7.1.tgz#8e0b19ba2ce6a0c364e3ea7afa0ecfe67b7f33d3" + integrity sha512-ckxfvNH5sUaJh+SbYbxpvB7urZTGS02jA91rFCNiL928CgE9FXXMyXxcJBY0n+CpmKE/eWh7qaV0+v+Dbwun3Q== dependencies: "@xmldom/xmldom" "^0.8.6" argparse "~1.0.3" @@ -4136,11 +4241,6 @@ nanocolors@^0.2.12: resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.2.12.tgz#4d05932e70116078673ea4cc6699a1c56cc77777" integrity sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -4671,23 +4771,14 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11: - version "8.4.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" - integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== +postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.24: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== dependencies: nanoid "^3.3.7" picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.4.21, postcss@^8.4.24: - version "8.4.28" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.28.tgz#c6cc681ed00109072816e1557f889ef51cf950a5" - integrity sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" + source-map-js "^1.2.0" pretty-error@^3.0.4: version "3.0.4" @@ -4831,9 +4922,9 @@ react-dom@^17.0.1: scheduler "^0.20.2" react-error-boundary@^4.0.11: - version "4.0.12" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.12.tgz#59f8f1dbc53bbbb34fc384c8db7cf4082cb63e2c" - integrity sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA== + version "4.0.13" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.13.tgz#80386b7b27b1131c5fbb7368b8c0d983354c7947" + integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ== dependencies: "@babel/runtime" "^7.12.5" @@ -4878,9 +4969,9 @@ react-redux@^7.2.6: prop-types "^15.7.2" react-is "^17.0.2" -"react-structural-metadata-editor@https://github.com/avalonmediasystem/react-structural-metadata-editor#avalon-7.7": - version "1.2.0" - resolved "https://github.com/avalonmediasystem/react-structural-metadata-editor#5ed306b30e762cb5b955a03142db13f125665c59" +"react-structural-metadata-editor@https://github.com/avalonmediasystem/react-structural-metadata-editor#avalon-7.7.1": + version "2.0.0" + resolved "https://github.com/avalonmediasystem/react-structural-metadata-editor#482222931c842ce864aa35cc7c19c9a09a0fe0c0" dependencies: "@babel/runtime" "^7.4.4" "@fortawesome/fontawesome-svg-core" "^1.2.4" @@ -5183,9 +5274,9 @@ safe-json-parse@4.0.0: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sanitize-html@^2.10.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.11.0.tgz#9a6434ee8fcaeddc740d8ae7cd5dd71d3981f8f6" - integrity sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA== + version "2.13.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae" + integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -5268,12 +5359,7 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" -semver@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^6.3.1: +semver@^6.0.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -5418,11 +5504,16 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + source-map-support@~0.5.12: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -5884,9 +5975,9 @@ webpack-cli@4: webpack-merge "^5.7.3" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3"