Integrate Wikidata card for music
authorDan Scott <dscott@laurentian.ca>
Tue, 23 May 2017 03:26:28 +0000 (23:26 -0400)
committerDan Scott <dscott@laurentian.ca>
Thu, 31 May 2018 20:21:28 +0000 (16:21 -0400)
Wikidata music infocard with Wikipedia description

Signed-off-by: Dan Scott <dscott@laurentian.ca>
Open-ILS/src/templates/opac/parts/js.tt2
Open-ILS/web/js/ui/default/opac/wikidata_music_card.js [new file with mode: 0644]

index 01fb9f9..229b87e 100644 (file)
@@ -143,3 +143,6 @@ var aou_hash = {
 
 <script type="text/javascript">if ($('client_tz_id')) { $('client_tz_id').value = OpenSRF.tz }</script>
 [%- END; # want_dojo -%]
+[% IF ctx.page == 'record' %]
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/opac/wikidata_music_card.js"></script>
+[% 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 (file)
index 0000000..9a72922
--- /dev/null
@@ -0,0 +1,289 @@
+;(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 = '[typeof~="MusicAlbum"]';
+
+  /* List of elements describing contributors */
+  var contributors_path = 'div[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 = '#contributors';
+
+  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() + " ";
+    }
+    /* Trim final space */
+    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 <https://en.wikipedia.org/> . ' +
+        '} ' +
+        '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);
+  }
+})()