sc-voice
Version:
SuttaCentral Voice
403 lines (380 loc) • 13.4 kB
JavaScript
(function(exports) {
const fs = require('fs');
const path = require('path');
const { logger, LogInstance } = require('log-instance');
const {
exec,
} = require('child_process');
const {
BilaraData,
BilaraPath,
Seeker,
} = require('scv-bilara');
const Playlist = require('./playlist');
const Sutta = require('./sutta');
const Task = require('./task');
const SuttaDuration = require('./sutta-duration');
const {
ScApi,
SuttaCentralId,
} = require('suttacentral-api');
const SuttaFactory = require('./sutta-factory');
const Words = require('./words');
const ROOT = path.join(__dirname, '..', '..', 'local', 'suttas');
const maxBuffer = 10 * 1024 * 1024;
const MAXRESULTS_LEGACY = 5;
const COLLECTIONS = {
an: {
name: 'an',
folder: 'an',
subchapters: true,
},
mn: {
name: 'mn',
folder: 'mn',
subchapters: false,
},
dn: {
name: 'dn',
folder: 'dn',
subchapters: false,
},
sn: {
name: 'sn',
folder: 'sn',
subchapters: true,
},
thig: {
name: 'thig',
folder: 'kn',
subchapters: true,
},
thag: {
name: 'thag',
folder: 'kn',
subchapters: true,
}
}
var suttaPaths = {};
var _suttaStore;
class SuttaStore {
constructor(opts={}) {
var that = this;
(opts.logger || logger).logInstance(this, opts);
this.scApi = opts.scApi || new ScApi();
this.suttaFactory = opts.suttaFactory || new SuttaFactory({
scApi: this.scApi,
autoSection: true,
suttaLoader: scid => that.loadBilaraSutta(scid),
});
this.bilaraData = opts.BilaraData || new BilaraData({
logger: this,
branch: "unpublished",
});
this.seeker = opts.Seeker || new Seeker({
bilaraData: this.bilaraData,
logger: this,
matchHighlight: false,
});
this.maxDuration = opts.maxDuration || 3 * 60 * 60;
this.maxResults = opts.maxResults || MAXRESULTS_LEGACY;
this.voice = opts.voice;
this.words = opts.words || new Words();
this.suttaDuration = opts.suttaDuration || new SuttaDuration();
Object.defineProperty(this, 'isInitialized', {
writable: true,
value: false,
});
}
static get suttaStore() {
if (_suttaStore == null) {
_suttaStore = new SuttaStore();
}
return _suttaStore;
}
async initialize() { try {
if (this.isInitialized) {
return Promise.resolve(this);
}
this.isInitialized = true;
await this.suttaFactory.initialize();
await this.seeker.initialize();
return this;
} catch(e) {
this.warn(`initialize()`);
throw e;
}}
static isUidPattern(pattern) {
var commaParts = pattern.toLowerCase().split(',')
.map(p=>p.trim());
return commaParts.reduce((acc,part) => {
return acc && /^[a-z]+ ?[0-9]+[-0-9a-z.:\/]*$/i.test(part);
}, true);
}
static sanitizePattern(pattern) {
if (!pattern) {
throw new Error("SuttaStore.search() pattern is required");
}
const MAX_PATTERN = 1024;
var excess = pattern.length - MAX_PATTERN;
if (excess > 0) {
throw new Error(
`Search text too long by ${excess} characters.`);
}
// replace quotes (code injection on grep argument)
pattern = pattern.replace(/["']/g,'.');
// eliminate tabs, newlines and carriage returns
pattern = pattern.replace(/\s/g,' ');
// remove control characters
pattern = pattern.replace(/[\u0000-\u001f\u007f]+/g,'');
// must be valid
new RegExp(pattern);
return pattern;
}
static normalizePattern(pattern) {
// normalize white space to space
pattern = pattern.replace(/[\s]+/g,' +');
return pattern;
}
static paliPattern(pattern) {
return /^[a-z]+$/i.test(pattern)
? pattern
.replace(/a/iug, '(a|ā)')
.replace(/i/iug, '(i|ī)')
.replace(/u/iug, '(u|ū)')
.replace(/m/iug, '(m|ṁ|ṃ)')
.replace(/d/iug, '(d|ḍ)')
.replace(/n/iug, '(n|ṅ|ñ|ṇ)')
.replace(/l/iug, '(l|ḷ)')
.replace(/t/iug, '(t|ṭ)')
: pattern;
}
async createPlaylist(...args) { try {
var opts = args[0];
if (typeof opts === 'string') {
opts = {
pattern: args[0],
maxResults: args[1],
};
}
var {
lang,
language,
method,
suttaRefs,
suttas,
resultPattern,
} = await this.findSuttas(opts);
lang = lang || language;
var maxDuration = opts.maxDuration || this.maxDuration;
var languages = opts.languages || ['pli', lang];
var playlist = new Playlist({ languages, });
suttas.forEach(sutta => playlist.addSutta(sutta));
var duration = playlist.stats().duration;
if (duration > this.maxDuration) {
languages = opts.languages || [lang];
playlist = new Playlist({ languages, });
var minutes = (this.maxDuration / 60).toFixed(0);
playlist.addTrack("createPlaylist_error1",
`Play list is too long to be played. `+
`All play lists must be less than `+
`${minutes} minutes long`);
}
return playlist;
} catch(e) {
this.warn(`createPlaylist()`, JSON.stringify(args), e.message);
throw e;
}}
findSuttas(...args) {
var that = this;
var pbody = (resolve,reject) => (async function(){try{
var res = await that.search.apply(that, args);
res.suttas = res.results.map(r=>r.sutta);
resolve(res);
} catch(e) {reject(e);} })();
return new Promise(pbody);
}
async loadBilaraSutta(opts) { try {
if (typeof opts === 'string') {
opts = {
scid: opts,
};
}
var {
scid,
language,
langTrans,
translator,
expand,
matchHighlight,
} = opts;
var lang = langTrans || language || scid.split('/')[1] || 'en';
var pattern = translator
? `${scid}/${lang}/${translator}`
: `${scid}/${lang}`;
var findOpts = {
pattern,
lang,
showMatchesOnly: false,
matchHighlight,
}
var bdres = await this.seeker.find(findOpts);
var mld = bdres.mlDocs[0];
if (mld == null) {
return null;
}
var mldRes = await this.mldResult(mld, lang);
return mldRes.sutta;
} catch(e) {
this.warn(`loadBilaraSutta(${JSON.stringify(opts)})`,
e.message);
throw e;
}}
async loadSutta(opts) { try {
var sutta = await this.loadBilaraSutta(opts);
if (!sutta) {
var {
scid,
language,
langTrans,
translator,
expand,
matchHighlight,
} = opts;
var suttaRef = `${scid}/${language}/${translator}`;
this.log(`loadSutta(${suttaRef}) legacy `);
sutta = await this.suttaFactory.loadSutta({
scid,
translator,
language,
});
}
return sutta;
} catch(e) {
this.warn(e);
throw e;
}}
async mldResult(mld, lang) { try {
if (mld == null) {
throw new Error(`Expected MLDoc`);
}
var {
scApi,
suttaFactory,
bilaraData: bd,
} = this;
var {
suid: sutta_uid,
translations,
} = mld;
lang = lang || mld.lang || 'en';
var trans = translations.filter(t => t.lang === lang)[0] ||
translations[0];
var author_uid = trans.author_uid;
var suttaplex = await bd
.loadSuttaplexJson(sutta_uid, lang, author_uid);
var authorInfo = bd.authorInfo(author_uid);
var author = authorInfo && authorInfo.name || author_uid;
var segments = mld.segments();
var titles = mld.titles();
var translation = {
author_uid,
lang,
};
var sutta = new Sutta({
sutta_uid,
author,
author_uid,
lang,
titles,
support: true,
suttaplex,
segments,
translation,
});
var sectSutta = suttaFactory.sectionSutta(sutta);
var quote = // prefer non-title quotes
segments.filter((s,i)=>s.matched && i > 1)[0] ||
segments[0];
var blurb = await this.bilaraData.readBlurb({
suid: sutta_uid,
lang,
});
sectSutta.blurb = blurb;
return {
count: mld.score,
uid: sutta_uid,
lang,
author,
author_short: author_uid.charAt(0).toUpperCase()
+ author_uid.slice(1),
author_uid: author_uid,
author_blurb: authorInfo && authorInfo.blurb,
nSegments: segments.length,
title: titles.slice(1).join(' \u2022 '),
collection_id: trans.collection,
quote,
suttaplex,
sutta: sectSutta,
stats: this.suttaDuration.measure(sutta),
};
} catch(e) {
this.warn(`mldResult()`, {
mld: {
suid: mld.suid,
author_uid: mld.author_uid,
},
e,
});
throw e;
}}
async search(...args) { try {
if (!this.isInitialized) {
throw new Error(`initialize() is required`);
}
var opts = args[0];
if (typeof opts === 'string') {
opts = {
pattern: args[0],
maxResults: args[1],
};
}
var pattern = SuttaStore.sanitizePattern(opts.pattern);
var lang = opts.language || 'en';
var maxDoc = opts.maxResults==null
? this.maxResults : opts.maxResults;
var maxDoc = Number(maxDoc);
if (isNaN(maxDoc)) {
throw new Error("search() maxResults must be a number");
}
var bdres;
var matchHighlight = SuttaStore.isUidPattern(pattern)
? false
: '<span class="scv-matched">$&</span>';
var maxGrepResults = Math.max(500, maxDoc*3);
var findOpts = {
pattern,
lang,
maxDoc, // user max documents
maxResults: maxGrepResults,
showMatchesOnly: false,
matchHighlight,
}
bdres = await this.seeker.find(findOpts);
bdres.results = [];
for (var i = 0; i < bdres.mlDocs.length; i++) {
var mld = bdres.mlDocs[i];
var mldRes = await this.mldResult(mld, lang);
bdres.results.push(mldRes);
}
return bdres;
} catch(e) {
this.warn(`search()`, JSON.stringify(args), e.message);
throw e;
}}
async nikayaSuttaIds(nikaya, language='en', author='sujato') {
return this.bilaraData.nikayaSuttaIds(nikaya, language, author);
}
}
module.exports = exports.SuttaStore = SuttaStore;
})(typeof exports === "object" ? exports : (exports = {}));