node-hitomi
Version:
Hitomi.la API for Node.js
237 lines (234 loc) • 8.22 kB
JavaScript
import { HitomiError, ErrorCode } from '../structures/error.mjs';
import { TranslatedGallery, GalleryReference, Gallery, Title } from '../structures/gallery.mjs';
import { RESOURCE_DOMAIN } from '../internal/constants.mjs';
import { IndexProvider } from '../internal/providers.mjs';
import { Base } from '../internal/base.mjs';
import { Image, Video } from '../structures/media.mjs';
import { Tag, Language } from '../structures/tag.mjs';
const SortType = {
DateAdded: 'added',
DatePublished: 'published',
Random: 'random',
PopularityDay: 'today',
PopularityWeek: 'week',
PopularityMonth: 'month',
PopularityYear: 'year'
};
class GalleryManager extends Base {
constructor(hitomi) {
super(hitomi);
this.defineProperties({
index: new IndexProvider(hitomi, 'galleries')
});
}
async retrieve(id) {
const rawGallery = JSON.parse((await this.hitomi.request(RESOURCE_DOMAIN, '/galleries/' + id + '.js', 2)).slice(18));
const dedicatedTags = [[], [], [], []];
const tags = [];
const files = [];
const translations = [];
let i = 0;
let type;
for(; i < GalleryManager.RAW_TYPES.length; i++) {
const rawPluralType = GalleryManager.RAW_TYPES[i] + 's';
type = i !== 2 ? GalleryManager.RAW_TYPES[i] : 'series';
if(rawGallery[rawPluralType]) {
for(let j = 0; j < rawGallery[rawPluralType].length; j++)
dedicatedTags[i].push(new Tag(this.hitomi, type, rawGallery[rawPluralType][j][GalleryManager.RAW_TYPES[i]]));
}
}
for(i = 0; i < rawGallery.tags.length; i++) {
if(Boolean(rawGallery.tags[i].male)) {
type = 'male';
} else if(Boolean(rawGallery.tags[i].female)) {
type = 'female';
} else {
type = 'tag';
}
tags.push(new Tag(this.hitomi, type, rawGallery.tags[i].tag, false));
}
const thumbnailIndex = Math.floor(rawGallery.files.length / 2);
for(i = 0; i < rawGallery.files.length; i++) {
files.push(new Image(this.hitomi, rawGallery.files[i].width, rawGallery.files[i].height, rawGallery.files[i].hash, rawGallery.files[i].name, Boolean(rawGallery.files[i].hasavif), true, false, !i || i === thumbnailIndex));
}
for(i = 0; i < rawGallery.languages.length; i++) {
translations.push(new TranslatedGallery(this.hitomi, rawGallery.languages[i].galleryid, new Language(this.hitomi, rawGallery.languages[i].name, rawGallery.languages[i].language_localname), rawGallery.languages[i].url));
}
const relations = [];
for(i = 0; i < rawGallery.related.length; i++) {
relations.push(new GalleryReference(this.hitomi, rawGallery.related[i]));
}
return new Gallery(this.hitomi, +rawGallery.id, rawGallery.language ? new Language(this.hitomi, rawGallery.language, rawGallery.language_localname) : null, rawGallery.galleryurl, new Title(rawGallery.title, rawGallery.japanese_title), rawGallery.type, dedicatedTags[0], dedicatedTags[1], dedicatedTags[2], dedicatedTags[3], tags, files, translations, relations, Boolean(rawGallery.blocked), new Date(rawGallery.date), rawGallery.datepublished ? new Date(rawGallery.datepublished) : null, rawGallery.videofilename ? new Video(this.hitomi, files[1].width, files[1].height, rawGallery.videofilename, files[1].hash) : null);
}
async requestIds(path, range, isNegative = false) {
const view = await this.hitomi.request(RESOURCE_DOMAIN, path, 1, range);
const ids = new Set();
for(let i = 0; i < view.byteLength; i += 4) {
ids.add(view.getInt32(i));
}
if(isNegative) {
ids.add(0);
}
return ids;
}
static createNozomiPath(options = {}) {
const language = options.language || 'all';
let orderBy = '';
if(options.orderBy) {
switch(options.orderBy) {
case SortType.DatePublished: {
orderBy = 'date/published';
}
case SortType.DateAdded:
case SortType.Random: {
break;
}
case SortType.PopularityDay:
case SortType.PopularityWeek:
case SortType.PopularityMonth:
case SortType.PopularityYear: {
orderBy = 'popular/' + options.orderBy;
break;
}
default: {
throw HitomiError.invalidMember('OrderBy', SortType);
}
}
}
if(!options.tag || options.tag.type === 'language') {
return '/n/' + (orderBy || 'index') + '-' + language + '.nozomi';
}
if(orderBy) {
orderBy += '/';
}
let area;
switch(options.tag.type) {
case 'male':
case 'female': {
area = 'tag/';
orderBy += options.tag.type + ':';
break;
}
default: {
area = options.tag.type + '/';
}
}
return '/n/' + area + orderBy + encodeURIComponent(options.tag.name) + '-' + language + '.nozomi';
}
createReferences(ids, shouldShuffle) {
const references = [];
for(const id of ids) {
references.push(new GalleryReference(this.hitomi, id));
}
if(shouldShuffle) {
let currentIndex = references.length;
let targetIndex;
while(currentIndex) {
targetIndex = Math.floor(Math.random() * currentIndex--);
const temporary = references[targetIndex];
references[targetIndex] = references[currentIndex];
references[currentIndex] = temporary;
}
}
return references;
}
async list(options = {}) {
const idSets = [];
const isRandom = options.orderBy === SortType.Random;
let language;
let i = 0;
let range;
if(options.page) {
const size = options.page.size ? options.page.size * 4 : 100;
const start = options.page.index ? options.page.index * size : 0;
range = start + '-' + (start + size - 1);
}
if(options.tags && options.tags.length) {
const tags = options.tags.slice().sort(Tag.compare);
if(!tags[0].isNegative) {
for(; i < tags.length && !tags[i].isNegative; i++) {
if(tags[i].type === 'language') {
language = tags[i].name;
break;
}
}
i = 0;
}
if(range) {
if(tags.length > 2 || tags.length === 2 && !language) {
throw new HitomiError(ErrorCode.InvalidCombination, 'Page', 'used with multiple tags', false);
}
if(tags[tags.length - 1].isNegative) {
throw new HitomiError(ErrorCode.InvalidCombination, 'Page', 'used with negative tag', false);
}
return this.createReferences(await this.requestIds(GalleryManager.createNozomiPath({
tag: tags[+(tags[0].type === 'language')],
orderBy: options.orderBy,
language: language
}), range), isRandom);
}
idSets.push(await this.requestIds(GalleryManager.createNozomiPath({
tag: tags[0].isNegative ? undefined : tags[i++],
orderBy: options.orderBy,
language: language
})));
for(; i < tags.length; i++) {
if(tags[i].type !== 'language' || !language && tags[i].isNegative) {
idSets.push(await this.requestIds(GalleryManager.createNozomiPath({
tag: tags[i],
language: language
}), undefined, tags[i].isNegative));
}
}
} else {
const path = GalleryManager.createNozomiPath({
orderBy: options.orderBy
});
if(range) {
return this.createReferences(await this.requestIds(path, range), isRandom);
}
if(options.orderBy) {
idSets.push(await this.requestIds(path));
}
}
if(options.title && options.title.length) {
const version = await this.index.retrieve();
const title = options.title.toLowerCase() + ' ';
const rootNode = await this.index.getNodeAtAddress(0n, version);
if(!rootNode) {
throw HitomiError.emptyRootNode;
}
i = 0;
let j = title.indexOf(' ');
while(j !== -1) {
if(j - i) {
const data = await this.index.binarySearch(await this.hitomi.hashTerm(title.slice(i, j)), rootNode, version);
if(!data) {
return [];
}
idSets.push(await this.requestIds('/galleriesindex/galleries.' + version + '.data', (data[0] + 4n) + '-' + (data[0] + BigInt(data[1]) - 1n)));
}
i = j + 1;
j = title.indexOf(' ', i);
}
}
if(idSets.length) {
for(i = 1; i < idSets.length; i++) {
if(!idSets[0].size) {
return [];
}
const isNegative = idSets[i].has(0);
for(const id of idSets[0]) {
if(isNegative === idSets[i].has(id)) {
idSets[0].delete(id);
}
}
}
} else {
idSets.push(await this.requestIds(GalleryManager.createNozomiPath()));
}
return this.createReferences(idSets[0], isRandom);
}
}
GalleryManager.RAW_TYPES = ['artist', 'group', 'parody', 'character'];
export { GalleryManager, SortType };