'use strict'; const inquirer = require('inquirer'); const logger = require('./logger')(__filename); const knex = require('./knex'); const argv = require('./argv'); const { flushOrphanedMedia } = require('./media'); const { graphql } = require('./web/graphql'); const releaseFields = ` id entryId shootId title url date description duration entity { id name slug parent { id name slug } } actors: releasesActors { actor { id name slug gender aliasFor entityId entryId } } tags: releasesTags { tag { id name slug } } chapters(orderBy: TIME_ASC) @include(if: $full) { id index time duration title description tags: chaptersTags { tag { id name slug } } poster: chaptersPoster { media { id path thumbnail s3: isS3 width height size } } photos: chaptersPhotos { media { id path thumbnail s3: isS3 width height size } } } poster: releasesPoster { media { id path thumbnail s3: isS3 width height size } } photos: releasesPhotos @include (if: $full) { media { id path thumbnail s3: isS3 width height size } } trailer: releasesTrailer @include (if: $full) { media { id path s3: isS3 vr: isVr quality size } } createdAt `; function curateRelease(release, withMedia = false, withPoster = true) { if (!release) { return null; } return { id: release.id, ...(release.relevance && { relevance: release.relevance }), entryId: release.entry_id, shootId: release.shoot_id, title: release.title, url: release.url, date: release.date, description: release.description, duration: release.duration, entity: release.entity && { id: release.entity.id, name: release.entity.name, slug: release.entity.slug, parent: release.parent && { id: release.parent.id, name: release.parent.name, slug: release.parent.slug, }, }, actors: (release.actors || []).map((actor) => ({ id: actor.id, name: actor.name, slug: actor.slug, gender: actor.gender, entityId: actor.entity_id, aliasFor: actor.alias_for, })), tags: (release.tags || []).map((tag) => ({ id: tag.id, name: tag.name, slug: tag.slug, })), chapters: (release.chapters || []).map((chapter) => ({ id: chapter.id, index: chapter.index, time: chapter.time, duration: chapter.duration, title: chapter.title, description: chapter.description, })), ...((withMedia || withPoster) && { poster: release.poster ? { id: release.poster.id, path: release.poster.path, thumbnail: release.poster.thumbnail, width: release.poster.width, height: release.poster.height, size: release.poster.size, } : null, }), ...(withMedia && { photos: (release.photos || []).map((photo) => ({ id: photo.id, path: photo.path, thumbnail: release.poster.thumbnail, width: photo.width, height: photo.height, size: photo.size, })), trailer: release.trailer ? { id: release.trailer.id, path: release.trailer.path, } : null, }), createdAt: release.created_at, }; } function curateGraphqlRelease(release) { if (!release) { return null; } return { id: release.id, ...(release.relevance && { relevance: release.relevance }), entryId: release.entryId, shootId: release.shootId, title: release.title || null, url: release.url || null, date: release.date, description: release.description || null, duration: release.duration, entity: release.entity, actors: release.actors.map((actor) => actor.actor), tags: release.tags.map((tag) => tag.tag), ...(release.chapters && { chapters: release.chapters.map((chapter) => ({ ...chapter, tags: chapter.tags.map((tag) => tag.tag), poster: chapter.poster?.media || null, photos: chapter.photos.map((photo) => photo.media), })) }), poster: release.poster?.media || null, ...(release.photos && { photos: release.photos.map((photo) => photo.media) }), trailer: release.trailer?.media || null, createdAt: release.createdAt, }; } async function fetchScene(releaseId) { const { release } = await graphql(` query Release( $releaseId: Int! $full: Boolean = true ) { release(id: $releaseId) { ${releaseFields} } } `, { releaseId: Number(releaseId), }); return curateGraphqlRelease(release); } async function fetchScenes(limit = 100) { const { releases } = await graphql(` query SearchReleases( $limit: Int = 20 $full: Boolean = false ) { releases( first: $limit orderBy: DATE_DESC ) { ${releaseFields} } } `, { limit: Math.min(limit, 10000), }); return releases.map((release) => curateGraphqlRelease(release)); } async function searchScenes(query, limit = 100, relevance = 0) { const { releases } = await graphql(` query SearchReleases( $query: String! $limit: Int = 20 $relevance: Float = 0.025 $full: Boolean = false ) { releases: searchReleases( query: $query first: $limit orderBy: RANK_DESC filter: { rank: { greaterThan: $relevance } } ) { rank release { ${releaseFields} } } } `, { query, limit, relevance, }); return releases.map((release) => curateGraphqlRelease({ ...release.release, relevance: release.rank })); } async function deleteScenes(sceneIds) { if (sceneIds.length === 0) { return 0; } // there can be too many scene IDs for where in, causing a stack depth error const deleteCount = await knex('releases') .whereRaw('id = ANY(:sceneIds)', { sceneIds }) .delete(); logger.info(`Removed ${deleteCount}/${sceneIds.length} scenes`); return deleteCount; } async function deleteMovies(movieIds) { if (movieIds.length === 0) { return 0; } await knex('movies_scenes') .whereIn('movie_id', movieIds) .delete(); const deleteCount = await knex('movies') .whereIn('id', movieIds) .delete(); logger.info(`Removed ${deleteCount}/${movieIds.length} movies`); return deleteCount; } async function deleteSeries(serieIds) { if (serieIds.length === 0) { return 0; } await knex('series_scenes') .whereIn('serie_id', serieIds) .delete(); const deleteCount = await knex('series') .whereIn('id', serieIds) .delete(); logger.info(`Removed ${deleteCount}/${serieIds.length} series`); return deleteCount; } async function flushScenes() { const sceneIds = await knex('releases').select('id').pluck('id'); const confirmed = await inquirer.prompt([{ type: 'confirm', name: 'flushScenes', message: `You are about to remove ${sceneIds.length} scenes. Are you sure?`, default: false, }]); if (!confirmed.flushScenes) { logger.warn('Confirmation rejected, not flushing scenes'); return; } const deleteCount = await deleteScenes(sceneIds); if (argv.flushOrphanedMedia !== false) { await flushOrphanedMedia(); } logger.info(`Removed ${deleteCount}/${sceneIds.length} scenes`); } async function flushMovies() { const movieIds = await knex('movies').select('id').pluck('id'); const confirmed = await inquirer.prompt([{ type: 'confirm', name: 'flushMovies', message: `You are about to remove ${movieIds.length} movies. Are you sure?`, default: false, }]); if (!confirmed.flushMovies) { logger.warn('Confirmation rejected, not flushing movies'); return; } const deleteCount = await deleteMovies(movieIds); if (argv.flushOrphanedMedia !== false) { await flushOrphanedMedia(); } logger.info(`Removed ${deleteCount}/${movieIds.length} movies`); } async function flushSeries() { const serieIds = await knex('series').select('id').pluck('id'); const confirmed = await inquirer.prompt([{ type: 'confirm', name: 'flushSeries', message: `You are about to remove ${serieIds.length} series. Are you sure?`, default: false, }]); if (!confirmed.flushSeries) { logger.warn('Confirmation rejected, not flushing series'); return; } const deleteCount = await deleteSeries(serieIds); if (argv.flushOrphanedMedia !== false) { await flushOrphanedMedia(); } logger.info(`Removed ${deleteCount}/${serieIds.length} series`); } async function flushBatches(batchIds) { const [sceneIds, movieIds] = await Promise.all([ knex('releases') .select('releases.id') .whereIn('created_batch_id', batchIds) .pluck('releases.id'), knex('movies').whereIn('created_batch_id', batchIds) .select('movies.id') .whereIn('created_batch_id', batchIds) .pluck('movies.id'), ]); const confirmed = await inquirer.prompt([{ type: 'confirm', name: 'flushBatches', message: `You are about to remove ${sceneIds.length} scenes and ${movieIds.length} movies from batches ${batchIds}. Are you sure?`, default: false, }]); if (!confirmed.flushBatches) { logger.warn(`Confirmation rejected, not flushing scenes or movies for batches ${batchIds}`); return; } const [deletedScenesCount, deletedMoviesCount] = await Promise.all([ deleteScenes(sceneIds), deleteMovies(movieIds), ]); logger.info(`Removed ${deletedScenesCount} scenes and ${deletedMoviesCount} movies for batches ${batchIds}`); if (argv.flushOrphanedMedia !== false) { await flushOrphanedMedia(); } } module.exports = { curateRelease, fetchScene, fetchScenes, flushBatches, flushMovies, flushSeries, flushScenes, searchScenes, deleteScenes, deleteMovies, deleteSeries, };