Refactored alerts to use application code, added regex. Updated Jules Jordan for the Ass Factory relaunch.

This commit is contained in:
DebaucheryLibrarian 2023-11-24 01:29:22 +01:00
parent 124ff3f5e3
commit 238dce78b5
79 changed files with 466 additions and 155 deletions

View File

@ -3,6 +3,11 @@
title="Add alert"
@close="$emit('close')"
>
<div
v-if="error"
class="dialog-error"
>{{ error }}</div>
<form
class="dialog-body"
@submit.prevent="addAlert"
@ -136,6 +141,72 @@
</Tooltip>
</div>
</div>
<div class="alert-section">
<h4 class="alert-heading">Matching</h4>
<ul class="matches nolist">
<li
v-for="(match, index) in matches"
:key="`match-${index}`"
class="match"
>
<span class="match-property">{{ match.property }}:&nbsp;</span>
<span
v-if="match.expression.slice(0, 1) === '/' && match.expression.slice(-1) === '/'"
class="match-expression"
><span class="match-slash">/</span>{{ match.expression.slice(1, -1) }}<span class="match-slash">/</span></span>
<span
v-else
class="match-expression"
>{{ match.expression }}</span>
<Icon
icon="cross3"
class="remove"
@click.native="removeMatch(index)"
/>
</li>
<Tooltip
v-if="!entity"
@open="$refs.expression?.focus()"
>
<li class="match placeholder">
Anything
<Icon
icon="plus3"
class="add"
/>
</li>
<template #tooltip>
<form
class="pattern-tooltip"
@submit.prevent="addMatch"
>
<select
v-model="matchProperty"
class="input"
>
<option value="title">Title</option>
<option value="description">Description</option>
</select>
<input
ref="expression"
v-model="matchExpression"
class="input"
placeholder="Expression, // for RegExp"
>
</form>
</template>
</Tooltip>
</ul>
</div>
</div>
<div class="dialog-section">
@ -194,7 +265,7 @@
<div class="dialog-actions right">
<button
:disabled="actors.length === 0 && tags.length === 0 && !entity"
:disabled="actors.length === 0 && tags.length === 0 && !entity && matches.length === 0"
type="submit"
class="button button-primary"
>Add alert</button>
@ -210,16 +281,23 @@ import Checkbox from '../form/checkbox.vue';
import Search from './search.vue';
async function addAlert() {
await this.$store.dispatch('addAlert', {
actors: this.actors.map((actor) => actor.id),
tags: this.tags.map((tag) => tag.id),
entity: this.entity?.id,
notify: this.notify,
email: this.email,
stashes: this.stashes.map((stash) => stash.id),
});
this.error = null;
this.$emit('close', true);
try {
await this.$store.dispatch('addAlert', {
actors: this.actors.map((actor) => actor.id),
tags: this.tags.map((tag) => tag.id),
matches: this.matches,
entity: this.entity?.id,
notify: this.notify,
email: this.email,
stashes: this.stashes.map((stash) => stash.id),
});
this.$emit('close', true);
} catch (error) {
this.error = error.message;
}
}
function addActor(actor) {
@ -255,6 +333,26 @@ function removeTag(tag) {
this.tags = this.tags.filter((listedTag) => listedTag.id !== tag.id);
}
function addMatch() {
if (!this.matchExpression) {
return;
}
this.matches = this.matches.concat({
property: this.matchProperty,
expression: this.matchExpression,
});
this.matchProperty = 'title';
this.matchExpression = null;
this.events.emit('blur');
}
function removeMatch(removeIndex) {
this.matches = this.matches.filter((match, matchIndex) => matchIndex !== removeIndex);
}
function addStash(stash) {
if (!this.stashes.some((selectedStash) => selectedStash.id === stash.id)) {
this.stashes = this.stashes.concat(stash);
@ -277,9 +375,13 @@ export default {
emits: ['close'],
data() {
return {
error: null,
actors: [],
tags: [],
entity: null,
matches: [],
matchProperty: 'title',
matchExpression: null,
notify: true,
email: false,
stashes: [],
@ -290,10 +392,12 @@ export default {
addActor,
addAlert,
addEntity,
addMatch,
addTag,
addStash,
removeActor,
removeEntity,
removeMatch,
removeTag,
removeStash,
},
@ -325,6 +429,15 @@ export default {
font-weight: normal;
}
.dialog-error {
padding: 1rem;
margin-bottom: 1rem;
background: var(--error);
color: var(--text-light);
font-weight: bold;
text-align: center;
}
.alert-heading {
margin: .75rem 0 .25rem 0;
}
@ -338,6 +451,34 @@ export default {
font-size: 0;
}
.match {
display: flex;
align-items: center;
padding: .25rem 0;
font-family: inherit;
.remove {
position: relative;
top: -.1rem;
right: 0;
}
}
.match-property {
text-transform: capitalize;
color: var(--shadow);
}
.match-expression {
flex-grow: 1;
}
.match-slash {
padding: 0 .1rem;
color: var(--primary);
font-weight: bold;
}
.actors > .actor,
.entity,
.tag,
@ -366,6 +507,14 @@ export default {
color: var(--text);
}
.pattern-tooltip {
display: flex;
gap: .5rem;
position: relative;
padding: .5rem;
overflow: hidden;
}
.remove {
width: 1rem;
height: 1rem;

View File

@ -99,7 +99,7 @@
<Icon
v-if="notification.alert"
v-tooltip="`You set an alert for <strong>${notification.alert.tags.map(tag => tag.name).join(', ') || 'all'}</strong> scenes with <strong>${notification.alert.actors.map(actor => actor.name).join(', ') || 'any actor'}</strong> for <strong>${notification.alert.entity?.name || 'any channel'}</strong>`"
v-tooltip="`You set an alert for <strong>${notification.alert.tags.map(tag => tag.name).join(', ') || 'all'}</strong> scenes with <strong>${notification.alert.actors.map(actor => actor.name).join(', ') || 'any actor'}</strong> from <strong>${notification.alert.entities.map((entity) => entity.name).join(', ') || 'any channel'}</strong> matching <strong>${notification.alert.matches.map((match) => `${match.property}: ${match.expression}`).join(', ') || 'anything'}</strong>`"
icon="question5"
@click.prevent.stop
/>
@ -145,12 +145,12 @@ export default {
default: 0,
},
},
emits: ['addAlert'],
data() {
return {
showAddAlert: false,
};
},
emits: ['addAlert'],
methods: {
checkNotifications,
checkNotification,

View File

@ -125,6 +125,7 @@ function mounted() {
}
export default {
emits: ['open', 'close'],
data() {
return {
opened: false,
@ -133,7 +134,6 @@ export default {
arrowOffset: 0,
};
},
emits: ['open', 'close'],
mounted,
methods: {
calculate,

View File

@ -93,17 +93,43 @@
</div>
<div
v-if="alert.entity"
v-if="alert.entities.length > 0"
class="alert-section alert-trigger"
>
<h4 class="alert-heading">Channel</h4>
<h4 class="alert-heading">{{ alert.entities.length > 1 ? 'Channels' : 'Channel' }}</h4>
<Entity
v-if="alert.entity"
:entity="alert.entity"
v-for="entity in alert.entities"
:key="`${alert.id}${entity.id}`"
:entity="entity"
class="entity"
/>
</div>
<div
v-if="alert.matches.length > 0"
class="alert-section alert-trigger"
>
<h4 class="alert-heading">Matches</h4>
<ul class="alert-matches nolist">
<li
v-for="match in alert.matches"
:key="`match-${match.id}`"
class="match"
>
<span
v-if="match.expression.slice(0, 1) === '/' && match.expression.slice(-1) === '/'"
class="match-expression"
><span class="match-slash">/</span>{{ match.expression.slice(1, -1) }}<span class="match-slash">/</span></span>
<span
v-else
class="match-expression"
>{{ match.expression }}</span>
</li>
</ul>
</div>
</div>
</div>
</template>
@ -227,26 +253,34 @@ export default {
}
.alert-actors,
.alert-tags {
.alert-tags,
.alert-matches {
display: flex;
grid-gap: .5rem;
}
.tag {
.tag,
.match {
color: var(--shadow-strong);
padding: .5rem;
border: solid 1px var(--shadow-hint);
font-size: .9rem;
font-weight: bold;
}
&:hover {
cursor: pointer;
border: solid 1px var(--primary);
}
.tag:hover {
cursor: pointer;
border: solid 1px var(--primary);
}
.entity {
width: 10rem;
height: 2.5rem;
}
.match-slash {
padding: 0 .1rem;
color: var(--primary);
font-weight: bold;
}
</style>

View File

@ -191,6 +191,10 @@ function curateAlert(alert) {
curatedAlert.entity = curateEntity(alert.entity.entity || alert.entity);
}
if (alert.entities) {
curatedAlert.entities = alert.entities.map((entity) => curateEntity(entity.entity || entity));
}
if (alert.stashes) {
curatedAlert.stashes = alert.stashes.map((stash) => curateStash(stash.stash || stash));
}

View File

@ -82,7 +82,7 @@ function initUiActions(store, _router) {
slug
}
}
entity: alertsEntity {
entities: alertsEntities {
entity {
id
name
@ -90,6 +90,10 @@ function initUiActions(store, _router) {
independent
}
}
matches: alertsMatches {
property
expression
}
}
}
totalCount

View File

@ -85,12 +85,17 @@ function initUsersActions(store, _router) {
slug
}
}
matches: alertsMatches {
id
property
expression
}
actors: alertsActors {
actor {
${actorFields}
}
}
entity: alertsEntity {
entities: alertsEntities {
entity {
id
name

View File

@ -0,0 +1,38 @@
const config = require('config');
exports.up = async (knex) => {
await knex.schema.alterTable('alerts', (table) => {
table.boolean('all')
.defaultTo(true);
});
await knex.schema.alterTable('alerts_entities', (table) => {
table.dropUnique('alert_id');
});
await knex.schema.createTable('alerts_matches', (table) => {
table.increments('id');
table.integer('alert_id')
.references('id')
.inTable('alerts')
.onDelete('cascade');
table.string('property');
table.string('expression');
});
await knex.raw(`
GRANT SELECT ON alerts_matches TO :visitor;
`, {
visitor: knex.raw(config.database.query.user),
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('alerts', (table) => {
table.dropColumn('all');
});
await knex.schema.dropTable('alerts_matches');
};

113
package-lock.json generated
View File

@ -36,6 +36,7 @@
"dayjs": "^1.8.21",
"dompurify": "^2.0.11",
"ejs": "^3.0.1",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-react-views": "^0.11.0",
@ -5205,6 +5206,14 @@
"node": ">=4"
}
},
"node_modules/chalk/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@ -7077,11 +7086,14 @@
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=0.8.0"
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escodegen": {
@ -7691,17 +7703,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/eslint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/eslint-scope": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz",
@ -8451,6 +8452,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/figures/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -11599,17 +11608,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/matcher/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -12579,6 +12577,15 @@
"node": ">=0.10.0"
}
},
"node_modules/node-sass/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true,
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/node-sass/node_modules/gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@ -17734,17 +17741,6 @@
"node": ">=8"
}
},
"node_modules/unprint/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unprint/node_modules/eslint": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz",
@ -23152,6 +23148,13 @@
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
}
}
},
"chardet": {
@ -24605,9 +24608,9 @@
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"escodegen": {
"version": "2.0.0",
@ -24739,11 +24742,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"eslint-scope": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-6.0.0.tgz",
@ -25627,6 +25625,13 @@
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"requires": {
"escape-string-regexp": "^1.0.5"
},
"dependencies": {
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="
}
}
},
"file-entry-cache": {
@ -28002,13 +28007,6 @@
"integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==",
"requires": {
"escape-string-regexp": "^4.0.0"
},
"dependencies": {
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
}
}
},
"media-typer": {
@ -28763,6 +28761,12 @@
"supports-color": "^2.0.0"
}
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"dev": true
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@ -32619,11 +32623,6 @@
}
}
},
"escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"eslint": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz",

View File

@ -95,6 +95,7 @@
"dayjs": "^1.8.21",
"dompurify": "^2.0.11",
"ejs": "^3.0.1",
"escape-string-regexp": "^4.0.0",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-react-views": "^0.11.0",

BIN
public/img/logos/julesjordan/lazy/favicon.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/img/logos/julesjordan/lazy/girlgirl.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/img/logos/julesjordan/lazy/julesjordan.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 745 B

BIN
public/img/logos/julesjordan/lazy/manuelferrara.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/img/logos/julesjordan/lazy/network.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/img/logos/julesjordan/lazy/spermswallowers.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

After

Width:  |  Height:  |  Size: 961 B

BIN
public/img/logos/julesjordan/lazy/theassfactory.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
public/img/logos/julesjordan/theassfactory.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 82 KiB

BIN
public/img/logos/julesjordan/thumbs/favicon.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/img/logos/julesjordan/thumbs/girlgirl.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/img/logos/julesjordan/thumbs/julesjordan.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
public/img/logos/julesjordan/thumbs/manuelferrara.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/img/logos/julesjordan/thumbs/network.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/img/logos/julesjordan/thumbs/spermswallowers.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/img/logos/julesjordan/thumbs/theassfactory.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 14 KiB

0
public/img/tags/blowjob/lazy/0.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

0
public/img/tags/blowjob/lazy/1.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

0
public/img/tags/blowjob/lazy/2.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

0
public/img/tags/blowjob/lazy/3.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

0
public/img/tags/blowjob/lazy/4.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

0
public/img/tags/blowjob/lazy/5.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

0
public/img/tags/blowjob/lazy/anissa_kate_vixen.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

0
public/img/tags/blowjob/lazy/anissa_kate_vixen_1.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

0
public/img/tags/blowjob/lazy/jane_wilde_evilangel.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

0
public/img/tags/blowjob/thumbs/0.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

0
public/img/tags/blowjob/thumbs/1.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

0
public/img/tags/blowjob/thumbs/2.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

0
public/img/tags/blowjob/thumbs/3.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

0
public/img/tags/blowjob/thumbs/4.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

0
public/img/tags/blowjob/thumbs/5.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

0
public/img/tags/blowjob/thumbs/anissa_kate_vixen.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -676,6 +676,7 @@ const tagMedia = [
['blowbang', 'zaawaadi_roccosiffredi_1', 'Zaawaadi in "My Name Is Zaawaadi"', 'roccosiffredi'],
['blowbang', 1, 'Nicole Black in GIO1680', 'analvids'],
['blowbang', 'gina_gerson_assholefever', 'Gina Gerson in "Oppa Gangbang Style"', 'assholefever'],
['blowjob', 'lily_lou_julesjordan', 'Lily Lou in "Lily Lou Has Her Ass Explored"', 'julesjordan'],
['blowjob', 'clanddi_jinkcego_ddfbusty_1', 'Clanddi Jinkcego', 'ddfbusty'],
['blowjob', 'juelz_ventura_babygotboobs', 'Juelz Ventura in "A Deep DP For Dessert"', 'babygotboobs'],
['blowjob', 4, 'Chloe Cherry in "Chloe\'s Big Anal"', 'darkx'],

View File

@ -1,5 +1,7 @@
'use strict';
const escapeRegexp = require('escape-string-regexp');
const knex = require('./knex');
const bulkInsert = require('./utils/bulk-insert');
const { HttpError } = require('./errors');
@ -9,10 +11,14 @@ async function addAlert(alert, sessionUser) {
throw new HttpError('You are not authenthicated', 401);
}
if (!alert.actors?.length > 0 && !alert.tags?.length > 0 && !alert.entity) {
if (!alert.actors?.length > 0 && !alert.tags?.length > 0 && !alert.entity && !alert.matches?.length > 0) {
throw new HttpError('Alert must contain at least one actor, tag or entity', 400);
}
if (alert.matches?.some((match) => !match.property || !match.expression)) {
throw new HttpError('Match must define a property and an expression', 400);
}
const [alertId] = await knex('alerts')
.insert({
user_id: sessionUser.id,
@ -30,6 +36,11 @@ async function addAlert(alert, sessionUser) {
alert_id: alertId,
tag_id: tagId,
})), false),
alert.matches?.length > 0 && bulkInsert('alerts_matches', alert.matches.map((match) => ({
alert_id: alertId,
property: match.property,
expression: match.expression,
})), false),
alert.stashes?.length > 0 && bulkInsert('alerts_stashes', alert.stashes.map((stashId) => ({
alert_id: alertId,
stash_id: stashId,
@ -48,88 +59,153 @@ async function removeAlert(alertId) {
}
async function notify(scenes) {
const releases = await knex.raw(`
SELECT alerts.id as alert_id, alerts.notify, alerts.email, releases.id as scene_id, users.id as user_id, COALESCE(json_agg(alerts_stashes.stash_id) FILTER (WHERE alerts_stashes.stash_id IS NOT NULL), '[]') as stashes
FROM releases
CROSS JOIN alerts
LEFT JOIN users ON users.id = alerts.user_id
LEFT JOIN alerts_stashes ON alerts_stashes.alert_id = alerts.id
/* match updated IDs from input */
WHERE (releases.id = ANY(:sceneIds))
/* match tags */
AND (NOT EXISTS (SELECT alerts_tags.alert_id
FROM alerts_tags
WHERE alerts_tags.alert_id = alerts.id)
OR (SELECT array_agg(releases_tags.tag_id)
FROM releases_tags
WHERE releases_tags.release_id = releases.id
GROUP BY releases_tags.release_id)
@> (SELECT array_agg(alerts_tags.tag_id)
FROM alerts_tags
WHERE alerts_tags.alert_id = alerts.id
GROUP BY alerts_tags.alert_id))
/* match actors */
AND (NOT EXISTS (SELECT alerts_actors.alert_id
FROM alerts_actors
WHERE alerts_actors.alert_id = alerts.id)
OR (SELECT array_agg(releases_actors.actor_id)
FROM releases_actors
WHERE releases_actors.release_id = releases.id
GROUP BY releases_actors.release_id)
@> (SELECT array_agg(alerts_actors.actor_id)
FROM alerts_actors
WHERE alerts_actors.alert_id = alerts.id
GROUP BY alerts_actors.alert_id))
/* match entity */
AND ((NOT EXISTS (SELECT alerts_entities.entity_id
FROM alerts_entities
WHERE alerts_entities.alert_id = alerts.id))
OR (releases.entity_id
= ANY(array(
/* include children of entities */
WITH RECURSIVE included AS (
SELECT entities.*
FROM alerts_entities
LEFT JOIN entities ON entities.id = alerts_entities.entity_id
WHERE alerts_entities.alert_id = alerts.id
const sceneIds = scenes.map((scene) => scene.id);
UNION ALL
const [
releasesActors,
releasesTags,
rawAlerts,
alertsActors,
alertsTags,
alertsEntities,
alertsMatches,
alertsStashes,
] = await Promise.all([
knex('releases_actors').whereIn('release_id', sceneIds),
knex('releases_tags').whereIn('release_id', sceneIds),
knex('alerts'),
knex('alerts_actors'),
knex('alerts_tags'),
knex('alerts_entities'),
knex('alerts_matches'),
knex('alerts_stashes'),
]);
SELECT entities.*
FROM entities
INNER JOIN included ON included.id = entities.parent_id
)
const actorIdsByReleaseId = releasesActors.reduce((acc, releaseActor) => {
if (!acc[releaseActor.release_id]) {
acc[releaseActor.release_id] = [];
}
SELECT included.id
FROM included
GROUP BY included.id
))))
GROUP BY releases.id, users.id, alerts.id;
`, {
sceneIds: scenes.map((scene) => scene.id),
acc[releaseActor.release_id].push(releaseActor.actor_id);
return acc;
}, {});
const tagIdsByReleaseId = releasesTags.reduce((acc, releaseTag) => {
if (!acc[releaseTag.release_id]) {
acc[releaseTag.release_id] = [];
}
acc[releaseTag.release_id].push(releaseTag.tag_id);
return acc;
}, {});
const alertsActorsByAlertId = alertsActors.reduce((acc, alertActor) => { if (!acc[alertActor.alert_id]) { acc[alertActor.alert_id] = []; } acc[alertActor.alert_id].push(alertActor.actor_id); return acc; }, {});
const alertsTagsByAlertId = alertsTags.reduce((acc, alertTag) => { if (!acc[alertTag.alert_id]) { acc[alertTag.alert_id] = []; } acc[alertTag.alert_id].push(alertTag.tag_id); return acc; }, {});
const alertsEntitiesByAlertId = alertsEntities.reduce((acc, alertEntity) => { if (!acc[alertEntity.alert_id]) { acc[alertEntity.alert_id] = []; } acc[alertEntity.alert_id].push(alertEntity.entity_id); return acc; }, {});
const alertsStashesByAlertId = alertsStashes.reduce((acc, alertStash) => { if (!acc[alertStash.alert_id]) { acc[alertStash.alert_id] = []; } acc[alertStash.alert_id].push(alertStash.stash_id); return acc; }, {});
const alertsMatchesByAlertId = alertsMatches.reduce((acc, alertMatch) => {
if (!acc[alertMatch.alert_id]) {
acc[alertMatch.alert_id] = [];
}
acc[alertMatch.alert_id].push({
property: alertMatch.property,
expression: /\/.*\//.test(alertMatch.expression)
? new RegExp(alertMatch.expression.slice(1, -1), 'ui')
: new RegExp(escapeRegexp(alertMatch.expression), 'ui'),
});
return acc;
}, {});
console.log(alertsStashesByAlertId);
const alerts = rawAlerts.map((alert) => ({
id: alert.id,
userId: alert.user_id,
notify: alert.notify,
email: alert.email,
all: alert.all,
actors: alertsActorsByAlertId[alert.id] || [],
tags: alertsTagsByAlertId[alert.id] || [],
entities: alertsEntitiesByAlertId[alert.id] || [],
matches: alertsMatchesByAlertId[alert.id] || [],
stashes: alertsStashesByAlertId[alert.id] || [],
}));
const curatedScenes = scenes.map((scene) => ({
id: scene.id,
title: scene.title,
description: scene.description,
actorIds: actorIdsByReleaseId[scene.id] || [],
tagIds: tagIdsByReleaseId[scene.id] || [],
entityId: scene.entity.id,
parentEntityId: scene.entity.parent?.id,
}));
const triggers = alerts.flatMap((alert) => {
const alertScenes = curatedScenes.filter((scene) => {
console.log(scene.title, alert.tags, scene.tagIds);
if (alert.actors.length > 0 && !alert.actors.every((actorId) => scene.actorIds.includes(actorId))) {
console.log('THROW ACTORS');
return false;
}
if (alert.tags.length > 0 && !alert.tags.every((tagId) => scene.tagIds.includes(tagId))) {
console.log('THROW TAGS');
return false;
}
// multiple entities can only be matched in OR mode
if (alert.entities.length > 0 && !alert.entities.some((alertEntityId) => alertEntityId === scene.entityId || alertEntityId === scene.parentEntityId)) {
console.log('THROW ENTITIES');
return false;
}
if (alert.matches.length > 0 && !alert.matches.every((match) => match.expression.test(scene[match.property]))) {
console.log('THROW MATCHES');
return false;
}
console.log('OK');
return true;
});
return alertScenes.map((scene) => ({
sceneId: scene.id,
alert,
}));
});
const notifications = releases.rows
.filter((alert) => alert.notify)
.map((notification) => ({
user_id: notification.user_id,
alert_id: notification.alert_id,
scene_id: notification.scene_id,
const notifications = Object.values(Object.fromEntries(triggers // prevent multiple notifications for the same scene
.filter((trigger) => trigger.alert.notify)
.map((trigger) => [`${trigger.alert.userId}:${trigger.sceneId}`, trigger])))
.map((trigger) => ({
user_id: trigger.alert.userId,
alert_id: trigger.alert.id,
scene_id: trigger.sceneId,
}));
const stashes = releases.rows
.filter((release) => release.stashes.length > 0)
.flatMap((release) => release.stashes.map((stash) => ({
scene_id: release.scene_id,
stash_id: stash,
})));
console.log('triggers', triggers);
const stashes = Object.values(Object.fromEntries(triggers.flatMap((trigger) => trigger.alert.stashes.map((stashId) => ({
scene_id: trigger.sceneId,
stash_id: stashId,
}))).map((stash) => [`${stash.stash_id}:${stash.scene_id}`, stash])));
console.log('stashes', stashes);
await Promise.all([
bulkInsert('notifications', notifications, false),
bulkInsert('stashes_scenes', stashes, false),
]);
return releases.rows;
return triggers;
}
async function updateNotification(notificationId, notification, sessionUser) {

View File

@ -32,7 +32,7 @@ function scrapeAll(scenes, site, entryIdFromTitle) {
release.title = title?.slice(0, title.match(/starring:/i)?.index || Infinity).trim();
release.url = query.url('.content_img a, .dvd_info > a, a.update_title, a[title]');
release.date = query.date('.update_date', 'MM/DD/YYYY');
release.date = query.date('.update_date', ['MM/DD/YYYY', 'YYYY-MM-DD']);
release.entryId = (entryIdFromTitle && slugify(release.title)) || element.dataset.setid || query.element('.rating_box')?.dataset.id || query.attribute('a img', 'id')?.match(/set-target-(\d+)/)?.[1];
@ -81,7 +81,7 @@ function scrapeUpcoming(scenes, channel) {
const release = {};
release.title = query.text('.overlay-text', { join: false })?.[0];
release.date = query.date('.overlay-text', 'MM/DD/YYYY');
release.date = query.date('.overlay-text', ['MM/DD/YYYY', 'YYYY-MM-DD']);
release.actors = query.all('.update_models a').map((actorEl) => ({
name: unprint.query.content(actorEl),
@ -159,7 +159,7 @@ async function scrapeScene({ html, query }, context) {
release.description = query.content('.update_description') || query.text('//div[./span[contains(text(), "Description")]]');
release.entryId = context.entity.parameters?.entryIdFromTitle ? slugify(release.title) : getEntryId(html);
release.date = query.date(['.update_date', '//div[./span[contains(text(), "Date")]]'], 'MM/DD/YYYY');
release.date = query.date(['.update_date', '//div[./span[contains(text(), "Date")]]'], ['MM/DD/YYYY', 'YYYY-MM-DD']);
release.actors = query.all('.backgroundcolor_info > .update_models a, .item .update_models a, .player-scene-description .update_models a').map((actorEl) => ({
name: unprint.query.content(actorEl),