traxxx/src/releases.js

415 lines
8.2 KiB
JavaScript

'use strict';
const inquirer = require('inquirer');
const logger = require('./logger')(__filename);
const knex = require('./knex');
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: chaptersPosterByChapterId {
media {
id
path
thumbnail
s3: isS3
width
height
size
}
}
photos: chaptersPhotos {
media {
id
path
thumbnail
s3: isS3
width
height
size
}
}
}
poster: releasesPosterByReleaseId {
media {
id
path
thumbnail
s3: isS3
width
height
size
}
}
photos: releasesPhotos @include (if: $full) {
media {
id
path
thumbnail
s3: isS3
width
height
size
}
}
trailer: releasesTrailerByReleaseId @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 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);
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);
await flushOrphanedMedia();
logger.info(`Removed ${deleteCount}/${movieIds.length} movies`);
}
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}`);
await flushOrphanedMedia();
}
module.exports = {
curateRelease,
fetchScene,
fetchScenes,
flushBatches,
flushMovies,
flushScenes,
searchScenes,
deleteScenes,
deleteMovies,
};