'use strict'; const config = require('config'); const inquirer = require('inquirer'); const logger = require('./logger')(__filename); const argv = require('./argv'); const knex = require('./knex'); const { deleteScenes } = require('./releases'); function curateEntity(entity, includeParameters = false) { if (!entity) { return null; } const curatedEntity = entity.id ? { id: entity.id, name: entity.name, url: entity.url, description: entity.description, slug: entity.slug, type: entity.type, aliases: entity.alias, parent: curateEntity(entity.parent, includeParameters), } : {}; if (entity.children) { curatedEntity.children = entity.children.map(child => curateEntity({ ...child, parent: curatedEntity.id ? curatedEntity : null, }, includeParameters)); } if (entity.tags) { curatedEntity.tags = entity.tags.map(tag => ({ id: tag.id, name: tag.name, slug: tag.slug, priority: tag.priority, })); } if (includeParameters) { curatedEntity.parameters = entity.parameters; } return curatedEntity; } async function curateEntities(entities, includeParameters) { return Promise.all(entities.map(async entity => curateEntity(entity, includeParameters))); } async function fetchIncludedEntities() { const include = { includeAll: !argv.networks && !argv.channels && !config.include?.networks && !config.include?.channels, includedNetworks: argv.networks || (!argv.channels && config.include?.networks) || [], includedChannels: argv.channels || (!argv.networks && config.include?.channels) || [], excludedNetworks: argv.excludeNetworks || config.exclude?.networks || [], excludedChannels: argv.excludeChannels || config.exclude?.channels || [], }; const rawNetworks = await knex.raw(` WITH RECURSIVE channels AS ( /* select configured channels and networks */ SELECT entities.* FROM entities WHERE CASE WHEN :includeAll THEN /* select all top level networks and independent channels */ entities.parent_id IS NULL ELSE ((entities.slug = ANY(:includedNetworks) AND entities.type = 'network') OR (entities.slug = ANY(:includedChannels) AND entities.type = 'channel')) END AND NOT ( (entities.slug = ANY(:excludedNetworks) AND entities.type = 'network') OR (entities.slug = ANY(:excludedChannels) AND entities.type = 'channel')) UNION ALL /* select recursive children of configured networks */ SELECT entities.* FROM entities INNER JOIN channels ON channels.id = entities.parent_id WHERE NOT ((entities.slug = ANY(:excludedNetworks) AND entities.type = 'network') OR (entities.slug = ANY(:excludedChannels) AND entities.type = 'channel')) ) /* select recursive channels as children of networks */ SELECT entities.*, json_agg(channels ORDER BY channels.id) as children FROM channels LEFT JOIN entities ON entities.id = channels.parent_id WHERE channels.type = 'channel' GROUP BY entities.id `, include); const curatedNetworks = rawNetworks.rows.map(entity => curateEntity(entity, true)); return curatedNetworks; } async function fetchEntity(entityId, type) { const entity = await knex('entities') .select(knex.raw(` entities.*, COALESCE(json_agg(children) FILTER (WHERE children.id IS NOT NULL), '[]') as children, COALESCE(json_agg(tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags, row_to_json(parents) as parent `)) .modify((queryBuilder) => { if (Number(entityId)) { queryBuilder.where('entities.id', entityId); return; } if (type) { queryBuilder .where('entities.type', type) .where('entities.slug', entityId) .orWhere(knex.raw(':entityId = ANY(entities.alias)', { entityId })); return; } throw new Error('Invalid ID or unspecified entity type'); }) .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .leftJoin('entities as children', 'children.parent_id', 'entities.id') .leftJoin('entities_tags', 'entities_tags.entity_id', 'entities.id') .leftJoin('tags', 'tags.id', 'entities_tags.tag_id') .groupBy('entities.id', 'parents.id') .first(); return curateEntity(entity); } async function fetchEntities(type, limit) { const entities = await knex('entities') .select(knex.raw(` entities.*, COALESCE(json_agg(tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags, row_to_json(parents) as parent `)) .modify((queryBuilder) => { if (type) { queryBuilder.where('entities.type', type); } }) .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .leftJoin('entities_tags', 'entities_tags.entity_id', 'entities.id') .leftJoin('tags', 'tags.id', 'entities_tags.tag_id') .groupBy('entities.id', 'parents.id') .limit(limit || 100); return curateEntities(entities); } async function searchEntities(query, type, limit) { const entities = knex .select(knex.raw(` entities.id, entities.name, entities.slug, entities.type, entities.url, entities.description, COALESCE(json_agg(tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags, row_to_json(parents) as parent `)) .from(knex.raw('search_entities(?) as entities', [query])) .modify((queryBuilder) => { if (type) { queryBuilder.where('entities.type', type); } }) .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .leftJoin('entities_tags', 'entities_tags.entity_id', 'entities.id') .leftJoin('tags', 'tags.id', 'entities_tags.tag_id') .groupBy('entities.id', 'entities.name', 'entities.slug', 'entities.type', 'entities.url', 'entities.description', 'parents.id') .limit(limit || 100); return curateEntities(await entities); } async function flushEntities(networkSlugs = [], channelSlugs = []) { const entitySlugs = networkSlugs.concat(channelSlugs).join(', '); const entityQuery = knex .withRecursive('selected_entities', knex.raw(` SELECT entities.* FROM entities WHERE entities.slug = ANY(:networkSlugs) AND entities.type = 'network' OR (entities.slug = ANY(:channelSlugs) AND entities.type = 'channel') UNION ALL SELECT entities.* FROM entities INNER JOIN selected_entities ON selected_entities.id = entities.parent_id `, { networkSlugs, channelSlugs, })); const sceneIds = await entityQuery .clone() .select('releases.id') .distinct('releases.id') .whereNotNull('releases.id') .from('selected_entities') .leftJoin('releases', 'releases.entity_id', 'selected_entities.id') .pluck('releases.id'); if (sceneIds.length === 0) { logger.info(`No scenes or movies found to remove for ${entitySlugs}`); return; } const confirmed = await inquirer.prompt([{ type: 'confirm', name: 'flushEntities', message: `You are about to remove ${sceneIds.length} scenes for ${entitySlugs}. Are you sure?`, default: false, }]); if (!confirmed.flushEntities) { logger.warn(`Confirmation rejected, not flushing scenes for: ${entitySlugs}`); return; } await deleteScenes(sceneIds); } module.exports = { curateEntity, curateEntities, fetchIncludedEntities, fetchEntity, fetchEntities, searchEntities, flushEntities, };