Added timerange filters. Refactored releases module for more efficient queries.

This commit is contained in:
ThePendulum 2019-11-15 01:27:58 +01:00
parent a089bf892c
commit 1c3f17ec82
13 changed files with 280 additions and 155 deletions

View File

@ -2,7 +2,7 @@
"root": true,
"extends": ["airbnb-base", "plugin:vue/recommended"],
"parserOptions": {
"ecmaVersion": 2017,
"ecmaVersion": 2019,
"sourceType": "module"
},
"rules": {

View File

@ -1,24 +1,70 @@
<template>
<div class="filter-bar noselect">
<Icon icon="filter" />
<span>
<label class="range">
<input
:id="`${_uid}-all`"
:checked="range === 'all'"
type="radio"
class="range-input"
@click="$emit('set-range', 'all')"
>
<label
:for="`${_uid}-all`"
class="range-button"
>All</label>
</label>
<Filters
class="filters-container"
:filter="filter"
@set-filter="filter => $emit('set-filter', filter)"
/>
<label class="range">
<input
:id="`${_uid}-new`"
:checked="range === 'new'"
type="radio"
class="range-input"
@click="$emit('set-range', 'new')"
>
<label
:for="`${_uid}-new`"
class="range-button"
>New</label>
</label>
<v-popover class="filters-compact">
<div>Filters</div>
<label class="range">
<input
:id="`${_uid}-upcoming`"
:checked="range === 'upcoming'"
type="radio"
class="range-input"
@click="$emit('set-range', 'upcoming')"
>
<label
:for="`${_uid}-upcoming`"
class="range-button"
>Upcoming</label>
</label>
</span>
<div slot="popover">
<Filters
:compact="true"
:filter="filter"
@set-filter="filter => $emit('set-filter', filter)"
/>
</div>
</v-popover>
<span>
<Icon icon="filter" />
<Filters
class="filters-container"
:filter="filter"
@set-filter="filter => $emit('set-filter', filter)"
/>
<v-popover class="filters-compact">
<div>Filters</div>
<div slot="popover">
<Filters
:compact="true"
:filter="filter"
@set-filter="filter => $emit('set-filter', filter)"
/>
</div>
</v-popover>
</span>
</div>
</template>
@ -34,6 +80,10 @@ export default {
type: Array,
default: () => [],
},
range: {
type: String,
default: null,
},
},
};
</script>
@ -43,7 +93,8 @@ export default {
.filter-bar {
background: $background;
display: block;
display: flex;
justify-content: space-between;
padding: .5rem 1rem;
font-size: 0;
box-shadow: 0 0 3px $shadow;
@ -64,6 +115,30 @@ export default {
margin: 0 0 0 .5rem;
}
.range-button {
color: $shadow;
background: $background;
display: inline-block;
padding: .5rem 1rem;
border: none;
box-shadow: 0 0 2px $shadow-weak;
font-size: .8rem;
font-weight: bold;
&:hover {
color: $text;
cursor: pointer;
}
}
.range-input {
display: none;
&:checked + .range-button {
color: $primary;
}
}
@media(max-width: $breakpoint) {
.filters-container {
display: none;

View File

@ -1,25 +1,6 @@
<template>
<div :class="{ compact }">
<ul class="filters">
<li
v-tooltip.bottom="'Not yet available'"
class="filter"
>
<label
class="toggle"
:class="{ active: !localFilter.includes('straight') }"
>
<input
v-model="localFilter"
value="straight"
type="checkbox"
class="check"
disabled
@change="$emit('set-filter', localFilter)"
>straight
</label>
</li>
<li class="filter">
<label
class="toggle"
@ -82,23 +63,6 @@
</label>
</li>
</ul>
<ul class="filters">
<li class="filter">
<label
class="toggle"
:class="{ active: !localFilter.includes('femdom') }"
>
<input
v-model="localFilter"
value="femdom"
type="checkbox"
class="check"
@change="$emit('set-filter', localFilter)"
>femdom
</label>
</li>
</ul>
</div>
</template>

View File

@ -2,7 +2,9 @@
<div class="content">
<FilterBar
:filter="filter"
:range="range"
@set-filter="setFilter"
@set-range="setRange"
/>
<div class="content-inner">
@ -23,9 +25,12 @@
import FilterBar from './filter-bar.vue';
import ReleaseTile from '../tile/release.vue';
import rangeDates from '../../js/range-dates';
async function fetchReleases() {
this.releases = await this.$store.dispatch('fetchReleases', {
filter: this.filter,
...rangeDates(this.range),
});
}
@ -36,6 +41,13 @@ async function setFilter(filter) {
await this.fetchReleases();
}
async function setRange(range) {
this.range = range;
localStorage.setItem('range', this.range);
await this.fetchReleases();
}
async function mounted() {
this.pageTitle = '';
@ -49,9 +61,11 @@ export default {
},
data() {
const storedFilter = localStorage.getItem('filter');
const storedRange = localStorage.getItem('range');
return {
filter: storedFilter ? storedFilter.split(',') : ['gay', 'transsexual'],
range: storedRange || 'new',
releases: [],
networks: [],
pageTitle: null,
@ -61,6 +75,7 @@ export default {
methods: {
fetchReleases,
setFilter,
setRange,
},
};
</script>

View File

@ -9,6 +9,7 @@
<div class="column">
<a
v-if="release.date"
v-tooltip.bottom="`View scene on ${release.site.name}`"
:title="`View scene on ${release.site.name}`"
:href="release.url"
target="_blank"
@ -21,6 +22,7 @@
<a
v-if="release.date"
v-tooltip.bottom="`View scene on ${release.site.name}`"
:title="`View scene on ${release.site.name}`"
:href="release.url"
target="_blank"
@ -33,6 +35,7 @@
<span
v-if="release.shootId"
v-tooltip.bottom="`Shoot #`"
class="tidbit shoot hideable"
>
<Icon icon="clapboard-play" />
@ -41,6 +44,7 @@
<span
v-if="release.duration"
v-tooltip.bottom="`Duration`"
class="tidbit duration hideable"
>
<Icon icon="stopwatch" />
@ -127,6 +131,20 @@
</ul>
</div>
<div
v-if="release.duration"
class="row duration showable"
>
<Icon icon="stopwatch" />
<span
v-if="release.duration >= 3600"
class="duration-segment"
>{{ Math.floor(release.duration / 3600) }}:</span>
<span class="duration-segment">{{ Math.floor((release.duration % 3600) / 60).toString().padStart(2, '0') }}:</span>
<span class="duration-segment">{{ (release.duration % 60).toString().padStart(2, '0') }}</span>
</div>
<p
v-if="release.description"
class="row description"
@ -150,29 +168,19 @@
>{{ release.studio.name }}</a>
</div>
<a
v-if="release.shootId"
:href="release.url"
:title="`release.shootId`"
target="_blank"
rel="noopener noreferrer"
class="row shoot showable"
>
<Icon icon="clapboard-play" />{{ release.shootId }}
</a>
<div
v-if="release.duration"
class="row duration showable"
v-if="release.shootId"
class="row showable"
>
<Icon icon="stopwatch" />
<Icon icon="clapboard-play" />
<span
v-if="release.duration >= 3600"
class="duration-segment"
>{{ Math.floor(release.duration / 3600) }}:</span>
<span class="duration-segment">{{ Math.floor((release.duration % 3600) / 60).toString().padStart(2, '0') }}:</span>
<span class="duration-segment">{{ (release.duration % 60).toString().padStart(2, '0') }}</span>
<a
:href="release.url"
:title="`release.shootId`"
target="_blank"
rel="noopener noreferrer"
class="link shoot"
>{{ release.shootId }}</a>
</div>
<span class="row">
@ -199,7 +207,7 @@ function pageTitle() {
}
async function mounted() {
[this.release] = await this.$store.dispatch('fetchReleases', { id: this.$route.params.releaseId });
this.release = await this.$store.dispatch('fetchReleases', { id: this.$route.params.releaseId });
}
export default {
@ -271,6 +279,7 @@ export default {
&.date,
&.duration,
&.shoot {
flex-shrink: 0;
padding: 1.25rem 1rem 1.25rem 0;
margin: 0 1rem 0 0;
}
@ -293,12 +302,14 @@ export default {
height: 3rem;
max-width: 15rem;
object-fit: contain;
object-position: 100% 50%;
}
.logo-network {
height: 1.5rem;
max-width: 10rem;
object-fit: contain;
object-position: 100% 50%;
}
.chain {
@ -373,7 +384,8 @@ export default {
}
.logo-site {
max-width: 10rem;
width: 15rem;
max-width: 100%;
}
}
</style>

View File

@ -46,6 +46,6 @@ body {
@media(max-width: $breakpoint) {
.scenes {
grid-template-columns: repeat(auto-fit, minmax(22.5rem, 1fr));
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}
}

18
assets/js/range-dates.js Normal file
View File

@ -0,0 +1,18 @@
function rangeDates(range) {
return ({
new: () => ({
after: new Date(0),
before: new Date(),
}),
upcoming: () => ({
after: new Date(),
before: new Date(2 ** 42),
}),
all: () => ({
after: new Date(0),
before: new Date(2 ** 42),
}),
})[range]();
}
export default rangeDates;

View File

@ -1,8 +1,22 @@
import dayjs from 'dayjs';
import { get } from '../api';
function initReleasesActions(_store, _router) {
async function fetchReleases({ _commit }, { id, filter }) {
const releases = await get(`/releases/${id || ''}`, { filter });
async function fetchReleases({ _commit }, {
id,
filter,
after,
before,
}) {
const afterString = dayjs(after).format('YYYY-MM-DD');
const beforeString = dayjs(before).format('YYYY-MM-DD');
const releases = await get(`/releases/${id || ''}`, {
filter,
after: afterString,
before: beforeString,
});
return releases;
}

View File

@ -42,7 +42,8 @@
/* $primary: #ff886c; */
.filter-bar[data-v-a678373a] {
background: #fff;
display: block;
display: flex;
justify-content: space-between;
padding: .5rem 1rem;
font-size: 0;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
@ -59,6 +60,26 @@
display: none;
margin: 0 0 0 .5rem;
}
.range-button[data-v-a678373a] {
color: rgba(0, 0, 0, 0.5);
background: #fff;
display: inline-block;
padding: .5rem 1rem;
border: none;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);
font-size: .8rem;
font-weight: bold;
}
.range-button[data-v-a678373a]:hover {
color: #222;
cursor: pointer;
}
.range-input[data-v-a678373a] {
display: none;
}
.range-input:checked + .range-button[data-v-a678373a] {
color: #ff6c88;
}
@media (max-width: 720px) {
.filters-container[data-v-a678373a] {
display: none;
@ -271,6 +292,7 @@
margin: 0 .25rem 0 0;
}
.tidbit.date[data-v-2bc41e74], .tidbit.duration[data-v-2bc41e74], .tidbit.shoot[data-v-2bc41e74] {
flex-shrink: 0;
padding: 1.25rem 1rem 1.25rem 0;
margin: 0 1rem 0 0;
}
@ -290,12 +312,16 @@
max-width: 15rem;
-o-object-fit: contain;
object-fit: contain;
-o-object-position: 100% 50%;
object-position: 100% 50%;
}
.logo-network[data-v-2bc41e74] {
height: 1.5rem;
max-width: 10rem;
-o-object-fit: contain;
object-fit: contain;
-o-object-position: 100% 50%;
object-position: 100% 50%;
}
.chain[data-v-2bc41e74] {
color: rgba(0, 0, 0, 0.5);
@ -355,7 +381,8 @@
display: block;
}
.logo-site[data-v-2bc41e74] {
max-width: 10rem;
width: 15rem;
max-width: 100%;
}
}
@ -686,7 +713,7 @@ body {
@media (max-width: 720px) {
.scenes {
grid-template-columns: repeat(auto-fit, minmax(22.5rem, 1fr)); } }
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); } }
/* $primary: #ff886c; */
.header[data-v-10b7ec04] {

View File

@ -992,6 +992,7 @@ function getSites(networksMap) {
name: 'Evil Angel',
url: 'https://evilangel.com',
description: 'Welcome to the award winning Evil Angel website, home to the most popular pornstars of today, yesterday and tomorrow in their most extreme and hardcore porn scenes to date. We feature almost 30 years of rough sex videos and hardcore anal porn like you\'ve never seen before, and have won countless AVN and XBiz awards including \'Best Site\' and \'Best Studio\'.',
parameters: JSON.stringify({ independent: true }),
network_id: networksMap['evilangel'],
},
// JULES JORDAN

View File

@ -1,6 +1,7 @@
'use strict';
const knex = require('./knex');
const whereOr = require('./utils/where-or');
async function curateRelease(release) {
const [actors, tags, media] = await Promise.all([
@ -70,10 +71,13 @@ function curateReleases(releases) {
return Promise.all(releases.map(async release => curateRelease(release)));
}
async function fetchReleases(releaseId, filter = []) {
// const straightFilter = filter.includes('straight') ? ['gay', 'lesbian'] : [];
const releases = await knex('releases')
function commonQuery(queryBuilder, {
filter = [],
after = new Date(0), // January 1970
before = new Date(2 ** 44), // May 2109
limit = 100,
}) {
queryBuilder
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
@ -84,7 +88,7 @@ async function fetchReleases(releaseId, filter = []) {
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description',
)
.whereNotExists((builder) => {
// apply filters
// apply tag filters
builder
.select('*')
.from('tags_associated')
@ -92,91 +96,58 @@ async function fetchReleases(releaseId, filter = []) {
.whereIn('tags.slug', filter)
.andWhereRaw('tags_associated.release_id = releases.id');
})
.andWhere(releaseId ? { 'releases.id': releaseId } : {})
.andWhere('date', '>', after)
.andWhere('date', '<=', before)
.orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }])
.limit(100);
return curateReleases(releases);
.limit(limit);
}
async function fetchSiteReleases(siteId, siteSlug) {
async function fetchReleases(queryObject = {}, options = {}) {
const releases = await knex('releases')
.where({ 'sites.id': siteId })
.orWhere({ 'sites.slug': siteSlug })
.select(
'releases.*',
'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters',
'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url',
)
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }])
.limit(100);
.modify(commonQuery, options)
.andWhere(builder => whereOr(queryObject, 'releases', builder));
return curateReleases(releases);
}
async function fetchNetworkReleases(networkId, networkSlug) {
async function fetchSiteReleases(queryObject, options = {}) {
const releases = await knex('releases')
.where({ 'networks.id': networkId })
.orWhere({ 'networks.slug': networkSlug })
.select(
'releases.*',
'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters',
'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url',
)
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }])
.limit(100);
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'sites', builder));
return curateReleases(releases);
}
async function fetchActorReleases(actorId, actorSlug) {
async function fetchNetworkReleases(queryObject, options = {}) {
const releases = await knex('releases')
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'networks', builder));
return curateReleases(releases);
}
async function fetchActorReleases(queryObject, options = {}) {
const releases = await knex('actors_associated')
.where({ 'actors.id': actorId })
.orWhere({ 'actors.slug': actorSlug })
.select(
'releases.*',
'actors.name as actor_name',
'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters',
'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url',
)
.leftJoin('releases', 'actors_associated.release_id', 'releases.id')
.leftJoin('actors', 'actors_associated.actor_id', 'actors.id')
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.orderBy([{ column: 'releases.date', order: 'desc' }, { column: 'releases.created_at', order: 'desc' }])
.limit(100);
.select(
'actors.name as actor_name',
)
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'actors', builder));
return curateReleases(releases);
}
async function fetchTagReleases(tagId, tagSlug) {
async function fetchTagReleases(queryObject, options = {}) {
const releases = await knex('tags_associated')
.where({ 'tags.id': tagId })
.orWhere({ 'tags.slug': tagSlug })
.select(
'releases.*',
'tags.name as tag_name',
'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id',
'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url',
)
.leftJoin('releases', 'tags_associated.release_id', 'releases.id')
.leftJoin('tags', 'tags_associated.tag_id', 'tags.id')
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.orderBy([{ column: 'releases.date', order: 'desc' }, { column: 'releases.created_at', order: 'desc' }])
.limit(100);
.select(
'tags.name as tag_name',
)
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'tags', builder));
return curateReleases(releases);
}

View File

@ -9,16 +9,33 @@ const {
} = require('../releases');
async function fetchReleasesApi(req, res) {
const releases = await fetchReleases(req.params.releaseId, req.query.filter ? [].concat(req.query.filter) : []);
const filter = req.query.filter ? [].concat(req.query.filter) : []; // don't filter for 'undefined'
const releases = await fetchReleases({}, {
filter,
after: req.query.after,
before: req.query.before,
});
res.send(releases);
}
async function fetchReleaseByIdApi(req, res) {
const [release] = await fetchReleases({
id: req.params.releaseId,
});
res.send(release);
}
async function fetchActorReleasesApi(req, res) {
const actorId = Number.isInteger(Number(req.params.actorId)) ? Number(req.params.actorId) : null;
const actorSlug = typeof req.params.actorId === 'string' ? req.params.actorId : null;
const releases = await fetchActorReleases(actorId, actorSlug);
const releases = await fetchActorReleases({
id: actorId,
slug: actorSlug,
});
res.send(releases);
}
@ -27,7 +44,10 @@ async function fetchNetworkReleasesApi(req, res) {
const networkId = typeof req.params.networkId === 'number' ? req.params.networkId : null;
const networkSlug = typeof req.params.networkId === 'string' ? req.params.networkId : null;
const releases = await fetchNetworkReleases(networkId, networkSlug);
const releases = await fetchNetworkReleases({
id: networkId,
slug: networkSlug,
});
res.send(releases);
}
@ -36,7 +56,10 @@ async function fetchSiteReleasesApi(req, res) {
const siteId = typeof req.params.siteId === 'number' ? req.params.siteId : null;
const siteSlug = typeof req.params.siteId === 'string' ? req.params.siteId : null;
const releases = await fetchSiteReleases(siteId, siteSlug);
const releases = await fetchSiteReleases({
id: siteId,
slug: siteSlug,
});
res.send(releases);
}
@ -45,13 +68,17 @@ async function fetchTagReleasesApi(req, res) {
const tagId = typeof req.params.tagId === 'number' ? req.params.tagId : null;
const tagSlug = typeof req.params.tagId === 'string' ? req.params.tagId : null;
const releases = await fetchTagReleases(tagId, tagSlug);
const releases = await fetchTagReleases({
id: tagId,
slug: tagSlug,
});
res.send(releases);
}
module.exports = {
fetchReleases: fetchReleasesApi,
fetchReleaseById: fetchReleaseByIdApi,
fetchActorReleases: fetchActorReleasesApi,
fetchNetworkReleases: fetchNetworkReleasesApi,
fetchSiteReleases: fetchSiteReleasesApi,

View File

@ -8,6 +8,7 @@ const bodyParser = require('body-parser');
const {
fetchReleases,
fetchReleaseById,
fetchActorReleases,
fetchNetworkReleases,
fetchSiteReleases,
@ -37,7 +38,7 @@ function initServer() {
router.use(bodyParser.json({ strict: false }));
router.get('/api/releases', fetchReleases);
router.get('/api/releases/:releaseId', fetchReleases);
router.get('/api/releases/:releaseId', fetchReleaseById);
router.get('/api/releases/networks', fetchNetworksFromReleases);
router.get('/api/actors', fetchActors);