'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, };