traxxx/src/tags.js

200 lines
4.9 KiB
JavaScript
Executable File

'use strict';
const knex = require('./knex');
const slugify = require('./utils/slugify');
const bulkInsert = require('./utils/bulk-insert');
function curateTagMedia(media) {
if (!media) {
return null;
}
return {
id: media.id,
path: media.path,
thumbnail: media.thumbnail,
lazy: media.lazy,
comment: media.comment,
credit: media.credit,
};
}
function curateTag(tag) {
if (!tag) {
return null;
}
const curatedTag = {
id: tag.id,
name: tag.name,
slug: tag.slug,
description: tag.description,
priority: tag.priority,
group: curateTag(tag.group),
aliasFor: curateTag(tag.alias),
aliases: (tag.aliases || []).map((aliasTag) => curateTag(aliasTag)),
};
if (tag.poster) {
curatedTag.poster = curateTagMedia(tag.poster);
}
if (tag.photos) {
curatedTag.photos = tag.photos.map((photo) => curateTagMedia(photo));
}
return curatedTag;
}
function withRelations(queryBuilder, withMedia) {
queryBuilder
.select(knex.raw(`
tags.*,
COALESCE(json_agg(DISTINCT aliases) FILTER (WHERE aliases.id IS NOT NULL), '[]') as aliases,
row_to_json(tags_groups) as group,
row_to_json(roots) as alias
`))
.leftJoin('tags_groups', 'tags_groups.id', 'tags.group_id')
.leftJoin('tags as roots', 'roots.id', 'tags.alias_for')
.leftJoin('tags as aliases', 'aliases.alias_for', 'tags.id')
.groupBy('tags.id', 'tags_groups.id', 'roots.id');
if (withMedia) {
queryBuilder
.select(knex.raw(`
row_to_json(posters) as poster,
COALESCE(json_agg(DISTINCT photos) FILTER (WHERE photos.id IS NOT NULL), '[]') as photos
`))
.leftJoin('tags_posters', 'tags_posters.tag_id', 'tags.id')
.leftJoin('tags_photos', 'tags_photos.tag_id', 'tags.id')
.leftJoin('media as posters', 'posters.id', 'tags_posters.media_id')
.leftJoin('media as photos', 'photos.id', 'tags_photos.media_id')
.groupBy('posters.id');
}
}
async function matchReleaseTags(releases) {
const rawTags = releases
.map((release) => release.tags).flat()
.filter(Boolean);
const casedTags = [...new Set(
rawTags
.concat(rawTags.map((tag) => tag.toLowerCase()))
.concat(rawTags.map((tag) => tag.toUpperCase())),
)];
const tagEntries = await knex('tags')
.select('tags.id', 'tags.name', 'tags.alias_for', 'tags.implied_tag_ids')
.whereIn('tags.name', casedTags);
const tagIdsBySlug = tagEntries
.reduce((acc, tag) => ({
...acc,
[slugify(tag.name)]: tag.alias_for || tag.id,
}), {});
return tagIdsBySlug;
}
async function getEntityTags(releases) {
const entityIds = Array.from(new Set(releases.map((release) => release.entity?.id).filter(Boolean)));
const entityTags = await knex('entities_tags')
.select('id', 'name', 'entity_id')
.whereIn('entity_id', entityIds)
.leftJoin('tags', 'tags.id', 'entities_tags.tag_id');
const entityTagIdsByEntityId = entityTags.reduce((acc, entityTag) => {
if (!acc[entityTag.entity_id]) {
acc[entityTag.entity_id] = [];
}
acc[entityTag.entity_id].push({
id: entityTag.id,
name: entityTag.name,
});
return acc;
}, {});
return entityTagIdsByEntityId;
}
function buildReleaseTagAssociations(releases, tagIdsBySlug, entityTagIdsByEntityId, type) {
const tagAssociations = releases
.map((release) => {
const entityTagIds = entityTagIdsByEntityId[release.entity?.id]?.map((tag) => ({ id: tag.id, original: tag.name })) || [];
const releaseTags = release.tags?.filter(Boolean) || [];
const releaseTagsWithIds = releaseTags.every((tag) => typeof tag === 'number')
? releaseTags // obsolete scraper returned pre-matched tags
: releaseTags.map((tag) => ({
id: tagIdsBySlug[slugify(tag)],
original: tag,
}));
const tags = [...new Set(
// filter duplicates and empties
releaseTagsWithIds
.concat(entityTagIds)
.filter(Boolean),
)]
.map((tag) => ({
[`${type}_id`]: release.id,
tag_id: tag.id,
original_tag: tag.original,
}));
return tags;
})
.flat();
return tagAssociations;
}
async function associateReleaseTags(releases, type = 'release') {
if (releases.length === 0) {
return;
}
const tagIdsBySlug = await matchReleaseTags(releases);
const entityTagIdsByEntityId = await getEntityTags(releases);
const tagAssociations = buildReleaseTagAssociations(releases, tagIdsBySlug, entityTagIdsByEntityId, type);
await bulkInsert(`${type}s_tags`, tagAssociations, false);
}
async function fetchTag(tagId) {
const tag = await knex('tags')
.modify((queryBuilder) => withRelations(queryBuilder, true))
.where((builder) => {
if (Number(tagId)) {
builder.where('tags.id', tagId);
return;
}
builder
.where('tags.name', tagId)
.orWhere('tags.slug', tagId);
})
.first();
return curateTag(tag);
}
async function fetchTags(limit = 100) {
const tags = await knex('tags')
.modify((queryBuilder) => withRelations(queryBuilder, false))
.limit(limit);
return tags.map((tag) => curateTag(tag));
}
module.exports = {
associateReleaseTags,
fetchTag,
fetchTags,
};