/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */ /* vim: set et ts=4 sw=4: */ /* * Copyright (c) 2017 Marcus Lundblad * * GNOME Maps 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. * * GNOME Maps is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License along * with GNOME Maps; if not, see . * * Author: Marcus Lundblad */ const GdkPixbuf = imports.gi.GdkPixbuf; const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Soup = imports.gi.Soup; const Format = imports.format; const Utils = imports.utils; /** * Regex matching editions of Wikipedia, e.g. "en", "arz", pt-BR", "simple". * See https://en.wikipedia.org/wiki/List_of_Wikipedias "WP code". */ const WP_REGEX = /^[a-z][a-z][a-z]?(\-[a-z]+)?$|^simple$/; let _soupSession = null; function _getSoupSession() { if (_soupSession === null) { _soupSession = new Soup.Session({ user_agent : 'gnome-maps/' + pkg.version }); } return _soupSession; } let _thumbnailCache = {}; let _metadataCache = {}; function getLanguage(wiki) { return wiki.split(':')[0]; } function getArticle(wiki) { return Soup.uri_encode(wiki.replace(/ /g, '_').split(':').splice(1).join(':'), '\''); } function getHtmlEntityEncodedArticle(wiki) { return GLib.markup_escape_text(wiki.split(':').splice(1).join(':'), -1); } /** * Determine if a Wikipedia reference tag is valid * (of the form "lang:Article title") */ function isValidWikipedia(wiki) { let parts = wiki.split(':'); if (parts.length < 2) return false; let wpCode = parts[0]; return wpCode.match(WP_REGEX) !== null; } /* * Fetch various metadata about a Wikipedia article, given the wiki language * and article title. * * @size is the maximum width of the thumbnail. * * Calls @metadataCb with the lang:title pair for the article and an object * containing information about the article. For the keys/values of this * object, see the relevant MediaWiki API documentation. * * Calls @thumbnailCb with the Gdk.Pixbuf of the icon when successful, otherwise * null. */ function fetchArticleInfo(wiki, size, metadataCb, thumbnailCb) { let lang = getLanguage(wiki); let title = getHtmlEntityEncodedArticle(wiki); let uri = Format.vprintf('https://%s.wikipedia.org/w/api.php', [ lang ]); let msg = Soup.form_request_new_from_hash('GET', uri, { action: 'query', titles: title, prop: 'extracts|pageimages|langlinks', format: 'json', /* Allow redirects, for example if an article is renamed. */ redirects: '1', /* Make sure we get all lang links */ lllimit: 'max', /* don't go past first section header */ exintro: 'yes', /* limit the length */ exchars: '200', /* for plain text rather than HTML */ explaintext: 'yes', pithumbsize: size + ''}); let session = _getSoupSession(); let cachedMetadata = _metadataCache[wiki]; if (cachedMetadata) { _onMetadataFetched(wiki, cachedMetadata, size, metadataCb, thumbnailCb); return; } session.queue_message(msg, (session, msg) => { if (msg.status_code !== Soup.KnownStatusCode.OK) { log("Failed to request Wikipedia metadata: " + msg.reason_phrase); metadataCb(null, {}); if (thumbnailCb) { thumbnailCb(null); } return; } let response = JSON.parse(msg.response_body.data); let pages = response.query.pages; if (pages) { /* we know there should be only one object instance in the "pages" * object, but the API specifies the sub-object as the page ID, * so we'll have to use this iteration approach here */ for (let pageId in pages) { let page = pages[pageId]; _metadataCache[wiki] = page; _onMetadataFetched(wiki, page, size, metadataCb, thumbnailCb); return; } } else { metadataCb(null, {}); if (thumbnailCb) { thumbnailCb(null); } } }); } function _onMetadataFetched(wiki, page, size, metadataCb, thumbnailCb) { /* Try to get a thumbnail *before* following language links--the primary article probably has the best thumbnail image */ if (thumbnailCb && page.thumbnail) { let source = page.thumbnail.source; _fetchThumbnailImage(wiki, size, source, thumbnailCb); thumbnailCb = null; } /* Follow language links if necessary */ let langlink = _findLanguageLink(wiki, page); if (langlink) { fetchArticleInfo(langlink, size, metadataCb, thumbnailCb); } else { metadataCb(wiki, page); if (thumbnailCb) { thumbnailCb(null); } } } function _fetchThumbnailImage(wiki, size, source, callback) { let uri = new Soup.URI(source); let msg = new Soup.Message({ method: 'GET', uri: uri }); let session = _getSoupSession(); let cachedThumbnail = _thumbnailCache[wiki + '/' + size]; if (cachedThumbnail) { callback(cachedThumbnail); return; } session.queue_message(msg, (session, msg) => { if (msg.status_code !== Soup.KnownStatusCode.OK) { log("Failed to download thumbnail: " + msg.reason_phrase); callback(null); return; } let contents = msg.response_body_data; let stream = Gio.MemoryInputStream.new_from_bytes(contents); try { let pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null); _thumbnailCache[wiki + '/' + size] = pixbuf; callback(pixbuf); } catch(e) { log("Failed to load pixbuf: " + e); callback(null); } stream.close(null); }); } /* Finds the best language to use, based on the language of the original article and the langlinks data from the Wikipedia API. Returns a lang:title string if that article should be used, or undefined if the original article should be used. */ function _findLanguageLink(wiki, page) { let originalLang = getLanguage(wiki); let languages = GLib.get_language_names().map((lang) => lang.split(/[\._\-]/)[0]); if (!languages.includes(originalLang)) { let langlinks = {}; for (let langlink of (page.langlinks || [])) { langlinks[langlink.lang] = langlink["*"]; } for (let language of languages) { if (language in langlinks) { return language + ":" + langlinks[language]; } } } }