@kecan0406/aninames 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/client/chunks.d.ts +1 -1
- package/lib/client/chunks.js +2 -2
- package/lib/client/format.d.ts +5 -1
- package/lib/client/format.js +13 -5
- package/lib/client/names.d.ts +8 -0
- package/lib/client/names.js +31 -6
- package/lib/model.d.ts +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Built sizes: `core.sqlite` 610 MB, `kowiki.sqlite` 25 MB, `unlicensed.sqlite` 83
|
|
|
56
56
|
| `<layer>/<table>/<hash>.json.gz` | about 32 KB of JSON rows sorted by key (16 KB for `entity`), named by the first 48 bits of the SHA-256 of the file |
|
|
57
57
|
| `NOTICE.md`, `index.html` | each layer's sources and terms |
|
|
58
58
|
|
|
59
|
-
The key tables are `flat` (folded names), `latin` (romaji keys), `sound` (sound keys across kana, Hangul and romaji), `part` (name tokens) and `initial` (Korean initials); their rows are `[key, entity number, name weight, flags, popularity, standing, bonus]`. `tag` holds the same rows for Danbooru tags, keyed by the tag folded like a name without its qualifier and weighted by the tag's posts. `entity` rows, keyed by entity number (which follows the entity ids' order), carry the id, names, Danbooru tags, works, family
|
|
59
|
+
The key tables are `flat` (folded names), `latin` (romaji keys), `sound` (sound keys across kana, Hangul and romaji), `part` (name tokens) and `initial` (Korean initials); their rows are `[key, entity number, name weight, flags, popularity, standing, bonus]`. `tag` holds the same rows for Danbooru tags, keyed by the tag folded like a name without its qualifier and weighted by the tag's posts. `home` holds them for the copyright tag each character comes from, weighted by the character's own posts; a `core` row whose character the `unlicensed` layer gives another home carries flag 64, which a client that reads `unlicensed` skips. `entity` rows, keyed by entity number (which follows the entity ids' order), carry the id, names, Danbooru tags, works, family, the ids other databases give one entity in that layer (TMDB's `tmdb:tv/…` and `tmdb:movie/…`, IGDB's `igdb:…` and Steam's `steam:…` among them, read from Wikidata), a character's home tag and a work's countries of origin (Wikidata P495, as ISO 3166-1 alpha-2 codes such as `JP`). Layers stay in separate files; the only values a file carries from other layers are derived numbers and flags (popularity, family standing, adult, which layers know the entity).
|
|
60
60
|
|
|
61
61
|
On the 10-04 build: 19,167 files, 149 MB gzip'd, a manifest of 236 KB gzip'd. `mise run compare` searches the probe sample (4,370 queries) both ways: the files find the entity first 87% of the time, SQLite 86% (FTS substring search is not in the files; typo search reads the rows around the query's position instead). A cold query needs a median of 2 rounds (p90 3), 31 files (p90 78) and 287 KB (p90 584 KB).
|
|
62
62
|
|
|
@@ -71,7 +71,7 @@ const [first] = await names.search('봇치', { limit: 10, signal })
|
|
|
71
71
|
const works = await names.lookup(first?.works ?? [])
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
`search` returns `Found` cards (`id`, `kind`, `label`, `names` with their `lang` and `kind`, `danbooru` tags with the most posts first, `works`, `refs`, `score`, `adult`); `search(query, { kind, limit, adult: false })` narrows it, `lookup(ids)` returns the cards of given ids in one round, and `tagged(tag, { kind, adult })` the cards of the entities that carry a Danbooru tag, most posts first, whatever its case, punctuation or qualifier (`soul-eater`, `soul_eater` and `soul_eater_(series)` find the same works); `prefix: true` also finds the tags that start with it. `cachedSource` keeps the files it fetched forever (their names are hashes of their contents), revalidates the manifest with its ETag or Last-Modified once a day (`maxAge`), answers from the cache when offline, refetches the manifest when the site has moved on to a new build, and deletes files a new manifest no longer names. `fileSource(dir)` reads a local `dist/site`.
|
|
74
|
+
`search` returns `Found` cards (`id`, `kind`, `label`, `names` with their `lang` and `kind`, `danbooru` tags with the most posts first, `works`, `refs`, `home`, the copyright tag a character comes from, `origin`, the countries a work comes from, `score`, `adult`); `search(query, { kind, limit, adult: false })` narrows it, `lookup(ids)` returns the cards of given ids in one round, and `tagged(tag, { kind, adult })` the cards of the entities that carry a Danbooru tag, most posts first, whatever its case, punctuation or qualifier (`soul-eater`, `soul_eater` and `soul_eater_(series)` find the same works); `prefix: true` also finds the tags that start with it. `cast(tag, { adult, prefix, limit })` returns the characters whose home is that tag, most posts first. `cachedSource` keeps the files it fetched forever (their names are hashes of their contents), revalidates the manifest with its ETag or Last-Modified once a day (`maxAge`), answers from the cache when offline, refetches the manifest when the site has moved on to a new build, and deletes files a new manifest no longer names. `fileSource(dir)` reads a local `dist/site`.
|
|
75
75
|
|
|
76
76
|
## Development
|
|
77
77
|
|
package/lib/client/chunks.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare class View {
|
|
|
19
19
|
constructor(manifest: Manifest, layers: Layer[], loaded: Loaded);
|
|
20
20
|
private rows;
|
|
21
21
|
keys(table: KeyTable, range: Range): KeyRow[];
|
|
22
|
-
tagged(key: string, prefix: boolean): KeyRow[];
|
|
22
|
+
tagged(key: string, prefix: boolean, table?: 'tag' | 'home'): KeyRow[];
|
|
23
23
|
private keysIn;
|
|
24
24
|
around(table: KeyTable, key: string, radius: number): KeyRow[];
|
|
25
25
|
entity(eid: number, mask?: number): EntityRow[];
|
package/lib/client/chunks.js
CHANGED
|
@@ -39,9 +39,9 @@ export class View {
|
|
|
39
39
|
keys(table, range) {
|
|
40
40
|
return this.layers.flatMap((layer) => this.keysIn(layer, table, this.manifest.layers.get(layer)?.keys[table], range));
|
|
41
41
|
}
|
|
42
|
-
tagged(key, prefix) {
|
|
42
|
+
tagged(key, prefix, table = 'tag') {
|
|
43
43
|
const range = { low: key, high: prefix ? `${key}\u{10FFFF}` : key };
|
|
44
|
-
return this.layers.flatMap((layer) => this.keysIn(layer,
|
|
44
|
+
return this.layers.flatMap((layer) => this.keysIn(layer, table, this.manifest.layers.get(layer)?.[table], range));
|
|
45
45
|
}
|
|
46
46
|
keysIn(layer, table, spans, range) {
|
|
47
47
|
if (!spans)
|
package/lib/client/format.d.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { type Kind, type Layer, type NameKind } from '../model.ts';
|
|
|
2
2
|
export declare const FORMAT = 1;
|
|
3
3
|
export declare const KEY_TABLES: readonly ["flat", "latin", "sound", "part", "initial"];
|
|
4
4
|
export type KeyTable = (typeof KEY_TABLES)[number];
|
|
5
|
-
export declare const TABLES: readonly ["flat", "latin", "sound", "part", "initial", "tag", "entity"];
|
|
5
|
+
export declare const TABLES: readonly ["flat", "latin", "sound", "part", "initial", "tag", "home", "entity"];
|
|
6
6
|
export type Table = (typeof TABLES)[number];
|
|
7
7
|
export declare const NAME_KINDS: readonly NameKind[];
|
|
8
8
|
export declare const TRUSTED: readonly string[];
|
|
9
9
|
export declare const CHARACTER = 1;
|
|
10
10
|
export declare const MAIN = 2;
|
|
11
11
|
export declare const ADULT = 4;
|
|
12
|
+
export declare const SHADOWED = 64;
|
|
12
13
|
export declare function present(layer: Layer): number;
|
|
13
14
|
export declare const HASH_LENGTH = 12;
|
|
14
15
|
export declare function chunkPath(layer: Layer, table: Table, hash: string): string;
|
|
@@ -35,6 +36,7 @@ export interface EntitySpans extends Spans {
|
|
|
35
36
|
export interface LayerSpans {
|
|
36
37
|
keys: Record<KeyTable, Spans>;
|
|
37
38
|
tag: Spans | undefined;
|
|
39
|
+
home: Spans | undefined;
|
|
38
40
|
entity: EntitySpans;
|
|
39
41
|
}
|
|
40
42
|
export interface Manifest {
|
|
@@ -73,5 +75,7 @@ export interface EntityRow {
|
|
|
73
75
|
extra: number;
|
|
74
76
|
adult: boolean;
|
|
75
77
|
refs: string[];
|
|
78
|
+
home: string | null;
|
|
79
|
+
origin: string[];
|
|
76
80
|
}
|
|
77
81
|
export declare function readRows(table: Table, value: unknown): KeyRow[] | EntityRow[] | undefined;
|
package/lib/client/format.js
CHANGED
|
@@ -3,7 +3,7 @@ import { flat } from "../text.js";
|
|
|
3
3
|
import { compare } from "./order.js";
|
|
4
4
|
export const FORMAT = 1;
|
|
5
5
|
export const KEY_TABLES = ['flat', 'latin', 'sound', 'part', 'initial'];
|
|
6
|
-
export const TABLES = [...KEY_TABLES, 'tag', 'entity'];
|
|
6
|
+
export const TABLES = [...KEY_TABLES, 'tag', 'home', 'entity'];
|
|
7
7
|
export const NAME_KINDS = [
|
|
8
8
|
'main',
|
|
9
9
|
'official',
|
|
@@ -18,6 +18,7 @@ export const TRUSTED = ['wikidata', 'aod', 'anidb', 'vndb'];
|
|
|
18
18
|
export const CHARACTER = 1;
|
|
19
19
|
export const MAIN = 2;
|
|
20
20
|
export const ADULT = 4;
|
|
21
|
+
export const SHADOWED = 64;
|
|
21
22
|
export function present(layer) {
|
|
22
23
|
return 8 << LAYERS.indexOf(layer);
|
|
23
24
|
}
|
|
@@ -127,7 +128,8 @@ export function readManifest(text) {
|
|
|
127
128
|
for (const table of KEY_TABLES)
|
|
128
129
|
keys[table] = spansOf(tables[table], `${layer}/${table}`);
|
|
129
130
|
const tag = tables.tag === undefined ? undefined : spansOf(tables.tag, `${layer}/tag`);
|
|
130
|
-
|
|
131
|
+
const home = tables.home === undefined ? undefined : spansOf(tables.home, `${layer}/home`);
|
|
132
|
+
layers.set(layer, { keys, tag, home, entity: unpack(tables.entity, `${layer}/entity`, true) });
|
|
131
133
|
}
|
|
132
134
|
if (!layers.has('core'))
|
|
133
135
|
throw bad('it has no core layer');
|
|
@@ -141,6 +143,8 @@ export function manifestPaths(manifest) {
|
|
|
141
143
|
paths.add(chunkPath(layer, table, hash));
|
|
142
144
|
for (const hash of spans.tag?.files ?? [])
|
|
143
145
|
paths.add(chunkPath(layer, 'tag', hash));
|
|
146
|
+
for (const hash of spans.home?.files ?? [])
|
|
147
|
+
paths.add(chunkPath(layer, 'home', hash));
|
|
144
148
|
for (const hash of spans.entity.files)
|
|
145
149
|
paths.add(chunkPath(layer, 'entity', hash));
|
|
146
150
|
}
|
|
@@ -188,10 +192,11 @@ const tagOf = (value) => Array.isArray(value) && typeof value[0] === 'string' &&
|
|
|
188
192
|
const workOf = (value) => Array.isArray(value) && isCount(value[0]) && typeof value[1] === 'string' ? [value[0], value[1]] : undefined;
|
|
189
193
|
const eidOf = (value) => (isCount(value) ? value : undefined);
|
|
190
194
|
const refOf = (value) => typeof value === 'string' && value.indexOf(':') > 0 ? value : undefined;
|
|
195
|
+
const countryOf = (value) => typeof value === 'string' && /^[A-Z]{2}$/.test(value) ? value : undefined;
|
|
191
196
|
function entityRow(value) {
|
|
192
197
|
if (!Array.isArray(value) || value.length < 10)
|
|
193
198
|
return undefined;
|
|
194
|
-
const [eid, id, rawNames, rawTags, rawWorks, rawReach, rank, standing, extra, adult, rawRefs = []] = value;
|
|
199
|
+
const [eid, id, rawNames, rawTags, rawWorks, rawReach, rank, standing, extra, adult, rawRefs = [], home = null, rawOrigin = [],] = value;
|
|
195
200
|
if (!isCount(eid) || typeof id !== 'string')
|
|
196
201
|
return undefined;
|
|
197
202
|
const kind = id.startsWith('w/') ? 'work' : id.startsWith('c/') ? 'character' : undefined;
|
|
@@ -202,9 +207,12 @@ function entityRow(value) {
|
|
|
202
207
|
const works = list(rawWorks, workOf);
|
|
203
208
|
const reach = list(rawReach, eidOf);
|
|
204
209
|
const refs = list(rawRefs, refOf);
|
|
205
|
-
|
|
210
|
+
const origin = list(rawOrigin, countryOf);
|
|
211
|
+
if (!names || !danbooru || !works || !reach || !refs || !origin)
|
|
206
212
|
return undefined;
|
|
207
|
-
|
|
213
|
+
if (home !== null && typeof home !== 'string')
|
|
214
|
+
return undefined;
|
|
215
|
+
return { eid, id, kind, names, danbooru, works, reach, rank, standing, extra, adult: adult === 1, refs, home, origin };
|
|
208
216
|
}
|
|
209
217
|
export function readRows(table, value) {
|
|
210
218
|
if (!Array.isArray(value))
|
package/lib/client/names.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface Found {
|
|
|
13
13
|
danbooru: string[];
|
|
14
14
|
works: string[];
|
|
15
15
|
refs: string[];
|
|
16
|
+
home: string | null;
|
|
17
|
+
origin: string[];
|
|
16
18
|
score: number;
|
|
17
19
|
adult: boolean;
|
|
18
20
|
}
|
|
@@ -42,4 +44,10 @@ export declare class Names {
|
|
|
42
44
|
prefix?: boolean;
|
|
43
45
|
signal?: AbortSignal;
|
|
44
46
|
}): Promise<Found[]>;
|
|
47
|
+
cast(tag: string, options?: {
|
|
48
|
+
adult?: boolean;
|
|
49
|
+
prefix?: boolean;
|
|
50
|
+
limit?: number;
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
}): Promise<Found[]>;
|
|
45
53
|
}
|
package/lib/client/names.js
CHANGED
|
@@ -3,7 +3,7 @@ import { LAYERS } from "../model.js";
|
|
|
3
3
|
import { flat, qualifiers } from "../text.js";
|
|
4
4
|
import { View } from "./chunks.js";
|
|
5
5
|
import { search } from "./engine.js";
|
|
6
|
-
import { ADULT, CHARACTER, MAIN, NAME_KINDS, parsePath, readManifest, readRows, } from "./format.js";
|
|
6
|
+
import { ADULT, CHARACTER, MAIN, NAME_KINDS, parsePath, readManifest, readRows, SHADOWED, } from "./format.js";
|
|
7
7
|
import { Missing } from "./source.js";
|
|
8
8
|
const KEEP = 256;
|
|
9
9
|
const PARALLEL = 16;
|
|
@@ -78,7 +78,21 @@ function card(rows, score, adult) {
|
|
|
78
78
|
.map((entry) => entry.tag);
|
|
79
79
|
const works = head.kind === 'character' ? [...new Set(rows.flatMap((row) => row.works.map(([, id]) => id)))] : [];
|
|
80
80
|
const refs = [...new Set(rows.flatMap((row) => row.refs))].sort();
|
|
81
|
-
|
|
81
|
+
const home = rows.findLast((row) => row.home !== null)?.home ?? null;
|
|
82
|
+
const origin = [...new Set(rows.flatMap((row) => row.origin))];
|
|
83
|
+
return {
|
|
84
|
+
id: head.id,
|
|
85
|
+
kind: head.kind,
|
|
86
|
+
label: label(head.id, rows),
|
|
87
|
+
names,
|
|
88
|
+
danbooru,
|
|
89
|
+
works,
|
|
90
|
+
refs,
|
|
91
|
+
home,
|
|
92
|
+
origin,
|
|
93
|
+
score,
|
|
94
|
+
adult,
|
|
95
|
+
};
|
|
82
96
|
}
|
|
83
97
|
export class Names {
|
|
84
98
|
#source;
|
|
@@ -120,25 +134,36 @@ export class Names {
|
|
|
120
134
|
});
|
|
121
135
|
}
|
|
122
136
|
tagged(tag, options = {}) {
|
|
123
|
-
|
|
137
|
+
return this.#tagged('tag', tag, options);
|
|
138
|
+
}
|
|
139
|
+
cast(tag, options = {}) {
|
|
140
|
+
return this.#tagged('home', tag, options);
|
|
141
|
+
}
|
|
142
|
+
#tagged(table, tag, options) {
|
|
143
|
+
const { kind, adult = true, prefix = false, limit = Number.POSITIVE_INFINITY, signal } = options;
|
|
124
144
|
const key = flat(qualifiers(tag).base);
|
|
125
145
|
return this.#run(signal, (view) => {
|
|
126
146
|
if (!key)
|
|
127
147
|
return [];
|
|
148
|
+
const covered = view.layers.includes('unlicensed');
|
|
128
149
|
const best = new Map();
|
|
129
|
-
for (const [, eid, posts, flags, , standing] of view.tagged(key, prefix)) {
|
|
150
|
+
for (const [, eid, posts, flags, , standing] of view.tagged(key, prefix, table)) {
|
|
130
151
|
if (kind && (flags & CHARACTER ? 'character' : 'work') !== kind)
|
|
131
152
|
continue;
|
|
132
153
|
if (!adult && flags & ADULT)
|
|
133
154
|
continue;
|
|
155
|
+
if (covered && flags & SHADOWED)
|
|
156
|
+
continue;
|
|
134
157
|
const known = best.get(eid);
|
|
135
|
-
const mask = flags & ~(CHARACTER | MAIN | ADULT);
|
|
158
|
+
const mask = flags & ~(CHARACTER | MAIN | ADULT | SHADOWED);
|
|
136
159
|
if (!known || posts > known.posts)
|
|
137
160
|
best.set(eid, { posts, mask: mask | (known?.mask ?? 0), flags, standing });
|
|
138
161
|
else
|
|
139
162
|
known.mask |= mask;
|
|
140
163
|
}
|
|
141
|
-
const order = [...best]
|
|
164
|
+
const order = [...best]
|
|
165
|
+
.sort(([a, x], [b, y]) => y.posts - x.posts || y.standing - x.standing || a - b)
|
|
166
|
+
.slice(0, limit);
|
|
142
167
|
view.labels = true;
|
|
143
168
|
const out = order.flatMap(([eid, hit]) => {
|
|
144
169
|
const rows = view.entity(eid, hit.mask);
|
package/lib/model.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type Layer = 'core' | 'kowiki' | 'unlicensed';
|
|
|
3
3
|
export declare const LAYERS: readonly Layer[];
|
|
4
4
|
export declare const USER_AGENT = "aninames/0.1 (+https://github.com/kecan0406/aninames)";
|
|
5
5
|
export type NameKind = 'main' | 'official' | 'synonym' | 'short' | 'reading' | 'redirect' | 'section' | 'generated';
|
|
6
|
-
export type Namespace = 'wikidata' | 'mal' | 'anilist' | 'anidb' | 'kitsu' | 'ann' | 'animeplanet' | 'anisearch' | 'livechart' | 'simkl' | 'animecountdown' | 'tmdb' | 'vndb' | 'bangumi' | 'danbooru' | 'kowiki' | 'anissia';
|
|
6
|
+
export type Namespace = 'wikidata' | 'mal' | 'anilist' | 'anidb' | 'kitsu' | 'ann' | 'animeplanet' | 'anisearch' | 'livechart' | 'simkl' | 'animecountdown' | 'tmdb' | 'igdb' | 'steam' | 'vndb' | 'bangumi' | 'danbooru' | 'kowiki' | 'anissia';
|
|
7
7
|
export type Ref = `${Namespace}:${string}`;
|
|
8
8
|
export interface Name {
|
|
9
9
|
text: string;
|
|
@@ -14,15 +14,19 @@ export interface Entry {
|
|
|
14
14
|
id: Ref;
|
|
15
15
|
kind?: Kind;
|
|
16
16
|
attach?: boolean;
|
|
17
|
+
family?: boolean;
|
|
17
18
|
refs?: Ref[];
|
|
18
19
|
names: Name[];
|
|
19
20
|
works?: Ref[];
|
|
21
|
+
series?: Ref[];
|
|
20
22
|
related?: Ref[];
|
|
23
|
+
near?: Ref[];
|
|
21
24
|
link?: string[];
|
|
22
25
|
rank?: number;
|
|
23
26
|
adult?: boolean;
|
|
24
27
|
type?: string;
|
|
25
28
|
year?: number;
|
|
29
|
+
origin?: string[];
|
|
26
30
|
}
|
|
27
31
|
export interface Source {
|
|
28
32
|
name: string;
|