Compare commits
6 Commits
ec4b15ce33
...
e4d6ff6ad1
| Author | SHA1 | Date | |
|---|---|---|---|
| e4d6ff6ad1 | |||
| 4c5ab2411e | |||
| 11c31c4c56 | |||
| 40c2bdb563 | |||
| 1374f90397 | |||
| 33733720c5 |
@@ -164,14 +164,14 @@ export async function interpolateProfiles(actorIdsOrNames, context, options = {}
|
|||||||
|
|
||||||
profile.avatar_media_id = actorProfiles
|
profile.avatar_media_id = actorProfiles
|
||||||
.map((actorProfile) => actorProfile.avatar)
|
.map((actorProfile) => actorProfile.avatar)
|
||||||
.filter((avatar) => avatar && (avatar.entropy === null || avatar.entropy > 5.5))
|
.filter((avatar) => avatar && (avatar.entropy === null || avatar.entropy > 5.5) && !options.avoidAvatarCredits?.includes(avatar.credit) && !options.excludeAvatarCredits?.includes(avatar.credit))
|
||||||
.sort((avatarA, avatarB) => avatarB.height - avatarA.height)[0]?.id || null;
|
.sort((avatarA, avatarB) => avatarB.height - avatarA.height)[0]?.id || null;
|
||||||
|
|
||||||
if (!profile.avatar_media_id) {
|
if (!profile.avatar_media_id) {
|
||||||
// try to settle for low quality avatar
|
// try to settle for low quality avatar
|
||||||
profile.avatar_media_id = actorProfiles
|
profile.avatar_media_id = actorProfiles
|
||||||
.map((actorProfile) => actorProfile.avatar)
|
.map((actorProfile) => actorProfile.avatar)
|
||||||
.filter((avatar) => avatar)
|
.filter((avatar) => !!avatar && !options?.excludeAvatarCredits?.includes(avatar.credit))
|
||||||
.sort((avatarA, avatarB) => avatarB.height - avatarA.height)[0]?.id || null;
|
.sort((avatarA, avatarB) => avatarB.height - avatarA.height)[0]?.id || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "traxxx-utils",
|
"name": "traxxx-utils",
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "traxxx-utils",
|
"name": "traxxx-utils",
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.25.7",
|
"@babel/cli": "^7.25.7",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "traxxx-common",
|
"name": "traxxx-common",
|
||||||
"version": "1.3.2",
|
"version": "1.3.4",
|
||||||
"description": "Common utilities for traxxx core and web.",
|
"description": "Common utilities for traxxx core and web.",
|
||||||
"main": "src/app.js",
|
"main": "src/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
300
scenes-revisions.mjs
Normal file
300
scenes-revisions.mjs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
const keyMap = {
|
||||||
|
datePrecision: 'date_precision',
|
||||||
|
productionDate: 'production_date',
|
||||||
|
productionLocation: 'production_location',
|
||||||
|
productionCity: 'production_city',
|
||||||
|
productionState: 'production_state',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function initSceneRevisions({
|
||||||
|
config,
|
||||||
|
knex,
|
||||||
|
mj,
|
||||||
|
fetchScenesById,
|
||||||
|
logger,
|
||||||
|
}) {
|
||||||
|
async function applySceneValueDelta(sceneId, delta, trx) {
|
||||||
|
return knex('releases')
|
||||||
|
.where('id', sceneId)
|
||||||
|
.update(keyMap[delta.key] || delta.key, delta.value)
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneActorsDelta(sceneId, delta, trx) {
|
||||||
|
await knex('releases_actors')
|
||||||
|
.where('release_id', sceneId)
|
||||||
|
.delete()
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
if (delta.value.length > 0) {
|
||||||
|
await knex('releases_actors')
|
||||||
|
.insert(delta.value.map((actorId) => ({
|
||||||
|
release_id: sceneId,
|
||||||
|
actor_id: actorId,
|
||||||
|
})))
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneTagsDelta(sceneId, delta, trx) {
|
||||||
|
// don't remove unidentified tags
|
||||||
|
await knex('releases_tags')
|
||||||
|
.where('release_id', sceneId)
|
||||||
|
.whereNotNull('tag_id')
|
||||||
|
.delete()
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
if (delta.value.length > 0) {
|
||||||
|
await knex('releases_tags')
|
||||||
|
.insert(delta.value.map((tag) => ({
|
||||||
|
release_id: sceneId,
|
||||||
|
tag_id: tag.id,
|
||||||
|
actor_id: tag.actorId,
|
||||||
|
source: 'editor',
|
||||||
|
})))
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneMoviesDelta(sceneId, delta, trx) {
|
||||||
|
await knex('movies_scenes')
|
||||||
|
.where('scene_id', sceneId)
|
||||||
|
.delete()
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
if (delta.value.length > 0) {
|
||||||
|
await knex('movies_scenes')
|
||||||
|
.insert(delta.value.map((movieId) => ({
|
||||||
|
scene_id: sceneId,
|
||||||
|
movie_id: movieId,
|
||||||
|
})))
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneDeleteDelta(sceneId, _delta, trx, reqUser) {
|
||||||
|
if (!reqUser.abilities.some((ability) => ability.subject === 'scene' && ability.action === 'delete')) {
|
||||||
|
throw new HttpError('You are not privileged to delete scenes', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex('releases')
|
||||||
|
.where('id', sceneId)
|
||||||
|
.delete()
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneRevision(revisionIds, reqUser) {
|
||||||
|
const revisions = await knex('scenes_revisions')
|
||||||
|
.whereIn('id', revisionIds)
|
||||||
|
.whereNull('applied_at'); // should not re-apply revision that was already applied
|
||||||
|
|
||||||
|
await revisions.reduce(async (chain, revision) => {
|
||||||
|
await chain;
|
||||||
|
|
||||||
|
await knex.transaction(async (trx) => {
|
||||||
|
await Promise.all(revision.deltas.map(async (delta) => {
|
||||||
|
if (delta.key === 'delete') {
|
||||||
|
return applySceneDeleteDelta(revision.scene_id, delta, trx, reqUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'date',
|
||||||
|
'datePrecision',
|
||||||
|
'duration',
|
||||||
|
'productionDate',
|
||||||
|
'productionLocation',
|
||||||
|
'productionCity',
|
||||||
|
'productionState',
|
||||||
|
].includes(delta.key)) {
|
||||||
|
return applySceneValueDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'actors') {
|
||||||
|
return applySceneActorsDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'tags') {
|
||||||
|
return applySceneTagsDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'movies') {
|
||||||
|
return applySceneMoviesDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
await knex('scenes_revisions')
|
||||||
|
.where('id', revision.id)
|
||||||
|
.update('applied_at', knex.fn.now())
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
// await trx.commit();
|
||||||
|
}).catch(async (error) => {
|
||||||
|
logger.error(`Failed to apply revision ${revision.id} on scene ${revision.scene_id}: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reviewSceneRevision(revisionId, isApproved, { feedback }, reqUser) {
|
||||||
|
if (!reqUser || reqUser.role === 'user') {
|
||||||
|
throw new HttpError('You are not permitted to approve revisions', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof isApproved !== 'boolean') {
|
||||||
|
throw new HttpError('You must either approve or reject the revision', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await knex('scenes_revisions')
|
||||||
|
.where('id', revisionId)
|
||||||
|
.whereNull('approved') // don't rerun reviewed revision, must be forked into new revision instead
|
||||||
|
.whereNull('applied_at')
|
||||||
|
.update({
|
||||||
|
approved: isApproved,
|
||||||
|
reviewed_at: knex.fn.now(),
|
||||||
|
reviewed_by: reqUser.id,
|
||||||
|
feedback,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updated === 0) {
|
||||||
|
throw new HttpError('This revision was already reviewed', 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isApproved) {
|
||||||
|
try {
|
||||||
|
await applySceneRevision([revisionId], reqUser);
|
||||||
|
} catch (error) {
|
||||||
|
await knex('scenes_revisions')
|
||||||
|
.where('id', revisionId)
|
||||||
|
.update({
|
||||||
|
approved: null,
|
||||||
|
reviewed_at: null,
|
||||||
|
reviewed_by: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSceneRevision(sceneId, { edits, comment, apply }, reqUser) {
|
||||||
|
const [
|
||||||
|
[scene],
|
||||||
|
openRevisions,
|
||||||
|
] = await Promise.all([
|
||||||
|
fetchScenesById([sceneId], {
|
||||||
|
reqUser,
|
||||||
|
includeAssets: true,
|
||||||
|
includePartOf: true,
|
||||||
|
}),
|
||||||
|
knex('scenes_revisions')
|
||||||
|
.where('user_id', reqUser.id)
|
||||||
|
.whereNull('approved'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!scene) {
|
||||||
|
throw new HttpError(`No scene with ID ${sceneId} found to update`, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openRevisions.length >= config.revisions.unapprovedLimit && reqUser.role !== 'admin') {
|
||||||
|
throw new HttpError(`You have ${config.revisions.unapprovedLimit} unapproved revisions, please wait for approval before submitting another revision.`, 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseScene = Object.fromEntries(Object.entries(scene).map(([key, values]) => {
|
||||||
|
if ([
|
||||||
|
'effectiveDate',
|
||||||
|
'isNew',
|
||||||
|
'network',
|
||||||
|
'stashes',
|
||||||
|
'watchUrl',
|
||||||
|
].includes(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.hash) {
|
||||||
|
return [key, values.hash];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.id) {
|
||||||
|
return [key, values.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'tags') {
|
||||||
|
return [key, values.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
actorId: tag.actorId,
|
||||||
|
}))];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [key, values];
|
||||||
|
}).filter(Boolean));
|
||||||
|
|
||||||
|
const deltas = Object.entries(edits).map(([key, value]) => {
|
||||||
|
if (key === 'delete') {
|
||||||
|
return { key: 'delete' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseScene[key] === value || typeof value === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'tags') {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: value.map((tag) => ({
|
||||||
|
id: tag.id,
|
||||||
|
actorId: tag.actorId,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const valueSet = new Set(value);
|
||||||
|
const baseSet = new Set(baseScene[key]);
|
||||||
|
|
||||||
|
if (valueSet.size === baseSet.size && baseScene[key].every((id) => valueSet.has(id))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value: Array.from(valueSet) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value };
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (deltas.length === 0) {
|
||||||
|
throw new HttpError('No effective changes provided', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [revisionEntry] = await knex('scenes_revisions')
|
||||||
|
.insert({
|
||||||
|
user_id: reqUser.id,
|
||||||
|
scene_id: scene.id,
|
||||||
|
base: JSON.stringify(baseScene),
|
||||||
|
deltas: JSON.stringify(deltas),
|
||||||
|
hash: mj.hash({
|
||||||
|
base: baseScene,
|
||||||
|
deltas,
|
||||||
|
}),
|
||||||
|
comment,
|
||||||
|
})
|
||||||
|
.returning('id');
|
||||||
|
|
||||||
|
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
||||||
|
// don't keep the editor waiting for the revision to apply
|
||||||
|
reviewSceneRevision(revisionEntry.id, true, {}, reqUser).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSceneRevision,
|
||||||
|
reviewSceneRevision,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user