PageSourceSearch

https://edtechbooks.org/js/etb/api.js

js edtechbooks.org collected 2026-10-01 10:22:05 UTC 34,097 bytes, 875 lines download raw bytes

1// Caches
2let authors = {};
3let badges = {};
4let books = {};
5let chapters = {};
6let translations = {};
7let collections = {};
8let glossary_terms = {};
9let languages = [];
10let licenses = [];
11let pages = {};
12let short_name_info = {};
13let articles = {};
14let site = null;
15
16// Maps
17let book_short_names = {};
18let chapter_short_names = {};
19let page_short_names = {};
20
21// Current Identifier Shortcuts
22let current_book = null;
23let current_chapter = null;
24let current_page = null;
25let current_user = null;
26
27let system_msg_closed = false;
28
29// Universal POST Function
30async function API(url, data = {}, options = {}) {
31    return new Promise(async (resolve, reject) => {
32        let startTime = new Date().getTime();
33        $.ajax({
34            url: url,
35            type: 'POST',
36            contentType: 'application/json',
37            data: JSON.stringify(data),
38            success: async function (json) {
39                if (json.status == 'success') {
40                    //console.warn(json);
41                    console.log('API execution time: ' + (new Date().getTime() - startTime) / 1000);
42                    if (json.msg) Toast('Notification', json.msg, 'bg-success text-light');
43                    resolve(json);
44                } else {
45                    //console.error(json.error);
46                    if (json.toast && !options.silent) Toast('API Error', json.error, 'bg-danger text-light');
47                    reject(json.error);
48                }
49            },
50            error: function (xhr, status, error) {
51                console.error(error);
52                Toast('Error', error, 'bg-danger text-light');
53                reject(error);
54            }
55        });
56    });
57}
58
59async function APIFile(url, file) {
60    return new Promise(async (resolve, reject) => {
61        let startTime = new Date().getTime();
62        let formData = new FormData();
63        formData.append('file', file);
64        $.ajax({
65            url: url,
66            type: 'POST',
67            data: formData,
68            contentType: false,
69            processData: false,
70            success: async function (json) {
71                if (json.status == 'success') {
72                    //console.warn(json);
73                    console.log('API execution time: ' + (new Date().getTime() - startTime) / 1000);
74                    if (json.msg) Toast('API Message', json.msg, 'bg-success text-light');
75                    resolve(json);
76                } else {
77                    console.error(json.error);
78                    if (json.toast) Toast('API Error', json.error, 'bg-danger text-light');
79                    reject(json.error);
80                }
81            },
82            error: function (xhr, status, error) {
83                console.error(error);
84                Toast('Error', error, 'bg-danger text-light');
85                reject(error);
86            }
87        });
88    });
89}
90
91// Authors
92async function GetAuthor(id, rebuild = false) {
93    if (authors[id] !== undefined && !rebuild) return authors[id];
94    try {
95        console.warn('Getting author');
96        let json = await API('/author/' + id + '/view');
97        authors[json.entity.author_id] = new Author(json.entity);
98        return authors[json.entity.author_id];
99    } catch (error) { console.error(error); return null; }
100}
101async function GetBookLinksForAuthor(id) {
102    try {
103        console.warn('Getting book links for author');
104        let json = await API('/author/' + id + '/getBookLinks');
105        return json.links;
106    } catch (error) { console.error(error); return null; }
107}
108
109async function GetChapterLinksForAuthor(id) {
110    try {
111        console.warn('Getting chapter links for author');
112        let json = await API('/author/' + id + '/getChapterLinks');
113        return json.links;
114    } catch (error) { console.error(error); return null; }
115}
116
117async function CreateArticle(id, category) {
118    try {
119        console.log('Adding article to category ' + category);
120        let json = await API('/author/' + id + '/createArticle', { category: category });
121        articles[json.entity.article_id] = new Article(json.entity);
122        return articles[json.entity.article_id];
123    } catch (error) { console.error(error); return false; }
124}
125async function DeleteArticle(author_id, article_id) {
126    try {
127        console.log('Deleting article ' + article_id);
128        let json = await API('/author/' + author_id + '/deleteArticle/' + article_id);
129        delete articles[article_id];
130        return true;
131    } catch (error) { console.error(error); return false; }
132}
133
134async function GetArticlesForAuthor(id, rebuild = false) {
135    if (authors[id] !== undefined && authors[id].articles !== undefined && !rebuild) return authors[id].articles;
136    try {
137        console.warn('Getting articles for author');
138        let json = await API('/author/' + id + '/getArticles');
139        authors[id].articles = [];
140        for (let article of json.articles) {
141            articles[article.article_id] = new Article(article);
142            authors[id].articles.push(articles[article.article_id]);
143        }
144        return authors[id].articles;
145    } catch (error) { console.error(error); return null; }
146}
147
148async function Impersonate(btn) {
149    try {
150        console.warn('Impersonating user');
151        let id = $(btn).attr('data-action-value');
152        console.log(id);
153        let json = await API('/author/' + id + '/impersonate');
154        await GetCurrentUser(true);
155        RenderFromPath('/', true);
156        return true;
157    } catch (error) { console.error(error); return false; }
158}
159
160async function GrantRole(id, role, conflicts = []) {
161    try {
162        console.warn('Granting role');
163        let json = await API('/author/' + id + '/grantRole/' + role, conflicts);
164        authors[json.entity.author_id] = json.entity;
165        return json.entity;
166    } catch (error) { console.error(error); return null; }
167}
168
169async function RevokeRole(id, role) {
170    try {
171        console.warn('Revoking role');
172        let json = await API('/author/' + id + '/revokeRole/' + role);
173        authors[json.entity.author_id] = json.entity;
174        return json.entity;
175    } catch (error) { console.error(error); return null; }
176}
177
178async function SetApprovalStatus(id, status) {
179    try {
180        console.warn('Setting approval status');
181        let json = await API('/author/' + id + '/setApproval/' + status);
182        authors[json.entity.author_id] = json.entity;
183        return json.entity;
184    } catch (error) { console.error(error); return null; }
185}
186
187// Badges
188async function GetBadge(id) {
189    if (id == null) return null;
190    if (badges[id] !== undefined) return badges[id];
191    try {
192        console.warn('Getting badge');
193        let json = await API('/badge/' + id);
194        badges[json.entity.badge_id] = json.entity;
195        return json.entity;
196    } catch (error) { console.error(error); return null; }
197}
198async function ListBadges(entity_type) {
199    try {
200        console.warn('Listing badges');
201        let json = await API('/badge/list/' + entity_type);
202        return json.entities;
203    } catch (error) { console.error(error); return []; }
204}
205async function GetBookLinksForBadge(id) {
206    try {
207        console.warn('Getting book links for badge');
208        let json = await API('/badge/' + id + '/getBookLinks');
209        return json.links;
210    } catch (error) { console.error(error); return []; }
211}
212
213// Books
214async function GetBook(id, rebuild = false, silent = false) {
215    if (books[id] !== undefined && !rebuild) return books[id];
216    if (book_short_names[id] !== undefined && books[book_short_names[id]] !== undefined && !rebuild) return books[book_short_names[id]];
217    try {
218        console.warn('Getting book');
219        let json = await API('/book/' + id + '/view', {}, { silent });
220        book_short_names[json.entity.short_name] = json.entity.id;
221        books[json.entity.id] = new Book(json.entity);
222        return books[json.entity.id];
223    } catch (error) { console.log(error); return null; }
224}
225async function GetBookChapterAuthors(id) {
226    try {
227        console.warn('Getting book chapter authors');
228        let json = await API('/book/' + id + '/chapterAuthors');
229        return json.entities;
230    } catch (error) { console.error(error); return []; }
231}
232async function GetIndexResults(id, term) {
233    try {
234        console.warn('Getting index results');
235        let json = await API('/book/' + id + '/indexResults', { term: term });
236        return json.entities;
237    } catch (error) { console.log(error); return []; }
238}
239async function RebuildPDF(btn) {
240    try {
241        console.warn('Rebuilding PDF');
242        let id = $(btn).attr('data-action-value');
243        await API('/pdf/' + id + '/rebuild');
244        Toast('Rebuilding PDF', 'The PDF is being rebuilt. This may take some time.', 'bg-primary');
245        return true;
246    } catch (error) { console.error(error); return null; }
247}
248async function SortBookChapters(data) {
249    try {
250        let book = books[$('#main-upper').attr('data-entity-id')];
251        await API('/book/' + book.id + '/sortChapters', data);
252        return true;
253    } catch (error) { console.error(error); return false; }
254}
255async function RenameBook(book_id, short_name) {
256    try {
257        console.log("Changing the book URI for " + book_id + " to " + short_name);
258        let json = await API('/book/' + book_id + '/rename/' + short_name);
259        current_book = await GetBook(book_id, true);
260        ChangePath('/' + current_book.short_name);
261        return true;
262    } catch (error) { console.error(error); return false; }
263}
264async function CreateChapter(btn) {
265    try {
266        DisableEditors();
267        let book_id = $(btn).closest('[data-entity-type="book"][data-entity-id]').attr('data-entity-id');
268        let json = await API('/book/' + book_id + '/createChapter');
269        current_book = await GetBook(book_id, true);
270        await current_book.RenderLower();
271        ApplyPermissions(current_book.lowerContainer, current_book);
272        current_book.RenderNavLinks(true);
273        Toast('Content Created', 'The content was created and added to the publication.', 'bg-success text-light');
274        let e = $('.toc-row').last()[0];
275        ScrollTo(e);
276        $(e).attr('data-pulse', true);
277        return true;
278    } catch (error) { console.error(error); return false; }
279}
280async function SubmitChapter(book_id, data) {
281    try {
282        let json = await API('/book/' + book_id + '/submitChapter', data);
283        Toast('Content Created', 'The content was submitted to the publication.', 'bg-success text-light');
284        return true;
285    } catch (error) { console.error(error); return false; }
286}
287async function ImportChapters(book_id, chapter_ids) {
288    try {
289        let json = await API('/book/' + book_id + '/importChapters', { chapter_ids: chapter_ids });
290        Toast('Content Imported', 'The content was imported into the publication.', 'bg-success text-light');
291        return true;
292    } catch (error) { console.error(error); return false; }
293}
294
295// Chapters
296async function GetChapter(book_short_name, chapter_short_name, rebuild = false) {
297    if (chapter_short_names[book_short_name] !== undefined && chapter_short_names[book_short_name][chapter_short_name] !== undefined && !rebuild) return await GetChapterFromID(chapter_short_names[book_short_name][chapter_short_name], rebuild);
298    try {
299        console.warn('Getting chapter id from path');
300        let json = await API('/chapter/getIDFromPath/' + book_short_name + '/' + chapter_short_name);
301        let chapter = await GetChapterFromID(json.entity_id, rebuild);
302        await chapter.Initialize();
303        return chapter;
304    } catch (error) { console.error(error); return null; }
305}
306async function GetRevisions(id, rebuild = false) {
307    if (chapters[id] === undefined) await GetChapterFromID(id);
308    if (chapters[id].revisions !== undefined && !rebuild) return chapters[id].revisions;
309    try {
310        console.warn('Getting revisions from id');
311        let json = await API('/chapter/' + id + '/revisions');
312        console.log(json);
313        chapters[id].revisions = json.entities;
314        return chapters[id].revisions;
315    } catch (error) { console.error(error); return null; }
316}
317async function GetRevisionsLean(id) {
318    try {
319        console.warn('Getting revisions lean from id');
320        let json = await API('/chapter/' + id + '/revisions/lean');
321        console.log(json);
322        return json.entities;
323    } catch (error) { console.error(error); return null; }
324}
325async function GetRevision(id, created) {
326    try {
327        console.warn('Getting revisions lean from id');
328        let json = await API('/chapter/' + id + '/revision/' + created);
329        console.log(json);
330        return json.revision;
331    } catch (error) { console.error(error); return null; }
332}
333async function GetChapterReviews(id, rebuild = false) {
334    if (chapters[id] === undefined) await GetChapterFromID(id);
335    if (chapters[id].reviews !== undefined && !rebuild) return chapters[id].reviews;
336    try {
337        console.warn('Getting reviews from id');
338        let json = await API('/chapter/' + id + '/reviews');
339        chapters[id].reviews = [];
340        for (let entity of json.entities) {
341            let review = new Review(entity);
342            chapters[id].reviews.push(review);
343        }
344        return chapters[id].reviews;
345    } catch (error) { console.error(error); return null; }
346}
347async function AddChapterReviewer(chapter_id, author_id, round) {
348    try {
349        let json = await API('/chapter/' + chapter_id + '/addReviewer', { author_id: author_id, round: round });
350        return json;
351    } catch (error) { console.error(error); return null; }
352}
353async function RemoveChapterReviewer(chapter_id, review_id) {
354    try {
355        let json = await API('/chapter/' + chapter_id + '/removeReviewer/' + review_id);
356        return json;
357    } catch (error) { console.error(error); return null; }
358}
359async function GetChapterFromID(id, rebuild = false) {
360    if (chapters[id] !== undefined && !rebuild) return chapters[id];
361    try {
362        console.warn('Getting chapter from id');
363        let json = await API('/chapter/' + id + '/view');
364        if (chapter_short_names[json.entity.book_short_name] == undefined) chapter_short_names[json.entity.book_short_name] = {};
365        chapter_short_names[json.entity.book_short_name][json.entity.short_name] = json.entity.id;
366        chapters[json.entity.id] = new Chapter(json.entity);
367        return chapters[json.entity.id];
368    } catch (error) { console.error(error); return null; }
369}
370async function MoveChapter(chapter_id, book_id) {
371    try {
372        let json = await API('/book/' + book_id + '/moveChapter/' + chapter_id);
373        chapters[json.entity.book_short_name] = {};
374        chapters[json.entity.book_short_name][json.entity.short_name] = json.entity;
375        current_book = await GetBook(json.entity.book_id, true);
376        current_chapter = await GetChapterFromID(chapter_id);
377        ChangePath('/' + json.entity.book_short_name + '/' + json.entity.short_name);
378        return true;
379    } catch (error) { console.error(error); return false; }
380}
381async function Rate(chapter_id, value) {
382    try {
383        let json = await API('/chapter/' + chapter_id + '/rate', { value: value });
384        return true;
385    } catch (error) { console.error(error); return false; }
386}
387async function DeleteBook(btn) {
388    console.log('Deleting the book.');
389    try {
390        let book_id = $(btn).closest('[data-entity-type="book"][data-entity-id]').attr('data-entity-id');
391        let json = await API('/book/' + book_id + '/delete');
392        current_book = null;
393        ChangePath('/');
394        $('.modal').modal('hide');
395        Toast('Content Deleted', 'The publication and all of its content was successfully deleted.', 'bg-danger text-light');
396        return true;
397    } catch (error) { console.error(error); return false; }
398}
399async function DeleteChapter(btn) {
400    console.log('Deleting the chapter.');
401    try {
402        let chapter_id = $(btn).closest('[data-entity-type="chapter"][data-entity-id]').attr('data-entity-id');
403        let json = await API('/chapter/' + chapter_id + '/delete');
404        current_book = await GetBook(current_book.book_id, true);
405        ChangePath('/' + current_book.short_name);
406        $('.modal').modal('hide');
407        Toast('Content Deleted', 'The content was successfully deleted.', 'bg-danger text-light');
408        return true;
409    } catch (error) { console.error(error); return false; }
410}
411async function RenameChapter(chapter_id, book_id, short_name) {
412    try {
413        let json = await API('/chapter/' + chapter_id + '/rename/' + short_name);
414        current_book = await GetBook(book_id, true);
415        current_chapter = await GetChapterFromID(chapter_id, true);
416        ChangePath('/' + current_book.short_name + '/' + current_chapter.short_name);
417        return true;
418    } catch (error) { console.error(error); return false; }
419}
420async function RebuildAudio(chapter_id) {
421    Toast('Rebuilding Audio', 'The audio is being rebuilt. This may take some time. Please try again later.', 'bg-primary');
422    try {
423        let json = await API('/audioElevenLabs/' + chapter_id + '/rebuild');
424        console.log(json);
425        Toast('Audio Rebuilt', 'The audio has been queued with ElevenLabs TTS.', 'bg-success');
426        return true;
427    } catch (elevenLabsError) {
428        console.warn('ElevenLabs failed, trying fallback:', elevenLabsError);
429        return RebuildAudioLocal(chapter_id);
430    }
431}
432async function RebuildAudioLocal(chapter_id) {
433    Toast('Rebuilding Audio Locally', 'The audio is being rebuilt. This may take some time. Please try again later.', 'bg-primary');
434    try {
435        // Fallback to old audio service
436        let json = await API('/audio/' + chapter_id + '/rebuild');
437        Toast('Audio Rebuilt', 'The audio has been queued with local TTS.', 'bg-success');
438        return true;
439    } catch (error) {
440        Toast('Audio Generation Error', 'There was an error generating the audio: ' + error.message, 'bg-danger');
441        console.error(error);
442        return false;
443    }
444}
445async function GenerateChapterAbstract(chapter_id) {
446    try {
447        console.warn('Generating chapter abstract');
448        let json = await API('/chapter/' + chapter_id + '/genai/abstract');
449        console.log(json);
450        return json.abstract;
451    } catch (error) { console.error(error); return null; }
452}
453async function GenerateChapterKeywords(chapter_id) {
454    try {
455        console.warn('Generating chapter keywords');
456        let json = await API('/chapter/' + chapter_id + '/genai/keywords');
457        console.log(json);
458        return json.keywords;
459    } catch (error) { console.error(error); return null; }
460}
461async function GenerateBookKeywords(book_id) {
462    try {
463        console.warn('Generating book keywords');
464        let json = await API('/book/' + book_id + '/genai/keywords');
465        console.log(json);
466        return json.keywords;
467    } catch (error) { console.error(error); return null; }
468}
469
470// Collections
471async function GetCollection(id, rebuild = false) {
472    if (collections[id] !== undefined && !rebuild) return collections[id];
473    try {
474        console.warn('Getting collection');
475        let json = await API('/collection/' + id);
476        collections[json.entity.collection_id] = new Collection(json.entity);
477        return collections[json.entity.collection_id];
478    } catch (error) { console.error(error); return null; }
479}
480async function ListCollections() {
481    try {
482        console.warn('Listing collections');
483        let json = await API('/collection/list');
484        return json.entities;
485    } catch (error) { console.error(error); return []; }
486}
487
488// Entity
489async function GetEntityInfoFromShortName(short_name, rebuild = false) {
490    if (short_name_info[short_name] !== undefined && !rebuild) return short_name_info[short_name];
491    try {
492        console.warn('Getting entity info from short name');
493        let json = await API('/entity/info/' + short_name);
494        short_name_info[short_name] = json.entity;
495        return json.entity;
496    } catch (error) { console.error(error); return null; }
497}
498async function GetAnalytics(entity, timeframe = 'monthly', verb = 'viewed') {
499    try {
500        console.warn('Getting entity analytics');
501        let json = await API('/' + entity.type + '/' + entity.id + '/analytics/' + timeframe + '/' + verb);
502        console.log(json.data);
503        return json.data;
504    } catch (error) { console.error(error); return null; }
505}
506async function ChangeAuthorVisibility(entity, author_id, shadow_author) {
507    try {
508        let json = await API('/' + entity.type + '/' + entity.id + '/changeAuthorVisibility/' + author_id + '/' + shadow_author);
509        Toast('Author Changed', 'The author\'s visibility has changed.', 'bg-success text-light');
510        return true;
511    } catch (error) { console.error(error); return false; }
512}
513async function RemoveAuthor(entity, author_id) {
514    try {
515        let json = await API('/' + entity.type + '/' + entity.id + '/removeAuthor/' + author_id);
516        Toast('Author Removed', 'The author was removed.', 'bg-warning text-light');
517        return true;
518    } catch (error) { console.error(error); return false; }
519}
520async function AddAuthor(entity, author_id) {
521    try {
522        let json = await API('/' + entity.type + '/' + entity.id + '/addAuthor/' + author_id);
523        Toast('Author Added', 'The author was added.', 'bg-success text-light');
524        return json;
525    } catch (error) { console.error(error); return false; }
526}
527async function SortAuthors(entity, data) {
528    try {
529        await API('/' + entity.type + '/' + entity.id + '/sortAuthors', data);
530        return true;
531    } catch (error) { console.error(error); return false; }
532}
533async function GetEntityImages(entity) {
534    try {
535        let json;
536        switch (entity.type) {
537            case 'book':
538                console.warn('Getting book images', entity);
539                json = await API('/book/' + entity.id + '/images');
540                return json.entities;
541            case 'page':
542                console.warn('Getting page images');
543                json = await API('/page/' + entity.id + '/images');
544                return json.entities;
545        }
546        return [];
547    } catch (error) { console.error(error); return []; }
548}
549async function DeleteEntityImage(entity, filename) {
550    try {
551        let json;
552        switch (entity.type) {
553            case 'book':
554                console.warn('Deleting book image');
555                json = await API('/book/' + entity.id + '/deleteImage', { filename: filename });
556                return true;
557        }
558        return false;
559    } catch (error) { console.error(error); return false; }
560}
561async function RenameEntityImage(entity, old_name, new_name) {
562    try {
563        let json;
564        switch (entity.type) {
565            case 'book':
566                console.warn('Renaming book image');
567                json = await API('/book/' + entity.id + '/renameImage', { old_filename: old_name, new_filename: new_name });
568                return json;
569        }
570        return false;
571    } catch (error) { console.error(error); return false; }
572}
573
574// Glossary
575async function GetGlossaryTerm(term_id, rebuild = false) {
576    if (glossary_terms[term_id] !== undefined && !rebuild) return glossary_terms[term_id];
577    try {
578        console.warn('Getting glossary term');
579        let json = await API('/glossary_term/' + term_id + '/view');
580        return new GlossaryTerm(json.entity);
581    } catch (error) { console.error(error); return null; }
582}
583async function CreateGlossaryTerm(book_id, word) {
584    try {
585        console.warn('Creating glossary term');
586        let json = await API('/glossary_term/create', { book_id: book_id, word: word });
587        return new GlossaryTerm(json.entity);
588    } catch (error) { console.error(error); return null; }
589}
590async function DeleteGlossaryTerm(term_id) {
591    try {
592        console.warn('Deleting glossary term');
593        let json = await API('/glossary_term/' + term_id + '/delete');
594        return true;
595    } catch (error) { console.error(error); return null; }
596}
597async function GetBookGlossaryTerms(book, rebuild = false) {
598    if (book.glossary_terms !== undefined && book.glossary_terms.length > 0 && !rebuild) return book.glossary_terms;
599    try {
600        console.warn('Listing glossary terms');
601        book.glossary_terms = [];
602        let json = await API('/book/' + book.book_id + '/glossaryTerms');
603        for (let entity of json.entities) {
604            book.glossary_terms.push(new GlossaryTerm(entity));
605            glossary_terms[entity.term_id] = entity;
606        }
607        return book.glossary_terms;
608    } catch (error) { console.error(error); return []; }
609}
610async function DefineGlossaryTerm(term_id) {
611    try {
612        console.warn('Defining glossary term');
613        let json = await API('/glossary_term/' + term_id + '/define');
614        return json.term;
615    } catch (error) { console.error(error); return null; }
616}
617
618// Images
619async function UploadImage(url, file) {
620    try {
621        console.warn('Uploading the image', url, file);
622        let json = await APIFile(url, file);
623        return json.entity;
624    } catch (error) { console.error(error); return []; }
625}
626
627async function SaveImageLocally(src, book_id) {
628    let json = await API('/book/' + book_id + '/saveImageLocally', { src: src });
629    return json.src;
630}
631
632// Keywords
633async function KeywordSearch(query) {
634    try {
635        console.warn('Searching for keywords');
636        let json = await API('/keyword/search', { query: query });
637        return json.keywords;
638    } catch (error) { console.error(error); return []; }
639}
640async function CreateKeywords(term) {
641    try {
642        console.warn('Creating keyword ' + term);
643        let json = await API('/keyword/create', { term: term });
644        return json.keywords;
645    } catch (error) { console.error(error); return []; }
646}
647async function RemoveKeyword(keyword_id, entity_type, entity_id) {
648    try {
649        console.warn('Removing keyword');
650        await API('/' + entity_type + '/' + entity_id + '/removeKeyword/' + keyword_id);
651        return true;
652    } catch (error) { console.error(error); return false; }
653}
654async function AddKeyword(keyword_id) {
655    try {
656        console.warn('Adding keyword');
657        let entity_type = (current_chapter !== null) ? 'chapter' : 'book';
658        let entity_id = (current_chapter !== null) ? current_chapter.id : current_book.id;
659        await API('/' + entity_type + '/' + entity_id + '/addKeyword/' + keyword_id);
660        return true;
661    } catch (error) { console.error(error); return false; }
662}
663async function GetKeyword(keyword_id) {
664    try {
665        console.warn('Getting keyword');
666        let json = await API('/keyword/' + keyword_id);
667        return json.entity;
668    } catch (error) { console.error(error); return null; }
669}
670
671// Languages
672async function GetLanguage(id) {
673    try {
674        if (id == undefined || id == null) return null;
675        for (let language of await GetLanguages()) {
676            if (language.language_id == id) return language;
677        }
678        console.warn('Getting language');
679        let json = await API('/language/' + id);
680        languages[json.entity.language_id] = json.entity;
681        return json.entity;
682    } catch (error) { console.error(error); return null; }
683}
684async function GetLanguages(rebuild = false) {
685    if (languages.length > 0 && !rebuild) return languages;
686    try {
687        console.warn('Getting languages');
688        let json = await API('/language/list');
689        languages = json.entities;
690        return languages;
691    } catch (error) { console.error(error); return []; }
692}
693async function GetTranslationID(chapter_id, language_id) {
694    try {
695        console.warn('Getting translation id');
696        let json = await API('/translation/id/' + chapter_id + '/' + language_id);
697        return json.translation_id;
698    } catch (error) { console.error(error); return null; }
699}
700async function GetTranslation(id, rebuild = false) {
701    if (translations[id] !== undefined && !rebuild) return translations[id];
702    try {
703        console.warn('Getting translation');
704        let json = await API('/translation/' + id + '/view');
705        translations[id] = json.entity;
706        return translations[id];
707    }
708    catch (error) { console.error(error); return null; }
709}
710async function RebuildTranslation(chapter_id, language_id) {
711    try {
712        console.warn('Rebuilding translation');
713        let json = await API(`/translation/${chapter_id}/${language_id}/rebuild`);
714        return true;
715    }
716    catch (error) { console.error(error); return null; }
717}
718
719// Licenses
720async function GetLicense(id) { // needs to be fixed
721    for (let license in await GetLicenses()) {
722        if (license.license_id == id) return license;
723    }
724    try {
725        console.warn('Getting license');
726        let json = await API('/license/' + id);
727        licenses[json.entity.license_id] = json.entity;
728        return json.entity;
729    } catch (error) { console.error(error); return null; }
730}
731async function GetLicenses(rebuild = false) {
732    if (licenses.length > 0 && !rebuild) return licenses;
733    try {
734        console.warn('Getting licenses');
735        let json = await API('/license/list');
736        licenses = json.entities;
737        return licenses;
738    } catch (error) { console.error(error); return []; }
739}
740
741// Login
742async function SetRedirect() {
743    try {
744        console.warn('Setting the redirect');
745        await API('/user/setRedirect', { url: window.location.href });
746        return true;
747    } catch (error) { console.error(error); return false; }
748}
749
750// Pages
751async function GetPage(id, rebuild = false) {
752    if (pages[id] !== undefined && !rebuild) return pages[id];
753    if (page_short_names[id] !== undefined && pages[page_short_names[id]] !== undefined && !rebuild) return pages[page_short_names[id]];
754    try {
755        console.warn('Getting page');
756        let json = await API('/page/' + id + '/view');
757        page_short_names[json.entity.short_name] = json.entity.id;
758        pages[json.entity.id] = new Page(json.entity);
759        return pages[json.entity.id];
760    } catch (error) { console.error(error); return null; }
761}
762
763// Saving
764async function SaveSetting(type, id, values = {}) {
765    try {
766        console.log('Saving');
767        await API('/' + type + '/' + id + '/saveSettings', values);
768        switch (type) { // Clear caches
769            case 'author':
770                delete (authors[id]);
771                break;
772            case 'book':
773                delete (books[id]);
774                break;
775            case 'chapter':
776                if (chapters[id] !== undefined) delete (books[chapters[id].book_id]);
777                delete (chapters[id]);
778                break;
779            case 'page':
780                delete (pages[id]);
781                break;
782            case 'glossary':
783                delete (glossary_terms[id]);
784                break;
785        }
786        return true;
787    } catch (error) { console.error(error); return false; }
788}
789
790// Screenshot
791async function RebuildChapterScreenshotClick(btn) {
792    try {
793        let chapter_id = $(btn).attr('data-action-value');
794        return await RebuildChapterScreenshot(chapter_id);
795    } catch (error) { console.error(error); return false; }
796}
797async function RebuildChapterScreenshot(chapter_id) {
798    try {
799        console.warn('Rebuilding screenshot');
800        await API('/screenshot/' + chapter_id);
801        return true;
802    } catch (error) { console.error(error); return false; }
803}
804
805// Search
806async function Search(string, types, limit, vars = {}) {
807    try {
808        console.warn('Searching');
809        let json = await API('/search', { term: string, types: types, limit: limit, vars: vars });
810        return json.results;
811    } catch (error) { console.error(error); return null; }
812}
813
814// Site
815async function GetSite(rebuild = false) {
816    if (site !== null && !rebuild) return site;
817    try {
818        console.warn('Getting site');
819        let json = await API('/site/info');
820        site = new Site(json.entity);
821        site.logged_in = json.logged_in === true;
822        if (site.stylesheet !== null) LoadCSS(site.stylesheet);
823        return site;
824    } catch (error) { console.error(error); return null; }
825}
826async function GetSiteBookLinks(offset, limit) {
827    try {
828        console.warn('Getting site book links');
829        let json = await API('/site/bookLinks', { offset: offset, limit: limit });
830        return json.links;
831    } catch (error) { console.error(error); return []; }
832}
833async function GetSiteAnalytics() {
834    try {
835        console.warn('Getting site analytics');
836        let json = await API('/site/analytics');
837        return json.data;
838    } catch (error) { console.error(error); return []; }
839}
840async function GetSiteJournalIssues(offset, limit) {
841    try {
842        console.warn('Getting site book links');
843        let json = await API('/site/journalIssues');
844        return json.links;
845    } catch (error) { console.error(error); return []; }
846}
847async function GetSiteFeaturedBooks(rebuild = false) {
848    if (site !== null && site.featured_book_links != undefined && !rebuild) return site.featured_book_links;
849    try {
850        console.warn('Getting site featured book links');
851        let json = await API('/site/featuredBookLinks');
852        site.featured_book_links = json.links;
853        return json.links;
854    } catch (error) { console.error(error); return []; }
855}
856async function GetEndorsements() {
857    try {
858        console.warn('Getting site endorsements');
859        let json = await API('/site/endorsements');
860        return json.entities;
861    } catch (error) { console.error(error); return []; }
862}
863
864// Users
865async function GetCurrentUser(rebuild = false) {
866    if (current_user != null && !rebuild) return current_user;
867    try {
868        console.warn('Getting current user');
869        let json = await API('/user/current');
870        current_user = new Author(json.entity);
871        await UpdateUserIcons();
872        await ChangeVisualMode(current_user.settings.visual_mode, false);
873        return current_user;
874    } catch (error) { return null; }
875}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.