--- /dev/null
+project(AniDbTitleSearch)
+
+cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR)
+if (POLICY CMP0043)
+ cmake_policy(SET CMP0043 NEW)
+endif()
+
+find_package(Qt5 COMPONENTS Core Network Sql REQUIRED)
+find_package(CutelystQt5 REQUIRED)
+
+# Auto generate moc files
+set(CMAKE_AUTOMOC ON)
+
+# As moc files are generated in the binary dir, tell CMake
+# to always look for includes there:
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+# Enable C++11 features
+add_definitions(-std=c++11)
+
+include_directories(
+ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CutelystQt5_INCLUDE_DIR}
+)
+
+file(GLOB_RECURSE TEMPLATES_SRC root/*)
+
+add_subdirectory(src)
--- /dev/null
+/*!***************************************************\r
+ * mark.js v8.4.0\r
+ * https://github.com/julmot/mark.js\r
+ * Copyright (c) 2014–2016, Julian Motz\r
+ * Released under the MIT license https://git.io/vwTVl\r
+ *****************************************************/\r
+"use strict";function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}var _extends=Object.assign||function(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)Object.prototype.hasOwnProperty.call(c,d)&&(a[d]=c[d])}return a},_createClass=function(){function a(a,b){for(var c=0;c<b.length;c++){var d=b[c];d.enumerable=d.enumerable||!1,d.configurable=!0,"value"in d&&(d.writable=!0),Object.defineProperty(a,d.key,d)}}return function(b,c,d){return c&&a(b.prototype,c),d&&a(b,d),b}}(),_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol?"symbol":typeof a};!function(a,b,c){"function"==typeof define&&define.amd?define(["jquery"],function(d){return a(b,c,d)}):"object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports?module.exports=a(b,c,require("jquery")):a(b,c,jQuery)}(function(a,b,c){var d=function(){function c(a){_classCallCheck(this,c),this.ctx=a}return _createClass(c,[{key:"log",value:function a(b){var c=arguments.length<=1||void 0===arguments[1]?"debug":arguments[1],a=this.opt.log;this.opt.debug&&"object"===("undefined"==typeof a?"undefined":_typeof(a))&&"function"==typeof a[c]&&a[c]("mark.js: "+b)}},{key:"escapeStr",value:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createRegExp",value:function(a){return a=this.escapeStr(a),Object.keys(this.opt.synonyms).length&&(a=this.createSynonymsRegExp(a)),this.opt.ignoreJoiners&&(a=this.setupIgnoreJoinersRegExp(a)),this.opt.diacritics&&(a=this.createDiacriticsRegExp(a)),a=this.createMergedBlanksRegExp(a),this.opt.ignoreJoiners&&(a=this.createIgnoreJoinersRegExp(a)),a=this.createAccuracyRegExp(a)}},{key:"createSynonymsRegExp",value:function(a){var b=this.opt.synonyms,c=this.opt.caseSensitive?"":"i";for(var d in b)if(b.hasOwnProperty(d)){var e=b[d],f=this.escapeStr(d),g=this.escapeStr(e);a=a.replace(new RegExp("("+f+"|"+g+")","gm"+c),"("+f+"|"+g+")")}return a}},{key:"setupIgnoreJoinersRegExp",value:function(a){return a.replace(/[^(|)]/g,function(a,b,c){var d=c.charAt(b+1);return/[(|)]/.test(d)||""===d?a:a+"\0"})}},{key:"createIgnoreJoinersRegExp",value:function(a){return a.split("\0").join("[\\u00ad|\\u200b|\\u200c|\\u200d]?")}},{key:"createDiacriticsRegExp",value:function(a){var b=this.opt.caseSensitive?"":"i",c=this.opt.caseSensitive?["aàáâãäåāąă","AÀÁÂÃÄÅĀĄĂ","cçćč","CÇĆČ","dđď","DĐĎ","eèéêëěēę","EÈÉÊËĚĒĘ","iìíîïī","IÌÍÎÏĪ","lł","LŁ","nñňń","NÑŇŃ","oòóôõöøō","OÒÓÔÕÖØŌ","rř","RŘ","sšśș","SŠŚȘ","tťț","TŤȚ","uùúûüůū","UÙÚÛÜŮŪ","yÿý","YŸÝ","zžżź","ZŽŻŹ"]:["aÀÁÂÃÄÅàáâãäåĀāąĄăĂ","cÇçćĆčČ","dđĐďĎ","eÈÉÊËèéêëěĚĒēęĘ","iÌÍÎÏìíîïĪī","lłŁ","nÑñňŇńŃ","oÒÓÔÕÖØòóôõöøŌō","rřŘ","sŠšśŚșȘ","tťŤțȚ","uÙÚÛÜùúûüůŮŪū","yŸÿýÝ","zŽžżŻźŹ"],d=[];return a.split("").forEach(function(e){c.every(function(c){if(c.indexOf(e)!==-1){if(d.indexOf(c)>-1)return!1;a=a.replace(new RegExp("["+c+"]","gm"+b),"["+c+"]"),d.push(c)}return!0})}),a}},{key:"createMergedBlanksRegExp",value:function(a){return a.replace(/[\s]+/gim,"[\\s]*")}},{key:"createAccuracyRegExp",value:function(a){var b=this,c=this.opt.accuracy,d="string"==typeof c?c:c.value,e="string"==typeof c?[]:c.limiters,f="";switch(e.forEach(function(a){f+="|"+b.escapeStr(a)}),d){case"partially":default:return"()("+a+")";case"complementary":return"()([^\\s"+f+"]*"+a+"[^\\s"+f+"]*)";case"exactly":return"(^|\\s"+f+")("+a+")(?=$|\\s"+f+")"}}},{key:"getSeparatedKeywords",value:function(a){var b=this,c=[];return a.forEach(function(a){b.opt.separateWordSearch?a.split(" ").forEach(function(a){a.trim()&&c.indexOf(a)===-1&&c.push(a)}):a.trim()&&c.indexOf(a)===-1&&c.push(a)}),{keywords:c.sort(function(a,b){return b.length-a.length}),length:c.length}}},{key:"getTextNodes",value:function(a){var b=this,c="",d=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(a){d.push({start:c.length,end:(c+=a.textContent).length,node:a})},function(a){return b.matchesExclude(a.parentNode,!0)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){a({value:c,nodes:d})})}},{key:"matchesExclude",value:function(a,b){var c=this.opt.exclude.concat(["script","style","title","head","html"]);return b&&(c=c.concat(["*[data-markjs='true']"])),e.matches(a,c)}},{key:"wrapRangeInTextNode",value:function(a,c,d){var e=this.opt.element?this.opt.element:"mark",f=a.splitText(c),g=f.splitText(d-c),h=b.createElement(e);return h.setAttribute("data-markjs","true"),this.opt.className&&h.setAttribute("class",this.opt.className),h.textContent=f.textContent,f.parentNode.replaceChild(h,f),g}},{key:"wrapRangeInMappedTextNode",value:function(a,b,c,d,e){var f=this;a.nodes.every(function(g,h){var i=a.nodes[h+1];if("undefined"==typeof i||i.start>b){var j=function(){var i=b-g.start,j=(c>g.end?g.end:c)-g.start;if(d(g.node)){g.node=f.wrapRangeInTextNode(g.node,i,j);var k=a.value.substr(0,g.start),l=a.value.substr(j+g.start);if(a.value=k+l,a.nodes.forEach(function(b,c){c>=h&&(a.nodes[c].start>0&&c!==h&&(a.nodes[c].start-=j),a.nodes[c].end-=j)}),c-=j,e(g.node.previousSibling,g.start),!(c>g.end))return{v:!1};b=g.end}}();if("object"===("undefined"==typeof j?"undefined":_typeof(j)))return j.v}return!0})}},{key:"wrapMatches",value:function(a,b,c,d,e){var f=this,g=0===b?0:b+1;this.getTextNodes(function(b){b.nodes.forEach(function(b){b=b.node;for(var e=void 0;null!==(e=a.exec(b.textContent))&&""!==e[g];)if(c(e[g],b)){var h=e.index;if(0!==g)for(var i=1;i<g;i++)h+=e[i].length;b=f.wrapRangeInTextNode(b,h,h+e[g].length),d(b.previousSibling),a.lastIndex=0}}),e()})}},{key:"wrapMatchesAcrossElements",value:function(a,b,c,d,e){var f=this,g=0===b?0:b+1;this.getTextNodes(function(b){for(var h=void 0;null!==(h=a.exec(b.value))&&""!==h[g];){var i=h.index;if(0!==g)for(var j=1;j<g;j++)i+=h[j].length;var k=i+h[g].length;f.wrapRangeInMappedTextNode(b,i,k,function(a){return c(h[g],a)},function(b,c){a.lastIndex=c,d(b)})}e()})}},{key:"unwrapMatches",value:function(a){for(var c=a.parentNode,d=b.createDocumentFragment();a.firstChild;)d.appendChild(a.removeChild(a.firstChild));c.replaceChild(d,a),c.normalize()}},{key:"markRegExp",value:function(a,b){var c=this;this.opt=b,this.log('Searching with expression "'+a+'"');var d=0,e="wrapMatches",f=function(a){d++,c.opt.each(a)};this.opt.acrossElements&&(e="wrapMatchesAcrossElements"),this[e](a,this.opt.ignoreGroups,function(a,b){return c.opt.filter(b,a,d)},f,function(){0===d&&c.opt.noMatch(a),c.opt.done(d)})}},{key:"mark",value:function(a,b){var c=this;this.opt=b;var d=0,e="wrapMatches",f=this.getSeparatedKeywords("string"==typeof a?[a]:a),g=f.keywords,h=f.length,i=this.opt.caseSensitive?"":"i",j=function a(b){var f=new RegExp(c.createRegExp(b),"gm"+i),j=0;c.log('Searching with expression "'+f+'"'),c[e](f,1,function(a,e){return c.opt.filter(e,b,d,j)},function(a){j++,d++,c.opt.each(a)},function(){0===j&&c.opt.noMatch(b),g[h-1]===b?c.opt.done(d):a(g[g.indexOf(b)+1])})};this.opt.acrossElements&&(e="wrapMatchesAcrossElements"),0===h?this.opt.done(d):j(g[0])}},{key:"unmark",value:function(a){var b=this;this.opt=a;var c=this.opt.element?this.opt.element:"*";c+="[data-markjs]",this.opt.className&&(c+="."+this.opt.className),this.log('Removal selector "'+c+'"'),this.iterator.forEachNode(NodeFilter.SHOW_ELEMENT,function(a){b.unwrapMatches(a)},function(a){var d=e.matches(a,c),f=b.matchesExclude(a,!1);return!d||f?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},this.opt.done)}},{key:"opt",set:function(b){this._opt=_extends({},{element:"",className:"",exclude:[],iframes:!1,separateWordSearch:!0,diacritics:!0,synonyms:{},accuracy:"partially",acrossElements:!1,caseSensitive:!1,ignoreJoiners:!1,ignoreGroups:0,each:function(){},noMatch:function(){},filter:function(){return!0},done:function(){},debug:!1,log:a.console},b)},get:function(){return this._opt}},{key:"iterator",get:function(){return this._iterator||(this._iterator=new e(this.ctx,this.opt.iframes,this.opt.exclude)),this._iterator}}]),c}(),e=function(){function a(b){var c=arguments.length<=1||void 0===arguments[1]||arguments[1],d=arguments.length<=2||void 0===arguments[2]?[]:arguments[2];_classCallCheck(this,a),this.ctx=b,this.iframes=c,this.exclude=d}return _createClass(a,[{key:"getContexts",value:function(){var a=void 0,b=[];return a="undefined"!=typeof this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:[this.ctx]:[],a.forEach(function(a){var c=b.filter(function(b){return b.contains(a)}).length>0;b.indexOf(a)!==-1||c||b.push(a)}),b}},{key:"getIframeContents",value:function(a,b){var c=arguments.length<=2||void 0===arguments[2]?function(){}:arguments[2],d=void 0;try{var e=a.contentWindow;if(d=e.document,!e||!d)throw new Error("iframe inaccessible")}catch(a){c()}d&&b(d)}},{key:"onIframeReady",value:function(a,b,c){var d=this;try{!function(){var e=a.contentWindow,f="about:blank",g="complete",h=function(){var b=a.getAttribute("src").trim(),c=e.location.href;return c===f&&b!==f&&b},i=function(){var e=function e(){try{h()||(a.removeEventListener("load",e),d.getIframeContents(a,b,c))}catch(a){c()}};a.addEventListener("load",e)};e.document.readyState===g?h()?i():d.getIframeContents(a,b,c):i()}()}catch(a){c()}}},{key:"waitForIframes",value:function(a,b){var c=this,d=0;this.forEachIframe(a,function(){return!0},function(a){d++,c.waitForIframes(a.querySelector("html"),function(){--d||b()})},function(a){a||b()})}},{key:"forEachIframe",value:function(b,c,d){var e=this,f=arguments.length<=3||void 0===arguments[3]?function(){}:arguments[3],g=b.querySelectorAll("iframe"),h=g.length,i=0;g=Array.prototype.slice.call(g);var j=function(){--h<=0&&f(i)};h||j(),g.forEach(function(b){a.matches(b,e.exclude)?j():e.onIframeReady(b,function(a){c(b)&&(i++,d(a)),j()},j)})}},{key:"createIterator",value:function(a,c,d){return b.createNodeIterator(a,c,d,!1)}},{key:"createInstanceOnIframe",value:function(b){return new a(b.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(a,b,c){var d=a.compareDocumentPosition(c),e=Node.DOCUMENT_POSITION_PRECEDING;if(d&e){if(null===b)return!0;var f=b.compareDocumentPosition(c),g=Node.DOCUMENT_POSITION_FOLLOWING;if(f&g)return!0}return!1}},{key:"getIteratorNode",value:function(a){var b=a.previousNode(),c=void 0;return c=null===b?a.nextNode():a.nextNode()&&a.nextNode(),{prevNode:b,node:c}}},{key:"checkIframeFilter",value:function(a,b,c,d){var e=!1,f=!1;return d.forEach(function(a,b){a.val===c&&(e=b,f=a.handled)}),this.compareNodeIframe(a,b,c)?(e!==!1||f?e===!1||f||(d[e].handled=!0):d.push({val:c,handled:!0}),!0):(e===!1&&d.push({val:c,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(a,b,c,d){var e=this;a.forEach(function(a){a.handled||e.getIframeContents(a.val,function(a){e.createInstanceOnIframe(a).forEachNode(b,c,d)})})}},{key:"iterateThroughNodes",value:function(a,b,c,d,e){for(var f=this,g=this.createIterator(b,a,d),h=[],i=void 0,j=void 0,k=function(){var a=f.getIteratorNode(g);return j=a.prevNode,i=a.node};k();)this.iframes&&this.forEachIframe(b,function(a){return f.checkIframeFilter(i,j,a,h)},function(b){f.createInstanceOnIframe(b).forEachNode(a,c,d)}),c(i);this.iframes&&this.handleOpenIframes(h,a,c,d),e()}},{key:"forEachNode",value:function(a,b,c){var d=this,e=arguments.length<=3||void 0===arguments[3]?function(){}:arguments[3],f=this.getContexts(),g=f.length;g||e(),f.forEach(function(f){var h=function(){d.iterateThroughNodes(a,f,b,c,function(){--g<=0&&e()})};d.iframes?d.waitForIframes(f,h):h()})}}],[{key:"matches",value:function(a,b){var c="string"==typeof b?[b]:b,d=a.matches||a.matchesSelector||a.msMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.webkitMatchesSelector;if(d){var e=!1;return c.every(function(b){return!d.call(a,b)||(e=!0,!1)}),e}return!1}}]),a}();return c.fn.mark=function(a,b){return new d(this.get()).mark(a,b),this},c.fn.markRegExp=function(a,b){return new d(this.get()).markRegExp(a,b),this},c.fn.unmark=function(a){return new d(this.get()).unmark(a),this},c},window,document);
\ No newline at end of file
--- /dev/null
+html, body{
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+#content_wrapper{
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+}
+
+#results {
+ flex: 1;
+ padding-left: 0.4em;
+ padding-right: 0.4em;
+}
+
+#result_query {
+ font-weight: bold;
+}
+
+#page_title, #titlecount {
+ padding-top: 0.4em;
+ padding-bottom: 0.4em;
+}
+
+#searchbox, #page_title, #titlecount {
+ display: table;
+ margin: auto;
+}
+
+#results_wrap {
+ display: table;
+ margin: auto;
+ padding-top: 1em;
+}
+
+#results table {
+ border-collapse: collapse;
+ margin: auto;
+}
+
+#results td {
+ vertical-align: top;
+ padding-left: 0.4em;
+ padding-right: 0.4em;
+}
+
+.main_title {
+ min-width: 40em;
+}
+
+.elapsed {
+ text-align: right;
+ float: right;
+}
+
+#credits {
+ display: block;
+ text-align: right;
+ padding-right: 0.5em;
+ padding-bottom: 0.1em;
+}
+
+#searchterm {
+ display: block;
+ border: 1px solid;
+ min-width: 30em;
+ font-size: 2em;
+ height: 1em;
+ padding: 0.2em;
+ vertical-align: middle;
+}
+
+#searchterm:empty:before {
+ content: attr(data-placeholder);
+}
+
+#searchterm br{
+ display: none
+}
+
+/* light *
+a, a:active, a:visited { color: #607890; }
+a:hover { color: #036; }
+tr:nth-child(even), tr:nth-child(even) mark, thead tr {background: #fdf6e3; }
+body, input, mark {
+ color: #586e75;
+ background: #eee8d5;
+}
+/**/
+
+/** dark */
+a, a:active, a:visited { color: #839496; }
+a:hover { color: #93a1a1; }
+tr:nth-child(even), tr:nth-child(even) mark, thead tr {background: #073642; }
+body, input, mark {
+ color: #93a1a1;
+ background: #002b36;
+}
+/**/
+
+#searchterm { border-color: #6c71c4; }
+
+.highlight0 { color: #6c71c4; }
+.highlight1 { color: #cb4b16; }
+.highlight2 { color: #2aa198; }
+.highlight3 { color: #dc322f; }
+.highlight4 { color: #859900; }
+.highlight5 { color: #d33682; }
+.highlight6 { color: #268bd2; }
+.highlight7 { color: #b58900; }
--- /dev/null
+// Underscore.js 1.8.3
+// http://underscorejs.org
+// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// Underscore may be freely distributed under the MIT license.
+(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])<u?i=a+1:o=a}return i},m.indexOf=r(1,m.findIndex,m.sortedIndex),m.lastIndexOf=r(-1,m.findLastIndex),m.range=function(n,t,r){null==t&&(t=n||0,n=0),r=r||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=Array(e),i=0;e>i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e<arguments.length;)i.push(arguments[e++]);return E(n,r,this,this,i)};return r},m.bindAll=function(n){var t,r,e=arguments.length;if(1>=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this);
+
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>AniDb Title Search</title>
+ <link rel="stylesheet" href="/static/style.css">
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.4.0/jquery.mark.min.js"></script>
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
+ </head>
+ <body>
+ <div id="content_wrapper">
+ <h1 id="page_title">AniDB Title Search</h1>
+ <div id="searchbox">
+ <div id="searchterm" type="text" data-placeholder="Type to Search..." contenteditable="true"></div>
+ <!-- <button id="search">search</button> -->
+ </div>
+ <div id="results"></div>
+ <div id="credits">
+ By <a href="//aptx.org">APTX</a>.<br />
+ Title data provided by <a href="//anidb.net">AniDB</a>.<br />
+ Powered by <a href="//localmylist.aptx.org">LocalMyList</a>,
+ <a href="https://www.postgresql.org">PostgreSQL</a>,
+ <a href="http://cutelyst.org">Cutelyst</a>,
+ <a href="https://qt.io">Qt</a>,
+ <a href="https://jquery.com">jQuery</a>,
+ <a href="https://markjs.io">mark.js</a>
+ and <a href="http://underscorejs.org">undescore.js</a>.<br />
+ Color scheme from
+ <a href="http://ethanschoonover.com/solarized">Solarized</a>.
+ </div>
+ </div>
+ <script type="text/javascript">// <![CDATA[
+ var titlecount = '<div id="titlecount">Searching through {{ title_count }} titles.</div>';
+ var lastQ;
+ var titleType = {
+ 1: 'primary',
+ 2: 'synonym',
+ 3: 'short',
+ 4: 'official',
+ };
+ var highlightColors = 8;
+ function updateHighlighting(query, elements) {
+ var parts = query.split(/\s+/);
+ var partPos = _.invert(parts);
+ parts.sort(function(a, b){
+ return b.length - a.length;
+ });
+ _.each(parts, function(part) {
+ _.each(elements, function(el) {
+ $(el).mark(part, {
+ "className": "highlight" + (partPos[part] % highlightColors),
+ });
+ });
+ });
+
+ }
+ function highlightQueryField(query) {
+ var caretPos = getCaretPosition($("#searchterm")[0]);
+ $("#searchterm").empty();
+ $("#searchterm").text(query);
+ updateHighlighting(query, ["#searchterm"]);
+ setCaretPosition($("#searchterm")[0], caretPos);
+ }
+ function loadAnimeTitles(aid, container) {
+ container = '#' + container;
+ if ($(container).html().length) {
+ $(container).empty();
+ } else {
+ $.getJSON('/anime/'
+ + encodeURIComponent(aid), {}, function(data) {
+ $(container).empty();
+ var results = '<ol>';
+ $.each(data.titles, function(i,item){
+ results += '<li>' + item.title + ' - '
+ + titleType[item.type] + ', ' + item.language
+ + '</li>';
+ });
+ results += '</ol>';
+ if (data.elapsed) {
+ var microseconds = parseInt(data.elapsed);
+ var miliseconds = microseconds / 1000;
+ results += '<span class="elapsed">Retrieved in '
+ + miliseconds + 'ms</span>';
+ }
+ $(container).append(results);
+ });
+ }
+ }
+ function generateResultList(data) {
+ var results = '<table><thead><tr><th>Match</th><th>Language</th>'
+ + '<th>Type</th><th>Main title</th><th>AniDB</th>'
+ + '<th>Similarity</th></tr></thead><tbody>';
+ $.each(data, function(i,item){
+ results += '<tr><td class="match">' + item.title + '</td>'
+ + '<td>' + item.language + '</td>'
+ + '<td>' + titleType[item.type] + '</td>'
+ + '<td class="main_title">'
+ + '<a href="javascript:loadAnimeTitles(' + item.aid
+ + ', \'resulttitle' + i + '\')" id="titles' + i + '">'
+ + item.official_title + '</a><div id="resulttitle' + i
+ + '"></div></td>'
+ + '<td><a href="http://anidb.net/a' + item.aid + '">a'
+ + item.aid + '</a></td>'
+ + '<td>' + ((1 - item.distance) * 100).toFixed(0)
+ + '%</td></tr>';
+ });
+ results += '</tbody></table>';
+ return results;
+ }
+ function generateNgResultList(data) {
+ var results = '<table><thead><tr><th>Match</th>'
+ + '<th>Type/Language</th><th>Main title</th>'
+ + '<th>AniDB</th><th>Similarity</th></tr></thead>'
+ + '<tbody>';
+
+ $.each(data, function(i,item){
+ var lang = _.chain(item.language)
+ .groupBy('type')
+ .map(function(v, k) {
+ return {type: k, languages: _.pluck(v, 'language')};
+ }).value();
+
+ var resultLang = lang.map(function(o, i, a) {
+ return titleType[o.type] + ': ' + o.languages.join(', ');
+ }).join(', ');
+
+ results += '<tr><td class="match">' + item.title + '</td>'
+ + '<td>' + resultLang + '</td>'
+ + '<td class="main_title">'
+ + '<a href="javascript:loadAnimeTitles(' + item.aid
+ + ', \'resulttitle' + i + '\')" id="titles' + i + '">'
+ + item.official_title + '</a><div id="resulttitle' + i
+ + '"></div></td>'
+ + '<td><a href="http://anidb.net/a' + item.aid + '">a'
+ + item.aid + '</a></td>'
+ + '<td>' + ((1 - item.distance) * 100).toFixed(0)
+ + '%</td></tr>';
+ });
+ results += '</tbody></table>';
+
+ return results;
+ }
+ function handleResponse(data) {
+ $('#results').empty();
+ var q = data.query;
+ var results = '<div id="results_wrap">';
+ if (data.result && data.result.length) {
+ results += 'Results for <span id="result_query">' + q
+ + '</span>:';
+ results += generateNgResultList(data.result);
+ } else if (data.suggestions && data.suggestions.length) {
+ results += 'Nothing found for <span id="result_query">' + q
+ + '</span>. Did you mean:';
+ results += generateResultList(data.suggestions);
+ }
+ if (data.elapsed) {
+ var microseconds = parseInt(data.elapsed);
+ var miliseconds = microseconds / 1000;
+ results += '<span class="elapsed">Search took '
+ + miliseconds + 'ms</span>';
+ }
+ results += '</div>';
+ $('#results').append(results);
+ updateHighlighting(q, [".match", "#result_query"]);
+ }
+ function liveSearch(q) {
+ var trimmedQ = q.trim();
+ if (trimmedQ == lastQ)
+ return;
+ lastQ = trimmedQ;
+ if (trimmedQ.length < 1) {
+ $('#results').empty();
+ $('#results').append(titlecount);
+ $('#searchterm').empty();
+ return;
+ }
+ highlightQueryField(q);
+
+ $.getJSON('/nglivequery?q=' + encodeURIComponent(trimmedQ), {},
+ handleResponse);
+ }
+ function readQueryFromField() {
+ return $('#searchterm').text();
+ }
+ function readQueryFromHash() {
+ var newQ = decodeURIComponent(window.location.hash.substring(1))
+ var oldQ = readQueryFromField().trim();
+ if (newQ == oldQ)
+ return;
+ $('#searchterm').text(newQ);
+ setCaretPosition($("#searchterm")[0], newQ.length);
+ highlightQueryField(newQ);
+ liveSearch(newQ);
+ }
+ function updateHash(val) {
+ if (!val.length)
+ window.location.hash = '';
+ else
+ window.location.hash = '#' + val;
+ }
+ function getCaretPosition(element) {
+ function isNodeADescendantOfParent(node, parent) {
+ while (node) {
+ if (node == parent)
+ return true;
+ node = node.parentNode;
+ }
+ return false;
+ }
+
+ var ret;
+ for (var i = 0; i < window.getSelection().rangeCount; ++i) {
+ var range = window.getSelection().getRangeAt(i);
+ range = range.cloneRange();
+ range.collapse(false);
+ var caretElement = range.commonAncestorContainer;
+
+ if (!isNodeADescendantOfParent(range.endContainer, element))
+ continue;
+
+ var preCaretRange = range.cloneRange();
+ preCaretRange.selectNodeContents(element);
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
+ ret = preCaretRange.toString().length;
+ break;
+ }
+ return ret;
+ }
+ function setCaretPosition(element, pos) {
+ function setCaretPositionSingleElement(element, pos) {
+ var selection = window.getSelection();
+ var range = document.createRange();
+ range.collapse(true);
+ range.setStart(element, pos);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+ function setCaretPositionHelper(element, left) {
+ var idx;
+ for (idx in element.childNodes) {
+ var node = element.childNodes[idx];
+ switch (node.nodeType) {
+ case 1: // Element node
+ left = setCaretPositionHelper(node, left);
+ if (!left)
+ return left;
+ break;
+ case 3: // Text node
+ var text = node.nodeValue;
+ if ((left - text.length) > 0) {
+ left -= text.length;
+ } else {
+ setCaretPositionSingleElement(node, left);
+ left = 0;
+ return left;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ return left;
+ }
+ setCaretPositionHelper(element, pos);
+ }
+
+ $('#searchterm').keyup(function() {
+ var q = readQueryFromField();
+ updateHash(q.trim());
+ liveSearch(q);
+ });
+ $(document).ready(function() {
+ readQueryFromHash();
+ $('#searchterm').focus();
+ if (readQueryFromField().length) {
+ setCaretPosition($("#searchterm")[0],
+ readQueryFromField().length);
+ } else {
+ $('#results').append(titlecount);
+ }
+ });
+ window.onhashchange = readQueryFromHash;
+ // ]]></script>
+ </body>
+</html>
--- /dev/null
+file(GLOB_RECURSE AniDbTitleSearch_SRCS *.cpp *.h)
+
+set(AniDbTitleSearch_SRCS
+ ${AniDbTitleSearch_SRCS}
+ ${TEMPLATES_SRC}
+)
+
+# Create the application
+add_library(AniDbTitleSearch SHARED ${AniDbTitleSearch_SRCS})
+
+# Link to Cutelyst
+target_link_libraries(AniDbTitleSearch
+ Cutelyst::Core
+ Cutelyst::StaticSimple
+ Cutelyst::View::JSON
+ Cutelyst::View::Grantlee
+ Cutelyst::Utils::Sql
+ Qt5::Core
+ Qt5::Network
+ Qt5::Sql
+)
--- /dev/null
+#include "anidbtitlesearch.h"
+
+#include <QSqlQuery>
+#include <QSqlError>
+#include <QSqlDriver>
+#include <Cutelyst/Plugins/StaticSimple/staticsimple.h>
+#include <Cutelyst/Plugins/View/JSON/viewjson.h>
+#include <Cutelyst/Plugins/View/Grantlee/grantleeview.h>
+#include <QSettings>
+
+#include <QDebug>
+#include "root.h"
+#include "titlesearch.h"
+
+using namespace Cutelyst;
+
+AniDbTitleSearch::AniDbTitleSearch(QObject *parent) : Application(parent)
+{
+}
+
+AniDbTitleSearch::~AniDbTitleSearch()
+{
+}
+
+bool AniDbTitleSearch::init()
+{
+ defaultHeaders().clear();
+ defaultHeaders().setContentTypeCharset("utf-8");
+
+ new Root(this);
+ new TitleSearch(this);
+
+ new StaticSimple(this);
+
+ new ViewJson(this, "JSON");
+ auto prettyJson = new ViewJson(this, "PrettyJSON");
+ prettyJson->setOutputFormat(ViewJson::Indented);
+ auto grantlee = new GrantleeView(this, "Grantlee");
+ grantlee->setIncludePaths({ pathTo({ "root" }) });
+
+ return true;
+}
+
+bool AniDbTitleSearch::postFork() {
+
+ QSettings s("/etc/AniDbTitleSearch.ini", QSettings::IniFormat);
+
+ QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL");
+ db.setDatabaseName(s.value("db").toString());
+ db.setUserName(s.value("user").toString());
+ db.setPassword(s.value("pass").toString());
+ if (!db.open()) {
+ qCritical() << "Failed to open database:" << db.lastError().text();
+ return false;
+ }
+ return true;
+}
--- /dev/null
+#ifndef ANIDBTITLESEARCH_H
+#define ANIDBTITLESEARCH_H
+
+#include <Cutelyst/Application>
+
+using namespace Cutelyst;
+
+class AniDbTitleSearch : public Application
+{
+ Q_OBJECT
+ CUTELYST_APPLICATION(IID "AniDbTitleSearch")
+public:
+ Q_INVOKABLE explicit AniDbTitleSearch(QObject *parent = 0);
+ ~AniDbTitleSearch();
+
+ bool init();
+ bool postFork();
+};
+
+#endif //ANIDBTITLESEARCH_H
--- /dev/null
+#include "root.h"
+
+#include <Cutelyst/Plugins/Utils/Sql>
+#include <QSqlQuery>
+
+Root::Root(QObject *parent) : Controller(parent)
+{
+}
+
+Root::~Root()
+{
+}
+
+void Root::index(Context *c)
+{
+ static int titleCount = []() {
+ QSqlQuery q = CPreparedSqlQuery(R"(
+ SELECT count(title) FROM anime_title
+ )");
+ if (!q.exec())
+ return -1;
+ q.next();
+ return q.value(0).toInt();
+ }();
+
+ c->stash()["title_count"] = titleCount;
+
+ c->stash()["template"] = "titlesearch.html";
+ c->setView("Grantlee");
+
+}
+
+void Root::defaultPage(Context *c)
+{
+ c->response()->body() = "Page not found!";
+ c->response()->setStatus(404);
+}
+
+void Root::End(Context *c)
+{
+ c->response()->setContentType("text/html");
+ c->response()->headers().setContentTypeCharset("utf-8");
+}
--- /dev/null
+#ifndef ROOT_H
+#define ROOT_H
+
+#include <Cutelyst/Controller>
+
+using namespace Cutelyst;
+
+class Root : public Controller
+{
+ Q_OBJECT
+ C_NAMESPACE("")
+public:
+ explicit Root(QObject *parent = 0);
+ ~Root();
+
+ C_ATTR(index, :Path :Args(0))
+ void index(Context *c);
+
+ C_ATTR(defaultPage, :Path)
+ void defaultPage(Context *c);
+
+private:
+ C_ATTR(End, :ActionClass("RenderView"))
+ void End(Context *c);
+};
+
+#endif //ROOT_H
--- /dev/null
+#pragma once
+#include <iterator>
+#include <utility>
+
+#include <QSqlQuery>
+#include <QSqlError>
+
+class SqlQueryResultIterator : public std::iterator<std::forward_iterator_tag, int>
+{
+ QSqlQuery *query;
+ bool sentinel;
+public:
+ SqlQueryResultIterator(QSqlQuery &query, bool sentinel) : query{&query}, sentinel{sentinel} {}
+ SqlQueryResultIterator(const SqlQueryResultIterator &it) : query{it.query}, sentinel{it.sentinel} {}
+ SqlQueryResultIterator &operator++() { query->next(); return *this; }
+ SqlQueryResultIterator operator++(int) = delete;
+ bool operator==(const SqlQueryResultIterator &rhs)
+ {
+ if (sentinel == rhs.sentinel)
+ {
+ if (sentinel)
+ return true;
+ return query->at() == rhs.query->at();
+ }
+ return query->at() == QSql::AfterLastRow;
+ }
+ bool operator!=(const SqlQueryResultIterator &rhs) { return !operator==(rhs); }
+ const QSqlQuery &operator*() { return *query; }
+};
+
+SqlQueryResultIterator begin(QSqlQuery &q)
+{
+ if (!q.isActive())
+ throw std::logic_error{"Trying to iterate over the results of a "
+ "query that has not been executed"};
+ if (!q.isSelect() || q.at() == QSql::AfterLastRow)
+ return {q, true};
+ if (q.at() == QSql::BeforeFirstRow && !q.next())
+ return {q, true};
+ return {q, false};
+}
+SqlQueryResultIterator end(QSqlQuery &q)
+{
+ return SqlQueryResultIterator{q, true};
+}
--- /dev/null
+#include "titlesearch.h"
+
+#include <chrono>
+
+#include <QSqlQuery>
+#include <QSqlError>
+#include <QSqlDriver>
+#include <QVariant>
+#include <QSet>
+#include <QJsonDocument>
+#include <QJsonArray>
+
+#include <QDebug>
+
+#include <Cutelyst/Plugins/Utils/Sql>
+
+#include "sqlqueryiterator.h"
+
+using namespace Cutelyst;
+using Clock = std::chrono::high_resolution_clock;
+
+QString toSearchQuery(const QString &string)
+{
+ const static QChar anyChar = QChar('%');
+ QString ret = string.trimmed();
+ ret.replace(QRegExp("\\s+"), anyChar);
+ ret = ret.append(anyChar).prepend(anyChar);
+ return ret;
+}
+
+TitleSearch::TitleSearch(QObject *parent) : Controller(parent)
+{
+}
+
+TitleSearch::~TitleSearch()
+{
+}
+
+void TitleSearch::livePreviewQuery(Context *c)
+{
+ const auto startTime = Clock::now();
+ QSqlQuery q = CPreparedSqlQuery(R"(
+ SELECT at.title_id, at.aid, at.type, trim(both from at.language), at.title, at2.title, at.title <-> :query2 distance
+ FROM anime_title at
+ JOIN anime_title at2 ON at.aid = at2.aid AND at2.type = 1
+ WHERE at.title ILIKE :query
+ ORDER BY distance ASC
+ LIMIT 10
+ )");
+
+ const QString userQuery = c->request()->queryParam("q");
+ const QString query = toSearchQuery(userQuery);
+ q.bindValue(":query", query);
+ q.bindValue(":query2", query);
+
+ if (q.exec()) {
+ QVariantList result;
+ for (const auto &e : q) {
+ QVariantHash row;
+ row["aid"] = e.value(1);
+ row["type"] = e.value(2);
+ row["language"] = e.value(3);
+ row["title"] = e.value(4);
+ row["official_title"] = e.value(5);
+ row["distance"] = e.value(6);
+ result += row;
+ }
+ c->stash()["result"] = result;
+
+ if (!result.size())
+ suggestionQuery(c);
+
+ } else {
+ c->stash()["error"] = "Query error";
+ }
+
+ c->stash()["query"] = userQuery;
+
+ const auto endTime = Clock::now();
+ const long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
+ c->stash()["elapsed"] = QByteArray::number(elapsed);
+}
+
+void TitleSearch::ngLivePreviewQuery(Context *c)
+{
+ const auto startTime = Clock::now();
+ QSqlQuery q = CPreparedSqlQuery(R"(
+ WITH mathcing_titles AS (
+ SELECT at.aid, at.type, trim(both from at.language) AS language, at.title as title, at2.title as official_title, at.title <-> :query distance
+ FROM anime_title at
+ JOIN anime_title at2 ON at.aid = at2.aid AND at2.type = 1
+ WHERE at.title ILIKE :query2
+ ORDER BY distance ASC
+ LIMIT 15
+ )
+ SELECT aid, array_to_json(array_agg(json_build_object('type', type, 'language', language))) AS language, title, official_title, distance FROM mathcing_titles
+ GROUP BY aid, title, official_title, distance
+ ORDER BY distance ASC, title DESC, official_title DESC
+ LIMIT 10
+ )");
+
+ const QString userQuery = c->request()->queryParam("q");
+ const QString query = toSearchQuery(userQuery);
+ q.bindValue(":query", query);
+ q.bindValue(":query2", query);
+
+ if (q.exec()) {
+ QVariantList result;
+ for (const auto &e : q) {
+ QVariantHash row;
+ row["aid"] = e.value(0);
+ row["language"] = QJsonDocument::fromJson(e.value(1).toString().toUtf8()).array().toVariantList();
+ row["title"] = e.value(2);
+ row["official_title"] = e.value(3);
+ row["distance"] = e.value(4);
+ result += row;
+ }
+ c->stash()["result"] = result;
+
+ if (!result.size())
+ suggestionQuery(c);
+
+ } else {
+ c->stash()["error"] = "Query error";
+ }
+
+ c->stash()["query"] = userQuery;
+
+ const auto endTime = Clock::now();
+ const long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
+ c->stash()["elapsed"] = QByteArray::number(elapsed);
+}
+
+void TitleSearch::suggestionQuery(Context *c)
+{
+ const auto startTime = Clock::now();
+ const QString query = toSearchQuery(c->request()->queryParam("q"));
+
+ QSqlQuery q = CPreparedSqlQuery(R"(
+ WITH top_matches AS (
+ SELECT at.aid, at.type, trim(both from at.language) AS language, at.title, at2.title official_title, at.title <-> :query distance
+ FROM anime_title at
+ JOIN anime_title at2 ON at.aid = at2.aid AND at2.type = 1
+ ORDER BY distance
+ LIMIT 30
+ )
+ SELECT * FROM (
+ SELECT DISTINCT ON (aid) aid, type, language, title, official_title, distance
+ FROM top_matches
+ LIMIT 10) t
+ ORDER BY distance
+ )");
+ q.bindValue(":query", query);
+
+ if (q.exec()) {
+ QVariantList result;
+ for (const auto &e : q) {
+ QVariantHash row;
+ row["aid"] = e.value(0);
+ row["type"] = e.value(1);
+ row["language"] = e.value(2);
+ row["title"] = e.value(3);
+ row["official_title"] = e.value(4);
+ row["distance"] = e.value(5);
+ result += row;
+ }
+ c->stash()["suggestions"] = result;
+ } else {
+ c->stash()["error"] = "Query error";
+ }
+
+ const auto endTime = Clock::now();
+ const long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
+ c->stash()["elapsed"] = QByteArray::number(elapsed);
+}
+
+void TitleSearch::animeTitleQuery(Context *c)
+{
+ const auto startTime = Clock::now();
+
+ const auto args = c->request()->arguments();
+ if (args.size() < 1)
+ return;
+ const auto aid = args[0].toInt();
+ if (!aid)
+ return;
+
+ QSqlQuery q = CPreparedSqlQuery(R"(
+ SELECT type, trim(both from language), title
+ FROM anime_title
+ WHERE aid = :aid
+ ORDER BY
+ CASE type
+ WHEN 1 THEN 1
+ WHEN 2 THEN 3
+ WHEN 3 THEN 4
+ WHEN 4 THEN 2
+ END,
+ language, title
+ )");
+
+ q.bindValue(":aid", aid);
+
+ if (q.exec()) {
+ QVariantList result;
+ for (const auto &e : q) {
+ QVariantHash row;
+ row["type"] = e.value(0);
+ row["language"] = e.value(1);
+ row["title"] = e.value(2);
+ result += row;
+ }
+ c->stash()["titles"] = result;
+ } else {
+ c->stash()["error"] = "Query error";
+ }
+
+ const auto endTime = Clock::now();
+ const long long elapsed = std::chrono::duration_cast<std::chrono::microseconds>(endTime - startTime).count();
+ c->stash()["elapsed"] = QByteArray::number(elapsed);
+}
+
+void TitleSearch::End(Context *c)
+{
+ c->setView("PrettyJSON");
+ c->response()->headers().setContentTypeCharset("utf-8");
+}
--- /dev/null
+#ifndef TITLESEARCH_H
+#define TITLESEARCH_H
+
+#include <Cutelyst/Controller>
+
+using namespace Cutelyst;
+
+class TitleSearch : public Controller
+{
+ Q_OBJECT
+ C_NAMESPACE("title")
+public:
+ explicit TitleSearch(QObject *parent = 0);
+ ~TitleSearch();
+
+ C_ATTR(livePreviewQuery, :Path("/livequery") :Args(0))
+ void livePreviewQuery(Context *c);
+
+ C_ATTR(ngLivePreviewQuery, :Path("/nglivequery") :Args(0))
+ void ngLivePreviewQuery(Context *c);
+
+ C_ATTR(suggestionQuery, :Path("/suggestionquery") :Args(0))
+ void suggestionQuery(Context *c);
+
+ C_ATTR(animeTitleQuery, :Path("/anime") :Args(1))
+ void animeTitleQuery(Context *c);
+
+private:
+ C_ATTR(End, :ActionClass("RenderView"))
+ void End(Context *c);
+};
+
+#endif //TITLESEARCH_H
+