Skip to content

Commit

Permalink
Merge pull request #5694 from avalonmediasystem/playlist_position
Browse files Browse the repository at this point in the history
Support positional URLs for playlist items
  • Loading branch information
masaball authored Mar 5, 2024
2 parents 62d9d86 + a1570e6 commit a142cb5
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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', git: 'https://github.com/samvera/iiif_manifest.git', branch: 'canvas-homepage'
gem 'rack-cors', require: 'rack/cors'
gem 'rails_same_site_cookie'
gem 'recaptcha', require: 'recaptcha/rails'
Expand Down
12 changes: 9 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ GIT
ims-lti
omniauth

GIT
remote: https://github.com/samvera/iiif_manifest.git
revision: e7aea3ab9614f645ccdb760f78954a7ef2cd893c
branch: canvas-homepage
specs:
iiif_manifest (1.4.0)
activesupport (>= 4)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -494,8 +502,6 @@ GEM
i18n (1.14.1)
concurrent-ruby (~> 1.0)
iconv (1.0.8)
iiif_manifest (1.4.0)
activesupport (>= 4)
ims-lti (1.1.13)
builder
oauth (>= 0.4.5, < 0.6)
Expand Down Expand Up @@ -1037,7 +1043,7 @@ DEPENDENCIES
httpx
hydra-head (~> 12.0)
iconv (~> 1.0.6)
iiif_manifest (>= 1.4.0)
iiif_manifest!
ims-lti (~> 1.1.13)
jbuilder (~> 2.0)
jquery-datatables
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/playlists_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions app/javascript/components/PlaylistRamp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,35 @@ const ExpandCollapseArrow = () => {
};

const Ramp = ({
base_url,
urls,
playlist_id,
playlist_item_ids,
token,
share,
comment_tag
}) => {
const [manifestUrl, setManifestUrl] = React.useState('');
const [activeItemTitle, setActiveItemTitle] = React.useState();
const [activeItemSummary, setActiveItemSummary] = React.useState();
const [startCanvasId, setStartCanvasId] = React.useState();

let interval;

const USER_AGENT = window.navigator.userAgent;
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);
Expand Down Expand Up @@ -84,7 +95,9 @@ const Ramp = ({
};

return (
<IIIFPlayer manifestUrl={manifestUrl} customErrorMessage='This playlist is empty.'>
<IIIFPlayer manifestUrl={manifestUrl}
customErrorMessage='This playlist is empty.'
startCanvasId={startCanvasId}>
<Row className="ramp--all-components ramp--playlist">
<Col sm={8}>
<MediaPlayer enableFileDownload={false} />
Expand Down
13 changes: 11 additions & 2 deletions app/models/iiif_playlist_canvas_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion app/views/playlists/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ Unless required by applicable law or agreed to in writing, software distributed
<div class="col-sm-12 px-0">
<%= 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') }
Expand Down
77 changes: 44 additions & 33 deletions spec/controllers/playlists_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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') }
Expand All @@ -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
Expand Down Expand Up @@ -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
14 changes: 13 additions & 1 deletion spec/models/iiif_playlist_canvas_presenter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit a142cb5

Please sign in to comment.