From e17b38aa35c2d604fd0a88aab21c4a12a539a9d8 Mon Sep 17 00:00:00 2001 From: Dan Scott Date: Mon, 22 May 2017 23:26:28 -0400 Subject: [PATCH] Generate Wikidata cards for multiple performers and ensembles Uses real-time Wikipedia description lookups, in English-only, to provide the first sentence of the Wikipedia description of the performer, as well as a link to the related Wikipedia page via the WP icon. Wikicards are built up from properties using a generic function; see the Twitter, Facebook, and Musicbrainz properties for examples. Signed-off-by: Dan Scott --- Open-ILS/src/templates/opac/parts/js.tt2 | 5 +- .../web/js/ui/default/opac/wikidata_music_card.js | 288 ++++++++++++++++++ test_wikidata.html | 331 +++++++++++++++++++++ 3 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/web/js/ui/default/opac/wikidata_music_card.js create mode 100644 test_wikidata.html diff --git a/Open-ILS/src/templates/opac/parts/js.tt2 b/Open-ILS/src/templates/opac/parts/js.tt2 index 74a9bd49cf..679a488b0f 100644 --- a/Open-ILS/src/templates/opac/parts/js.tt2 +++ b/Open-ILS/src/templates/opac/parts/js.tt2 @@ -150,10 +150,13 @@ var aou_hash = { [%- END; # want_dojo -%] +[%- IF ctx.page == 'record' %] + +[% END -%] + [%- IF ctx.max_cart_size; %] [%- END; %] - diff --git a/Open-ILS/web/js/ui/default/opac/wikidata_music_card.js b/Open-ILS/web/js/ui/default/opac/wikidata_music_card.js new file mode 100644 index 0000000000..2e4bb13afc --- /dev/null +++ b/Open-ILS/web/js/ui/default/opac/wikidata_music_card.js @@ -0,0 +1,288 @@ +;(function () { + /** + * Display an infocard with data pulled from Wikidata + * + * Copyright 2017,2018 Dan Scott (dan@coffeecode.net), Laurentian University + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 2 of the License, or (at your + * option) any later version. + * + * Based on the primary contributor's name, search Wikidata via SPARQL for + * musicians or bands using an exact string match of the label or alias, + * and retrieve the data of interest. + * + * Current limitations: + * * Only activated for musical recordings, to avoid ambiguous results + * * Hard-coded for English + * + * To use this in your catalogue, you will need to modify a few of the CSS + * queries to find the contributors, their names, and identify appropriate + * locations for inserting the clickable note icon and the infocard. + */ + + /* Path for the schema.org type declaration */ + var type_path = 'div[typeof~="MusicAlbum"]'; + + /* List of elements describing contributors */ + var contributors_path = 'span[resource^="#schemacontrib"]'; + + /* Path to find the name within each contributor element */ + var contributor_name = 'span[property="name"]'; + + /* Within each contributor element, where we can append a Wikidata clickable icon */ + var name_path = 'span[property="description"]'; + + /* Path for appending the requested infocards */ + var infocard_location = '#rdetail_title+div'; + + var icon_node; + var note; + var img; + var wd; + + /* Ensure this is a musical recording, based on http://schema.org/MusicAlbum */ + if (document.querySelector(type_path)) { + var contributor_nodes = document.querySelectorAll(contributors_path); + for (var node of contributor_nodes) { + /* Insert clickable icon here */ + icon_node = node.querySelector(name_path); + if (icon_node) { + img = document.createElement('img'); + img.src = 'https://www.wikidata.org/static/favicon/wikidata.ico'; + img.alt = 'Show data from Wikidata'; + img.class = 'wikidata'; + img.style.width = '1em'; + img.style.paddingLeft = '0.5em'; + img.style.verticalAlign = 'middle'; + img.addEventListener('click', perform, { once: true }); + node.insertBefore(img, icon_node.nextSibling); + } + } + } + + function getContribName(node) { + var raw_name = node.querySelector(contributor_name).textContent.trim(); + var lastchar = raw_name[raw_name.length - 1]; + var entity_name = ''; + if (lastchar === '.' || lastchar === ',') { + raw_name = raw_name.slice(0, -1); + } + /* Change "Silver, Liberty" to "Liberty Silver" for the lookup */ + var inverse = raw_name.split(','); + for (x in inverse.reverse()) { + entity_name += inverse[x].trim() + " "; + } + return entity_name.trim(); + } + + function perform(e) { + var entity_name = getContribName(this.parentNode); + findPerformer(icon_node, entity_name); + e.preventDefault(); + e.stopPropagation(); + } + + function findPerformer(icon_node, entity_name) { + var url = 'https://query.wikidata.org/sparql'; + var query = 'SELECT DISTINCT ?item ?itemLabel ?itemDescription ?image ' + + '(GROUP_CONCAT(DISTINCT ?instrumentLabel;separator="; ") AS ?instruments) ' + + '?birthPlace ?birthPlaceLabel ' + + '?website ?musicbrainz ?songKick ?twitter ?facebook ?wplink ' + + 'WHERE { ' + + '?item rdfs:label|skos:altLabel|wdt:P1449 "' + entity_name + '"@en . ' + + '{ ?item wdt:P31/wdt:P279* wd:Q215380 . } ' + // instance of = any subclass of band + 'UNION ' + + '{ ?item wdt:P106/wdt:P279* wd:Q639669 . } ' + // occupation = any subclass of musician + 'UNION ' + + '{ ?item wdt:P31/wdt:P279* wd:Q2088357 . } ' + // instance of = any subclass of musical ensemble + 'OPTIONAL { ?item wdt:P3478 ?songKick } . ' + + 'OPTIONAL { ?item wdt:P19 ?birthPlace } . ' + + 'OPTIONAL { ?item wdt:P1303 ?instrument } . ' + + 'OPTIONAL { ?item wdt:P856 ?website } . ' + + 'OPTIONAL { ?item wdt:P434 ?musicbrainz } . ' + + 'OPTIONAL { ?item wdt:P2002 ?twitter } . ' + + 'OPTIONAL { ?item wdt:P2013 ?facebook } . ' + + 'OPTIONAL { ?item wdt:P18 ?image } . ' + + 'OPTIONAL { ' + + '?wplink schema:about ?item . ' + + '?wplink schema:inLanguage "en" . ' + + '?wplink schema:isPartOf . ' + + '} ' + + 'SERVICE wikibase:label { ' + + ' bd:serviceParam wikibase:language "en". ' + + ' ?instrument rdfs:label ?instrumentLabel. ' + + ' ?item rdfs:label ?itemLabel. ' + + ' ?item schema:description ?itemDescription. ' + + ' ?birthPlace rdfs:label ?birthPlaceLabel ' + + '} ' + + '} ' + + 'GROUP BY ?item ?itemLabel ?itemDescription ?image ?birthPlace ?birthPlaceLabel ?website ?musicbrainz ?songKick ?twitter ?facebook ?wplink' + 'LIMIT 10'; + var q = '?query=' + encodeURIComponent(query); + + var req = new window.XMLHttpRequest(); + req.open('GET', url + q); + req.setRequestHeader('Accept', 'application/sparql-results+json'); + if (req.responseType && (req.responseType = 'json')) { + req.onload = function (evt) { + var r = req.response.results.bindings[0]; + if (r !== undefined) { + generateCard(icon_node, r); + // console.log(r); + } + } + } else { + // IE 10/11 + req.onload = function (evt) { + var r = JSON.parse(req.responseText).results.bindings[0]; + if (r !== undefined) { + generateCard(icon_node, r); + // console.log(r); + } + } + } + req.send(); + } + + function addWikipedia(musician, r) { + var wpapi = 'https://en.wikipedia.org/w/api.php?origin=*&format=json&action=query&prop=extracts&explaintext=true&exintro=true&titles='; + var wplink = r.wplink.value; + // Strip the WP title from the link + wptitle = wplink.substring(wplink.lastIndexOf('/') + 1); + + var req = new window.XMLHttpRequest(); + req.open('GET', wpapi + wptitle); + req.onload = function (evt) { + var r = JSON.parse(req.response); + var k = Object.getOwnPropertyNames(r.query.pages)[0]; + var description = r.query.pages[k].extract; + + // Just the first line will do + var linefeed = description.indexOf('\n'); + var period = description.indexOf('. '); + if (linefeed > 0) { + description = description.substring(0, linefeed); + } else if (period > 0) { + description = description.substring(0, period + 1); + } + var wdd = document.createElement('div'); + wdd.style['margin-top'] = '1em'; + var wpa = document.createElement('a'); + wpa.href = wplink; + + // Link to Wikipedia via their icon + var img = document.createElement('img'); + img.src = 'https://en.wikipedia.org/static/favicon/wikipedia.ico'; + img.alt = 'View on Wikipedia'; + img.class = 'wikidata'; + img.style.width = '1em'; + img.style.paddingLeft = '0.5em'; + + wpa.appendChild(img); + wdd.innerText = description; + wdd.appendChild(wpa); + musician.appendChild(wdd); + } + req.send(); + } + + function generateCard(icon_node, r) { + var auth_div = document.querySelector(infocard_location); + var musician = document.createElement('div'); + var uri = r.item.value; + var wdid = uri.substr(uri.lastIndexOf('/') + 1); + musician.id = 'musician_' + wdid; + musician.style.padding = '0.5em 1em 1em 1em'; + musician.style.border = 'thin blue solid'; + musician.style.overflow = 'hidden'; + + addWDValue('itemLabel', null, null, false); + + if (r.hasOwnProperty('image')) { + var img = document.createElement('img'); + img.src = r.image.value.replace('http:', 'https:'); + img.style.float = 'left'; + img.style.width = '150px'; + img.style['margin-right'] = '1em'; + musician.appendChild(img); + } + + if (r.hasOwnProperty('itemDescription')) { + var description = r.itemDescription.value; + var wdd = document.createElement('div'); + var wddl = document.createElement('label'); + wddl.innerText = description; + wdd.appendChild(wddl); + musician.appendChild(wdd); + } + + // Get the description from Wikipedia + if (r.hasOwnProperty('wplink')) { + addWikipedia(musician, r); + } + + function addWDValue(property, propertyLabel, label, isLink, linkFormatter) { + var value = ''; + if (r.hasOwnProperty(property) && r[property].value.trim()) { + value = r[property].value; + var valueDiv = document.createElement('div'); + var labelText = value; + if (!propertyLabel && !label && !isLink) { + var strong = document.createElement('strong'); + valueDiv.appendChild(strong); + strong.innerText = labelText; + } + if (propertyLabel) { + var valueLabel = document.createElement('label'); + valueLabel.innerText = propertyLabel; + valueDiv.appendChild(valueLabel); + } + if (label) { + if (r.hasOwnProperty(label)) { + labelText = r[label].value; + } else { + labelText = label; + } + } + if (isLink) { + var valueLink = document.createElement('a'); + if (linkFormatter) { + valueLink.href = linkFormatter(value); + } else { + valueLink.href = value; + } + valueLink.innerText = labelText; + valueDiv.appendChild(valueLink); + } else if (label !== null) { + var valueText = document.createElement('span'); + valueText.innerText = labelText; + valueDiv.appendChild(valueText); + } + musician.appendChild(valueDiv); + } + } + + addWDValue('instruments', 'Instruments: ', 'instruments', false); + addWDValue('birthPlace', 'Birth place: ', 'birthPlaceLabel', true); + addWDValue('website', 'Web site: ', null, true); + addWDValue('musicbrainz', null, 'Discography (Musicbrainz)', true, function(value) { return 'https://musicbrainz.org/artist/' + value }); + addWDValue('songKick', null, 'Tour dates (Songkick)', true, function(value) { return 'http://www.songkick.com/artists/' + value }); + addWDValue('twitter', 'Twitter: ', null, true, function(value) { return 'https://twitter.com/' + value }); + addWDValue('facebook', 'Facebook: ', null, true, function(value) { return 'https://www.facebook.com/' + value }); + + var wd = document.createElement('div'); + var wdl = document.createElement('label'); + wdl.innerText = 'Edit on Wikidata: '; + var wdv = document.createElement('a'); + wdv.href = uri; + wdv.innerText = wdid; + wd.appendChild(wdl); + wd.appendChild(wdv); + musician.appendChild(wd); + + /* Append the Wikidata infocard to the page */ + auth_div.appendChild(musician); + } +})() diff --git a/test_wikidata.html b/test_wikidata.html new file mode 100644 index 0000000000..3383a22d58 --- /dev/null +++ b/test_wikidata.html @@ -0,0 +1,331 @@ + + + + + + + + + The 1985 Juno Awards collection - Laurentian University + + + + + + + + + + + +

Catalog

+ + +
+ +
+ + + +

Record Details

+

Catalog Search

+ +
+ +
+ +
+ + + + + +
+ +
+
+

The 1985 Juno Awards collection

+ +
Adams, Bryan (Singer). Hart, Corey (Singer). Silver, Liberty (Singer). McLauchlan, Murray (Singer). Thompson, Don (Instrumentalist). Luba (Singer). Janz, Paul (Singer). lang, k.d. (Kathryn Dawn), 1961- (Singer). Idle Eyes (Performer). The Family Brown (Performer). Parachute Club (Performer). Oxford String Quartet (Performer). +
+ + + +
+
+ Image of item + +
+
+
+ + + + Phonograph music recording + Phonograph music recording + + +
+
+ +
+
+ Print / Email Actions Image + Print / + Email +
+ + + +
+
+
+ +
+

Available copies

+
    +
  • + 1 of 1 copy available at Conifer. + (Show) +
  • +
  • + 1 of 1 copy available at Laurentian University. +
  • +
+
+ + +

Current holds

+

+ 0 current holds with 1 total copy. +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LocationCall Number / Copy NotesBarcodeShelving LocationStatusDue Date
J.N. Desmarais Library + + + LP 3689 + 30007006013980 Long-play records (3rd floor)Available-
+
+
+ + +
+ +

Record details

+
    +
  • + Physical Description: + 1 sound disc : 33 1/3 rpm, stereo +
  • +
  • + Publisher: + + Montréal, Qué. : + Radio Canada International, + + 1986.
  • +
+ + +

Content descriptions

+ + + + + + +
Formatted Contents Note: Heaven / Adams -- Never surrender / Hart -- Lost somewhere inside your love / Silver -- Railroad man / McLauchlan -- Tokyo Rose / Idle Eyes -- I've never been in love before / Thompson -- Let it go / Luba -- Go to pieces / Janz -- Wouldn't you love us together again / The Family Brown -- At the feet of the moon / Parachute Club -- Pine and stew / lang -- Minuetto from Quartet in B flat major / Mozart.
+ + + + + + + + + + + + +
Subject: Popular music > Canada.
String quartets.
Country music > Canada.
+ + + +
+
+ +
+
+ +
+
+ + + + +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ + + +
+ +
+
+
+
+

Additional Resources

+ + + + + + + -- 2.11.0