diff --git a/.editorconfig b/.editorconfig index 70d1e851a..2b49a63cc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,7 +5,7 @@ root = true [*] end_of_line = lf insert_final_newline = true -indent_style = space +indent_style = tab indent_size = 4 # Matches multiple files with brace expansion notation diff --git a/.eslintrc b/.eslintrc index dd96e40e9..4e4ee9f2a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,13 +7,14 @@ "sourceType": "module" }, "rules": { + "indent": ["error", "tab"], + "no-tabs": "off", "no-unused-vars": ["error", {"argsIgnorePattern": "^_"}], "no-console": 0, - "indent": "off", "template-curly-spacing": "off", "max-len": 0, "vue/no-v-html": 0, - "vue/html-indent": ["error", 4], + "vue/html-indent": ["error", "tab"], "vue/multiline-html-element-content-newline": 0, "vue/singleline-html-element-content-newline": 0, "no-param-reassign": ["error", { diff --git a/assets/components/actors/actor.vue b/assets/components/actors/actor.vue index d6bda5c25..1822532e2 100644 --- a/assets/components/actors/actor.vue +++ b/assets/components/actors/actor.vue @@ -1,248 +1,248 @@ @@ -426,7 +426,7 @@ export default { } .bio-value { - margin: 0 0 0 2rem; + margin: 0 0 0 2rem; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; diff --git a/assets/components/networks/network.vue b/assets/components/networks/network.vue index 5954eb253..59128739f 100644 --- a/assets/components/networks/network.vue +++ b/assets/components/networks/network.vue @@ -1,124 +1,124 @@ diff --git a/assets/components/tile/site.vue b/assets/components/tile/site.vue index 2be37b39b..08ff0e8a8 100644 --- a/assets/components/tile/site.vue +++ b/assets/components/tile/site.vue @@ -1,25 +1,25 @@ diff --git a/assets/js/actors/actions.js b/assets/js/actors/actions.js index a5db238b1..ee2de9c91 100644 --- a/assets/js/actors/actions.js +++ b/assets/js/actors/actions.js @@ -1,59 +1,61 @@ import { graphql, get } from '../api'; import { - releasePosterFragment, - releaseActorsFragment, - releaseTagsFragment, + releasePosterFragment, + releaseActorsFragment, + releaseTagsFragment, } from '../fragments'; import { curateRelease } from '../curate'; import getDateRange from '../get-date-range'; function curateActor(actor) { - if (!actor) { - return null; - } + if (!actor) { + return null; + } - const curatedActor = { - ...actor, - height: actor.heightMetric && { - metric: actor.heightMetric, - imperial: actor.heightImperial, - }, - weight: actor.weightMetric && { - metric: actor.weightMetric, - imperial: actor.weightImperial, - }, - origin: actor.birthCountry && { - city: actor.birthCity, - state: actor.birthState, - country: actor.birthCountry, - }, - residence: actor.residenceCountry && { - city: actor.residenceCity, - state: actor.residenceState, - country: actor.residenceCountry, - }, - }; + const curatedActor = { + ...actor, + height: actor.heightMetric && { + metric: actor.heightMetric, + imperial: actor.heightImperial, + }, + weight: actor.weightMetric && { + metric: actor.weightMetric, + imperial: actor.weightImperial, + }, + origin: actor.birthCountry && { + city: actor.birthCity, + state: actor.birthState, + country: actor.birthCountry, + }, + residence: actor.residenceCountry && { + city: actor.residenceCity, + state: actor.residenceState, + country: actor.residenceCountry, + }, + scrapedAt: new Date(actor.createdAt), + updatedAt: new Date(actor.updatedAt), + }; - if (actor.avatar) { - curatedActor.avatar = actor.avatar.media; - } + if (actor.avatar) { + curatedActor.avatar = actor.avatar.media; + } - if (actor.releases) { - curatedActor.releases = actor.releases.map(release => curateRelease(release.release)); - } + if (actor.releases) { + curatedActor.releases = actor.releases.map(release => curateRelease(release.release)); + } - if (actor.photos) { - curatedActor.photos = actor.photos.map(photo => photo.media); - } + if (actor.photos) { + curatedActor.photos = actor.photos.map(photo => photo.media); + } - return curatedActor; + return curatedActor; } function initActorActions(store, _router) { - async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) { - const { before, after, orderBy } = getDateRange(range); + async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) { + const { before, after, orderBy } = getDateRange(range); - const { actors: [actor] } = await graphql(` + const { actors: [actor] } = await graphql(` query Actor( $actorSlug: String! $limit:Int = 1000, @@ -90,6 +92,8 @@ function initActorActions(store, _router) { tattoos piercings description + createdAt + updatedAt network { id name @@ -184,27 +188,27 @@ function initActorActions(store, _router) { } } `, { - actorSlug, - limit, - after, - before, - orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC', - exclude: store.state.ui.filter, - }); + actorSlug, + limit, + after, + before, + orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC', + exclude: store.state.ui.filter, + }); - return curateActor(actor); - } + return curateActor(actor); + } - async function fetchActors({ _commit }, { - limit = 100, - letter, - gender, - }) { - const genderFilter = gender === null - ? 'isNull: true' - : `equalTo: "${gender}"`; + async function fetchActors({ _commit }, { + limit = 100, + letter, + gender, + }) { + const genderFilter = gender === null + ? 'isNull: true' + : `equalTo: "${gender}"`; - const { actors } = await graphql(` + const { actors } = await graphql(` query Actors( $limit: Int, $letter: String! = "", @@ -249,28 +253,28 @@ function initActorActions(store, _router) { } } `, { - limit, - letter, - }); + limit, + letter, + }); - return actors.map(actor => curateActor(actor)); - } + return actors.map(actor => curateActor(actor)); + } - async function fetchActorReleases({ _commit }, actorId) { - const releases = await get(`/actors/${actorId}/releases`, { - filter: store.state.ui.filter, - after: store.getters.after, - before: store.getters.before, - }); + async function fetchActorReleases({ _commit }, actorId) { + const releases = await get(`/actors/${actorId}/releases`, { + filter: store.state.ui.filter, + after: store.getters.after, + before: store.getters.before, + }); - return releases; - } + return releases; + } - return { - fetchActorBySlug, - fetchActors, - fetchActorReleases, - }; + return { + fetchActorBySlug, + fetchActors, + fetchActorReleases, + }; } export default initActorActions; diff --git a/assets/js/actors/actors.js b/assets/js/actors/actors.js index 59be7fdf8..8e71e8861 100644 --- a/assets/js/actors/actors.js +++ b/assets/js/actors/actors.js @@ -3,11 +3,11 @@ import mutations from './mutations'; import actions from './actions'; function initActorsStore(store, router) { - return { - state, - mutations, - actions: actions(store, router), - }; + return { + state, + mutations, + actions: actions(store, router), + }; } export default initActorsStore; diff --git a/assets/js/api.js b/assets/js/api.js index 5fd6a0412..eeb358572 100644 --- a/assets/js/api.js +++ b/assets/js/api.js @@ -1,71 +1,71 @@ import config from 'config'; async function get(endpoint, query = {}) { - const curatedQuery = Object.entries(query).reduce((acc, [key, value]) => (value ? { ...acc, [key]: value } : acc), {}); // remove empty values - const q = new URLSearchParams(curatedQuery).toString(); + const curatedQuery = Object.entries(query).reduce((acc, [key, value]) => (value ? { ...acc, [key]: value } : acc), {}); // remove empty values + const q = new URLSearchParams(curatedQuery).toString(); - const res = await fetch(`${config.api.url}${endpoint}?${q}`, { - method: 'GET', - mode: 'cors', - credentials: 'same-origin', - }); + const res = await fetch(`${config.api.url}${endpoint}?${q}`, { + method: 'GET', + mode: 'cors', + credentials: 'same-origin', + }); - if (res.ok) { - return res.json(); - } + if (res.ok) { + return res.json(); + } - const errorMsg = await res.text(); + const errorMsg = await res.text(); - throw new Error(errorMsg); + throw new Error(errorMsg); } async function post(endpoint, data) { - const res = await fetch(`${config.api.url}${endpoint}`, { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'same-origin', - body: JSON.stringify(data), - }); + const res = await fetch(`${config.api.url}${endpoint}`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify(data), + }); - if (res.ok) { - return res.json(); - } + if (res.ok) { + return res.json(); + } - const errorMsg = await res.text(); + const errorMsg = await res.text(); - throw new Error(errorMsg); + throw new Error(errorMsg); } async function graphql(query, variables = null) { - const res = await fetch('/graphql', { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'same-origin', - body: JSON.stringify({ - query, - variables, - }), - }); + const res = await fetch('/graphql', { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify({ + query, + variables, + }), + }); - if (res.ok) { - const { data } = await res.json(); + if (res.ok) { + const { data } = await res.json(); - return data; - } + return data; + } - const errorMsg = await res.text(); + const errorMsg = await res.text(); - throw new Error(errorMsg); + throw new Error(errorMsg); } export { - get, - post, - graphql, + get, + post, + graphql, }; diff --git a/assets/js/auth/auth.js b/assets/js/auth/auth.js index d3eb5076b..cb99d9da1 100644 --- a/assets/js/auth/auth.js +++ b/assets/js/auth/auth.js @@ -3,11 +3,11 @@ import mutations from './mutations'; import actions from './actions'; function initAuthStore(store, router) { - return { - state, - mutations, - actions: actions(store, router), - }; + return { + state, + mutations, + actions: actions(store, router), + }; } export default initAuthStore; diff --git a/assets/js/auth/state.js b/assets/js/auth/state.js index 427473f09..b857bfbf1 100644 --- a/assets/js/auth/state.js +++ b/assets/js/auth/state.js @@ -1,4 +1,4 @@ export default { - authenticated: false, - user: null, + authenticated: false, + user: null, }; diff --git a/assets/js/config/default.js b/assets/js/config/default.js index 3d86df363..7dc928771 100644 --- a/assets/js/config/default.js +++ b/assets/js/config/default.js @@ -1,10 +1,10 @@ export default { - api: { - url: `${window.location.origin}/api`, - }, - filename: { - pattern: '{site.name} - {title} ({actors.$n.name}, {date} {shootId})', - separator: ', ', - date: 'DD-MM-YYYY', - }, + api: { + url: `${window.location.origin}/api`, + }, + filename: { + pattern: '{site.name} - {title} ({actors.$n.name}, {date} {shootId})', + separator: ', ', + date: 'DD-MM-YYYY', + }, }; diff --git a/assets/js/curate.js b/assets/js/curate.js index 5c31e372c..31550a2d9 100644 --- a/assets/js/curate.js +++ b/assets/js/curate.js @@ -1,94 +1,94 @@ import dayjs from 'dayjs'; function curateActor(actor, release) { - const curatedActor = { - ...actor, - origin: actor.originCountry && { - country: actor.originCountry, - }, - }; + const curatedActor = { + ...actor, + origin: actor.originCountry && { + country: actor.originCountry, + }, + }; - if (actor.avatar) curatedActor.avatar = actor.avatar.media; + if (actor.avatar) curatedActor.avatar = actor.avatar.media; - if (release && release.date && curatedActor.birthdate) { - curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year'); - } + if (release && release.date && curatedActor.birthdate) { + curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year'); + } - return curatedActor; + return curatedActor; } function curateRelease(release) { - const curatedRelease = { - ...release, - actors: [], - poster: release.poster && release.poster.media, - tags: release.tags ? release.tags.map(({ tag }) => tag) : [], - }; + const curatedRelease = { + ...release, + actors: [], + poster: release.poster && release.poster.media, + tags: release.tags ? release.tags.map(({ tag }) => tag) : [], + }; - if (release.site) curatedRelease.network = release.site.network; - if (release.scenes) curatedRelease.scenes = release.scenes.map(({ scene }) => curateRelease(scene)); - if (release.movies) curatedRelease.movies = release.movies.map(({ movie }) => curateRelease(movie)); - if (release.photos) curatedRelease.photos = release.photos.map(({ media }) => media); - if (release.covers) curatedRelease.covers = release.covers.map(({ media }) => media); - if (release.trailer) curatedRelease.trailer = release.trailer.media; - if (release.teaser) curatedRelease.teaser = release.teaser.media; - if (release.actors) curatedRelease.actors = release.actors.map(({ actor }) => curateActor(actor, curatedRelease)); - if (release.movieTags && release.movieTags.length > 0) curatedRelease.tags = release.movieTags.map(({ tag }) => tag); - if (release.movieActors && release.movieActors.length > 0) curatedRelease.actors = release.movieActors.map(({ actor }) => curateActor(actor, curatedRelease)); + if (release.site) curatedRelease.network = release.site.network; + if (release.scenes) curatedRelease.scenes = release.scenes.map(({ scene }) => curateRelease(scene)); + if (release.movies) curatedRelease.movies = release.movies.map(({ movie }) => curateRelease(movie)); + if (release.photos) curatedRelease.photos = release.photos.map(({ media }) => media); + if (release.covers) curatedRelease.covers = release.covers.map(({ media }) => media); + if (release.trailer) curatedRelease.trailer = release.trailer.media; + if (release.teaser) curatedRelease.teaser = release.teaser.media; + if (release.actors) curatedRelease.actors = release.actors.map(({ actor }) => curateActor(actor, curatedRelease)); + if (release.movieTags && release.movieTags.length > 0) curatedRelease.tags = release.movieTags.map(({ tag }) => tag); + if (release.movieActors && release.movieActors.length > 0) curatedRelease.actors = release.movieActors.map(({ actor }) => curateActor(actor, curatedRelease)); - return curatedRelease; + return curatedRelease; } function curateSite(site, network) { - const curatedSite = { - id: site.id, - name: site.name, - slug: site.slug, - url: site.url, - independent: site.independent, - }; + const curatedSite = { + id: site.id, + name: site.name, + slug: site.slug, + url: site.url, + independent: site.independent, + }; - if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release)); - if (site.network || network) curatedSite.network = site.network || network; - if (site.tags) curatedSite.tags = site.tags.map(({ tag }) => tag); + if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release)); + if (site.network || network) curatedSite.network = site.network || network; + if (site.tags) curatedSite.tags = site.tags.map(({ tag }) => tag); - return curatedSite; + return curatedSite; } function curateNetwork(network, releases) { - const curatedNetwork = { - id: network.id, - name: network.name, - slug: network.slug, - url: network.url, - networks: [], - }; + const curatedNetwork = { + id: network.id, + name: network.name, + slug: network.slug, + url: network.url, + networks: [], + }; - if (network.parent) curatedNetwork.parent = curateNetwork(network.parent); - if (network.sites) curatedNetwork.sites = network.sites.map(site => curateSite(site, curatedNetwork)); - if (network.networks) curatedNetwork.networks = network.networks.map(subNetwork => curateNetwork(subNetwork)); - if (network.studios) curatedNetwork.studios = network.studios; - if (releases) curatedNetwork.releases = releases.map(release => curateRelease(release)); + if (network.parent) curatedNetwork.parent = curateNetwork(network.parent); + if (network.sites) curatedNetwork.sites = network.sites.map(site => curateSite(site, curatedNetwork)); + if (network.networks) curatedNetwork.networks = network.networks.map(subNetwork => curateNetwork(subNetwork)); + if (network.studios) curatedNetwork.studios = network.studios; + if (releases) curatedNetwork.releases = releases.map(release => curateRelease(release)); - return curatedNetwork; + return curatedNetwork; } function curateTag(tag) { - const curatedTag = { - ...tag, - }; + const curatedTag = { + ...tag, + }; - if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release)); - if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media); - if (tag.poster) curatedTag.poster = tag.poster.media; + if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release)); + if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media); + if (tag.poster) curatedTag.poster = tag.poster.media; - return curatedTag; + return curatedTag; } export { - curateActor, - curateRelease, - curateSite, - curateNetwork, - curateTag, + curateActor, + curateRelease, + curateSite, + curateNetwork, + curateTag, }; diff --git a/assets/js/fragments.js b/assets/js/fragments.js index 60915fbe7..d89c7bdc2 100644 --- a/assets/js/fragments.js +++ b/assets/js/fragments.js @@ -278,14 +278,14 @@ const releaseFragment = ` `; export { - releaseActorsFragment, - releaseFields, - releaseTagsFragment, - releasePosterFragment, - releasePhotosFragment, - releaseTrailerFragment, - releasesFragment, - releaseFragment, - siteFragment, - sitesFragment, + releaseActorsFragment, + releaseFields, + releaseTagsFragment, + releasePosterFragment, + releasePhotosFragment, + releaseTrailerFragment, + releasesFragment, + releaseFragment, + siteFragment, + sitesFragment, }; diff --git a/assets/js/get-date-range.js b/assets/js/get-date-range.js index ce486752e..4eb86ba8a 100644 --- a/assets/js/get-date-range.js +++ b/assets/js/get-date-range.js @@ -1,30 +1,30 @@ import dayjs from 'dayjs'; const dateRanges = { - latest: () => ({ - after: '1900-01-01', - before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'), - orderBy: 'DATE_DESC', - }), - upcoming: () => ({ - after: dayjs(new Date()).format('YYYY-MM-DD'), - before: '2100-01-01', - orderBy: 'DATE_ASC', - }), - new: () => ({ - after: '1900-01-01', - before: '2100-01-01', - orderBy: 'CREATED_AT_DESC', - }), - all: () => ({ - after: '1900-01-01', - before: '2100-01-01', - orderBy: 'DATE_DESC', - }), + latest: () => ({ + after: '1900-01-01', + before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'), + orderBy: 'DATE_DESC', + }), + upcoming: () => ({ + after: dayjs(new Date()).format('YYYY-MM-DD'), + before: '2100-01-01', + orderBy: 'DATE_ASC', + }), + new: () => ({ + after: '1900-01-01', + before: '2100-01-01', + orderBy: 'CREATED_AT_DESC', + }), + all: () => ({ + after: '1900-01-01', + before: '2100-01-01', + orderBy: 'DATE_DESC', + }), }; function getDateRange(range) { - return dateRanges[range](); + return dateRanges[range](); } export default getDateRange; diff --git a/assets/js/main.js b/assets/js/main.js index ad41d4ccc..b50ac989f 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -14,48 +14,48 @@ import Container from '../components/container/container.vue'; import Icon from '../components/icon/icon.vue'; function init() { - const store = initStore(router); + const store = initStore(router); - initUiObservers(store, router); + initUiObservers(store, router); - if (window.env.sfw) { - store.dispatch('setSfw', true); - } + if (window.env.sfw) { + store.dispatch('setSfw', true); + } - Vue.mixin({ - components: { - Icon, - }, - watch: { - pageTitle(title) { - if (title) { - document.title = `traxxx - ${title}`; - return; - } + Vue.mixin({ + components: { + Icon, + }, + watch: { + pageTitle(title) { + if (title) { + document.title = `traxxx - ${title}`; + return; + } - document.title = 'traxxx'; - }, - }, - methods: { - formatDate: (date, format) => dayjs(date).format(format), - isAfter: (dateA, dateB) => dayjs(dateA).isAfter(dateB), - isBefore: (dateA, dateB) => dayjs(dateA).isBefore(dateB), - }, - }); + document.title = 'traxxx'; + }, + }, + methods: { + formatDate: (date, format) => dayjs(date).format(format), + isAfter: (dateA, dateB) => dayjs(dateA).isAfter(dateB), + isBefore: (dateA, dateB) => dayjs(dateA).isBefore(dateB), + }, + }); - Vue.use(VTooltip); - Vue.use(VueLazyLoad, { - throttleWait: 0, - }); + Vue.use(VTooltip); + Vue.use(VueLazyLoad, { + throttleWait: 0, + }); - new Vue({ // eslint-disable-line no-new - el: '#container', - store, - router, - render(createElement) { - return createElement(Container); - }, - }); + new Vue({ // eslint-disable-line no-new + el: '#container', + store, + router, + render(createElement) { + return createElement(Container); + }, + }); } init(); diff --git a/assets/js/networks/actions.js b/assets/js/networks/actions.js index b24a87f80..758c9f926 100644 --- a/assets/js/networks/actions.js +++ b/assets/js/networks/actions.js @@ -4,10 +4,10 @@ import { curateNetwork } from '../curate'; import getDateRange from '../get-date-range'; function initNetworksActions(store, _router) { - async function fetchNetworkBySlug({ _commit }, { networkSlug, limit = 100, range = 'latest' }) { - const { before, after, orderBy } = getDateRange(range); + async function fetchNetworkBySlug({ _commit }, { networkSlug, limit = 100, range = 'latest' }) { + const { before, after, orderBy } = getDateRange(range); - const { network, releases } = await graphql(` + const { network, releases } = await graphql(` query Network( $networkSlug: String! $limit:Int = 1000, @@ -107,21 +107,21 @@ function initNetworksActions(store, _router) { } } `, { - networkSlug, - limit, - after, - before, - orderBy, - afterTime: store.getters.after, - beforeTime: store.getters.before, - exclude: store.state.ui.filter, - }); + networkSlug, + limit, + after, + before, + orderBy, + afterTime: store.getters.after, + beforeTime: store.getters.before, + exclude: store.state.ui.filter, + }); - return curateNetwork(network, releases); - } + return curateNetwork(network, releases); + } - async function fetchNetworks({ _commit }) { - const { networks } = await graphql(` + async function fetchNetworks({ _commit }) { + const { networks } = await graphql(` query Networks { networks(orderBy: NAME_ASC) { id @@ -133,13 +133,13 @@ function initNetworksActions(store, _router) { } `); - return networks.map(network => curateNetwork(network)); - } + return networks.map(network => curateNetwork(network)); + } - return { - fetchNetworkBySlug, - fetchNetworks, - }; + return { + fetchNetworkBySlug, + fetchNetworks, + }; } export default initNetworksActions; diff --git a/assets/js/networks/networks.js b/assets/js/networks/networks.js index 44aeaa6c8..92968ef1c 100644 --- a/assets/js/networks/networks.js +++ b/assets/js/networks/networks.js @@ -3,11 +3,11 @@ import mutations from './mutations'; import actions from './actions'; function initNetworksStore(store, router) { - return { - state, - mutations, - actions: actions(store, router), - }; + return { + state, + mutations, + actions: actions(store, router), + }; } export default initNetworksStore; diff --git a/assets/js/releases/actions.js b/assets/js/releases/actions.js index d5a7a8b70..11054a961 100644 --- a/assets/js/releases/actions.js +++ b/assets/js/releases/actions.js @@ -4,10 +4,10 @@ import { curateRelease } from '../curate'; import getDateRange from '../get-date-range'; function initReleasesActions(store, _router) { - async function fetchReleases({ _commit }, { limit = 100, range = 'latest' }) { - const { before, after, orderBy } = getDateRange(range); + async function fetchReleases({ _commit }, { limit = 100, range = 'latest' }) { + const { before, after, orderBy } = getDateRange(range); - const { releases } = await graphql(` + const { releases } = await graphql(` query Releases( $limit:Int = 1000, $after:Date = "1900-01-01", @@ -18,18 +18,18 @@ function initReleasesActions(store, _router) { ${releasesFragment} } `, { - limit, - after, - before, - orderBy, - exclude: store.state.ui.filter, - }); + limit, + after, + before, + orderBy, + exclude: store.state.ui.filter, + }); - return releases.map(release => curateRelease(release)); - } + return releases.map(release => curateRelease(release)); + } - async function searchReleases({ _commit }, { query, limit = 20 }) { - const res = await graphql(` + async function searchReleases({ _commit }, { query, limit = 20 }) { + const res = await graphql(` query SearchReleases( $query: String! $limit: Int = 20 @@ -88,34 +88,34 @@ function initReleasesActions(store, _router) { } } `, { - query, - limit, - }); + query, + limit, + }); - if (!res) return []; + if (!res) return []; - return res.releases.map(release => curateRelease(release)); - } + return res.releases.map(release => curateRelease(release)); + } - async function fetchReleaseById({ _commit }, releaseId) { - // const release = await get(`/releases/${releaseId}`); + async function fetchReleaseById({ _commit }, releaseId) { + // const release = await get(`/releases/${releaseId}`); - const { release } = await graphql(` + const { release } = await graphql(` query Release($releaseId:Int!) { ${releaseFragment} } `, { - releaseId: Number(releaseId), - }); + releaseId: Number(releaseId), + }); - return curateRelease(release); - } + return curateRelease(release); + } - return { - fetchReleases, - fetchReleaseById, - searchReleases, - }; + return { + fetchReleases, + fetchReleaseById, + searchReleases, + }; } export default initReleasesActions; diff --git a/assets/js/releases/mutations.js b/assets/js/releases/mutations.js index d2c2c16a2..ad325269a 100644 --- a/assets/js/releases/mutations.js +++ b/assets/js/releases/mutations.js @@ -1,14 +1,14 @@ import Vue from 'vue'; function setCache(state, { target, releases }) { - Vue.set(state.cache, target, releases); + Vue.set(state.cache, target, releases); } function deleteCache(state, target) { - Vue.delete(state.cache, target); + Vue.delete(state.cache, target); } export default { - setCache, - deleteCache, + setCache, + deleteCache, }; diff --git a/assets/js/releases/releases.js b/assets/js/releases/releases.js index 3d53e4de4..d2875c4b9 100644 --- a/assets/js/releases/releases.js +++ b/assets/js/releases/releases.js @@ -3,11 +3,11 @@ import mutations from './mutations'; import actions from './actions'; function initReleasesStore(store, router) { - return { - state, - mutations, - actions: actions(store, router), - }; + return { + state, + mutations, + actions: actions(store, router), + }; } export default initReleasesStore; diff --git a/assets/js/releases/state.js b/assets/js/releases/state.js index 4b0777750..16c8b052e 100644 --- a/assets/js/releases/state.js +++ b/assets/js/releases/state.js @@ -1,3 +1,3 @@ export default { - cache: {}, + cache: {}, }; diff --git a/assets/js/router.js b/assets/js/router.js index d2f85a3fc..aebece79b 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -16,139 +16,139 @@ import NotFound from '../components/errors/404.vue'; Vue.use(VueRouter); const routes = [ - { - path: '/', - redirect: { - name: 'latest', - }, - }, - { - path: '/home', - redirect: { - name: 'latest', - }, - }, - { - path: '/latest', - component: Home, - name: 'latest', - }, - { - path: '/upcoming', - component: Home, - name: 'upcoming', - }, - { - path: '/new', - component: Home, - name: 'new', - }, - { - path: '/scene/:releaseId/:releaseSlug?', - component: Release, - name: 'scene', - }, - { - path: '/movie/:releaseId/:releaseSlug?', - component: Release, - name: 'movie', - }, - { - path: '/actor/:actorSlug', - name: 'actor', - redirect: from => ({ - name: 'actorRange', - params: { - ...from.params, - range: 'latest', - }, - }), - }, - { - path: '/actor/:actorSlug/:range', - component: Actor, - name: 'actorRange', - }, - { - path: '/site/:siteSlug', - component: Site, - name: 'site', - redirect: from => ({ - name: 'siteRange', - params: { - ...from.params, - range: 'latest', - }, - }), - }, - { - path: '/site/:siteSlug/:range', - component: Site, - name: 'siteRange', - }, - { - path: '/network/:networkSlug', - component: Network, - name: 'network', - redirect: from => ({ - name: 'networkRange', - params: { - ...from.params, - range: 'latest', - }, - }), - }, - { - path: '/network/:networkSlug/:range', - component: Network, - name: 'networkRange', - }, - { - path: '/tag/:tagSlug', - component: Tag, - name: 'tag', - redirect: from => ({ - name: 'tagRange', - params: { - ...from.params, - range: 'latest', - }, - }), - }, - { - path: '/tag/:tagSlug/:range', - component: Tag, - name: 'tagRange', - }, - { - path: '/actors/:gender?/:letter?', - component: Actors, - name: 'actors', - }, - { - path: '/networks', - component: Networks, - name: 'networks', - }, - { - path: '/tags', - component: Tags, - name: 'tags', - }, - { - path: '/search', - component: Search, - name: 'search', - }, - { - path: '*', - component: NotFound, - }, + { + path: '/', + redirect: { + name: 'latest', + }, + }, + { + path: '/home', + redirect: { + name: 'latest', + }, + }, + { + path: '/latest', + component: Home, + name: 'latest', + }, + { + path: '/upcoming', + component: Home, + name: 'upcoming', + }, + { + path: '/new', + component: Home, + name: 'new', + }, + { + path: '/scene/:releaseId/:releaseSlug?', + component: Release, + name: 'scene', + }, + { + path: '/movie/:releaseId/:releaseSlug?', + component: Release, + name: 'movie', + }, + { + path: '/actor/:actorSlug', + name: 'actor', + redirect: from => ({ + name: 'actorRange', + params: { + ...from.params, + range: 'latest', + }, + }), + }, + { + path: '/actor/:actorSlug/:range', + component: Actor, + name: 'actorRange', + }, + { + path: '/site/:siteSlug', + component: Site, + name: 'site', + redirect: from => ({ + name: 'siteRange', + params: { + ...from.params, + range: 'latest', + }, + }), + }, + { + path: '/site/:siteSlug/:range', + component: Site, + name: 'siteRange', + }, + { + path: '/network/:networkSlug', + component: Network, + name: 'network', + redirect: from => ({ + name: 'networkRange', + params: { + ...from.params, + range: 'latest', + }, + }), + }, + { + path: '/network/:networkSlug/:range', + component: Network, + name: 'networkRange', + }, + { + path: '/tag/:tagSlug', + component: Tag, + name: 'tag', + redirect: from => ({ + name: 'tagRange', + params: { + ...from.params, + range: 'latest', + }, + }), + }, + { + path: '/tag/:tagSlug/:range', + component: Tag, + name: 'tagRange', + }, + { + path: '/actors/:gender?/:letter?', + component: Actors, + name: 'actors', + }, + { + path: '/networks', + component: Networks, + name: 'networks', + }, + { + path: '/tags', + component: Tags, + name: 'tags', + }, + { + path: '/search', + component: Search, + name: 'search', + }, + { + path: '*', + component: NotFound, + }, ]; const router = new VueRouter({ - mode: 'history', - routes, + mode: 'history', + routes, }); export default router; diff --git a/assets/js/sites/actions.js b/assets/js/sites/actions.js index 619680d98..4ae530a8a 100644 --- a/assets/js/sites/actions.js +++ b/assets/js/sites/actions.js @@ -4,10 +4,10 @@ import { curateSite } from '../curate'; import getDateRange from '../get-date-range'; function initSitesActions(store, _router) { - async function fetchSiteBySlug({ _commit }, { siteSlug, limit = 100, range = 'latest' }) { - const { before, after, orderBy } = getDateRange(range); + async function fetchSiteBySlug({ _commit }, { siteSlug, limit = 100, range = 'latest' }) { + const { before, after, orderBy } = getDateRange(range); - const { site } = await graphql(` + const { site } = await graphql(` query Site( $siteSlug: String!, $limit:Int = 100, @@ -37,20 +37,20 @@ function initSitesActions(store, _router) { } } `, { - siteSlug, - limit, - after, - before, - orderBy, - isNew: store.getters.isNew, - exclude: store.state.ui.filter, - }); + siteSlug, + limit, + after, + before, + orderBy, + isNew: store.getters.isNew, + exclude: store.state.ui.filter, + }); - return curateSite(site); - } + return curateSite(site); + } - async function fetchSites({ _commit }, { limit = 100 }) { - const { sites } = await graphql(` + async function fetchSites({ _commit }, { limit = 100 }) { + const { sites } = await graphql(` query Sites( $actorSlug: String! $limit:Int = 100, @@ -64,16 +64,16 @@ function initSitesActions(store, _router) { } } `, { - limit, - after: store.getters.after, - before: store.getters.before, - }); + limit, + after: store.getters.after, + before: store.getters.before, + }); - return sites; - } + return sites; + } - async function searchSites({ _commit }, { query, limit = 20 }) { - const { sites } = await graphql(` + async function searchSites({ _commit }, { query, limit = 20 }) { + const { sites } = await graphql(` query SearchSites( $query: String! $limit:Int = 20, @@ -93,18 +93,18 @@ function initSitesActions(store, _router) { } } `, { - query, - limit, - }); + query, + limit, + }); - return sites; - } + return sites; + } - return { - fetchSiteBySlug, - fetchSites, - searchSites, - }; + return { + fetchSiteBySlug, + fetchSites, + searchSites, + }; } export default initSitesActions; diff --git a/assets/js/sites/sites.js b/assets/js/sites/sites.js index 81cc00284..c29c90017 100644 --- a/assets/js/sites/sites.js +++ b/assets/js/sites/sites.js @@ -3,11 +3,11 @@ import mutations from './mutations'; import actions from './actions'; function initSitesStore(store, router) { - return { - state, - mutations, - actions: actions(store, router), - }; + return { + state, + mutations, + actions: actions(store, router), + }; } export default initSitesStore; diff --git a/assets/js/store.js b/assets/js/store.js index dce594c23..663fae277 100644 --- a/assets/js/store.js +++ b/assets/js/store.js @@ -10,19 +10,19 @@ import initActorsStore from './actors/actors'; import initTagsStore from './tags/tags'; function initStore(router) { - Vue.use(Vuex); + Vue.use(Vuex); - const store = new Vuex.Store(); + const store = new Vuex.Store(); - store.registerModule('ui', initUiStore(store, router)); - store.registerModule('auth', initAuthStore(store, router)); - store.registerModule('releases', initReleasesStore(store, router)); - store.registerModule('actors', initActorsStore(store, router)); - store.registerModule('sites', initSitesStore(store, router)); - store.registerModule('networks', initNetworksStore(store, router)); - store.registerModule('tags', initTagsStore(store, router)); + store.registerModule('ui', initUiStore(store, router)); + store.registerModule('auth', initAuthStore(store, router)); + store.registerModule('releases', initReleasesStore(store, router)); + store.registerModule('actors', initActorsStore(store, router)); + store.registerModule('sites', initSitesStore(store, router)); + store.registerModule('networks', initNetworksStore(store, router)); + store.registerModule('tags', initTagsStore(store, router)); - return store; + return store; } export default initStore; diff --git a/assets/js/tags/actions.js b/assets/js/tags/actions.js index f86bf2353..8a435907d 100644 --- a/assets/js/tags/actions.js +++ b/assets/js/tags/actions.js @@ -1,15 +1,15 @@ import { graphql, get } from '../api'; import { - releaseFields, + releaseFields, } from '../fragments'; import { curateTag } from '../curate'; import getDateRange from '../get-date-range'; function initTagsActions(store, _router) { - async function fetchTagBySlug({ _commit }, { tagSlug, limit = 100, range = 'latest' }) { - const { before, after, orderBy } = getDateRange(range); + async function fetchTagBySlug({ _commit }, { tagSlug, limit = 100, range = 'latest' }) { + const { before, after, orderBy } = getDateRange(range); - const { tagBySlug } = await graphql(` + const { tagBySlug } = await graphql(` query Tag( $tagSlug:String! $limit:Int = 1000, @@ -85,24 +85,24 @@ function initTagsActions(store, _router) { } } `, { - tagSlug, - limit, - after, - before, - orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC', - exclude: store.state.ui.filter, - }); + tagSlug, + limit, + after, + before, + orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC', + exclude: store.state.ui.filter, + }); - return curateTag(tagBySlug, store); - } + return curateTag(tagBySlug, store); + } - async function fetchTags({ _commit }, { - limit = 100, - slugs = [], - _group, - _priority, - }) { - const { tags } = await graphql(` + async function fetchTags({ _commit }, { + limit = 100, + slugs = [], + _group, + _priority, + }) { + const { tags } = await graphql(` query Tags( $slugs: [String!] = [], $limit: Int = 100 @@ -133,28 +133,28 @@ function initTagsActions(store, _router) { } } `, { - slugs, - limit, - }); + slugs, + limit, + }); - return tags.map(tag => curateTag(tag, store.state.ui.sfw)); - } + return tags.map(tag => curateTag(tag, store.state.ui.sfw)); + } - async function fetchTagReleases({ _commit }, tagId) { - const releases = await get(`/tags/${tagId}/releases`, { - filter: store.state.ui.filter, - after: store.getters.after, - before: store.getters.before, - }); + async function fetchTagReleases({ _commit }, tagId) { + const releases = await get(`/tags/${tagId}/releases`, { + filter: store.state.ui.filter, + after: store.getters.after, + before: store.getters.before, + }); - return releases; - } + return releases; + } - return { - fetchTagBySlug, - fetchTags, - fetchTagReleases, - }; + return { + fetchTagBySlug, + fetchTags, + fetchTagReleases, + }; } export default initTagsActions; diff --git a/assets/js/tags/tags.js b/assets/js/tags/tags.js index 47b5b20f1..92a56f8d2 100644 --- a/assets/js/tags/tags.js +++ b/assets/js/tags/tags.js @@ -3,11 +3,11 @@ import mutations from './mutations'; import actions from './actions'; function initTagsStore(store, router) { - return { - state, - mutations, - actions: actions(store, router), - }; + return { + state, + mutations, + actions: actions(store, router), + }; } export default initTagsStore; diff --git a/assets/js/ui/actions.js b/assets/js/ui/actions.js index 6481ddd59..a53d2b3a3 100644 --- a/assets/js/ui/actions.js +++ b/assets/js/ui/actions.js @@ -1,35 +1,35 @@ function initUiActions(_store, _router) { - function setFilter({ commit }, filter) { - commit('setFilter', filter); - localStorage.setItem('filter', filter); - } + function setFilter({ commit }, filter) { + commit('setFilter', filter); + localStorage.setItem('filter', filter); + } - function setRange({ commit }, range) { - commit('setRange', range); - } + function setRange({ commit }, range) { + commit('setRange', range); + } - function setBatch({ commit }, batch) { - commit('setBatch', batch); - localStorage.setItem('batch', batch); - } + function setBatch({ commit }, batch) { + commit('setBatch', batch); + localStorage.setItem('batch', batch); + } - function setTheme({ commit }, theme) { - commit('setTheme', theme); - localStorage.setItem('theme', theme); - } + function setTheme({ commit }, theme) { + commit('setTheme', theme); + localStorage.setItem('theme', theme); + } - async function setSfw({ commit }, sfw) { - commit('setSfw', sfw); - localStorage.setItem('sfw', sfw); - } + async function setSfw({ commit }, sfw) { + commit('setSfw', sfw); + localStorage.setItem('sfw', sfw); + } - return { - setFilter, - setRange, - setBatch, - setSfw, - setTheme, - }; + return { + setFilter, + setRange, + setBatch, + setSfw, + setTheme, + }; } export default initUiActions; diff --git a/assets/js/ui/getters.js b/assets/js/ui/getters.js index 9b6a96112..9352d136f 100644 --- a/assets/js/ui/getters.js +++ b/assets/js/ui/getters.js @@ -1,47 +1,47 @@ import dayjs from 'dayjs'; const dateRanges = { - latest: () => ({ - after: '1900-01-01', - before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'), - orderBy: 'DATE_DESC', - }), - upcoming: () => ({ - after: dayjs(new Date()).format('YYYY-MM-DD'), - before: '2100-01-01', - orderBy: 'DATE_ASC', - }), - new: () => ({ - after: '1900-01-01', - before: '2100-01-01', - orderBy: 'CREATED_AT_DESC', - }), - all: () => ({ - after: '1900-01-01', - before: '2100-01-01', - orderBy: 'DATE_DESC', - }), + latest: () => ({ + after: '1900-01-01', + before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'), + orderBy: 'DATE_DESC', + }), + upcoming: () => ({ + after: dayjs(new Date()).format('YYYY-MM-DD'), + before: '2100-01-01', + orderBy: 'DATE_ASC', + }), + new: () => ({ + after: '1900-01-01', + before: '2100-01-01', + orderBy: 'CREATED_AT_DESC', + }), + all: () => ({ + after: '1900-01-01', + before: '2100-01-01', + orderBy: 'DATE_DESC', + }), }; function rangeDates(state) { - return dateRanges[state.range](); + return dateRanges[state.range](); } function before(state) { - return dateRanges[state.range]().before; + return dateRanges[state.range]().before; } function after(state) { - return dateRanges[state.range]().after; + return dateRanges[state.range]().after; } function orderBy(state) { - return dateRanges[state.range]().orderBy; + return dateRanges[state.range]().orderBy; } export default { - rangeDates, - before, - after, - orderBy, + rangeDates, + before, + after, + orderBy, }; diff --git a/assets/js/ui/mutations.js b/assets/js/ui/mutations.js index 85ed471a0..728955cef 100644 --- a/assets/js/ui/mutations.js +++ b/assets/js/ui/mutations.js @@ -1,27 +1,27 @@ function setFilter(state, filter) { - state.filter = filter; + state.filter = filter; } function setRange(state, range) { - state.range = range; + state.range = range; } function setBatch(state, batch) { - state.batch = batch; + state.batch = batch; } function setSfw(state, sfw) { - state.sfw = sfw; + state.sfw = sfw; } function setTheme(state, theme) { - state.theme = theme; + state.theme = theme; } export default { - setFilter, - setRange, - setBatch, - setSfw, - setTheme, + setFilter, + setRange, + setBatch, + setSfw, + setTheme, }; diff --git a/assets/js/ui/observers.js b/assets/js/ui/observers.js index 7f4cf96f0..1094c2702 100644 --- a/assets/js/ui/observers.js +++ b/assets/js/ui/observers.js @@ -1,25 +1,25 @@ function initUiObservers(store, _router) { - document.addEventListener('keypress', (event) => { - if (event.target.tagName === 'INPUT') { - return; - } + document.addEventListener('keypress', (event) => { + if (event.target.tagName === 'INPUT') { + return; + } - if (event.key === 's') { - store.dispatch('setSfw', true); - } + if (event.key === 's') { + store.dispatch('setSfw', true); + } - if (event.key === 'n') { - store.dispatch('setSfw', false); - } + if (event.key === 'n') { + store.dispatch('setSfw', false); + } - if (event.key === 'd') { - store.dispatch('setTheme', 'dark'); - } + if (event.key === 'd') { + store.dispatch('setTheme', 'dark'); + } - if (event.key === 'l') { - store.dispatch('setTheme', 'light'); - } - }); + if (event.key === 'l') { + store.dispatch('setTheme', 'light'); + } + }); } export default initUiObservers; diff --git a/assets/js/ui/state.js b/assets/js/ui/state.js index 0ade8df61..ba91e6c83 100644 --- a/assets/js/ui/state.js +++ b/assets/js/ui/state.js @@ -4,9 +4,9 @@ const storedSfw = localStorage.getItem('sfw'); const storedTheme = localStorage.getItem('theme'); export default { - filter: storedFilter ? storedFilter.split(',') : ['gay', 'transsexual'], - range: 'latest', - batch: storedBatch || 'all', - sfw: storedSfw === 'true' || false, - theme: storedTheme || 'light', + filter: storedFilter ? storedFilter.split(',') : ['gay', 'transsexual'], + range: 'latest', + batch: storedBatch || 'all', + sfw: storedSfw === 'true' || false, + theme: storedTheme || 'light', }; diff --git a/assets/js/ui/ui.js b/assets/js/ui/ui.js index 35b0e5ef0..f569a9a72 100644 --- a/assets/js/ui/ui.js +++ b/assets/js/ui/ui.js @@ -4,12 +4,12 @@ import getters from './getters'; import actions from './actions'; function initUiStore(store, router) { - return { - state, - mutations, - getters, - actions: actions(store, router), - }; + return { + state, + mutations, + getters, + actions: actions(store, router), + }; } export default initUiStore; diff --git a/config/default.js b/config/default.js index 80c905106..03f0f3d38 100644 --- a/config/default.js +++ b/config/default.js @@ -1,177 +1,181 @@ module.exports = { - database: { - host: '127.0.0.1', - user: 'user', - password: 'password', - database: 'traxxx', - }, - web: { - host: '0.0.0.0', - port: 5000, - sfwHost: '0.0.0.0', - sfwPort: 5001, - }, - // include: [], - // exclude: [], - exclude: [ - ['21sextreme', [ - // no longer updated - 'mightymistress', - 'dominatedgirls', - 'homepornreality', - 'peeandblow', - 'cummingmatures', - 'mandyiskinky', - 'speculumplays', - 'creampiereality', - ]], - ['aziani', [ - 'amberathome', - 'marycarey', - 'racqueldevonshire', - ]], - ['blowpass', ['sunlustxxx']], - ['ddfnetwork', [ - 'fuckinhd', - 'bustylover', - ]], - ['famedigital', [ - 'daringsex', - 'lowartfilms', - ]], - ['pornpros', [ - 'milfhumiliation', - 'humiliated', - 'flexiblepositions', - 'publicviolations', - 'amateurviolations', - 'squirtdisgrace', - 'cumdisgrace', - 'webcamhackers', - 'collegeteens', - ]], - ['score', [ - 'bigboobbundle', - 'milfbundle', - 'pornmegaload', - 'scorelandtv', - 'scoretv', - ]], - ], - profiles: [ - [ - 'evilangel', - 'famedigital', - ], - [ - // Gamma; Evil Angel + Devil's Film, Pure Taboo (unavailable), Burning Angel and Wicked have their own assets - 'xempire', - 'blowpass', - ], - [ - // MindGeek; Brazzers and Mile High Media have their own assets - 'realitykings', - 'mofos', - 'digitalplayground', - 'twistys', - 'babes', - 'fakehub', - 'sexyhub', - 'metrohd', - 'iconmale', - 'men', - 'transangels', - ], - 'wicked', - 'burningangel', - 'brazzers', - 'milehighmedia', - [ - 'vixen', - 'tushy', - 'blacked', - 'tushyraw', - 'blackedraw', - 'deeper', - ], - [ - // Nubiles - 'nubiles', - 'nubilesporn', - 'deeplush', - 'brattysis', - 'nfbusty', - 'anilos', - 'hotcrazymess', - 'thatsitcomshow', - ], - '21sextury', - 'julesjordan', - 'naughtyamerica', - 'cherrypimps', - 'pimpxxx', - [ - 'hussiepass', - 'hushpass', - 'interracialpass', - 'interracialpovs', - 'povpornstars', - 'seehimfuck', - 'eyeontheguy', - ], - [ - // Full Porn Network - 'analized', - 'hergape', - 'jamesdeen', - 'dtfsluts', - 'analbbc', - 'analviolation', - 'baddaddypov', - 'girlfaction', - 'homemadeanalwhores', - 'mugfucked', - 'onlyprince', - 'pervertgallery', - 'povperverts', - ], - 'private', - 'ddfnetwork', - 'bangbros', - 'kellymadison', - 'gangbangcreampie', - 'gloryholesecrets', - 'aziani', - 'legalporno', - 'score', - 'boobpedia', - 'pornhub', - 'freeones', - 'freeonesLegacy', - ], - proxy: { - enable: false, - host: '', - port: 8888, - hostnames: [ - 'www.vixen.com', - 'www.blacked.com', - 'www.blackedraw.com', - 'www.tushy.com', - 'www.tushyraw.com', - 'www.deeper.com', - ], - }, - fetchAfter: [1, 'week'], - nullDateLimit: 3, - media: { - path: './media', - thumbnailSize: 320, // width for 16:9 will be exactly 576px - thumbnailQuality: 100, - lazySize: 90, - lazyQuality: 90, - videoQuality: [480, 360, 320, 540, 720, 1080, 2160, 270, 240, 180], - limit: 25, // max number of photos per release - }, - titleSlugLength: 50, + database: { + host: '127.0.0.1', + user: 'user', + password: 'password', + database: 'traxxx', + }, + web: { + host: '0.0.0.0', + port: 5000, + sfwHost: '0.0.0.0', + sfwPort: 5001, + }, + // include: [], + // exclude: [], + exclude: [ + ['21sextreme', [ + // no longer updated + 'mightymistress', + 'dominatedgirls', + 'homepornreality', + 'peeandblow', + 'cummingmatures', + 'mandyiskinky', + 'speculumplays', + 'creampiereality', + ]], + ['aziani', [ + 'amberathome', + 'marycarey', + 'racqueldevonshire', + ]], + 'boobpedia', + ['blowpass', ['sunlustxxx']], + ['ddfnetwork', [ + 'fuckinhd', + 'bustylover', + ]], + ['famedigital', [ + 'daringsex', + 'lowartfilms', + ]], + 'freeones', + ['pornpros', [ + 'milfhumiliation', + 'humiliated', + 'flexiblepositions', + 'publicviolations', + 'amateurviolations', + 'squirtdisgrace', + 'cumdisgrace', + 'webcamhackers', + 'collegeteens', + ]], + ['score', [ + 'bigboobbundle', + 'milfbundle', + 'pornmegaload', + 'scorelandtv', + 'scoretv', + ]], + ['mindgeek', [ + 'pornhub', + ]], + ], + profiles: [ + [ + 'evilangel', + 'famedigital', + ], + [ + // Gamma; Evil Angel + Devil's Film, Pure Taboo (unavailable), Burning Angel and Wicked have their own assets + 'xempire', + 'blowpass', + ], + [ + // MindGeek; Brazzers and Mile High Media have their own assets + 'realitykings', + 'mofos', + 'digitalplayground', + 'twistys', + 'babes', + 'fakehub', + 'sexyhub', + 'metrohd', + 'iconmale', + 'men', + 'transangels', + ], + 'wicked', + 'burningangel', + 'brazzers', + 'milehighmedia', + [ + 'vixen', + 'tushy', + 'blacked', + 'tushyraw', + 'blackedraw', + 'deeper', + ], + [ + // Nubiles + 'nubiles', + 'nubilesporn', + 'deeplush', + 'brattysis', + 'nfbusty', + 'anilos', + 'hotcrazymess', + 'thatsitcomshow', + ], + '21sextury', + 'julesjordan', + 'naughtyamerica', + 'cherrypimps', + 'pimpxxx', + [ + 'hussiepass', + 'hushpass', + 'interracialpass', + 'interracialpovs', + 'povpornstars', + 'seehimfuck', + 'eyeontheguy', + ], + [ + // Full Porn Network + 'analized', + 'hergape', + 'jamesdeen', + 'dtfsluts', + 'analbbc', + 'analviolation', + 'baddaddypov', + 'girlfaction', + 'homemadeanalwhores', + 'mugfucked', + 'onlyprince', + 'pervertgallery', + 'povperverts', + ], + 'private', + 'ddfnetwork', + 'bangbros', + 'kellymadison', + 'gangbangcreampie', + 'gloryholesecrets', + 'aziani', + 'legalporno', + 'score', + 'boobpedia', + 'pornhub', + 'freeones', + ], + proxy: { + enable: false, + host: '', + port: 8888, + hostnames: [ + 'www.vixen.com', + 'www.blacked.com', + 'www.blackedraw.com', + 'www.tushy.com', + 'www.tushyraw.com', + 'www.deeper.com', + ], + }, + fetchAfter: [1, 'week'], + nullDateLimit: 3, + media: { + path: './media', + thumbnailSize: 320, // width for 16:9 will be exactly 576px + thumbnailQuality: 100, + lazySize: 90, + lazyQuality: 90, + videoQuality: [480, 360, 320, 540, 720, 1080, 2160, 270, 240, 180], + limit: 25, // max number of photos per release + }, + titleSlugLength: 50, }; diff --git a/migrations/20190325001339_releases.js b/migrations/20190325001339_releases.js index 7801b468d..043c8adde 100644 --- a/migrations/20190325001339_releases.js +++ b/migrations/20190325001339_releases.js @@ -1,758 +1,773 @@ exports.up = knex => Promise.resolve() - .then(() => knex.schema.createTable('countries', (table) => { - table.string('alpha2', 2) - .unique() - .primary(); + .then(() => knex.schema.createTable('countries', (table) => { + table.string('alpha2', 2) + .unique() + .primary(); - table.string('alpha3', 3) - .unique(); + table.string('alpha3', 3) + .unique(); - table.string('name') - .notNullable(); + table.string('name') + .notNullable(); - table.string('alias'); + table.string('alias'); - table.integer('code', 3); - table.string('nationality'); - table.integer('priority', 2) - .defaultTo(0); - })) - .then(() => knex.schema.createTable('media', (table) => { - table.string('id', 21) - .primary(); + table.integer('code', 3); + table.string('nationality'); + table.integer('priority', 2) + .defaultTo(0); + })) + .then(() => knex.schema.createTable('media', (table) => { + table.string('id', 21) + .primary(); - table.string('path'); - table.string('thumbnail'); - table.string('lazy'); - table.integer('index'); - table.string('mime'); + table.string('path'); + table.string('thumbnail'); + table.string('lazy'); + table.integer('index'); + table.string('mime'); - table.string('hash'); + table.string('hash'); - table.integer('size', 12); - table.integer('quality', 6); - table.integer('width', 6); - table.integer('height', 6); - table.float('entropy'); + table.integer('size', 12); + table.integer('quality', 6); + table.integer('width', 6); + table.integer('height', 6); + table.float('entropy'); - table.string('scraper', 32); - table.string('copyright', 100); + table.string('scraper', 32); + table.string('copyright', 100); - table.string('source', 2100); - table.string('source_page', 2100); + table.string('source', 2100); + table.string('source_page', 2100); - table.text('comment'); - table.string('group'); + table.text('comment'); + table.string('group'); - table.unique('hash'); - table.unique('source'); + table.unique('hash'); + table.unique('source'); - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('media_sfw', (table) => { - table.string('id', 21) - .primary(); + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('media_sfw', (table) => { + table.string('id', 21) + .primary(); - table.string('media_id', 21) - .references('id') - .inTable('media') - .unique(); - })) - .then(() => knex.raw(` + table.string('media_id', 21) + .references('id') + .inTable('media') + .unique(); + })) + .then(() => knex.raw(` CREATE FUNCTION get_random_sfw_media_id() RETURNS varchar AS $$ SELECT media_id FROM media_sfw ORDER BY random() LIMIT 1; $$ LANGUAGE sql STABLE; `)) - .then(() => knex.schema.alterTable('media', (table) => { - table.string('sfw_media_id', 21) - .references('id') - .inTable('media') - .defaultTo(knex.raw('get_random_sfw_media_id()')); - })) - .then(() => knex.schema.createTable('tags_groups', (table) => { - table.increments('id', 12); - - table.string('name', 32); - table.text('description'); - - table.string('slug', 32) - .unique(); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('tags', (table) => { - table.increments('id', 12); - table.string('name'); - - table.text('description'); - - table.integer('priority', 2) - .defaultTo(0); - - table.boolean('secondary') - .defaultTo(false); - - table.integer('group_id', 12) - .references('id') - .inTable('tags_groups'); - - table.integer('alias_for', 12) - .references('id') - .inTable('tags'); - - table.string('slug', 32) - .unique(); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('tags_posters', (table) => { - table.integer('tag_id', 12) - .notNullable() - .references('id') - .inTable('tags'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique('tag_id'); - })) - .then(() => knex.schema.createTable('tags_photos', (table) => { - table.integer('tag_id', 12) - .notNullable() - .references('id') - .inTable('tags'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['tag_id', 'media_id']); - })) - .then(() => knex.schema.createTable('networks', (table) => { - table.increments('id', 12); - - table.string('name'); - table.string('url'); - table.text('description'); - table.json('parameters'); - - table.integer('parent_id', 12) - .references('id') - .inTable('networks'); - - table.string('slug', 32) - .unique(); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('networks_social', (table) => { - table.increments('id', 16); - - table.string('url'); - table.string('platform'); - - table.integer('network_id', 12) - .notNullable() - .references('id') - .inTable('networks'); - - table.unique(['url', 'network_id']); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('sites', (table) => { - table.increments('id', 12); - - table.integer('network_id', 12) - .notNullable() - .references('id') - .inTable('networks'); - - table.string('name'); - table.string('slug', 32) - .unique(); - - table.string('alias'); - - table.string('url'); - table.text('description'); - table.json('parameters'); - - table.integer('priority', 3) - .defaultTo(0); - - table.boolean('show') - .defaultTo(true); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('sites_tags', (table) => { - table.integer('tag_id', 12) - .notNullable() - .references('id') - .inTable('tags'); - - table.integer('site_id', 12) - .notNullable() - .references('id') - .inTable('sites'); - - table.boolean('inherit') - .defaultTo(false); - - table.unique(['tag_id', 'site_id']); - })) - .then(() => knex.schema.createTable('sites_social', (table) => { - table.increments('id', 16); - - table.string('url'); - table.string('platform'); - - table.integer('site_id', 12) - .notNullable() - .references('id') - .inTable('sites'); - - table.unique(['url', 'site_id']); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('studios', (table) => { - table.increments('id', 12); - - table.integer('network_id', 12) - .notNullable() - .references('id') - .inTable('networks'); - - table.string('name'); - table.string('url'); - table.text('description'); - - table.string('slug', 32) - .unique(); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('actors', (table) => { - table.increments('id', 12); - - table.string('name') - .notNullable(); - - table.string('slug', 32); - - table.integer('network_id', 12) - .references('id') - .inTable('networks'); - - table.unique(['slug', 'network_id']); - - table.integer('alias_for', 12) - .references('id') - .inTable('actors'); - - table.date('birthdate'); - table.string('gender', 18); - table.text('description'); - - table.string('birth_city'); - table.string('birth_state'); - table.string('birth_country_alpha2', 2) - .references('alpha2') - .inTable('countries'); - - table.string('residence_city'); - table.string('residence_state'); - table.string('residence_country_alpha2', 2) - .references('alpha2') - .inTable('countries'); - - table.string('ethnicity'); - - table.string('bust', 10); - table.integer('waist', 3); - table.integer('hip', 3); - table.boolean('natural_boobs'); - - table.integer('height', 3); - table.integer('weight', 3); - table.string('eyes'); - table.string('hair'); - - table.boolean('has_tattoos'); - table.boolean('has_piercings'); - table.string('piercings'); - table.string('tattoos'); - - table.integer('batch_id', 12); - - table.datetime('updated_at') - .defaultTo(knex.fn.now()); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('actors_profiles', (table) => { - table.increments('id', 12); - - table.integer('actor_id', 12) - .references('id') - .inTable('actors'); - - table.integer('network_id', 12) - .references('id') - .inTable('networks'); - - table.date('birthdate'); - table.string('gender', 18); - table.text('description'); - - table.string('birth_city'); - table.string('birth_state'); - table.string('birth_country_alpha2', 2) - .references('alpha2') - .inTable('countries'); - - table.string('residence_city'); - table.string('residence_state'); - table.string('residence_country_alpha2', 2) - .references('alpha2') - .inTable('countries'); - - table.string('ethnicity'); - - table.string('bust', 10); - table.integer('waist', 3); - table.integer('hip', 3); - table.boolean('natural_boobs'); - - table.integer('height', 3); - table.integer('weight', 3); - table.string('eyes'); - table.string('hair'); - - table.boolean('has_tattoos'); - table.boolean('has_piercings'); - table.string('piercings'); - table.string('tattoos'); - - table.datetime('scraped_at'); - table.boolean('scrape_success'); - - table.datetime('updated_at') - .defaultTo(knex.fn.now()); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('body', (table) => { - table.string('slug', 20) - .primary(); - - table.string('name'); - })) - .then(() => knex('body').insert([ - // head - { slug: 'head', name: 'head' }, - { slug: 'face', name: 'face' }, - { slug: 'scalp', name: 'scalp' }, - { slug: 'forehead', name: 'forehead' }, - { slug: 'temple', name: 'temple' }, - { slug: 'cheek', name: 'cheek' }, - { slug: 'jaw', name: 'jaw' }, - { slug: 'chin', name: 'chin' }, - { slug: 'neck', name: 'neck' }, - { slug: 'throat', name: 'throat' }, - // eyes - { slug: 'eyelid', name: 'eyelid' }, - { slug: 'eyeball', name: 'eyeball' }, - { slug: 'eyebrow', name: 'eyebrow' }, - // mouth - { slug: 'tongue', name: 'tongue' }, - { slug: 'lip', name: 'lip' }, - { slug: 'upper-lip', name: 'upper lip' }, - { slug: 'lower-lip', name: 'lower lip' }, - { slug: 'inner-lip', name: 'inner lip' }, - { slug: 'inner-lower-lip', name: 'inner lower lip' }, - { slug: 'inner-upper-lip', name: 'inner upper lip' }, - { slug: 'philtrum', name: 'philtrum' }, - { slug: 'above-lip', name: 'above lip' }, - { slug: 'below-lip', name: 'below lip' }, - // nose - { slug: 'nose', name: 'nose' }, - { slug: 'third-eye', name: 'third eye' }, - { slug: 'bridge', name: 'bridge' }, - { slug: 'nostril', name: 'nostril' }, - { slug: 'septum', name: 'septum' }, - { slug: 'septril', name: 'septril' }, - // ear - { slug: 'ear', name: 'ear' }, - { slug: 'earlobe', name: 'earlobe' }, - { slug: 'helix', name: 'helix' }, - { slug: 'tragus', name: 'tragus' }, - { slug: 'conch', name: 'conch' }, - { slug: 'rook', name: 'rook' }, - { slug: 'behind-ear', name: 'behind ear' }, - // arms - { slug: 'arm', name: 'arm' }, - { slug: 'upper-arm', name: 'upper arm' }, - { slug: 'forearm', name: 'forearm' }, - { slug: 'elbow', name: 'elbow' }, - { slug: 'inner-elbow', name: 'inner elbow' }, - { slug: 'outer-elbow', name: 'outer elbow' }, - // hands - { slug: 'hand', name: 'hand' }, - { slug: 'fingers', name: 'fingers' }, - { slug: 'knuckles', name: 'knuckles' }, - { slug: 'thumb', name: 'thumb' }, - { slug: 'index-finger', name: 'index finger' }, - { slug: 'middle-finger', name: 'middle finger' }, - { slug: 'ring-finger', name: 'ring finger' }, - { slug: 'pinky', name: 'pinky' }, - { slug: 'back-of-hand', name: 'back of hand' }, - { slug: 'inner-wrist', name: 'inner wrist' }, - { slug: 'outer-wrist', name: 'outer wrist' }, - // torso - { slug: 'shoulder', name: 'shoulder' }, - { slug: 'collarbone', name: 'collarbone' }, - { slug: 'chest', name: 'chest' }, - { slug: 'rib-cage', name: 'rib cage' }, - { slug: 'breastbone', name: 'breastbone' }, - { slug: 'underboob', name: 'underboob' }, - { slug: 'sideboob', name: 'sideboob' }, - { slug: 'boob', name: 'boob' }, - { slug: 'nipple', name: 'nipple' }, - { slug: 'abdomen', name: 'abdomen' }, - { slug: 'navel', name: 'navel' }, - { slug: 'pelvis', name: 'pelvis' }, - // back - { slug: 'back', name: 'back' }, - { slug: 'upper-back', name: 'upper back' }, - { slug: 'middle-back', name: 'lower back' }, - { slug: 'lower-back', name: 'lower back' }, - { slug: 'spine', name: 'spine' }, - // bottom - { slug: 'butt', name: 'butt' }, - { slug: 'hip', name: 'hip' }, - { slug: 'anus', name: 'anus' }, - // genitals - { slug: 'pubic-mound', name: 'pubic mound' }, - { slug: 'vagina', name: 'vagina' }, - { slug: 'outer-labia', name: 'outer labia' }, - { slug: 'inner-labia', name: 'inner labia' }, - { slug: 'clitoris', name: 'clitoris' }, - { slug: 'penis', name: 'penis' }, - { slug: 'glans', name: 'glans' }, - { slug: 'foreskin', name: 'foreskin' }, - { slug: 'shaft', name: 'shaft' }, - { slug: 'scrotum', name: 'scrotum' }, - // legs - { slug: 'leg', name: 'leg' }, - { slug: 'groin', name: 'groin' }, - { slug: 'upper-leg', name: 'upper leg' }, - { slug: 'thigh', name: 'thigh' }, - { slug: 'lower-leg', name: 'lower leg' }, - { slug: 'shin', name: 'shin' }, - { slug: 'calf', name: 'calf' }, - { slug: 'knee', name: 'knee' }, - { slug: 'inner-knee', name: 'inner knee' }, - // feet - { slug: 'inner-ankle', name: 'inner ankle' }, - { slug: 'outer-ankle', name: 'outer ankle' }, - { slug: 'foot', name: 'foot' }, - { slug: 'toes', name: 'toes' }, - { slug: 'big-toe', name: 'big toe' }, - { slug: 'index-toe', name: 'index toe' }, - { slug: 'middle-toe', name: 'middle toe' }, - { slug: 'fourth-toe', name: 'fourth toe' }, - { slug: 'little-toe', name: 'little toe' }, - ])) - .then(() => knex.schema.createTable('actors_tattoos', (table) => { - table.increments('id'); - - table.integer('actor_id', 12) - .notNullable() - .references('id') - .inTable('actors'); - - table.string('body_slug', 20) - .references('slug') - .inTable('body'); - - table.enum('side', ['left', 'right', 'center', 'both']); - - table.string('description'); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('actors_piercings', (table) => { - table.increments('id'); - - table.integer('actor_id', 12) - .notNullable() - .references('id') - .inTable('actors'); - - table.string('body_slug', 20) - .references('slug') - .inTable('body'); - - table.enum('side', ['left', 'right', 'center', 'both']); - - table.string('description'); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('actors_avatars', (table) => { - table.integer('profile_id', 12) - .notNullable() - .references('id') - .inTable('actors_profiles'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique('profile_id'); - })) - .then(() => knex.schema.createTable('actors_photos', (table) => { - table.integer('actor_id', 12) - .notNullable() - .references('id') - .inTable('actors'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['actor_id', 'media_id']); - })) - .then(() => knex.schema.createTable('actors_social', (table) => { - table.increments('id', 16); - - table.string('url'); - table.string('platform'); - - table.integer('actor_id', 12) - .notNullable() - .references('id') - .inTable('actors'); - - table.unique(['url', 'actor_id']); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('directors', (table) => { - table.increments('id', 12); - - table.string('name'); - table.integer('alias_for', 12) - .references('id') - .inTable('directors'); - - table.string('slug', 32) - .unique(); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('batches', (table) => { - table.increments('id', 12); - table.text('comment'); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('releases', (table) => { - table.increments('id', 16); - - table.integer('site_id', 12) - .notNullable() - .references('id') - .inTable('sites'); - - table.integer('studio_id', 12) - .references('id') - .inTable('studios'); - - table.string('type', 10) - .defaultTo('scene'); - - table.string('shoot_id'); - table.string('entry_id'); - table.unique(['site_id', 'entry_id', 'type']); - - table.string('url', 1000); - table.string('title'); - table.string('slug'); - table.date('date'); - table.text('description'); - - table.integer('duration') - .unsigned(); - - table.boolean('deep'); - table.string('deep_url', 1000); - - table.integer('created_batch_id', 12) - .references('id') - .inTable('batches'); - - table.integer('updated_batch_id', 12) - .references('id') - .inTable('batches'); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('releases_actors', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.integer('actor_id', 12) - .notNullable() - .references('id') - .inTable('actors'); - - table.unique(['release_id', 'actor_id']); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('releases_movies', (table) => { - table.integer('movie_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.integer('scene_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.unique(['movie_id', 'scene_id']); - - table.datetime('created_at') - .defaultTo(knex.fn.now()); - })) - .then(() => knex.schema.createTable('releases_directors', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.integer('director_id', 8) - .notNullable() - .references('id') - .inTable('directors'); - - table.unique(['release_id', 'director_id']); - })) - .then(() => knex.schema.createTable('releases_posters', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique('release_id'); - })) - .then(() => knex.schema.createTable('releases_covers', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['release_id', 'media_id']); - })) - .then(() => knex.schema.createTable('releases_trailers', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique('release_id'); - })) - .then(() => knex.schema.createTable('releases_teasers', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique('release_id'); - })) - .then(() => knex.schema.createTable('releases_photos', (table) => { - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.string('media_id', 21) - .notNullable() - .references('id') - .inTable('media'); - - table.unique(['release_id', 'media_id']); - })) - .then(() => knex.schema.createTable('releases_tags', (table) => { - table.integer('tag_id', 12) - .notNullable() - .references('id') - .inTable('tags'); - - table.integer('release_id', 16) - .notNullable() - .references('id') - .inTable('releases'); - - table.unique(['tag_id', 'release_id']); - })) - .then(() => knex.schema.createTable('releases_search', (table) => { - table.integer('release_id', 16) - .references('id') - .inTable('releases'); - })) - .then(() => knex.raw(` + .then(() => knex.schema.alterTable('media', (table) => { + table.string('sfw_media_id', 21) + .references('id') + .inTable('media') + .defaultTo(knex.raw('get_random_sfw_media_id()')); + })) + .then(() => knex.schema.createTable('tags_groups', (table) => { + table.increments('id', 12); + + table.string('name', 32); + table.text('description'); + + table.string('slug', 32) + .unique(); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('tags', (table) => { + table.increments('id', 12); + table.string('name'); + + table.text('description'); + + table.integer('priority', 2) + .defaultTo(0); + + table.boolean('secondary') + .defaultTo(false); + + table.integer('group_id', 12) + .references('id') + .inTable('tags_groups'); + + table.integer('alias_for', 12) + .references('id') + .inTable('tags'); + + table.string('slug', 32) + .unique(); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('tags_posters', (table) => { + table.integer('tag_id', 12) + .notNullable() + .references('id') + .inTable('tags'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique('tag_id'); + })) + .then(() => knex.schema.createTable('tags_photos', (table) => { + table.integer('tag_id', 12) + .notNullable() + .references('id') + .inTable('tags'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['tag_id', 'media_id']); + })) + .then(() => knex.schema.createTable('networks', (table) => { + table.increments('id', 12); + + table.string('name'); + table.string('url'); + table.text('description'); + table.json('parameters'); + + table.integer('parent_id', 12) + .references('id') + .inTable('networks'); + + table.string('slug', 32) + .unique(); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('networks_social', (table) => { + table.increments('id', 16); + + table.string('url'); + table.string('platform'); + + table.integer('network_id', 12) + .notNullable() + .references('id') + .inTable('networks'); + + table.unique(['url', 'network_id']); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('sites', (table) => { + table.increments('id', 12); + + table.integer('network_id', 12) + .notNullable() + .references('id') + .inTable('networks'); + + table.string('name'); + table.string('slug', 32) + .unique(); + + table.string('alias'); + + table.string('url'); + table.text('description'); + table.json('parameters'); + + table.integer('priority', 3) + .defaultTo(0); + + table.boolean('show') + .defaultTo(true); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('sites_tags', (table) => { + table.integer('tag_id', 12) + .notNullable() + .references('id') + .inTable('tags'); + + table.integer('site_id', 12) + .notNullable() + .references('id') + .inTable('sites'); + + table.boolean('inherit') + .defaultTo(false); + + table.unique(['tag_id', 'site_id']); + })) + .then(() => knex.schema.createTable('sites_social', (table) => { + table.increments('id', 16); + + table.string('url'); + table.string('platform'); + + table.integer('site_id', 12) + .notNullable() + .references('id') + .inTable('sites'); + + table.unique(['url', 'site_id']); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('studios', (table) => { + table.increments('id', 12); + + table.integer('network_id', 12) + .notNullable() + .references('id') + .inTable('networks'); + + table.string('name'); + table.string('url'); + table.text('description'); + + table.string('slug', 32) + .unique(); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('batches', (table) => { + table.increments('id', 12); + table.text('comment'); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('actors', (table) => { + table.increments('id', 12); + + table.string('name') + .notNullable(); + + table.string('slug', 32); + + table.integer('network_id', 12) + .references('id') + .inTable('networks'); + + table.unique(['slug', 'network_id']); + + table.integer('alias_for', 12) + .references('id') + .inTable('actors'); + + table.date('birthdate'); + table.string('gender', 18); + table.text('description'); + + table.string('birth_city'); + table.string('birth_state'); + table.string('birth_country_alpha2', 2) + .references('alpha2') + .inTable('countries'); + + table.string('residence_city'); + table.string('residence_state'); + table.string('residence_country_alpha2', 2) + .references('alpha2') + .inTable('countries'); + + table.string('ethnicity'); + + table.string('bust', 10); + table.integer('waist', 3); + table.integer('hip', 3); + table.boolean('natural_boobs'); + + table.integer('height', 3); + table.integer('weight', 3); + table.string('eyes'); + table.string('hair'); + + table.boolean('has_tattoos'); + table.boolean('has_piercings'); + table.string('piercings'); + table.string('tattoos'); + + table.integer('batch_id', 12) + .references('id') + .inTable('batches'); + + table.datetime('updated_at') + .defaultTo(knex.fn.now()); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('actors_profiles', (table) => { + table.increments('id', 12); + + table.integer('actor_id', 12) + .references('id') + .inTable('actors'); + + table.integer('network_id', 12) + .references('id') + .inTable('networks'); + + table.integer('site_id', 12) + .references('id') + .inTable('sites'); + + table.unique(['actor_id', 'network_id']); + table.unique(['actor_id', 'site_id']); + + table.date('birthdate'); + table.string('gender', 18); + table.text('description'); + + table.string('birth_city'); + table.string('birth_state'); + table.string('birth_country_alpha2', 2) + .references('alpha2') + .inTable('countries'); + + table.string('residence_city'); + table.string('residence_state'); + table.string('residence_country_alpha2', 2) + .references('alpha2') + .inTable('countries'); + + table.string('ethnicity'); + + table.string('bust', 10); + table.integer('waist', 3); + table.integer('hip', 3); + table.boolean('natural_boobs'); + + table.integer('height', 3); + table.integer('weight', 3); + table.string('eyes'); + table.string('hair'); + + table.boolean('has_tattoos'); + table.boolean('has_piercings'); + table.string('piercings'); + table.string('tattoos'); + + table.datetime('scraped_at'); + table.boolean('scrape_success'); + + table.datetime('updated_at') + .defaultTo(knex.fn.now()); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('body', (table) => { + table.string('slug', 20) + .primary(); + + table.string('name'); + })) + .then(() => knex('body').insert([ + // head + { slug: 'head', name: 'head' }, + { slug: 'face', name: 'face' }, + { slug: 'scalp', name: 'scalp' }, + { slug: 'forehead', name: 'forehead' }, + { slug: 'temple', name: 'temple' }, + { slug: 'cheek', name: 'cheek' }, + { slug: 'jaw', name: 'jaw' }, + { slug: 'chin', name: 'chin' }, + { slug: 'neck', name: 'neck' }, + { slug: 'throat', name: 'throat' }, + // eyes + { slug: 'eyelid', name: 'eyelid' }, + { slug: 'eyeball', name: 'eyeball' }, + { slug: 'eyebrow', name: 'eyebrow' }, + // mouth + { slug: 'tongue', name: 'tongue' }, + { slug: 'lip', name: 'lip' }, + { slug: 'upper-lip', name: 'upper lip' }, + { slug: 'lower-lip', name: 'lower lip' }, + { slug: 'inner-lip', name: 'inner lip' }, + { slug: 'inner-lower-lip', name: 'inner lower lip' }, + { slug: 'inner-upper-lip', name: 'inner upper lip' }, + { slug: 'philtrum', name: 'philtrum' }, + { slug: 'above-lip', name: 'above lip' }, + { slug: 'below-lip', name: 'below lip' }, + // nose + { slug: 'nose', name: 'nose' }, + { slug: 'third-eye', name: 'third eye' }, + { slug: 'bridge', name: 'bridge' }, + { slug: 'nostril', name: 'nostril' }, + { slug: 'septum', name: 'septum' }, + { slug: 'septril', name: 'septril' }, + // ear + { slug: 'ear', name: 'ear' }, + { slug: 'earlobe', name: 'earlobe' }, + { slug: 'helix', name: 'helix' }, + { slug: 'tragus', name: 'tragus' }, + { slug: 'conch', name: 'conch' }, + { slug: 'rook', name: 'rook' }, + { slug: 'behind-ear', name: 'behind ear' }, + // arms + { slug: 'arm', name: 'arm' }, + { slug: 'upper-arm', name: 'upper arm' }, + { slug: 'forearm', name: 'forearm' }, + { slug: 'elbow', name: 'elbow' }, + { slug: 'inner-elbow', name: 'inner elbow' }, + { slug: 'outer-elbow', name: 'outer elbow' }, + // hands + { slug: 'hand', name: 'hand' }, + { slug: 'fingers', name: 'fingers' }, + { slug: 'knuckles', name: 'knuckles' }, + { slug: 'thumb', name: 'thumb' }, + { slug: 'index-finger', name: 'index finger' }, + { slug: 'middle-finger', name: 'middle finger' }, + { slug: 'ring-finger', name: 'ring finger' }, + { slug: 'pinky', name: 'pinky' }, + { slug: 'back-of-hand', name: 'back of hand' }, + { slug: 'inner-wrist', name: 'inner wrist' }, + { slug: 'outer-wrist', name: 'outer wrist' }, + // torso + { slug: 'shoulder', name: 'shoulder' }, + { slug: 'collarbone', name: 'collarbone' }, + { slug: 'chest', name: 'chest' }, + { slug: 'rib-cage', name: 'rib cage' }, + { slug: 'breastbone', name: 'breastbone' }, + { slug: 'underboob', name: 'underboob' }, + { slug: 'sideboob', name: 'sideboob' }, + { slug: 'boob', name: 'boob' }, + { slug: 'nipple', name: 'nipple' }, + { slug: 'abdomen', name: 'abdomen' }, + { slug: 'navel', name: 'navel' }, + { slug: 'pelvis', name: 'pelvis' }, + // back + { slug: 'back', name: 'back' }, + { slug: 'upper-back', name: 'upper back' }, + { slug: 'middle-back', name: 'lower back' }, + { slug: 'lower-back', name: 'lower back' }, + { slug: 'spine', name: 'spine' }, + // bottom + { slug: 'butt', name: 'butt' }, + { slug: 'hip', name: 'hip' }, + { slug: 'anus', name: 'anus' }, + // genitals + { slug: 'pubic-mound', name: 'pubic mound' }, + { slug: 'vagina', name: 'vagina' }, + { slug: 'outer-labia', name: 'outer labia' }, + { slug: 'inner-labia', name: 'inner labia' }, + { slug: 'clitoris', name: 'clitoris' }, + { slug: 'penis', name: 'penis' }, + { slug: 'glans', name: 'glans' }, + { slug: 'foreskin', name: 'foreskin' }, + { slug: 'shaft', name: 'shaft' }, + { slug: 'scrotum', name: 'scrotum' }, + // legs + { slug: 'leg', name: 'leg' }, + { slug: 'groin', name: 'groin' }, + { slug: 'upper-leg', name: 'upper leg' }, + { slug: 'thigh', name: 'thigh' }, + { slug: 'lower-leg', name: 'lower leg' }, + { slug: 'shin', name: 'shin' }, + { slug: 'calf', name: 'calf' }, + { slug: 'knee', name: 'knee' }, + { slug: 'inner-knee', name: 'inner knee' }, + // feet + { slug: 'inner-ankle', name: 'inner ankle' }, + { slug: 'outer-ankle', name: 'outer ankle' }, + { slug: 'foot', name: 'foot' }, + { slug: 'toes', name: 'toes' }, + { slug: 'big-toe', name: 'big toe' }, + { slug: 'index-toe', name: 'index toe' }, + { slug: 'middle-toe', name: 'middle toe' }, + { slug: 'fourth-toe', name: 'fourth toe' }, + { slug: 'little-toe', name: 'little toe' }, + ])) + .then(() => knex.schema.createTable('actors_tattoos', (table) => { + table.increments('id'); + + table.integer('actor_id', 12) + .notNullable() + .references('id') + .inTable('actors'); + + table.string('body_slug', 20) + .references('slug') + .inTable('body'); + + table.enum('side', ['left', 'right', 'center', 'both']); + + table.string('description'); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('actors_piercings', (table) => { + table.increments('id'); + + table.integer('actor_id', 12) + .notNullable() + .references('id') + .inTable('actors'); + + table.string('body_slug', 20) + .references('slug') + .inTable('body'); + + table.enum('side', ['left', 'right', 'center', 'both']); + + table.string('description'); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('actors_avatars', (table) => { + table.integer('profile_id', 12) + .notNullable() + .references('id') + .inTable('actors_profiles'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique('profile_id'); + })) + .then(() => knex.schema.createTable('actors_photos', (table) => { + table.integer('actor_id', 12) + .notNullable() + .references('id') + .inTable('actors'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['actor_id', 'media_id']); + })) + .then(() => knex.schema.createTable('actors_social', (table) => { + table.increments('id', 16); + + table.string('url'); + table.string('platform'); + + table.integer('actor_id', 12) + .notNullable() + .references('id') + .inTable('actors'); + + table.unique(['url', 'actor_id']); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('directors', (table) => { + table.increments('id', 12); + + table.string('name'); + table.integer('alias_for', 12) + .references('id') + .inTable('directors'); + + table.string('slug', 32) + .unique(); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('releases', (table) => { + table.increments('id', 16); + + table.integer('site_id', 12) + .references('id') + .inTable('sites'); + + table.integer('network_id', 12) + .references('id') + .inTable('networks'); + + table.integer('studio_id', 12) + .references('id') + .inTable('studios'); + + table.string('type', 10) + .defaultTo('scene'); + + table.string('shoot_id'); + table.string('entry_id'); + table.unique(['site_id', 'network_id', 'entry_id', 'type']); + + table.string('url', 1000); + table.string('title'); + table.string('slug'); + table.date('date'); + table.text('description'); + + table.integer('duration') + .unsigned(); + + table.boolean('deep'); + table.string('deep_url', 1000); + + table.integer('created_batch_id', 12) + .references('id') + .inTable('batches'); + + table.integer('updated_batch_id', 12) + .references('id') + .inTable('batches'); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('releases_actors', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.integer('actor_id', 12) + .notNullable() + .references('id') + .inTable('actors'); + + table.unique(['release_id', 'actor_id']); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('releases_movies', (table) => { + table.integer('movie_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.integer('scene_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.unique(['movie_id', 'scene_id']); + + table.datetime('created_at') + .defaultTo(knex.fn.now()); + })) + .then(() => knex.schema.createTable('releases_directors', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.integer('director_id', 8) + .notNullable() + .references('id') + .inTable('directors'); + + table.unique(['release_id', 'director_id']); + })) + .then(() => knex.schema.createTable('releases_posters', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique('release_id'); + })) + .then(() => knex.schema.createTable('releases_covers', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['release_id', 'media_id']); + })) + .then(() => knex.schema.createTable('releases_trailers', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique('release_id'); + })) + .then(() => knex.schema.createTable('releases_teasers', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique('release_id'); + })) + .then(() => knex.schema.createTable('releases_photos', (table) => { + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.string('media_id', 21) + .notNullable() + .references('id') + .inTable('media'); + + table.unique(['release_id', 'media_id']); + })) + .then(() => knex.schema.createTable('releases_tags', (table) => { + table.integer('tag_id', 12) + .notNullable() + .references('id') + .inTable('tags'); + + table.integer('release_id', 16) + .notNullable() + .references('id') + .inTable('releases'); + + table.unique(['tag_id', 'release_id']); + })) + .then(() => knex.schema.createTable('releases_search', (table) => { + table.integer('release_id', 16) + .references('id') + .inTable('releases'); + })) + .then(() => knex.raw(` + ALTER TABLE releases + ADD CONSTRAINT ensure_site_or_network CHECK (site_id IS NOT NULL OR network_id IS NOT NULL); + ALTER TABLE releases_search ADD COLUMN document tsvector; diff --git a/public/img/logos/gamma/favicon.png b/public/img/logos/gamma/favicon.png new file mode 100644 index 000000000..b3dc4c6ef Binary files /dev/null and b/public/img/logos/gamma/favicon.png differ diff --git a/public/img/logos/mindgeek/favicon.png b/public/img/logos/mindgeek/favicon.png index 44e7f0d0a..ba1930b17 100644 Binary files a/public/img/logos/mindgeek/favicon.png and b/public/img/logos/mindgeek/favicon.png differ diff --git a/public/img/logos/mindgeek/lazy/favicon.png b/public/img/logos/mindgeek/lazy/favicon.png new file mode 100644 index 000000000..d0c145d95 Binary files /dev/null and b/public/img/logos/mindgeek/lazy/favicon.png differ diff --git a/public/img/logos/mindgeek/lazy/network.png b/public/img/logos/mindgeek/lazy/network.png index 56d4c911d..4d510b3d6 100644 Binary files a/public/img/logos/mindgeek/lazy/network.png and b/public/img/logos/mindgeek/lazy/network.png differ diff --git a/public/img/logos/mindgeek/lazy/pornhub.png b/public/img/logos/mindgeek/lazy/pornhub.png new file mode 100644 index 000000000..3fb6ab427 Binary files /dev/null and b/public/img/logos/mindgeek/lazy/pornhub.png differ diff --git a/public/img/logos/mindgeek/lazy/transangels.png b/public/img/logos/mindgeek/lazy/transangels.png index b7d7f4759..39e53a0e4 100644 Binary files a/public/img/logos/mindgeek/lazy/transangels.png and b/public/img/logos/mindgeek/lazy/transangels.png differ diff --git a/public/img/logos/mindgeek/lazy/trueamateurs.png b/public/img/logos/mindgeek/lazy/trueamateurs.png index 25174584b..a46cc1620 100644 Binary files a/public/img/logos/mindgeek/lazy/trueamateurs.png and b/public/img/logos/mindgeek/lazy/trueamateurs.png differ diff --git a/public/img/logos/mindgeek/lazy/tube8vip.png b/public/img/logos/mindgeek/lazy/tube8vip.png index 19e41bb47..785716c32 100644 Binary files a/public/img/logos/mindgeek/lazy/tube8vip.png and b/public/img/logos/mindgeek/lazy/tube8vip.png differ diff --git a/public/img/logos/mindgeek/misc/porn-hub-gay.png b/public/img/logos/mindgeek/misc/porn-hub-gay.png new file mode 100644 index 000000000..bb7dfc3e1 Binary files /dev/null and b/public/img/logos/mindgeek/misc/porn-hub-gay.png differ diff --git a/public/img/logos/mindgeek/network.png b/public/img/logos/mindgeek/network.png index 14d9833d9..56a651504 100644 Binary files a/public/img/logos/mindgeek/network.png and b/public/img/logos/mindgeek/network.png differ diff --git a/public/img/logos/mindgeek/pornhub.png b/public/img/logos/mindgeek/pornhub.png new file mode 100644 index 000000000..a69775be9 Binary files /dev/null and b/public/img/logos/mindgeek/pornhub.png differ diff --git a/public/img/logos/mindgeek/thumbs/favicon.png b/public/img/logos/mindgeek/thumbs/favicon.png new file mode 100644 index 000000000..692f41d5a Binary files /dev/null and b/public/img/logos/mindgeek/thumbs/favicon.png differ diff --git a/public/img/logos/mindgeek/thumbs/network.png b/public/img/logos/mindgeek/thumbs/network.png index 6a5cd428c..0fca53542 100644 Binary files a/public/img/logos/mindgeek/thumbs/network.png and b/public/img/logos/mindgeek/thumbs/network.png differ diff --git a/public/img/logos/mindgeek/thumbs/pornhub.png b/public/img/logos/mindgeek/thumbs/pornhub.png new file mode 100644 index 000000000..eb275b6ab Binary files /dev/null and b/public/img/logos/mindgeek/thumbs/pornhub.png differ diff --git a/public/img/logos/mindgeek/thumbs/transangels.png b/public/img/logos/mindgeek/thumbs/transangels.png index 6d36404b7..a45a3100c 100644 Binary files a/public/img/logos/mindgeek/thumbs/transangels.png and b/public/img/logos/mindgeek/thumbs/transangels.png differ diff --git a/public/img/logos/mindgeek/thumbs/trueamateurs.png b/public/img/logos/mindgeek/thumbs/trueamateurs.png index 3acf2280f..177390aa2 100644 Binary files a/public/img/logos/mindgeek/thumbs/trueamateurs.png and b/public/img/logos/mindgeek/thumbs/trueamateurs.png differ diff --git a/public/img/logos/mindgeek/thumbs/tube8vip.png b/public/img/logos/mindgeek/thumbs/tube8vip.png index 1db88a523..25001426e 100644 Binary files a/public/img/logos/mindgeek/thumbs/tube8vip.png and b/public/img/logos/mindgeek/thumbs/tube8vip.png differ diff --git a/public/img/logos/mindgeek/transangels.png b/public/img/logos/mindgeek/transangels.png index 3b7f87db6..edee1d433 100644 Binary files a/public/img/logos/mindgeek/transangels.png and b/public/img/logos/mindgeek/transangels.png differ diff --git a/public/img/logos/mindgeek/trueamateurs.png b/public/img/logos/mindgeek/trueamateurs.png index dc8c0b0ea..000ef4fa2 100644 Binary files a/public/img/logos/mindgeek/trueamateurs.png and b/public/img/logos/mindgeek/trueamateurs.png differ diff --git a/public/img/logos/mindgeek/tube8vip.png b/public/img/logos/mindgeek/tube8vip.png index 14efc2a18..9bca04ada 100644 Binary files a/public/img/logos/mindgeek/tube8vip.png and b/public/img/logos/mindgeek/tube8vip.png differ diff --git a/public/img/tags/bukkake/0.jpeg b/public/img/tags/bukkake/0.jpeg new file mode 100644 index 000000000..f1da17ca3 Binary files /dev/null and b/public/img/tags/bukkake/0.jpeg differ diff --git a/public/img/tags/bukkake/lazy/0.jpeg b/public/img/tags/bukkake/lazy/0.jpeg new file mode 100644 index 000000000..a3c3c0ebd Binary files /dev/null and b/public/img/tags/bukkake/lazy/0.jpeg differ diff --git a/public/img/tags/bukkake/thumbs/0.jpeg b/public/img/tags/bukkake/thumbs/0.jpeg new file mode 100644 index 000000000..4be40d7d6 Binary files /dev/null and b/public/img/tags/bukkake/thumbs/0.jpeg differ diff --git a/seeds/02_sites.js b/seeds/02_sites.js index 94dc70dd3..2adf6a7b6 100644 --- a/seeds/02_sites.js +++ b/seeds/02_sites.js @@ -2,475 +2,475 @@ const upsert = require('../src/utils/upsert'); /* eslint-disable max-len */ const sites = [ - // 21NATURALS - { - slug: '21naturals', - name: '21Naturals', - alias: ['21 naturals', '21na'], - url: 'https://www.21naturals.com', - network: '21naturals', - parameters: { - extract: '21naturals', - }, - }, - { - slug: '21footart', - name: '21 Foot Art', - alias: ['21fa'], - url: 'https://21footart.21naturals.com', - network: '21naturals', - }, - { - slug: '21eroticanal', - name: '21 Erotic Anal', - alias: ['21ea'], - url: 'https://21eroticanal.21naturals.com', - network: '21naturals', - parameters: { - scene: 'https://21naturals.com/en/video', - }, - }, - // 21SEXTREME - { - slug: 'grandpasfuckteens', - name: 'Grandpas Fuck Teens', - alias: ['gft'], - url: 'https://grandpasfuckteens.21sextreme.com', - network: '21sextreme', - }, - { - slug: 'oldyounglesbianlove', - name: 'Old Young Lesbian Love', - url: 'https://oldyounglesbianlove.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'lustygrandmas', - name: 'Lusty Grandmas', - url: 'https://lustygrandmas.21sextreme.com', - network: '21sextreme', - }, - { - slug: 'teachmefisting', - name: 'Teach Me Fisting', - alias: ['tmf'], - url: 'https://teachmefisting.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'zoliboy', - name: 'Zoliboy', - alias: ['zb'], - url: 'https://zoliboy.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'mightymistress', - name: 'Mighty Mistress', - url: 'https://mightymistress.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'dominatedgirls', - name: 'Dominated Girls', - url: 'https://dominatedgirls.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'homepornreality', - name: 'Home Porn Reality', - url: 'https://homepornreality.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'peeandblow', - name: 'Pee and Blow', - url: 'https://peeandblow.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'cummingmatures', - name: 'Cumming Matures', - url: 'https://cummingmatures.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'mandyiskinky', - name: 'Mandy Is Kinky', - url: 'https://mandyiskinky.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'speculumplays', - name: 'Speculum Plays', - url: 'https://speculumplays.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - { - slug: 'creampiereality', - name: 'Creampie Reality', - url: 'https://creampiereality.21sextreme.com', - network: '21sextreme', - parameters: { - scene: 'https://21sextreme.com/en/video', - }, - }, - // 21SEXTURY - { - slug: 'analteenangels', - name: 'Anal Teen Angels', - url: 'https://www.analteenangels.com', - description: 'AnalTeenAngels is presented by the 21Sextury nextwork and features young, European teens in hardcore anal porn. Watch these barely legal teens have their first anal sex and give up their ass for some anal pounding!', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - }, - }, - { - slug: 'assholefever', - name: 'Asshole Fever', - url: 'https://www.assholefever.com', - description: 'Welcome to AssholeFever, the most hardcore anal site on the net. Watch your favorite pornstars and anal sluts from all over the world in big booty hardcore porn, anal gape, beads, anal creampie and more! Look inside if you dare!', - network: '21sextury', - parameters: { - networkReferer: true, - }, - }, - { - slug: 'buttplays', - name: 'Butt Plays', - alias: ['bp'], - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'clubsandy', - name: 'Club Sandy', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'deepthroatfrenzy', - name: 'Deepthroat Frenzy', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'dpfanatics', - name: 'DP Fanatics', - alias: ['dpf'], - url: 'https://www.dpfanatics.com', - description: 'Welcome to DPFanatics, brought to you by 21Sextury. DP Fanatics brings you the best DP sex and double penetration porn you can find. Double vaginal penetration, double anal, amateur and teen DP inside!', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - }, - }, - { - slug: 'footsiebabes', - name: 'Footsie Babes', - url: 'https://www.footsiebabes.com', - description: 'Welcome to FootsieBabes.com, bringing you the best foot porn, teen feet and foot worship you can find on the net. Watch stocking porn, footjobs, feet tickling and more inside!', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'gapeland', - name: 'Gapeland', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'lezcuties', - name: 'Lez Cuties', - url: 'https://www.lezcuties.com', - description: 'LezCuties brings you the cutest lesbian coeds and tiny teen lesbians in HD lesbian porn. Watch as European teens explore themselves and lick each other\'s tight lesbian pussy while their parents aren\'t home.', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - }, - }, - { - slug: 'onlyswallows', - name: 'Only Swallows', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'alettaoceanempire', - name: 'Aletta Ocean Empire', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'analqueenalysa', - name: 'Anal Queen Alysa', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'blueangellive', - name: 'Blue Angel Live', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'pixandvideo', - name: 'Pix and Video', - alias: ['pav'], - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'cheatingwhorewives', - name: 'Cheating Whore Wives', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'cutiesgalore', - name: 'Cuties Galore', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'hotmilfclub', - name: 'Hot MILF Club', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'letsplaylez', - name: 'Lets Play Lez', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'nudefightclub', - name: 'Nude Fight Club', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'sexwithkathianobili', - name: 'Sex With Kathia Nobili', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - { - slug: 'sweetsophiemoone', - name: 'Sweet Sophie Moone', - network: '21sextury', - parameters: { - referer: 'https://www.21sextury.com', - scene: 'https://www.21sextury.com/en/video', - photos: 'https://www.21sextury.com/en/photo', - }, - }, - // ADULT TIME - { - name: 'ASMR Fantasy', - slug: 'asmrfantasy', - url: 'https://asmrfantasy.com', - network: 'adulttime', - parameters: { - referer: 'https://freetour.adulttime.com/en/join', - deep: 'https://21sextury.com/en/video', - scene: false, - }, - }, - { - name: 'Bubblegum Dungeon', - slug: 'bubblegumdungeon', - url: 'https://www.bubblegumdungeon.com', - network: 'adulttime', - parameters: { - referer: 'https://freetour.bubblegumdungeon.com/en/join', - deep: 'https://21sextury.com/en/video', - scene: false, - }, - }, - { - name: 'Lady Gonzo', - slug: 'ladygonzo', - url: 'https://www.ladygonzo.com', - description: 'LadyGonzo.com is a new Adult Time porn series featuring Joanna Angel shooting hardcore sex and gonzo porn movies the way she\'d like to see it!', - network: 'adulttime', - }, - { - name: 'Girls Under Arrest', - slug: 'girlsunderarrest', - url: 'https://www.girlsunderarrest.com', - parameters: { - referer: 'https://www.isthisreal.com', - scene: 'https://www.isthisreal.com/en/video/girlsunderarrest', - }, - network: 'adulttime', - }, - // AMATEUR ALLURE - { - name: 'Amateur Allure', - slug: 'amateurallure', - alias: ['aa'], - url: 'https://www.amateurallure.com', - parameters: { - upcoming: false, - latest: 'https://www.amateurallure.com/tour/updates/page_%d.html', - photos: 'https://www.amateurallure.com/tour/gallery.php', - }, - network: 'amateurallure', - }, - { - name: 'Swallow Salon', - slug: 'swallowsalon', - alias: ['swsn'], - url: 'https://www.swallowsalon.com', - parameters: { - upcoming: false, - latest: 'https://www.swallowsalon.com/categories/movies_%d_d.html', - photos: 'https://www.swallowsalon.com/gallery.php', - }, - network: 'amateurallure', - }, - // ASSYLUM - { - slug: 'assylum', - name: 'Assylum', - url: 'https://www.assylum.com', - description: 'At Assylum, submissive girls get dominated with rough anal sex, ass to mouth, hard BDSM, and sexual humiliation and degradation.', - network: 'assylum', - tags: ['bdsm'], - parameters: { - a: 68, - }, - }, - { - slug: 'slavemouth', - name: 'Slave Mouth', - url: 'https://www.slavemouth.com', - description: 'Submissive girls get their mouths punished hard by Dr. Mercies, with facefucking, gagging, frozen cum bukkake, face bondage, ass eating, and sexual degradation.', - network: 'assylum', - tags: ['bdsm'], - parameters: { - a: 183, - }, - }, - // AZIANI - { - slug: 'gangbangcreampie', - name: 'Gangbang Creampie', - url: 'https://www.gangbangcreampie.com', - network: 'aziani', - tags: ['gangbang', 'creampie'], - }, - { - slug: 'gloryholesecrets', - name: 'Glory Hole Secrets', - url: 'https://www.gloryholesecrets.com', - network: 'aziani', - tags: ['gloryhole'], - }, - { - slug: 'aziani', - name: 'Aziani', - url: 'https://www.aziani.com', - network: 'aziani', - }, - /* offline + // 21NATURALS + { + slug: '21naturals', + name: '21Naturals', + alias: ['21 naturals', '21na'], + url: 'https://www.21naturals.com', + network: '21naturals', + parameters: { + extract: '21naturals', + }, + }, + { + slug: '21footart', + name: '21 Foot Art', + alias: ['21fa'], + url: 'https://21footart.21naturals.com', + network: '21naturals', + }, + { + slug: '21eroticanal', + name: '21 Erotic Anal', + alias: ['21ea'], + url: 'https://21eroticanal.21naturals.com', + network: '21naturals', + parameters: { + scene: 'https://21naturals.com/en/video', + }, + }, + // 21SEXTREME + { + slug: 'grandpasfuckteens', + name: 'Grandpas Fuck Teens', + alias: ['gft'], + url: 'https://grandpasfuckteens.21sextreme.com', + network: '21sextreme', + }, + { + slug: 'oldyounglesbianlove', + name: 'Old Young Lesbian Love', + url: 'https://oldyounglesbianlove.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'lustygrandmas', + name: 'Lusty Grandmas', + url: 'https://lustygrandmas.21sextreme.com', + network: '21sextreme', + }, + { + slug: 'teachmefisting', + name: 'Teach Me Fisting', + alias: ['tmf'], + url: 'https://teachmefisting.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'zoliboy', + name: 'Zoliboy', + alias: ['zb'], + url: 'https://zoliboy.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'mightymistress', + name: 'Mighty Mistress', + url: 'https://mightymistress.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'dominatedgirls', + name: 'Dominated Girls', + url: 'https://dominatedgirls.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'homepornreality', + name: 'Home Porn Reality', + url: 'https://homepornreality.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'peeandblow', + name: 'Pee and Blow', + url: 'https://peeandblow.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'cummingmatures', + name: 'Cumming Matures', + url: 'https://cummingmatures.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'mandyiskinky', + name: 'Mandy Is Kinky', + url: 'https://mandyiskinky.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'speculumplays', + name: 'Speculum Plays', + url: 'https://speculumplays.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + { + slug: 'creampiereality', + name: 'Creampie Reality', + url: 'https://creampiereality.21sextreme.com', + network: '21sextreme', + parameters: { + scene: 'https://21sextreme.com/en/video', + }, + }, + // 21SEXTURY + { + slug: 'analteenangels', + name: 'Anal Teen Angels', + url: 'https://www.analteenangels.com', + description: 'AnalTeenAngels is presented by the 21Sextury nextwork and features young, European teens in hardcore anal porn. Watch these barely legal teens have their first anal sex and give up their ass for some anal pounding!', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + }, + }, + { + slug: 'assholefever', + name: 'Asshole Fever', + url: 'https://www.assholefever.com', + description: 'Welcome to AssholeFever, the most hardcore anal site on the net. Watch your favorite pornstars and anal sluts from all over the world in big booty hardcore porn, anal gape, beads, anal creampie and more! Look inside if you dare!', + network: '21sextury', + parameters: { + networkReferer: true, + }, + }, + { + slug: 'buttplays', + name: 'Butt Plays', + alias: ['bp'], + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'clubsandy', + name: 'Club Sandy', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'deepthroatfrenzy', + name: 'Deepthroat Frenzy', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'dpfanatics', + name: 'DP Fanatics', + alias: ['dpf'], + url: 'https://www.dpfanatics.com', + description: 'Welcome to DPFanatics, brought to you by 21Sextury. DP Fanatics brings you the best DP sex and double penetration porn you can find. Double vaginal penetration, double anal, amateur and teen DP inside!', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + }, + }, + { + slug: 'footsiebabes', + name: 'Footsie Babes', + url: 'https://www.footsiebabes.com', + description: 'Welcome to FootsieBabes.com, bringing you the best foot porn, teen feet and foot worship you can find on the net. Watch stocking porn, footjobs, feet tickling and more inside!', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'gapeland', + name: 'Gapeland', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'lezcuties', + name: 'Lez Cuties', + url: 'https://www.lezcuties.com', + description: 'LezCuties brings you the cutest lesbian coeds and tiny teen lesbians in HD lesbian porn. Watch as European teens explore themselves and lick each other\'s tight lesbian pussy while their parents aren\'t home.', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + }, + }, + { + slug: 'onlyswallows', + name: 'Only Swallows', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'alettaoceanempire', + name: 'Aletta Ocean Empire', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'analqueenalysa', + name: 'Anal Queen Alysa', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'blueangellive', + name: 'Blue Angel Live', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'pixandvideo', + name: 'Pix and Video', + alias: ['pav'], + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'cheatingwhorewives', + name: 'Cheating Whore Wives', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'cutiesgalore', + name: 'Cuties Galore', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'hotmilfclub', + name: 'Hot MILF Club', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'letsplaylez', + name: 'Lets Play Lez', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'nudefightclub', + name: 'Nude Fight Club', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'sexwithkathianobili', + name: 'Sex With Kathia Nobili', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + { + slug: 'sweetsophiemoone', + name: 'Sweet Sophie Moone', + network: '21sextury', + parameters: { + referer: 'https://www.21sextury.com', + scene: 'https://www.21sextury.com/en/video', + photos: 'https://www.21sextury.com/en/photo', + }, + }, + // ADULT TIME + { + name: 'ASMR Fantasy', + slug: 'asmrfantasy', + url: 'https://asmrfantasy.com', + network: 'adulttime', + parameters: { + referer: 'https://freetour.adulttime.com/en/join', + deep: 'https://21sextury.com/en/video', + scene: false, + }, + }, + { + name: 'Bubblegum Dungeon', + slug: 'bubblegumdungeon', + url: 'https://www.bubblegumdungeon.com', + network: 'adulttime', + parameters: { + referer: 'https://freetour.bubblegumdungeon.com/en/join', + deep: 'https://21sextury.com/en/video', + scene: false, + }, + }, + { + name: 'Lady Gonzo', + slug: 'ladygonzo', + url: 'https://www.ladygonzo.com', + description: 'LadyGonzo.com is a new Adult Time porn series featuring Joanna Angel shooting hardcore sex and gonzo porn movies the way she\'d like to see it!', + network: 'adulttime', + }, + { + name: 'Girls Under Arrest', + slug: 'girlsunderarrest', + url: 'https://www.girlsunderarrest.com', + parameters: { + referer: 'https://www.isthisreal.com', + scene: 'https://www.isthisreal.com/en/video/girlsunderarrest', + }, + network: 'adulttime', + }, + // AMATEUR ALLURE + { + name: 'Amateur Allure', + slug: 'amateurallure', + alias: ['aa'], + url: 'https://www.amateurallure.com', + parameters: { + upcoming: false, + latest: 'https://www.amateurallure.com/tour/updates/page_%d.html', + photos: 'https://www.amateurallure.com/tour/gallery.php', + }, + network: 'amateurallure', + }, + { + name: 'Swallow Salon', + slug: 'swallowsalon', + alias: ['swsn'], + url: 'https://www.swallowsalon.com', + parameters: { + upcoming: false, + latest: 'https://www.swallowsalon.com/categories/movies_%d_d.html', + photos: 'https://www.swallowsalon.com/gallery.php', + }, + network: 'amateurallure', + }, + // ASSYLUM + { + slug: 'assylum', + name: 'Assylum', + url: 'https://www.assylum.com', + description: 'At Assylum, submissive girls get dominated with rough anal sex, ass to mouth, hard BDSM, and sexual humiliation and degradation.', + network: 'assylum', + tags: ['bdsm'], + parameters: { + a: 68, + }, + }, + { + slug: 'slavemouth', + name: 'Slave Mouth', + url: 'https://www.slavemouth.com', + description: 'Submissive girls get their mouths punished hard by Dr. Mercies, with facefucking, gagging, frozen cum bukkake, face bondage, ass eating, and sexual degradation.', + network: 'assylum', + tags: ['bdsm'], + parameters: { + a: 183, + }, + }, + // AZIANI + { + slug: 'gangbangcreampie', + name: 'Gangbang Creampie', + url: 'https://www.gangbangcreampie.com', + network: 'aziani', + tags: ['gangbang', 'creampie'], + }, + { + slug: 'gloryholesecrets', + name: 'Glory Hole Secrets', + url: 'https://www.gloryholesecrets.com', + network: 'aziani', + tags: ['gloryhole'], + }, + { + slug: 'aziani', + name: 'Aziani', + url: 'https://www.aziani.com', + network: 'aziani', + }, + /* offline { slug: 'portagloryhole', name: 'Porta Gloryhole', @@ -509,3876 +509,3883 @@ const sites = [ network: 'aziani', }, */ - // BABES - { - name: 'Babes', - url: 'https://www.babes.com/scenes?site=213', - slug: 'babes', - network: 'babes', - }, - { - name: 'Babes Unleashed', - url: 'https://www.babes.com/scenes?site=218', - slug: 'babesunleashed', - network: 'babes', - }, - { - name: 'Black Is Better', - url: 'https://www.babes.com/scenes?site=217', - slug: 'blackisbetter', - network: 'babes', - }, - { - name: 'Elegant Anal', - url: 'https://www.babes.com/scenes?site=216', - slug: 'elegantanal', - network: 'babes', - }, - { - name: 'Office Obsession', - url: 'https://www.babes.com/scenes?site=214', - slug: 'officeobsession', - network: 'babes', - }, - { - name: 'Step Mom Lessons', - url: 'https://www.babes.com/scenes?site=215', - slug: 'stepmomlessons', - network: 'babes', - }, - // BAM VISIONS - { - slug: 'bamvisions', - name: 'BAM Visions', - url: 'https://tour.bamvisions.com', - parameters: { independent: true }, - network: 'bamvisions', - }, - // BANG - { - name: 'Trickery', - slug: 'bangtrickery', - url: 'https://www.bang.com/original/4800/bang-trickery', - parameters: { siteId: 4800 }, - network: 'bang', - }, - { - name: 'Yngr', - slug: 'yngrcom', - alias: ['byngr'], - // url: 'https://www.bang.com/original/5010/bang-yngr', - url: 'https://yngr.com', - parameters: { siteId: 5010 }, - network: 'bang', - }, - { - name: 'Roadside XXX', - slug: 'bangroadsidexxx', - // url: 'https://www.bang.com/original/4864/roadside-xxx', - url: 'https://roadsidexxx.com', - parameters: { siteId: 4864 }, - network: 'bang', - }, - { - name: 'Surprise', - slug: 'bangsurprise', - url: 'https://www.bang.com/original/5000/bang-surprise', - parameters: { siteId: 5000 }, - network: 'bang', - }, - { - name: 'Real Teens', - slug: 'bangrealteens', - alias: ['brealteens'], - url: 'https://www.bang.com/original/3366/bang-real-teens', - parameters: { siteId: 3366 }, - network: 'bang', - }, - { - name: 'FCK.news', - slug: 'bangfakenews', - // url: 'https://www.bang.com/original/4998/bang-fckNews', - url: 'https://fck.news', - parameters: { siteId: 4998 }, - network: 'bang', - }, - { - name: 'Pretty & Raw', - slug: 'prettyandraw', - // url: 'https://www.bang.com/original/4792/bang-pretty-and-raw', - url: 'https://prettyandraw.com', - parameters: { siteId: 4792 }, - network: 'bang', - }, - { - name: 'Japan', - slug: 'bangjapan', - url: 'https://www.bang.com/original/3079/bang-japan', - parameters: { siteId: 3079, ignore: true }, - network: 'bang', - }, - { - name: 'Rammed', - slug: 'bangrammed', - url: 'https://www.bang.com/original/4836/bang-rammed', - parameters: { siteId: 4836 }, - network: 'bang', - }, - { - name: 'Glamkore', - slug: 'bangglamkore', - alias: ['bglamkore'], - url: 'https://www.bang.com/original/4586/bang-glamkore', - parameters: { siteId: 4586 }, - network: 'bang', - }, - { - name: 'Screw The Cops', - slug: 'screwthecops', - url: 'https://www.bang.com/original/4710/bang-screw-cops', - parameters: { siteId: 4710 }, - network: 'bang', - }, - { - name: 'Real MILFs', - slug: 'bangrealmilfs', - alias: ['brealmilfs'], - url: 'https://www.bang.com/original/4448/bang-real-milfs', - parameters: { siteId: 4448 }, - network: 'bang', - }, - { - name: 'Confessions', - slug: 'bangconfessions', - alias: ['bconfessions'], - url: 'https://www.bang.com/original/4308/bang-confessions', - parameters: { siteId: 4308 }, - network: 'bang', - }, - { - name: 'Casting', - slug: 'bangcasting', - alias: ['bcasting'], - url: 'https://www.bang.com/original/3261/bang-casting', - parameters: { siteId: 3261 }, - network: 'bang', - }, - // BANGBROS - { - name: 'Ass Parade', - url: 'https://bangbros.com/websites/assparade', - slug: 'assparade', - description: null, - network: 'bangbros', - parameters: { code: 'ap' }, - }, - { - name: 'AvaSpice', - url: 'https://bangbros.com/websites/avaspice', - slug: 'avaspice', - description: null, - network: 'bangbros', - parameters: { code: 'av' }, - }, - { - name: 'Back Room Facials', - url: 'https://bangbros.com/websites/backroomfacials', - slug: 'backroomfacials', - description: null, - network: 'bangbros', - parameters: { code: 'brf' }, - }, - { - name: 'Backroom MILF', - url: 'https://bangbros.com/websites/backroommilf', - slug: 'backroommilf', - description: null, - network: 'bangbros', - parameters: { code: 'mf' }, - }, - { - name: 'Ball Honeys', - url: 'https://bangbros.com/websites/ballhoneys', - slug: 'ballhoneys', - description: null, - network: 'bangbros', - parameters: { code: 'es' }, - }, - { - name: 'BangBros 18', - url: 'https://bangbros.com/websites/bangbros18', - slug: 'bangbros18', - description: null, - network: 'bangbros', - parameters: { code: 'bbe' }, - }, - { - name: 'BangBros Angels', - url: 'https://bangbros.com/websites/bangbrosangels', - slug: 'bangbrosangels', - description: null, - network: 'bangbros', - parameters: { code: 'bng' }, - }, - { - name: 'Bangbros Clips', - url: 'https://bangbros.com/websites/bangbrosclips', - slug: 'bangbrosclips', - description: null, - network: 'bangbros', - parameters: { code: 'bbc' }, - }, - { - name: 'BangBros Remastered', - url: 'https://bangbros.com/websites/remaster', - slug: 'bangbrosremastered', - description: null, - network: 'bangbros', - parameters: { code: 'rm' }, - }, - { - name: 'Bang Bus', - url: 'https://bangbros.com/websites/bangbus', - slug: 'bangbus', - description: null, - network: 'bangbros', - parameters: { code: 'bb' }, - }, - { - name: 'Bang Casting', - url: 'https://bangbros.com/websites/bangcasting', - slug: 'bangbroscasting', - description: null, - network: 'bangbros', - parameters: { code: 'hih' }, - }, - { - name: 'Bang POV', - url: 'https://bangbros.com/websites/bangpov', - slug: 'bangpov', - description: null, - network: 'bangbros', - parameters: { code: 'bpov' }, - }, - { - name: 'Bang Tryouts', - url: 'https://bangbros.com/websites/bangtryouts', - slug: 'bangtryouts', - description: null, - network: 'bangbros', - parameters: { code: 'bto' }, - }, - { - name: 'Big Mouthfuls', - url: 'https://bangbros.com/websites/bigmouthfuls', - slug: 'bigmouthfuls', - description: null, - network: 'bangbros', - parameters: { code: 'bmf' }, - }, - { - name: 'Big Tit Cream Pie', - alias: ['btc'], - slug: 'bigtitcreampie', - url: 'https://bangbros.com/websites/bigtitcreampie', - description: null, - network: 'bangbros', - parameters: { code: 'btcp' }, - }, - { - name: 'Big Tits, Round Asses', - url: 'https://bangbros.com/websites/bigtitsroundasses', - alias: ['btra'], - slug: 'bigtitsroundasses', - description: null, - network: 'bangbros', - parameters: { code: 'btra' }, - }, - { - name: 'BlowJob Fridays', - url: 'https://bangbros.com/websites/blowjobfridays', - slug: 'blowjobfridays', - description: null, - network: 'bangbros', - parameters: { code: 'bj' }, - }, - { - name: 'Blowjob Ninjas', - url: 'https://bangbros.com/websites/blowjobninjas', - slug: 'blowjobninjas', - description: null, - network: 'bangbros', - parameters: { code: 'aa' }, - }, - { - name: 'Boob Squad', - url: 'https://bangbros.com/websites/boobsquad', - slug: 'boobsquad', - description: null, - network: 'bangbros', - parameters: { code: 'bs' }, - }, - { - name: 'Brown Bunnies', - url: 'https://bangbros.com/websites/brownbunnies', - slug: 'brownbunnies', - description: null, - network: 'bangbros', - parameters: { code: 'bkb' }, - }, - { - name: 'Can He Score?', - url: 'https://bangbros.com/websites/canhescore', - slug: 'canhescore', - description: null, - network: 'bangbros', - parameters: { code: 'bd' }, - }, - { - name: 'Casting', - url: 'https://bangbros.com/websites/casting', - slug: 'casting', - description: null, - network: 'bangbros', - parameters: { code: 'ca' }, - }, - { - name: 'Chongas', - url: 'https://bangbros.com/websites/chongas', - slug: 'chongas', - description: null, - network: 'bangbros', - parameters: { code: 'ch' }, - }, - { - name: 'Colombia Fuck Fest', - url: 'https://bangbros.com/websites/colombiafuckfest', - slug: 'colombiafuckfest', - description: null, - network: 'bangbros', - parameters: { code: 'cff' }, - }, - { - name: 'Dirty World Tour', - url: 'https://bangbros.com/websites/dirtyworldtour', - slug: 'dirtyworldtour', - description: null, - network: 'bangbros', - parameters: { code: 'bf' }, - }, - { - name: 'Dorm Invasion', - url: 'https://bangbros.com/websites/dorminvasion', - slug: 'dorminvasion', - description: null, - network: 'bangbros', - parameters: { code: 'di' }, - }, - { - name: 'Facial Fest', - url: 'https://bangbros.com/websites/facialfest', - slug: 'facialfest', - description: null, - network: 'bangbros', - parameters: { code: 'ff' }, - }, - { - name: 'Fuck Team Five', - url: 'https://bangbros.com/websites/fuckteamfive', - slug: 'fuckteamfive', - description: null, - network: 'bangbros', - parameters: { code: 'bbw' }, - }, - { - name: 'Glory Hole Loads', - url: 'https://bangbros.com/websites/gloryholeloads', - slug: 'gloryholeloads', - description: null, - network: 'bangbros', - parameters: { code: 'ghl' }, - }, - { - name: 'Latina Rampage', - url: 'https://bangbros.com/websites/latinarampage', - slug: 'latinarampage', - description: null, - network: 'bangbros', - parameters: { code: 'lrp' }, - }, - { - name: 'Living With Anna', - url: 'https://bangbros.com/websites/livingwithanna', - slug: 'livingwithanna', - description: null, - network: 'bangbros', - parameters: { code: 'lr' }, - }, - { - name: 'Magical Feet', - url: 'https://bangbros.com/websites/magicalfeet', - slug: 'magicalfeet', - description: null, - network: 'bangbros', - parameters: { code: 'fj' }, - }, - { - name: 'Milf Soup', - url: 'https://bangbros.com/websites/milfsoup', - slug: 'milfsoup', - description: null, - network: 'bangbros', - parameters: { code: 'ms' }, - }, - { - name: 'MomIsHorny', - url: 'https://bangbros.com/websites/momishorny', - slug: 'momishorny', - description: null, - network: 'bangbros', - parameters: { code: 'mih' }, - }, - { - name: 'Monsters of Cock', - url: 'https://bangbros.com/websites/monstersofcock', - slug: 'monstersofcock', - description: null, - network: 'bangbros', - parameters: { code: 'mc' }, - }, - { - name: 'Mr CamelToe', - url: 'https://bangbros.com/websites/mrcameltoe', - slug: 'mrcameltoe', - description: null, - network: 'bangbros', - parameters: { code: 'ct' }, - }, - { - name: 'My Dirty Maid', - slug: 'mydirtymaid', - alias: ['mdm'], - url: 'https://bangbros.com/websites/mydirtymaid', - description: null, - network: 'bangbros', - parameters: { code: 'mda' }, - }, - { - name: 'My Life In Brazil', - url: 'https://bangbros.com/websites/mylifeinbrazil', - slug: 'mylifeinbrazil', - description: null, - network: 'bangbros', - parameters: { code: 'mb' }, - }, - { - name: 'Newbie Black', - url: 'https://bangbros.com/websites/newbieblack', - slug: 'newbieblack', - description: null, - network: 'bangbros', - parameters: { code: 'blkg' }, - }, - { - name: 'Party of Three', - url: 'https://bangbros.com/websites/partyofthree', - slug: 'partyofthree', - description: null, - network: 'bangbros', - parameters: { code: 'ls' }, - }, - { - name: 'Pawg', - url: 'https://bangbros.com/websites/pawg', - slug: 'pawg', - description: null, - network: 'bangbros', - parameters: { code: 'pwg' }, - }, - { - name: 'Penny Show', - url: 'https://bangbros.com/websites/pennyshow', - slug: 'pennyshow', - description: null, - network: 'bangbros', - parameters: { code: 'ps' }, - }, - { - name: 'Porn Star Spa', - url: 'https://bangbros.com/websites/pornstarspa', - slug: 'pornstarspa', - description: null, - network: 'bangbros', - parameters: { code: 'pos' }, - }, - { - name: 'Power Munch', - url: 'https://bangbros.com/websites/powermunch', - slug: 'powermunch', - description: null, - network: 'bangbros', - parameters: { code: 'pm' }, - }, - { - name: 'Public Bang', - url: 'https://bangbros.com/websites/publicbang', - slug: 'publicbang', - description: null, - network: 'bangbros', - parameters: { code: 'pb' }, - }, - { - name: 'Slutty White Girls', - url: 'https://bangbros.com/websites/sluttywhitegirls', - slug: 'sluttywhitegirls', - description: null, - network: 'bangbros', - parameters: { code: 'swg' }, - }, - { - name: 'Stepmom Videos', - url: 'https://bangbros.com/websites/stepmomvideos', - slug: 'stepmomvideos', - description: null, - network: 'bangbros', - parameters: { code: 'smv' }, - }, - { - name: 'Street Ranger', - url: 'https://bangbros.com/websites/thewheeler', - slug: 'streetranger', - description: null, - network: 'bangbros', - parameters: { code: 'sg' }, - }, - { - name: 'Tugjobs', - url: 'https://bangbros.com/websites/tugjobs', - slug: 'tugjobs', - description: null, - network: 'bangbros', - parameters: { code: 'hj' }, - }, - { - name: 'Working Latinas', - url: 'https://bangbros.com/websites/workinglatinas', - slug: 'workinglatinas', - description: null, - network: 'bangbros', - parameters: { code: 'lw' }, - }, - { - name: 'MILF Lessons', - url: 'https://bangbros.com/websites/milflessons', - slug: 'milflessons', - description: null, - network: 'bangbros', - parameters: { code: 'ml' }, - }, - { - name: 'Mr. Anal', - url: 'https://bangbros.com/websites/mranal', - slug: 'mranal', - description: null, - network: 'bangbros', - parameters: { code: 'ma' }, - }, - // BLOWPASS - { - slug: '1000facials', - name: '1000 Facials', - alias: ['1kf'], - url: 'https://www.1000facials.com', - description: 'Welcome to 1000Facials.com, your source for the best facial porn with huge cumshots on your favorite teen and MILF pornstars. Watch all the blowjob action inside!', - network: 'blowpass', - parameters: { - latest: '/en/scenes/updates/%d/Category/0/Pornstar/0', - upcoming: '/en/scenes/upcoming', - }, - }, - { - slug: 'immorallive', - name: 'Immoral Live', - alias: ['il'], - url: 'https://www.immorallive.com', - description: 'Watch live sex shows and videos on ImmoralLive.com, featuring wild and crazy sex orgies, group sex, blowjob competitions and toy play from the famous Porno Dan. The hottest pornstars and amateur girls cum hard inside', - network: 'blowpass', - parameters: { - latest: '/en/videos/All-Categories/0/All-Pornstars/0/All/0/', - upcoming: '/en/videos/All-Categories/0/All-Pornstars/0/All/0/1/upcoming', - }, - }, - { - slug: 'mommyblowsbest', - name: 'Mommy Blows Best', - alias: ['mbb'], - url: 'https://www.mommyblowsbest.com', - description: 'Welcome to MommyBlowsBest.com. Home to thousands of MILF blowjobs and hot mom porn! Come see why experience counts, right here at MommyBlowsBest.com!', - network: 'blowpass', - parameters: { - latest: '/en/scenes/updates/0/Category/0/Actor/', - upcoming: '/en/scenes/upcoming', - }, - }, - { - slug: 'onlyteenblowjobs', - name: 'Only Teen Blowjobs', - alias: ['otb'], - url: 'https://www.onlyteenblowjobs.com', - description: 'OnlyTeenBlowjobs.com brings you the best teen blowjob porn featuring today\'s hottest young pornstars and amateurs. Watch as teens use their little mouths to suck and deepthroat the biggest of cocks!', - network: 'blowpass', - parameters: { - latest: '/en/scenes/updates/0/Category/0/Actor/', - upcoming: '/en/scenes/upcoming', - }, - }, - { - slug: 'throated', - name: 'Throated', - alias: ['ted'], - url: 'https://www.throated.com', - description: 'Throated.com is your portal for extreme throat fuck porn, face fucking videos and deepthroat gagging pornstars. Watch teens and MILFs go balls deep, swallowing cock in HD!', - network: 'blowpass', - parameters: { - latest: '/en/videos/latest/All-Categories/0/All-Pornstars/0/', - upcoming: '/en/videos/upcoming', - }, - }, - { - slug: 'sunlustxxx', - name: 'Sun Lust XXX', - url: 'https://www.sunlustxxx.com', - description: '', - network: 'blowpass', - show: true, // site offline, use only for indexing old scenes - }, - // BOOBPEDIA - { - slug: 'boobpedia', - name: 'Boobpedia', - url: 'https://www.boobpedia.com', - network: 'boobpedia', - }, - // BRAZZERS - { - slug: 'momsincontrol', - name: 'Moms in Control', - alias: ['mic'], - url: 'https://www.brazzers.com/sites/view/id/155/moms-in-control', - description: "There's nothing hotter than seeing a wholesome MILf get dirty, and that's exactly what MILFs in Control is all about: the hottest, sluttiest cougars in the business taking control of sexy situations to get exactly what they want. Feast your eyes as these mature beauties suck and fuck huge cocks, dominating big-dick studs and hot teen sluts until they get the cum that all MILFs crave!", - network: 'brazzers', - }, - { - slug: 'pornstarslikeitbig', - name: 'Pornstars Like It Big', - alias: ['plib'], - url: 'https://www.brazzers.com/sites/view/id/24/pornstars-like-it-big', - description: "A real big dick, that's what everyone wants. Porn-stars are no exception, all the biggest stars agree; BIG COCK is for them. Check out how it stretches their tiny pussies and cums on their round tits. We've got the best chicks jocking the biggest dicks.", - network: 'brazzers', - }, - { - slug: 'bigtitsatwork', - name: 'Big Tits at Work', - alias: ['btaw'], - url: 'https://www.brazzers.com/sites/view/id/15/big-tits-at-work', - description: 'Sitting at your desk, wishing you can fuck every busty coworker you have? Well, stop dreaming and step inside Big Tits At Work where you can watch real life work adventures caught on tape. Nothing But Big Breasted Work Professionals getting drilled all day long...', - network: 'brazzers', - }, - { - slug: 'bigtitsatschool', - name: 'Big Tits at School', - alias: ['btas'], - url: 'https://www.brazzers.com/sites/view/id/20/big-tits-at-school', - description: "The windows have been fogging up at Big Tits At School. Just take a peek inside one of our classrooms and you'll see our smoking hot busty students and big boobed dominant teachers getting their wet pussies stuffed with cock. Stay in your seat! you haven't been dismissed yet.", - network: 'brazzers', - }, - { - slug: 'babygotboobs', - name: 'Baby Got Boobs', - alias: ['bgb'], - url: 'https://www.brazzers.com/sites/view/id/9/baby-got-boobs', - description: "From fresh-faced teen to total slut, baby has boobs and she isn't afraid to show them. But does she know how to use them? These teens crave monster cock in their tight pussies, whether they're ready for a big dicking is another matter.", - network: 'brazzers', - }, - { - slug: 'realwifestories', - name: 'Real Wife Stories', - alias: ['rws'], - url: 'https://www.brazzers.com/sites/view/id/52/real-wife-stories', - description: "You might bring home the bacon, but your wife is still starving. That slut is hungry for cock, she can't get enough, and if you starve her any more she'll get it wherever she can. Better leave work early, or your big-titted wife might just have some giant cock getting squeezed into her waiting pussy, and it won't be yours.", - network: 'brazzers', - }, - { - slug: 'teenslikeitbig', - name: 'Teens Like It Big', - alias: ['tlib'], - url: 'https://www.brazzers.com/sites/view/id/51/teens-like-it-big', - description: "Whether they know it or not, teens love big stiff cocks in their tight pussies. Nothing goes better together than a tight, willing teen and a huge dick. In her bedroom or sneaking out to her boyfriend's, teens just want it all. Cum inside to see greedy sluts get more than they asked for", - network: 'brazzers', - }, - { - slug: 'zzseries', - name: 'ZZ Series', - alias: ['zzs'], - url: 'https://www.brazzers.com/sites/view/id/81/zz-series', - description: 'This is the spot for all our high-end content. ZZ series is exclusive footage that offers only the best in terms of story, stars and action. Check out the hottest porn-stars having the nastiest sex here at the ZZ series', - network: 'brazzers', - }, - { - slug: 'mommygotboobs', - name: 'Mommy Got Boobs', - alias: ['mgb'], - url: 'https://www.brazzers.com/sites/view/id/10/mommy-got-boobs', - description: "When hubby's away MILFS will play. Older women just crave cock, and they're experienced enough to know that only a young stud will do. Big-titted sluts everywhere are sucking and fucking in secret, giving it away to anybody they can. At Mommy Got Boobs, you can get some MILF of your own.", - network: 'brazzers', - }, - { - slug: 'milfslikeitbig', - name: 'MILFs Like It Big', - alias: ['mlib'], - url: 'https://www.brazzers.com/sites/view/id/36/milfs-like-it-big', - description: "When hubby's away milfy will play. These bored housewives want to get fucked and they want it now. They're experienced and know what they want. America's suburbs are full of these cum-de-sacs just waiting to get laid. Their round tits and thick asses are just begging for it. Cum inside, but don't park out front!", - network: 'brazzers', - }, - { - slug: 'bigtitsinuniform', - name: 'Big Tits In Uniform', - alias: ['btiu'], - url: 'https://www.brazzers.com/sites/view/id/73/big-tits-in-uniform', - description: "Big titted wonders are all around us, doing the toughest jobs in the tightest uniforms. Look at them just bursting out of that blouse, or over there, bulging under that nurse's uniform. You know when those tight uniforms come off these sluts go wild, sucking and fucking cocks left and right, their big tits just bouncing. I can't wait to punch the clock.", - network: 'brazzers', - }, - { - slug: 'doctoradventures', - name: 'Doctor Adventures', - alias: ['da'], - url: 'https://www.brazzers.com/sites/view/id/5/doctor-adventures', - description: 'Ever had fantasies about fucking your hot doctor? Live out your fantasies on doctoradventures.com. Countless doctor, patient scenarios come to life on this site with the sexiest and bustiest doctors imaginable! This is your one stop for the best in doctor porn in the world!', - network: 'brazzers', - }, - { - slug: 'brazzersexxtra', - name: 'Brazzers Exxtra', - alias: ['bex'], - url: 'https://www.brazzers.com/sites/view/id/152/brazzers-exxtra', - description: "\"Brazzers Exxtra\" is a doorway to new, unseen hardcore content! There are countless Brazzers videos that were not released throughout the years and we haven't been able to show them to you until now. Random videos staring the world's most popular pornstars, fresh new industry faces and a whole lot more! We'll even throw in an occasional free video from our friends at Mofos, Twisty's and Babes! Check it all out and let us know what you think. If you want more, we'll get it for you!", - network: 'brazzers', - }, - { - slug: 'bigtitsinsports', - name: 'Big Tits In Sports', - alias: ['btis'], - url: 'https://www.brazzers.com/sites/view/id/54/big-tits-in-sports', - description: 'Watch them bounce, watch them score and look at the way they handle those balls! Big tits in sports is here and so are the best big titted, athletic babes. Facials on the court and threesomes on the field, these busty sluts are ready for anything, even if it means playing dirty. Could you take them 1 on 1?', - network: 'brazzers', - }, - { - slug: 'brazzersvault', - name: 'Brazzers Vault', - url: 'https://www.brazzers.com/sites/view/id/56/brazzers-vault', - description: "We've got a whole super computer full of this stuff, technicians are working round the clock in the basement just to keep the thing from overheating. Yeah, this porno is hot. We need to get it out of before the whole thing melts down, that's why it's on the net, for you our loyal Brazzers Members. All the best scenes from all the best girls. In the World. Period.", - network: 'brazzers', - }, - { - slug: 'bigbuttslikeitbig', - name: 'Big Butts Like It Big', - alias: ['bblib'], - url: 'https://www.brazzers.com/sites/view/id/53/big-butts-like-it-big', - description: "You have to pair like with like. And big butts have to have big dicks to go with them. There's really no choice for these big round asses and the babes who fuck with them. Big assed bitches love it hard and deep, and won't have it any other way. Let the ass stuffing begin.", - network: 'brazzers', - }, - { - slug: 'bigwetbutts', - name: 'Big Wet Butts', - alias: ['bwb'], - url: 'https://www.brazzers.com/sites/view/id/8/big-wet-butts', - description: 'A nice, big, round butt is a special shape. Begging for powerful doggy style or straight anal penetration, cover a big butt in oil and it becomes a big wet butt, a true rarity. Watch these soft, tight asses get slathered and pounded like you only wish you could. Look at it bounce!', - network: 'brazzers', - }, - { - slug: 'daywithapornstar', - name: 'Day With A Pornstar', - alias: ['dwp'], - url: 'https://www.brazzers.com/sites/view/id/59/day-with-a-pornstar', - description: "We all know what our favorite stars can do on camera. We're familiar with the way they fuck and suck. What you don't get to see is what they do on their own time. Day With a Porn-star will show you everything, from crazy parties to total babe pals. Nobody else has access like this, it's the closest you get to living the dream.", - network: 'brazzers', - }, - { - slug: 'dirtymasseur', - name: 'Dirty Masseur', - alias: ['dm'], - url: 'https://www.brazzers.com/sites/view/id/150/dirty-masseur', - description: "Take a moment and unwind. Lay down, relax, and enjoy watching and wanking to these luscious Brazzers beauties getting good and greasy. Boobs, butts, and other lady-parts are at their prettiest when shimmering with slick oil. Book an appointment, and slide on in with a lubed babe. Believe me when I say, you'll have the happiest of endings...", - network: 'brazzers', - }, - { - slug: 'hotandmean', - name: 'Hot And Mean', - alias: ['ham'], - url: 'https://www.brazzers.com/sites/view/id/78/hot-and-mean', - description: "The hottest bitches run together. Hot, mean lesbians love to fuck each other and hate each other for being so beautiful. These lesbo sluts can't get enough pussy and love girl on girl action. Forget the dicks, these chicks don't need 'em. You can watch though, they love that.", - network: 'brazzers', - }, - { - slug: 'brazzersenespanol', - name: 'Brazzers en Español', - url: 'https://www.brazzers.com/sites/view/id/157/brazzers-en-espanol', - description: 'Brazzers en Español - El mejor sitio porno en alta definición del mundo ¡Ofreciéndole los vídeos para adultos en alta definición, descargables y en streaming, más exclusivos de Internet! Brazzers cuenta con las estrellas porno más sexys a través de los sitios más calientes en la red. Las estrellas porno y las escenas más calientes en internet. ¡Tendrá acceso a más sexo anal, tetas grandes y culos calientes de los que jamás soñó!', - network: 'brazzers', - }, - { - slug: 'brazzerslive', - name: 'Brazzers Live', - url: 'https://www.brazzers.com/sites/view/id/156/brazzers-live', - description: 'Brazzers is the industry leader for premium porn that breaks the mold. Pioneering its legendary LIVE SHOWS, ZZ is constantly redefining what hardcore erotica is about. Our wild fuck marathons are loaded with the steamiest improvised sex around. Catch a bevy of naked bodacious babes who ravage the biggest dicks with ease and in real-time. Our monster cock hunks rise to the occasion and feed these ravenous vixens who possess an insatiable appetite for cum.', - network: 'brazzers', - }, - { - slug: 'sexproadventures', - name: 'SexPro Adventures', - url: 'https://www.brazzers.com/sites/view/id/23/sexpro-adventures', - description: "Having trouble with your dick-style? The sex pros are here and they'll teach you everything you need to know to be a better man. At your place or theirs, these sluts just want to have a good time. Don't worry, she's a professional.", - network: 'brazzers', - }, - { - slug: 'shesgonnasquirt', - name: 'Shes Gonna Squirt', - url: 'https://www.brazzers.com/sites/view/id/151/shes-gonna-squirt', - description: "Enter the wet world of female ejaculation at shesgonnasquirt! Exclusive hardcore porn of your top pornstars squirting will excite you beyond belief. She's Gonna Squirt is home to the best in HD squirting sex videos. How to make a girl's pussy squirt is an art and should no longer remain a mystery, so join now to become a master.", - network: 'brazzers', - }, - { - slug: 'assesinpublic', - name: 'Asses In Public', - url: 'https://www.brazzers.com/sites/view/id/50/asses-in-public', - description: "Sex in public can present its challenges, never fear, we're willing to accept them. There's something hot about asses out in the street that we just can't deny. Porn-stars fucking on public or just hot girls showing their asses in the airport, we've got both and then some. Asses in Public has the roundest asses and the biggest tits just hanging out, where WILL we show up next?", - network: 'brazzers', - }, - { - slug: 'bustyz', - name: 'Bustyz', - url: 'https://www.brazzers.com/sites/view/id/6/bustyz', - description: "If the internet was a town we'd have the biggest tits around. We still do though, because Bustyz features only the best endowed porn stars in solo and group action. Watch these big-titted babes take cock, suck twat and show off their massive jugs. Real or fake, we don't judge, everyone's welcome under the big tit tent", - network: 'brazzers', - }, - { - slug: 'bustyandreal', - name: 'Busty & Real', - url: 'https://www.brazzers.com/sites/view/id/2/busty-real', - description: "Sometimes you need to take a break from the silicon football set. Busty and real has all the real jugs you need. Round. Soft. and as real as they come. These babes are rocking exactly what momma gave them. They've not afraid to show off their assets and get slammed with dick in the process.", - network: 'brazzers', - }, - { - slug: 'hotchicksbigasses', - name: 'Hot Chicks Big Asses', - url: 'https://www.brazzers.com/sites/view/id/7/hot-chicks-big-asses', - description: 'Everyone gather round; the giant ass. A babe can be hot in a lot of ways and having a big round ass is one of the best. All shapes, sizes and types these girls are the best of the best. Round, supple, jiggling asses taking on dicks and other pussies in equal measure.', - network: 'brazzers', - }, - { - slug: 'cfnm', - name: 'CFNM', - url: 'https://www.brazzers.com/sites/view/id/154/cfnm', - description: "Welcome to the world of clothed female sluts fucking, humiliating and dominating naked men, giving them a dose of what it feels like to be owned. If you love women with power dominating wimpy guys and showing them who's boss; women who crave for cock but get it exactly how they want it, that's what you'll find here. Simply put, the guys don't fuck the women, the women fuck the guys and make them feel like whores!", - network: 'brazzers', - }, - { - slug: 'jugfuckers', - name: 'JugFuckers', - url: 'https://www.brazzers.com/sites/view/id/12/jugfuckers', - description: "Like a sex hot-dog, a big dick fits nicely between two soft, round tits. Tit-fucking isn't easy and never will be. Our girls are pros and take big loads on their faces and tits with a smile. From DD to the smallest things going, we've got every type of tit- fuck around.", - network: 'brazzers', - }, - { - slug: 'teenslikeitblack', - name: 'Teens Like It Black', - url: 'https://www.brazzers.com/sites/view/id/57/teens-like-it-black', - description: "Teens just wanna piss their parents off; no rules, spring break, big black cocks. They love pushing things to the limit, how big and black can it be? Only teen girls know. Watch them get more than they bargained for, long black cocks drilling their tight, inexperienced pussies. It's an epic fuck when the biggest and the tightest meet.", - network: 'brazzers', - }, - { - slug: 'racksandblacks', - name: 'Racks & Blacks', - url: 'https://www.brazzers.com/sites/view/id/11/racks-blacks', - description: "All the interracial action you need is here. Big 'ol black cocks ramming and jamming pussies to the limit. All types of different girls fall prey to the venerable black dick. Wet pussies and fat asses? Bring it on. There's nothing our stable of asses can't handle, they'll keep cumming and cumming.", - network: 'brazzers', - }, - { - slug: 'buttsandblacks', - name: 'Butts & Blacks', - url: 'https://www.brazzers.com/sites/view/id/3/butts-blacks', - description: "Giant black dicks paired with round asses and garnished with the tightest pussies of all colors. Butts and Blacks delivers on its name sake, only the biggest dicks rocking the thickest chicks. These round honeys can take it all in and bounce around like it's a pogo stick. Come check out these soft round asses getting the attention they deserve.", - network: 'brazzers', - }, - // BURNING ANGEL - { - name: 'Burning Angel', - slug: 'burningangel', - alias: ['burna'], - url: 'https://www.burningangel.com', - network: 'burningangel', - parameters: { independent: true }, - }, - // CHERRY PIMPS - { - slug: 'cherrypimps', - name: 'Cherry Pimps', - alias: ['cps'], - url: 'https://cherrypimps.com', - description: 'CherryPimps your premium porn site to Download and Stream the hottest and most exclusive 4K HD videos and pictures on your phone, tablet, TV or console.', - network: 'cherrypimps', - parameters: { - extract: true, - }, - }, - { - slug: 'wildoncam', - name: 'Wild On Cam', - alias: ['woc'], - url: 'https://wildoncam.com', - tags: ['live'], - network: 'cherrypimps', - }, - { - slug: 'britneyamber', - name: 'Britney Amber', - url: 'https://www.britneyamber.com', - network: 'cherrypimps', - parameters: { - extract: true, - }, - }, - // DDF NETWORK - { - slug: 'ddfbusty', - name: 'DDF Busty', - alias: ['ddfb'], - url: 'https://ddfbusty.com', - description: 'Gorgeous Babes with big tits and Euro pornstars with huge natural boobs filmed in Exclusive Full HD, 4K, & VR porn videos.', - network: 'ddfnetwork', - }, - { - slug: 'handsonhardcore', - name: 'Hands on Hardcore', - alias: ['hoh'], - url: 'https://handsonhardcore.com', - description: 'Hardcore Sex & Anal Fucking Exclusive XXX Videos in VR, 4K and full HD with Hot European Pornstars', - network: 'ddfnetwork', - }, - { - slug: 'houseoftaboo', - name: 'House of Taboo', - alias: ['hotb', 'hotab'], - url: 'https://houseoftaboo.com', - description: 'Exclusive BDSM Porn & Extreme Sex Videos Produced in VR, 4K and full HD with The Hottest European Fetish Pornstars', - network: 'ddfnetwork', - }, - { - slug: 'ddfnetworkvr', - name: 'DDF Network VR', - alias: ['ddfvr'], - url: 'https://ddfnetworkvr.com', - description: 'VR Porn Videos shot Exclusively in 180 3D 4K Virtual Reality featuring the Hottest European & American VR Pornstar Babes', - network: 'ddfnetwork', - }, - { - slug: 'eurogirlsongirls', - name: 'Euro Girls on Girls', - url: 'https://eurogirlsongirls.com', - description: 'Hot Lesbian Sex & Glamour Lesbian Porn Videos and Photos Starring Gorgeous European Pornstars in 4K and Full HD VR.', - network: 'ddfnetwork', - }, - { - slug: '1byday', - name: '1By-Day', - url: 'https://1by-day.com', - description: 'Ultra Sexy Exclusive Solo Masturbation Videos in VR, 4K and full HD showcasing Glamour Babes & Intense Orgasms', - network: 'ddfnetwork', - }, - { - slug: 'euroteenerotica', - name: 'Euro Teen Erotica', - alias: ['ete'], - url: 'https://euroteenerotica.com', - description: 'Teen Threesomes & Barely Legal Porn Videos in 4K, VR and FULL HD with Hot Nymphomaniac Teen Babes', - network: 'ddfnetwork', - }, - { - slug: 'hotlegsandfeet', - name: 'Hot Legs and Feet', - url: 'https://hotlegsandfeet.com', - description: 'Foot Fetish & Sexy Legs Porn Videos with Hot and Sexy Euro Pornstars', - network: 'ddfnetwork', - }, - { - slug: 'onlyblowjob', - name: 'Only Blowjob', - alias: ['obj'], - url: 'https://onlyblowjob.com', - description: 'Fantasy Blowjobs & POV Cock Sucking Videos and Photos Produced in VR, 4K and full HD featuring Sexy European Pornstars', - network: 'ddfnetwork', - }, - { - slug: 'fuckinhd', - name: 'Fuck in HD', - url: 'https://fuckinhd.com', - description: 'HD Hardcore Sex & XXX Fantasy Porn Videos and Photos Produced in full HD featuring a Variety of Hardcore Porn Niches.', - network: 'ddfnetwork', - parameters: { native: true }, - }, - { - slug: 'bustylover', - name: 'Busty Lover', - url: 'https://bustylover.com', - network: 'ddfnetwork', - parameters: { native: true }, - }, - // DIGITAL PLAYGROUND - { - slug: 'digitalplayground', - name: 'Digital Playground', - url: 'https://www.digitalplayground.com/scenes', - description: '', - parameters: { extract: true }, - network: 'digitalplayground', - }, - { - slug: 'episodes', - name: 'Episodes', - url: 'https://www.digitalplayground.com/scenes?site=206', - description: '', - network: 'digitalplayground', - }, - { - slug: 'flixxx', - name: 'Flixxx', - url: 'https://www.digitalplayground.com/scenes?site=207', - description: '', - network: 'digitalplayground', - }, - { - slug: 'rawcut', - name: 'Raw Cut', - url: 'https://www.digitalplayground.com/scenes?site=208', - description: '', - network: 'digitalplayground', - }, - { - slug: 'dpstarepisodes', - name: 'DP Star Episodes', - url: 'https://www.digitalplayground.com/scenes?site=209', - description: '', - network: 'digitalplayground', - }, - { - slug: 'blockbuster', - name: 'Blockbuster', - url: 'https://www.digitalplayground.com/scenes?site=211', - description: '', - network: 'digitalplayground', - }, - { - slug: 'dpparodies', - name: 'DP Parodies', - url: 'https://www.digitalplayground.com/scenes?site=212', - description: '', - tags: ['parody'], - network: 'digitalplayground', - }, - // DOGFART NETWORK - { - slug: 'blacksonblondes', - name: 'Blacks On Blondes', - url: 'https://www.blacksonblondes.com/tour', - description: 'Blacks On Blondes is the Worlds Largest and Best Interracial Sex and Interracial Porn website. Black Men and White Women. BlacksOnBlondes has 23 years worth of Hardcore Interracial Content. Featuring the entire Legendary Dogfart Movie Archive', - network: 'dogfartnetwork', - }, - { - slug: 'cuckoldsessions', - name: 'Cuckold Sessions', - url: 'https://www.cuckoldsessions.com/tour', - description: 'Dogfart, the #1 Interracial Network in the World Presents CuckoldSessions.com/tour - Hardcore Cuckold Fetish Videos', - network: 'dogfartnetwork', - }, - { - slug: 'gloryhole', - name: 'Glory Hole', - url: 'https://www.gloryhole.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'blacksoncougars', - name: 'Blacks On Cougars', - url: 'https://www.blacksoncougars.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'wefuckblackgirls', - name: 'We Fuck Black Girls', - alias: ['wfbg'], - url: 'https://www.wefuckblackgirls.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'watchingmymomgoblack', - name: 'Watching My Mom Go Black', - url: 'https://www.watchingmymomgoblack.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'interracialblowbang', - name: 'Interracial Blowbang', - url: 'https://www.interracialblowbang.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'cumbang', - name: 'Cumbang', - url: 'https://www.cumbang.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'interracialpickups', - name: 'Interracial Pickups', - url: 'https://www.interracialpickups.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'watchingmydaughtergoblack', - name: 'Watching My Daughter Go Black', - url: 'https://www.watchingmydaughtergoblack.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'zebragirls', - name: 'Zebra Girls', - url: 'https://www.zebragirls.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'gloryholeinitiations', - name: 'Gloryhole Initiations', - url: 'https://www.gloryhole-initiations.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'dogfartbehindthescenes', - name: 'Dogfart Behind The Scenes', - url: 'https://www.dogfartbehindthescenes.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'blackmeatwhitefeet', - name: 'Black Meat White Feet', - url: 'https://www.blackmeatwhitefeet.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'springthomas', - name: 'Spring Thomas', - url: 'https://www.springthomas.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'katiethomas', - name: 'Katie Thomas', - url: 'https://www.katiethomas.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'ruthblackwell', - name: 'Ruth Blackwell', - url: 'https://www.ruthblackwell.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'candymonroe', - name: 'Candy Monroe', - url: 'https://www.candymonroe.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'wifewriting', - name: 'Wife Writing', - url: 'https://www.wifewriting.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'barbcummings', - name: 'Barb Cummings', - url: 'https://www.barbcummings.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'theminion', - name: 'The Minion', - url: 'https://www.theminion.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'blacksonboys', - name: 'Blacks On Boys', - url: 'https://www.blacksonboys.com/tour', - description: '', - network: 'dogfartnetwork', - }, - { - slug: 'gloryholesandhandjobs', - name: 'Gloryholes And Handjobs', - url: 'https://www.gloryholesandhandjobs.com/tour', - description: '', - network: 'dogfartnetwork', - }, - // EVIL ANGEL - { - slug: 'evilangel', - name: 'Evil Angel', - url: 'https://www.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: { independent: true }, - network: 'evilangel', - }, - // FAKE HUB - { - slug: 'fakeagentuk', - name: 'Fake Agent UK', - url: 'https://www.fakehub.com/scenes?site=277', - description: '', - network: 'fakehub', - }, - { - slug: 'fakecop', - name: 'Fake Cop', - url: 'https://www.fakehub.com/scenes?site=278', - description: '', - network: 'fakehub', - }, - { - slug: 'fakehospital', - name: 'Fake Hospital', - url: 'https://www.fakehub.com/scenes?site=279', - description: '', - network: 'fakehub', - }, - { - slug: 'fakeagent', - name: 'Fake Agent', - alias: ['fka'], - url: 'https://www.fakehub.com/scenes?site=280', - description: '', - network: 'fakehub', - }, - { - slug: 'faketaxi', - name: 'Fake Taxi', - alias: ['ftx'], - url: 'https://www.fakehub.com/scenes?site=281', - description: '', - network: 'fakehub', - }, - { - slug: 'publicagent', - name: 'Public Agent', - alias: ['pba'], - url: 'https://www.fakehub.com/scenes?site=282', - description: '', - network: 'fakehub', - }, - { - slug: 'femaleagent', - name: 'Female Agent', - url: 'https://www.fakehub.com/scenes?site=283', - description: '', - network: 'fakehub', - }, - { - slug: 'femalefaketaxi', - name: 'Female Fake Taxi', - alias: ['fft'], - url: 'https://www.fakehub.com/scenes?site=284', - description: '', - network: 'fakehub', - }, - { - slug: 'fakedrivingschool', - name: 'Fake Driving School', - alias: ['fds'], - url: 'https://www.fakehub.com/scenes?site=285', - description: '', - network: 'fakehub', - }, - { - slug: 'fakehuboriginals', - name: 'Fake Hub Originals', - alias: ['fho'], - url: 'https://www.fakehub.com/scenes?site=287', - description: '', - network: 'fakehub', - }, - { - slug: 'fakehostel', - name: 'Fake Hostel', - alias: ['fhl'], - url: 'https://www.fakehub.com/scenes?site=288', - description: '', - network: 'fakehub', - }, - // FAME DIGITAL - { - slug: 'devilsfilm', - name: 'Devil\'s Film', - url: 'https://www.devilsfilm.com', - description: 'Welcome to the best porn network, DevilsFilm.com, featuring teens, MILFs, trans and interracial porn with all of your favorite pornstars in 4k ultra HD!', - parameters: { api: true }, - network: 'famedigital', - }, - { - slug: 'lowartfilms', - name: 'Low Art Films', - url: 'https://www.lowartfilms.com', - description: 'Artistic Hardcore Porn Videos', - network: 'famedigital', - parameters: { - latest: '/en/All/scenes/0/latest/', - upcoming: '/en/All/scenes/0/upcoming', - }, - }, - { - slug: 'daringsex', - name: 'Daring Sex', - url: 'https://www.daringsexhd.com/', - description: 'Welcome the official Daring Sex site, home of high quality erotica, sensual porn and hardcore exploration of the darker side of sexuality. Here you will find a variety of videos for lovers looking for a bit of extra, or something darker with an element of control.', - network: 'famedigital', - parameters: { api: true }, - show: false, // no data sources - }, - { - slug: 'peternorth', - name: 'Peter North', - url: 'https://www.peternorth.com', - description: 'PeterNorth.com features hundreds of cumshots and deepthroat blowjob videos with the hottest teens & MILFs. Watch 25 years of Peter North inside!', - network: 'famedigital', - parameters: { - latest: '/en/videos/AllCategories/0/3/0/All-Dvds/0/latest/', - upcoming: '/en/videos/AllCategories/0/3/0/All-Dvds/0/upcoming', - }, - }, - { - slug: 'roccosiffredi', - name: 'Rocco Siffredi', - url: 'https://www.roccosiffredi.com', - description: 'Welcome to the official RoccoSiffredi.com, the Italian Stallion, with hardcore anal fucking and rough sex from the man himself who has coined the term hardcore.', - parameters: { api: true }, - network: 'famedigital', - }, - { - slug: 'silverstonedvd', - name: 'Silverstone DVD', - url: 'https://www.silverstonedvd.com', - description: 'Welcome to SilverStoneDVDs.com to enjoy unlimited streaming & downloads of teen porn, hot latina anal, young and dumb blowjob, DPs and hardcore porn.', - network: 'famedigital', - parameters: { - latest: '/en/All/scenes/0/latest/', - upcoming: '/en/All/scenes/0/upcoming', - }, - }, - { - slug: 'silviasaint', - name: 'Silvia Saint', - url: 'https://www.silviasaint.com', - description: 'Welcome to Silvia Saint official website. You can see Silvia Saint videos, pictures and blog!', - network: 'famedigital', - parameters: { - latest: '/en/scenes/All/0/', - upcoming: '/en/scenes/All/0/1/upcoming', - }, - }, - { - slug: 'whiteghetto', - name: 'White Ghetto', - url: 'https://www.whiteghetto.com', - description: 'Welcome to WhiteGhetto.com. Home of MILFs, GILFs, Midget porn, Indian babes, hairy pussies and more unusual and oddity porn!', - network: 'famedigital', - parameters: { - latest: '/en/scenes/All/0/superCat/0/latest/', - upcoming: '/en/scenes/All/0/superCat/0/upcoming', - }, - }, - // FANTASY MASSAGE - // Club Fantasy Massage is an aggregate site - { - slug: 'fantasymassage', - name: 'Fantasy Massage', - alias: ['fms'], - url: 'https://www.fantasymassage.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/fantasymassage/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/fantasymassage/AllCategories/0/Actor/0/upcoming/', - }, - }, - { - slug: 'allgirlmassage', - name: 'All Girl Massage', - alias: ['agm'], - url: 'https://www.allgirlmassage.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/allgirlmassage/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/allgirlmassage/AllCategories/0/Actor/0/upcoming/', - photos: 'https://www.fantasymassage.com/en/photo', - }, - }, - { - slug: 'nurumassage', - name: 'Nuru Massage', - alias: ['num'], - url: 'https://www.nurumassage.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/nurumassage/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/nurumassage/AllCategories/0/Actor/0/upcoming/', - photos: 'https://www.fantasymassage.com/en/photo', - }, - }, - { - slug: 'trickyspa', - name: 'Tricky Spa', - alias: ['tspa'], - url: 'https://www.trickyspa.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/trickyspa/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/trickyspa/AllCategories/0/Actor/0/upcoming/', - photos: 'https://www.fantasymassage.com/en/photo', - }, - }, - { - slug: 'soapymassage', - name: 'Soapy Massage', - url: 'https://www.soapymassage.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/soapymassage/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/soapymassage/AllCategories/0/Actor/0/upcoming/', - photos: 'https://www.fantasymassage.com/en/photo', - }, - }, - { - slug: 'milkingtable', - name: 'Milking Table', - url: 'https://www.milkingtable.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/milkingtable/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/milkingtable/AllCategories/0/Actor/0/upcoming/', - photos: 'https://www.fantasymassage.com/en/photo', - }, - }, - { - slug: 'massageparlor', - name: 'Massage Parlor', - url: 'https://www.massage-parlor.com', - network: 'fantasymassage', - parameters: { - latest: 'https://www.fantasymassage.com/en/allvideos/massage-parlor/AllCategories/0/AllPornstars/0/updates/', - upcoming: 'https://www.fantasymassage.com/en/allvideos/massage-parlor/AllCategories/0/Actor/0/upcoming/', - photos: 'https://www.fantasymassage.com/en/photo', - }, - }, - // FREEONES - { - slug: 'freeones', - name: 'FreeOnes', - url: 'https://www.freeones.com', - network: 'freeones', - }, - { - slug: 'freeoneslegacy', - name: 'FreeOnes (Legacy)', - url: 'https://www.freeones.com', - network: 'freeones', - }, - // FULL PORN NETWORK - { - slug: 'analbbc', - name: 'Anal BBC', - url: 'https://analbbc.com', - tags: ['anal', 'bbc'], - network: 'fullpornnetwork', - }, - { - slug: 'analviolation', - name: 'Anal Violation', - url: 'https://analviolation.com', - tags: ['anal'], - network: 'fullpornnetwork', - }, - { - slug: 'analized', - name: 'ANALIZED', - url: 'https://analized.com', - tags: ['anal'], - network: 'fullpornnetwork', - }, - { - slug: 'baddaddypov', - name: 'Bad Daddy POV', - alias: ['bdpov'], - url: 'https://baddaddypov.com', - tags: ['pov', 'family'], - network: 'fullpornnetwork', - }, - { - slug: 'dtfsluts', - name: 'DTF Sluts', - url: 'https://dtfsluts.com', - network: 'fullpornnetwork', - }, - { - slug: 'girlfaction', - name: 'Girlfaction', - url: 'https://girlfaction.com', - tags: ['lesbian'], - network: 'fullpornnetwork', - }, - { - slug: 'hergape', - name: 'Her Gape', - url: 'https://hergape.com', - tags: ['anal'], - network: 'fullpornnetwork', - }, - { - slug: 'homemadeanalwhores', - name: 'Homemade Anal Whores', - url: 'https://homemadeanalwhores.com', - tags: ['anal'], - network: 'fullpornnetwork', - }, - { - slug: 'jamesdeen', - name: 'James Deen', - url: 'https://jamesdeen.com', - network: 'fullpornnetwork', - }, - { - slug: 'onlyprince', - name: 'Only Prince', - url: 'https://onlyprince.com', - tags: ['bbc'], - network: 'fullpornnetwork', - }, - { - slug: 'pervertgallery', - name: 'Pervert Gallery', - url: 'http://pervertgallery.com', - network: 'fullpornnetwork', - }, - { - slug: 'povperverts', - name: 'POV Perverts', - url: 'http://povperverts.net', - tags: ['pov'], - network: 'fullpornnetwork', - }, - { - slug: 'teenageanalsluts', - name: 'Teenage Anal Sluts', - url: 'https://teenageanalsluts.com', - tags: ['anal'], - network: 'fullpornnetwork', - }, - { - slug: 'twistedvisual', - name: 'Twisted Visual', - url: 'https://twistedvisual.com', - network: 'fullpornnetwork', - }, - { - slug: 'yourmomdoesanal', - name: 'Your Mom Does Anal', - url: 'http://yourmomdoesanal.com', - tags: ['anal', 'milf'], - network: 'fullpornnetwork', - }, - { - slug: 'yourmomdoesporn', - name: 'Your Mom Does Porn', - url: 'https://yourmomdoesporn.com', - tags: ['milf'], - network: 'fullpornnetwork', - }, - { - slug: 'mugfucked', - name: 'Mugfucked', - url: 'https://mugfucked.com', - tags: ['facefucking', 'blowjob'], - network: 'fullpornnetwork', - }, - // GIRLSWAY - { - slug: 'girlsway', - name: 'Girlsway', - alias: ['gw'], - url: 'https://www.girlsway.com', - description: 'Girlsway.com has the best lesbian porn videos online! The hottest pornstars & first time lesbians in real girl on girl sex, tribbing, squirting & pussy licking action right HERE!', - tags: ['lesbian'], - network: 'girlsway', - parameters: { - scene: 'https://www.girlsway.com/en/video/girlsway', - }, - }, - { - slug: 'girlstryanal', - name: 'Girls Try Anal', - alias: ['gta'], - url: 'https://www.girlstryanal.com', - network: 'girlsway', - parameters: { - referer: 'https://www.girlsway.com', - mobile: 'https://m.dpfanatics.com/en/video', - }, - }, - { - slug: 'mommysgirl', - name: 'Mommy\'s Girl', - alias: ['mmgs'], - url: 'https://www.mommysgirl.com', - network: 'girlsway', - parameters: { - mobile: 'https://m.dpfanatics.com/en/video', - }, - }, - { - slug: 'webyoung', - name: 'Web Young', - url: 'https://www.webyoung.com', - network: 'girlsway', - parameters: { - referer: 'https://www.girlsway.com', - mobile: 'https://m.dpfanatics.com/en/video', - }, - }, - { - slug: 'sextapelesbians', - name: 'Sex Tape Lesbians', - url: 'https://www.sextapelesbians.com', - network: 'girlsway', - parameters: { - scene: 'https://www.girlsway.com/en/video/sextapelesbians', // sextapelesbians.com redirects to isthisreal.com - referer: 'https://www.girlsway.com', - }, - }, - { - slug: 'momsonmoms', - name: 'Moms On Moms', - url: 'https://www.girlsway.com/en/videos/momsonmoms', - network: 'girlsway', - parameters: { - scene: 'https://www.girlsway.com/en/video/sextapelesbians', - referer: 'https://www.girlsway.com', - }, - }, - // HUSSIE PASS - { - slug: 'hussiepass', - name: 'Hussie Pass', - url: 'https://www.hussiepass.com', - network: 'hussiepass', - }, - { - slug: 'eyeontheguy', - name: 'Eye On The Guy', - url: 'https://eyeontheguy.com', - tags: ['male-focus'], - network: 'hussiepass', - parameters: { - t1: true, - }, - }, - { - slug: 'seehimfuck', - name: 'See Him Fuck', - url: 'https://seehimfuck.com', - tags: ['male-focus'], - network: 'hussiepass', - parameters: { - tour: true, - }, - }, - { - slug: 'interracialpovs', - name: 'Interracial POVs', - url: 'https://www.interracialpovs.com', - tags: ['interracial', 'pov'], - network: 'hussiepass', - parameters: { - tour: true, - }, - }, - { - slug: 'povpornstars', - name: 'POV Pornstars', - url: 'http://www.povpornstars.com', - tags: ['pov'], - network: 'hussiepass', - parameters: { - latest: 'http://www.povpornstars.com/tour/categories/movies_%d_d.html', - profile: 'http://www.povpornstars.com/tour/models/%s.html', - tour: true, - }, - }, - // HUSH PASS - { - slug: 'shotherfirst', - name: 'Shot Her First', - url: 'https://shotherfirst.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/shot-her-first_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'whitezilla', - name: 'WhiteZilla', - url: 'https://whitezilla.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/whitezilla_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'frathousefuckfest', - name: 'Frat House Fuck Fest', - url: 'https://frathousefuckfest.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/frat-house-fuck-fest_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'freakyfirsttimers', - name: 'Freaky First Timers', - url: 'https://freakyfirsttimers.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/freaky-first-timers_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'milfinvaders', - name: 'MILF Invaders', - url: 'https://milfinvaders.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/milf-invaders_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'housewivesneedcash', - name: 'Housewives Need Cash', - url: 'https://housewivesneedcash.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/housewives-need-cash_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'bubblebuttbonanza', - name: 'Bubble Butt Bonanza', - url: 'https://bubblebuttbonanza.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/bubble-butt-bonanza_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'suburbansexparty', - name: 'Suburban Sex Party', - url: 'https://suburbansexparty.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/suburban-sex-party_%_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'buttnakedinthestreets', - name: 'Butt Naked In The Streets', - url: 'https://buttnakedinthestreets.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/ButtNakedInStreets_%d_d.html', - media: 'https://hushpass.com', - match: 'Butt Naked In Streets', - t1: true, - }, - }, - { - slug: 'muffbumperpatrol', - name: 'Muff Bumper Patrol', - url: 'https://muffbumperpatrol.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/muff-bumper-patrol_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'biggathananigga', - name: 'Bigga Than A Nigga', - url: 'https://biggathananigga.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/bigga-than-a-nigga_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'bachelorpartyfuckfest', - name: 'Bachelor Party Fuck Fest', - url: 'https://bachelorpartyfuckfest.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/bachelor-party-fuck-fest_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'teencumdumpsters', - name: 'Teen Cum Dumpsters', - url: 'https://teencumdumpsters.com', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/teen-cum-dumpsters_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'povhunnies', - name: 'POV Hunnies', - network: 'hushpass', - parameters: { - latest: 'https://hushpass.com/t1/categories/POVHunnies_%d_d.html', - media: 'https://hushpass.com', - t1: true, - }, - }, - { - slug: 'hushpass', - name: 'Hush Pass', - url: 'https://hushpass.com', - network: 'hushpass', - parameters: { - t1: true, - accFilter: true, - }, - }, - // INTERRACIAL PASS - { - slug: '2bigtobetrue', - name: '2 Big To Be True', - url: 'https://www.2bigtobetrue.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/2-big-to-be-true_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'abominableblackman', - name: 'Abominable Black Man', - url: 'https://www.abominableblackman.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/abominable-black-man_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'bootyannihilation', - name: 'Booty Annihilation', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/BootyAnnihilation_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'daddysworstnightmare', - name: 'Daddy\'s Worst Nightmare', - url: 'https://www.daddysworstnightmare.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/daddys-worst-nightmare_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'monstercockfuckfest', - name: 'Monster Cock Fuck Fest', - url: 'https://www.monstercockfuckfest.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/monster-cock-fuck-fest_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'mydaughtersfuckingablackdude', - name: 'My Daughter\'s Fucking A Black Dude', - url: 'https://www.mydaughtersfuckingablackdude.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/my-daughters-fucking-a-black-dude_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'mymomsfuckingblackzilla', - name: 'My Mom\'s Fucking Blackzilla', - url: 'https://www.mymomsfuckingblackzilla.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/my-moms-fucking-blackzilla_%d_d.html', - media: 'https://www.interracialpass.com', - t1: true, - }, - }, - { - slug: 'mywifesfirstmonstercock', - name: 'My Wife\'s First Monster Cock', - url: 'https://www.mywifesfirstmonstercock.com/', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - latest: 'https://www.interracialpass.com/t1/categories/my-wifes-first-monster-cock_%d_d.html', - media: 'https://www.interracialpass.com', - match: 'My Wifes First Monster Cock', - t1: true, - }, - }, - { - slug: 'interracialpass', - name: 'Interracial Pass', - url: 'https://www.interracialpass.com', - tags: ['interracial'], - network: 'interracialpass', - parameters: { - t1: true, - accFilter: true, - }, - }, - // INSEX - { - slug: 'sexuallybroken', - name: 'Sexually Broken', - alias: ['seb'], - url: 'https://www.sexuallybroken.com', - tags: ['bdsm'], - network: 'insex', - }, - { - slug: 'infernalrestraints', - name: 'Infernal Restraints', - alias: ['infr'], - url: 'https://www.infernalrestraints.com', - tags: ['bdsm'], - network: 'insex', - }, - { - slug: 'hardtied', - name: 'Hardtied', - url: 'https://www.hardtied.com', - tags: ['bdsm'], - network: 'insex', - }, - { - slug: 'realtimebondage', - name: 'Real Time Bondage', - alias: ['rtb'], - url: 'https://www.realtimebondage.com', - tags: ['bdsm', 'live'], - network: 'insex', - }, - { - slug: 'topgrl', - name: 'TopGrl', - alias: ['tg'], - url: 'https://www.topgrl.com', - tags: ['bdsm', 'femdom'], - network: 'insex', - }, - { - slug: 'paintoy', - name: 'Paintoy', - url: 'https://www.paintoy.com', - tags: ['bdsm'], - network: 'insex', - }, - { - slug: 'aganmedon', - name: 'Agan Medon', - url: 'https://www.aganmedon.com', - tags: ['bdsm', 'animated'], - network: 'insex', - }, - { - slug: 'sensualpain', - name: 'Sensual Pain', - url: 'https://www.sensualpain.com', - tags: ['bdsm'], - network: 'insex', - }, - // JAYS POV - { - slug: 'jayspov', - name: 'Jay\'s POV', - url: 'https://jayspov.net', - network: 'jayrock', - }, - { - slug: 'cospimps', - name: 'CosPimps', - url: 'https://cospimps.com', - network: 'jayrock', - }, - { - slug: 'blackforwife', - name: 'Black for Wife', - url: 'https://www.blackforwife.com', - network: 'jayrock', - parameters: { - referer: 'https://freetour.adulttime.com/en/blackforwife', - useGamma: true, - scene: false, - deep: 'https://21sextury.com/en/video', - photos: false, - }, - }, - // JESSE LOADS MONSTER FACIALS - { - slug: 'jesseloadsmonsterfacials', - name: 'Jesse Loads Monster Facials', - url: 'http://www.jesseloadsmonsterfacials.com', - network: 'jesseloadsmonsterfacials', - tags: ['facial', 'blowjob'], - parameters: { - independent: true, - }, - }, - // JULES JORDAN - { - slug: 'julesjordan', - name: 'Jules Jordan', - url: 'https://www.julesjordan.com', - description: 'Jules Jordan\'s Official Membership Site', - network: 'julesjordan', - }, - { - slug: 'theassfactory', - name: 'The Ass Factory', - url: 'https://www.theassfactory.com', - network: 'julesjordan', - }, - { - slug: 'spermswallowers', - name: 'Sperm Swallowers', - url: 'https://www.spermswallowers.com', - network: 'julesjordan', - }, - { - slug: 'manuelferrara', - name: 'Manuel Ferrara', - alias: ['mfa'], - url: 'https://www.manuelferrara.com', - network: 'julesjordan', - }, - { - slug: 'girlgirl', - name: 'Girl Girl', - url: 'https://www.girlgirl.com', - tags: ['lesbian'], - network: 'julesjordan', - }, - // KELLY MADISON MEDIA - { - slug: 'teenfidelity', - name: 'Teen Fidelity', - alias: ['tf'], - url: 'https://www.teenfidelity.com', - description: 'Home of Kelly Madison and Ryan Madison', - network: 'kellymadison', - }, - { - slug: 'pornfidelity', - name: 'Porn Fidelity', - alias: ['pf'], - url: 'https://www.pornfidelity.com', - description: 'Home of Kelly Madison and Ryan Madison', - network: 'kellymadison', - }, - { - slug: 'kellymadison', - name: 'Kelly Madison', - url: 'https://www.pornfidelity.com', - description: 'Home of Kelly Madison and Ryan Madison', - network: 'kellymadison', - }, - // KINK - { - slug: 'thirtyminutesoftorment', - name: '30 Minutes of Torment', - url: 'https://www.kink.com/channel/30minutesoftorment', - description: 'Thick-Muscled Men Endure 30 Minutes Of BDSM Torment By A Pain-Inducing Dom. Can they take 30 Minutes of Torment? Watch as top gay pornstars take on the challenge of a lifetime. Bondage, BDSM, punishment, huge insertions, & more!', - network: 'kink', - }, - { - slug: 'boundgangbangs', - name: 'Bound Gangbangs', - alias: ['bgb', 'bgbs'], - url: 'https://www.kink.com/channel/boundgangbangs', - description: 'Powerless whores tied in bondage and stuffed with a cock in every hole. At BoundGangbangs women get surprise extreme gangbangs, blindfolds, deepthroat blowjobs, sex punishment, bondage, double penetration and interracial sex.', - network: 'kink', - }, - { - slug: 'boundgods', - name: 'Bound Gods', - url: 'https://www.kink.com/channel/boundgods', - description: 'Muscle Studs Are Bound, Gagged & Spread For A Deep Cock Pounding. Not even the most rock hard muscled studs can escape punishment & submission on BoundGods.com Watch the hottest studs get tied down, fucked & submitted.', - tags: ['gay'], - network: 'kink', - }, - { - slug: 'boundinpublic', - name: 'Bound in Public', - url: 'https://www.kink.com/channel/boundinpublic', - description: 'Cum Starved Sluts Humiliated And Fucked Hard In Public By Hung Studs.', - network: 'kink', - }, - { - slug: 'brutalsessions', - name: 'Brutal Sessions', - url: 'https://www.kink.com/channel/brutalsessions', - description: "Hardcore BDSM jam packed with XXX fucking in bondage! We're taking dungeon sex beyond the castle!", - network: 'kink', - }, - { - slug: 'buttmachineboys', - name: 'Butt Machine Boys', - url: 'https://www.kink.com/channel/buttmachineboys', - description: 'Powerful Fucking Machines Pound Hot Men Hard & Deep.', - tags: ['gay'], - network: 'kink', - }, - { - slug: 'devicebondage', - name: 'Device Bondage', - alias: ['deb'], - url: 'https://www.kink.com/channel/devicebondage', - description: 'The Domination Of Sluts In Barbaric Metal Devices. Device Bondage takes BDSM porn to new levels with extreme restraints & unique devices with beautiful pornstars to huge, forced squirting orgasms.', - network: 'kink', - }, - { - slug: 'divinebitches', - name: 'Divine Bitches', - url: 'https://www.kink.com/channel/divinebitches', - description: 'Beautiful Women Dominate Submissive Men With Pain, Humiliation And Strap-On Fucking. The best in femdom and bondage. Men on Divine Bitches respond with obedience, ass worship, cunt worship, oral servitude, pantyhose worship, and foot worship.', - tags: ['femdom'], - network: 'kink', - }, - { - slug: 'electrosluts', - name: 'Electrosluts', - url: 'https://www.kink.com/channel/electrosluts', - description: 'Lezdoms Take Submissive Sluts To Their Limits, Shocking & Tormenting Their Wet Hot Pussies. Pornstars live out their electric bondage fantasies while dominatrixes use electrodes, paddles, caddle prods, & more to bring them to intense orgasms!', - network: 'kink', - }, - { - slug: 'everythingbutt', - name: 'Everything Butt', - url: 'https://www.kink.com/channel/everythingbutt', - description: 'Gaping Anal Holes Are Stuffed & Stretched To The Max. Anal Fisting, Enemas & Rimming Has Never Tasted So Good. EverythingButt.com explores the extreme limits of FemDom lesbian anal. Watch asses get destroyed by brutal fistings, huge insertions, double anal & more!', - network: 'kink', - }, - { - slug: 'filthyfemdom', - name: 'Filthy Femdom', - url: 'https://www.kink.com/channel/filthyfemdom', - description: 'Powerful women dominate your dirty dreams of sweet pain, seductive bondage, and sexual servitude.', - tags: ['femdom'], - network: 'kink', - }, - { - slug: 'familiestied', - name: 'Families Tied', - url: 'https://www.kink.com/channel/familiestied', - description: 'Intense BDSM family role play threesomes & more.', - network: 'kink', - }, - { - slug: 'footworship', - name: 'Foot Worship', - url: 'https://www.kink.com/channel/footworship', - description: 'Satisfy Your Foot Fetish With The Kinkiest Foot Action. Enjoy Trampling, Foot Jobs, High Heels, And Pantyhose.', - network: 'kink', - }, - { - slug: 'fuckedandbound', - name: 'Fucked and Bound', - alias: ['fab'], - url: 'https://www.kink.com/channel/fuckedandbound', - description: 'Extreme Anal, Rope Bondage, & Brutal Face Fucking.', - network: 'kink', - }, - { - slug: 'fuckingmachines', - name: 'Fucking Machines', - alias: ['fm', 'fum'], - url: 'https://www.kink.com/channel/fuckingmachines', - description: 'Machines Fucking Squirting Pussies With Extreme Insertions. Fucking Machines is the ultimate hardcore sex toy porn. Huge dildos strapped to sex machines relentlessly fucking pornstars to real squirting oragsms!', - network: 'kink', - }, - { - slug: 'hardcoregangbang', - name: 'Hardcore Gangbang', - url: 'https://www.kink.com/channel/hardcoregangbang', - description: "Where all women's hardcore gangbang fantasies come true. Watch extreme, brutal gangbangs with pornstars, models, & MILFs that crave cock in every hole. HardcoreGangbang.com has the best creampie gang bangs online.", - network: 'kink', - }, - { - slug: 'hogtied', - name: 'Hogtied', - alias: ['ht'], - url: 'https://www.kink.com/channel/hogtied', - description: 'Your favorite girls restrained with rope, punished & trained. Hogtied is the original extreme bondage porn website. Watch top pornstars and pain sluts in brutal bondage, getting tormented, and forced to orgasm!', - network: 'kink', - }, - { - slug: 'kinkfeatures', - name: 'Kink Features', - url: 'https://www.kink.com/channel/kinkfeatures', - description: 'Curated scenes by Kink\'s very best directors.', - network: 'kink', - }, - { - slug: 'kinkuniversity', - name: 'Kink University', - url: 'https://www.kink.com/channel/kinkuniversity', - description: 'Learn BDSM Technical Skills & Theories From Respected Teachers In The Kink Community. Learn BDSM skills and improve your sex techniques. Video tutorials feature top sex ed experts and hardcore demos on topics from bondage to relationships.', - network: 'kink', - }, - { - slug: 'meninpain', - name: 'Men In Pain', - url: 'https://www.kink.com/channel/meninpain', - description: 'Submissive Men Violated With Verbal Humiliation And Harsh Punishment By Beautiful Dominatrices.', - network: 'kink', - }, - { - slug: 'menonedge', - name: 'Men on Edge', - url: 'https://www.kink.com/channel/menonedge', - description: "Hot Guys Begging To Cum Are Brought To The Edge Of Complete Submission And Allowed To Blow Their Loads. Men on Edge has perfected the art of gay BDSM & edging porn. Watch straight men bound up & edged by dominant gay pornstars until they can't help but cum!", - tags: ['gay'], - network: 'kink', - }, - { - slug: 'nakedkombat', - name: 'Naked Kombat', - url: 'https://www.kink.com/channel/nakedkombat', - description: 'Fight Fit Studs Go Head To Head In A Battle For Dominance. The Loser Gets Pinned And Punish Fucked Without Mercy', - network: 'kink', - }, - { - slug: 'publicdisgrace', - name: 'Public Disgrace', - alias: ['pud'], - url: 'https://www.kink.com/channel/publicdisgrace', - description: 'Women Bound Stripped And Punished In Public Get Hardcore Fucked Where Everyone Can See. Unscripted public humiliation & punishment of submissive slaves in real life locations. PublicDisgrace features the best outdoor BDSM & voyeur porn!', - network: 'kink', - }, - { - slug: 'sadisticrope', - name: 'Sadistic Rope', - alias: ['sr'], - url: 'https://www.kink.com/channel/sadisticrope', - description: 'Innocence Taken By Extreme Rope Bondage, Hardcore BDSM And Pussy-Destroying Orgasms.', - network: 'kink', - }, - { - slug: 'sexandsubmission', - name: 'Sex and Submission', - alias: ['sas'], - url: 'https://www.kink.com/channel/sexandsubmission', - description: 'Submissive Sluts Are Dominated With Rough Sex And Bondage. Real pornstars, hardcore bondage, master & slave roles are what SexAndSubmission.com is all about. Watch submissive sluts give in to total domination!', - network: 'kink', - }, - { - slug: 'strugglingbabes', - name: 'Struggling Babes', - url: 'https://www.kink.com/channel/strugglingbabes', - description: 'Demystifying and celebrating alternative sexuality by providing the most authentic kinky videos. Experience the other side of porn.', - network: 'kink', - }, - { - slug: 'thetrainingofo', - name: 'The Training of O', - alias: ['tto'], - url: 'https://www.kink.com/channel/thetrainingofo', - description: 'Slaves Are Trained And Rewarded With Hardcore Bondage And Sex. Watch real pornstars undergo extreme slave training through hardcore bondage & BDSM porn. The Training of O is the ultimate slave / master experience!', - network: 'kink', - }, - { - slug: 'theupperfloor', - name: 'The Upper Floor', - alias: ['tuf'], - url: 'https://www.kink.com/channel/theupperfloor', - description: 'Trained slaves serve the house and their master in intense BDSM and kinky threesomes. The Upper Floor is a voyeuristic look into BDSM and fetish porn shoots with real submissive pornstars living out their kinky fantasies live on cam.', - network: 'kink', - }, - { - slug: 'tspussyhunters', - name: 'TS Pussy Hunters', - url: 'https://www.kink.com/channel/tspussyhunters', - description: 'Hot TS cocks prey on the wet pussies of submissive ladies who are fucked hard till they cum. Dominant TS femme fatales with the hardest dicks, the softest tits, and the worst intentions dominate, bind, and punish bitches on the ultimate transfucking porn site.', - tags: ['transsexual'], - network: 'kink', - }, - { - slug: 'tsseduction', - name: 'TS Seduction', - url: 'https://www.kink.com/channel/tsseduction', - description: 'Sexy TS Women With Huge Cocks Dominate The Holes Of Straight Boys. Real TS women who are drop-dead gorgeous from their pretty faces to their big tits to their hard TS cocks. TS Seduction is the ultimate in transsexual bondage porn.', - network: 'kink', - }, - { - slug: 'ultimatesurrender', - name: 'Ultimate Surrender', - url: 'https://www.kink.com/channel/ultimatesurrender', - description: 'Competitive Female Wrestling Where The Loser Gets Strap-On Punish Fucked. Ultimate Surrender features hardcore naked female wrestling porn videos where the winner gets to dominate the loser with some kinky lesbian FemDom!', - network: 'kink', - }, - { - slug: 'waterbondage', - name: 'Water Bondage', - url: 'https://www.kink.com/channel/waterbondage', - description: 'Helpless Bound Beauties Sprayed, Dunked And Tormented Until They Cum Hard & Wet.', - network: 'kink', - }, - { - slug: 'whippedass', - name: 'Whipped Ass', - alias: ['wpa', 'wa'], - url: 'https://www.kink.com/channel/whippedass', - description: 'Beautiful Submissive Sluts Take A Hard Fucking From Powerful Dominant Women. Watch brutal lesbian dominatrixes push submissive sluts to their orgasmic breaking points on WhippedAss! Hardcore fisting, huge strapons & face sitting!', - network: 'kink', - }, - { - slug: 'wiredpussy', - name: 'Wired Pussy', - url: 'https://www.kink.com/channel/wiredpussy', - description: 'Gorgeous Women Submit To Electricity, Are Zapped, Shocked & Prodded To Orgasm.', - network: 'kink', - }, - // LEGALPORNO - { - slug: 'legalporno', - name: 'LegalPorno', - alias: ['clip'], - url: 'https://www.legalporno.com', - description: 'The Best HD Porn For You!', - parameters: { independent: true }, - network: 'legalporno', - }, - // METRO HD - { - slug: 'devianthardcore', - name: 'Deviant Hardcore', - url: 'https://www.devianthardcore.com', - tags: ['bdsm'], - parameters: { - siteId: 305, - native: true, - }, - network: 'metrohd', - }, - { - slug: 'shewillcheat', - name: 'She Will Cheat', - url: 'https://www.shewillcheat.com', - parameters: { - siteId: 306, - native: true, - }, - network: 'metrohd', - }, - { - slug: 'familyhookups', - name: 'Family Hookups', - url: 'https://www.familyhookups.com', - tags: ['family'], - parameters: { - siteId: 307, - native: true, - }, - network: 'metrohd', - }, - { - slug: 'kinkyspa', - name: 'Kinky Spa', - url: 'https://www.kinkyspa.com', - tags: ['massage'], - parameters: { - siteId: 308, - native: true, - }, - network: 'metrohd', - }, - { - slug: 'girlgrind', - name: 'Girl Grind', - url: 'https://www.girlgrind.com', - tags: ['lesbian'], - parameters: { - siteId: 309, - native: true, - }, - network: 'metrohd', - }, - // MEN - { - slug: 'bigdicksatschool', - name: 'Big Dicks At School', - url: 'https://www.bigdicksatschool.com', - description: '', - parameters: { siteId: 252 }, - tags: ['gay'], - network: 'men', - }, - { - slug: 'drillmyhole', - name: 'Drill My Hole', - url: 'https://www.drillmyhole.com', - description: '', - parameters: { siteId: 253 }, - tags: ['gay'], - network: 'men', - }, - { - slug: 'str8togay', - name: 'Str8 to Gay', - url: 'https://www.str8togay.com', - tags: ['gay'], - parameters: { siteId: 254 }, - network: 'men', - }, - { - slug: 'thegayoffice', - name: 'The Gay Office', - url: 'https://www.thegayoffice.com', - tags: ['gay'], - parameters: { siteId: 255 }, - network: 'men', - }, - { - slug: 'jizzorgy', - name: 'Jizz Orgy', - url: 'https://www.jizzorgy.com', - tags: ['gay'], - parameters: { siteId: 256 }, - network: 'men', - }, - { - slug: 'menofuk', - name: 'Men of UK', - url: 'https://www.menofuk.com', - tags: ['gay'], - parameters: { siteId: 258 }, - network: 'men', - }, - { - slug: 'toptobottom', - name: 'Top to Bottom', - url: 'https://www.toptobottom.com', - tags: ['gay'], - parameters: { siteId: 259 }, - network: 'men', - }, - { - slug: 'godsofmen', - name: 'Gods of Men', - url: 'https://www.godsofmen.com', - tags: ['gay'], - parameters: { siteId: 260 }, - network: 'men', - }, - // MINDGEEK - { - slug: 'tube8vip', - name: 'Tube8Vip', - url: 'https://www.tube8vip.com', - description: '', - parameters: { native: true }, - network: 'mindgeek', - }, - { - slug: 'transangels', - name: 'TransAngels', - url: 'https://www.transangels.com', - tags: ['transsexual'], - parameters: { native: true }, - network: 'mindgeek', - }, - { - slug: 'trueamateurs', - name: 'True Amateurs', - url: 'https://www.trueamateurs.com', - description: 'TrueAmateurs.com is the best homemade porn from real amateurs. Watch these real hot couples in our exclusive scenes.', - parameters: { native: true }, - network: 'mindgeek', - }, - // MIKE ADRIANO - { - slug: 'trueanal', - name: 'True Anal', - url: 'https://trueanal.com', - description: 'TrueAnal is the hottest site with all hardcore Anal content and only the most popular pornstars getting their asses pounded and gapped with huge cock and more!', - tags: ['anal'], - network: 'mikeadriano', - }, - { - slug: 'allanal', - name: 'All Anal', - url: 'https://allanal.com', - description: 'Popular babes getting their tight asses filled with cock! Pure anal fucking only at AllAnal!', - tags: ['anal', 'mff'], - network: 'mikeadriano', - }, - { - slug: 'nympho', - name: 'Nympho', - url: 'https://nympho.com', - description: 'These Babes have an appetite for nasty, sloppy fucking!', - network: 'mikeadriano', - }, - { - slug: 'swallowed', - name: 'Swallowed', - url: 'https://swallowed.com', - description: 'Swallowed is a Premium adult website for the hottest Blowjobs content online with only the most popular pornstars swallowing cock!', - tags: ['blowjob', 'deepthroat', 'facefucking'], - network: 'mikeadriano', - }, - // MILE HIGH MEDIA - { - slug: 'doghousedigital', - name: 'Doghouse Digital', - url: 'https://www.doghousedigital.com', - parameters: { siteId: 321 }, - network: 'milehighmedia', - }, - { - slug: 'milehighmedia', - name: 'Mile High Media', - url: 'https://www.milehighmedia.com/scenes?site=323', - network: 'milehighmedia', - }, - { - slug: 'realityjunkies', - name: 'Reality Junkies', - url: 'https://www.realityjunkies.com', - parameters: { siteId: 324 }, - network: 'milehighmedia', - }, - { - slug: 'sweetheartvideo', - name: 'Sweetheart Video', - url: 'https://www.sweetheartvideo.com', - parameters: { siteId: 325 }, - network: 'milehighmedia', - }, - { - slug: 'sweetsinner', - name: 'Sweet Sinner', - url: 'https://www.sweetsinner.com', - parameters: { siteId: 326 }, - network: 'milehighmedia', - }, - { - slug: 'iconmale', - name: 'Icon Male', - url: 'https://www.iconmale.com', - tags: ['gay'], - parameters: { native: true }, - network: 'milehighmedia', - }, - // MOFOS - { - slug: 'girlsgonepink', - name: 'Girls Gone Pink', - url: 'https://www.mofos.com/scenes?site=204', - description: "There comes a point in every woman's life when she gets a little curious about what some hot girl on girl sex could be like. Whether they're lesbian or just straight and incredibly daring and open-minded, the end result is the same. GirlsGonePink.com is full of soft lips, long flowing hair, and sensual feminine figures that are enough to get any horny minx's blood pumping. Premium full-length lesbian porn videos await you full of perfect boobs, pointy nipples, round butts, and luscious legs that usually stay separated!", - network: 'mofos', - }, - { - slug: 'ebonysextapes', - name: 'Ebony Sex Tapes', - url: 'https://www.mofos.com/scenes?site=202', - description: 'Once you go black, you never go back! Did you think that was only how white women feel about black men? Well if you did, that can only mean you never had a stacked, curvy, big ass, beautiful black teen riding your hard white cock. Watch these lucky guys fuck their Ebony Girlfriends at EbonySexTapes.com.', - network: 'mofos', - }, - { - slug: 'sharemybf', - name: 'Share My BF', - alias: ['smb'], - url: 'https://www.mofos.com/scenes?site=201', - description: 'Would your cock be able to handle 2 wet pussies at the same time? One hot teen riding your face while the other deepthroats you. You know your GF tells all her friends how big your dick is. Imagine if you can fuck her and her friend at the same time? Live the fantasy at ShareMyBF.com.', - network: 'mofos', - }, - { - slug: 'dontbreakme', - name: "Don't Break Me", - alias: ['dbm'], - url: 'https://www.mofos.com/scenes?site=198', - description: 'DontBreakMe.com is about tiny spinners fucking big guys with massive dicks! Most of these chicks are shorter than 5 feet tall and weigh less than 100lbs. Meanwhile, these girls are paired up with guys who tower over them by at least 1.5 feet and have 9" dicks!! The look on their faces when they see that huge dick pop out of his pants is priceless. While it turns them on they usually get a bit nervous: "how will I squeeze that huge cock inside?" Ouch! Check it out.', - network: 'mofos', - }, - { - slug: 'iknowthatgirl', - name: 'I Know That Girl', - alias: ['iktg'], - url: 'https://www.mofos.com/scenes?site=183', - description: 'Every single gorgeous girl you see on this site is 100% Real! They are all part of the biggest user submitted, amateur video site in the world...IKnowThatGirl.com! Hot young girlfriends getting kinky on camera, sucking and fucking, even stuffing dildos up their tight pussies, all filmed on home video and leaked to us by some lowlife, soon to be ex-boyfriend or former best friend! Oh well... Enjoy!', - network: 'mofos', - }, - { - slug: 'letstryanal', - name: 'Lets Try Anal', - alias: ['lta'], - url: 'https://www.mofos.com/scenes?site=189', - description: "This isn't just another anal site! Letstryanal.com features the hottest real footage of amateur girls and their first time ass fucking experiences. Watch it all... innocent girlfriends being convinced to try anal, their faces of pain and screaming as they beg their boyfriend to \"please go slower\" while a large cock penetrates their tight asses for the first time! Let's face it, there is nothing like seeing a cock disappear in a virgin asshole. It's so hot!", - network: 'mofos', - }, - { - slug: 'latinasextapes', - name: 'Latina Sex Tapes', - alias: ['lst'], - url: 'https://www.mofos.com/scenes?site=188', - description: "100% Real Latina Girls getting fucked by their boyfriends, filmed and submitted to us for Big $$$! Watch amazing real footage and private videos of these beautiful amateur girls, their perfectly tanned bodies, mouth-watering curves, luscious round asses, and mind blowing accents! We've only kept the best, most outstanding sex videos and uploaded them for you to watch. You'll be amazed with what we received, and more is on the way!", - network: 'mofos', - }, - { - slug: 'publicpickups', - name: 'Public Pickups', - alias: ['ppu'], - url: 'https://www.mofos.com/scenes?site=190', - description: "Check out the hottest REAL footage of young girls getting picked up and fucked in public! The girls are usually shy around guys approaching them with a video camera, but that's the fun part. Besides their shyness slowly disappears after they're offered money to get dirty. While it's a real turn on seeing the girls flash and get fondled in public... the hottest part is watching them get fucked everywhere...in cars, parks, clubs, even the library!", - network: 'mofos', - }, - { - slug: 'pervsonpatrol', - name: 'Pervs On Patrol', - alias: ['pop'], - url: 'https://www.mofos.com/scenes?site=185', - description: "A while back, this beautiful girl who lived next door use to always undress with her window opened. This girl had no fucking clue that I was jerking off over her from across the yard. One day I decided to grab my dad's camera and start filming her. It was amazing... until she finally caught me. Fuck, this girl was PISSED!..., but could you fucking believe that once she calmed down she was actually a little turned on by the whole situation,... and what happened next changed my life!", - network: 'mofos', - }, - { - slug: 'strandedteens', - name: 'Stranded Teens', - alias: ['sts'], - url: 'https://www.mofos.com/scenes?site=192', - description: "Watch videos on StrandedTeens.com and you will never look at a hitchhiker the same way again! Some of these girls will do anything for a ride or simply to make a friend - even the shy ones. From giving road head to getting ass-fucked on the hood of the car, you can watch it all. Check it out now, you won't be disappointed!", - network: 'mofos', - }, - { - slug: 'realslutparty', - name: 'Real Slut Party', - url: 'https://www.mofos.com/scenes?site=184', - description: "Wanna see the most mind blowing college sex parties from across the country? It's the real deal, all caught on video and submitted by you! Insane college craziness, pussy packed house parties, holiday orgies, backyard BBQ's gone wrong and hundreds of tight, young girls getting crazy, stripped down, and on the prowl for all the cock they can find!", - network: 'mofos', - }, - { - slug: 'mofoslab', - name: 'MOFOS Lab', - url: 'https://www.mofos.com/scenes?site=205', - description: "We've received your feedback and are experimenting with turning your wildest fantasies into the ultimate POV experience; this is Mofos Lab! Featuring today's hottest and freshest talent, immerse yourself in an exciting Mofos venture that brings you the edgiest new content!", - network: 'mofos', - }, - { - slug: 'mofosbsides', - name: 'Mofos B Sides', - url: 'https://www.mofos.com/scenes?site=191', - description: "Mofos B-Sides is a doorway to new, unseen amateur video! Hundreds of clips have been submitted to Mofos through the years and we've never shown them to you until now. We'll give you a little bit at a time, from random girls in random scenario\\’s, and maybe even an occasional free video from our friends at Brazzers, Twisty's and Babes! Check it all out and let us know what you think.", - network: 'mofos', - }, - { - slug: 'shesafreak', - name: "She's A Freak", - alias: ['saf'], - url: 'https://www.mofos.com/scenes?site=187', - description: "Fresh, young amateur girls with beautiful tight bodies, pushing themselves to the limit! It's just another great way that today's hottest new models are choosing to showcase their stunning bodies and show all of us that they're ready for more! Soaking wet masturbation, fisting, squirting, double penetration and anal toys are just some of the things they do to show us how freaky they can be and how ready they are to graduate from toys to thick, fat cock!", - network: 'mofos', - }, - // NAUGHTY AMERICA - { - slug: 'myfriendshotmom', - name: 'My Friend\'s Hot Mom', - alias: ['mfhm'], - url: 'https://www.naughtyamerica.com/site/my-friend-s-hot-mom', - network: 'naughtyamerica', - }, - { - slug: 'slutstepmom', - name: 'Slut Step Mom', - url: 'https://www.naughtyamerica.com/site/slut-step-mom', - network: 'naughtyamerica', - }, - { - slug: 'openfamily', - name: 'Open Family', - url: 'https://www.naughtyamerica.com/site/open-family', - network: 'naughtyamerica', - }, - { - slug: 'sleazystepdad', - name: 'Sleazy Stepdad', - url: 'https://www.naughtyamerica.com/site/sleazy-stepdad', - network: 'naughtyamerica', - }, - { - slug: 'watchyourmom', - name: 'Watch Your Mom', - url: 'https://www.naughtyamerica.com/site/watch-your-mom', - network: 'naughtyamerica', - }, - { - slug: 'bigcockbully', - name: 'Big Cock Bully', - alias: ['bcb'], - url: 'https://www.naughtyamerica.com/site/big-cock-bully', - network: 'naughtyamerica', - }, - { - slug: 'bigcockhero', - name: 'Big Cock Hero', - alias: ['bch'], - url: 'https://www.naughtyamerica.com/site/big-cock-hero', - network: 'naughtyamerica', - }, - { - slug: 'mysistershotfriend', - name: "My Sister's Hot Friend", - alias: ['mshf'], - url: 'https://www.naughtyamerica.com/site/my-sister-s-hot-friend', - network: 'naughtyamerica', - }, - { - slug: 'myfirstsexteacher', - name: 'My First Sex Teacher', - alias: ['mfst'], - url: 'https://www.naughtyamerica.com/site/my-first-sex-teacher', - network: 'naughtyamerica', - }, - { - slug: 'slutstepsister', - name: 'Slut Step Sister', - url: 'https://www.naughtyamerica.com/site/slut-step-sister', - network: 'naughtyamerica', - }, - { - slug: 'teenslovecream', - name: 'Teens Love Cream', - url: 'https://www.naughtyamerica.com/site/teens-love-cream', - network: 'naughtyamerica', - }, - { - slug: 'latinastepmom', - name: 'Latina Step Mom', - url: 'https://www.naughtyamerica.com/site/latina-step-mom', - network: 'naughtyamerica', - }, - { - slug: 'seducedbyacougar', - name: 'Seduced By A Cougar', - url: 'https://www.naughtyamerica.com/site/seduced-by-a-cougar', - network: 'naughtyamerica', - }, - { - slug: 'showmybf', - name: 'Show My BF', - url: 'https://www.naughtyamerica.com/site/show-my-bf', - network: 'naughtyamerica', - }, - { - slug: 'mydaughtershotfriend', - name: "My Daughter's Hot Friend", - alias: ['mdhf'], - url: 'https://www.naughtyamerica.com/site/my-daughter-s-hot-friend', - network: 'naughtyamerica', - }, - { - slug: 'lasluts', - name: 'LA Sluts', - url: 'https://www.naughtyamerica.com/site/la-sluts', - network: 'naughtyamerica', - }, - { - slug: 'mywifeismypornstar', - name: 'My Wife Is My Pornstar', - url: 'https://www.naughtyamerica.com/site/my-wife-is-my-pornstar', - network: 'naughtyamerica', - }, - { - slug: 'watchyourwife', - name: 'Watch Your Wife', - url: 'https://www.naughtyamerica.com/site/watch-your-wife', - network: 'naughtyamerica', - }, - { - slug: 'tonightsgirlfriendclassic', - alias: ['togc'], - name: "Tonight's Girlfriend", - url: 'https://www.naughtyamerica.com/site/tonight-s-girlfriend-classic', - network: 'naughtyamerica', - }, - { - slug: 'wivesonvacation', - name: 'Wives on Vacation', - alias: ['wov'], - url: 'https://www.naughtyamerica.com/site/wives-on-vacation', - network: 'naughtyamerica', - }, - { - slug: 'naughtyweddings', - name: 'Naughty Weddings', - alias: ['nw'], - url: 'https://www.naughtyamerica.com/site/naughty-weddings', - network: 'naughtyamerica', - }, - { - slug: 'dirtywivesclub', - name: 'Dirty Wives Club', - alias: ['dwc'], - url: 'https://www.naughtyamerica.com/site/dirty-wives-club', - network: 'naughtyamerica', - }, - { - slug: 'mydadshotgirlfriend', - name: "My Dad's Hot Girlfriend", - alias: ['mdhg'], - url: 'https://www.naughtyamerica.com/site/my-dad-s-hot-girlfriend', - network: 'naughtyamerica', - }, - { - slug: 'mygirllovesanal', - name: 'My Girl Loves Anal', - url: 'https://www.naughtyamerica.com/site/my-girl-loves-anal', - network: 'naughtyamerica', - }, - { - slug: 'analcollege', - name: 'Anal College', - url: 'https://www.naughtyamerica.com/site/anal-college', - network: 'naughtyamerica', - }, - { - slug: 'lesbiangirlongirl', - name: 'Lesbian Girl on Girl', - url: 'https://www.naughtyamerica.com/site/lesbian-girl-on-girl', - network: 'naughtyamerica', - }, - { - slug: 'naughtyoffice', - name: 'Naughty Office', - alias: ['no'], - url: 'https://www.naughtyamerica.com/site/naughty-office', - network: 'naughtyamerica', - }, - { - slug: 'ihaveawife', - name: 'I Have a Wife', - alias: ['ihaw'], - url: 'https://www.naughtyamerica.com/site/i-have-a-wife', - network: 'naughtyamerica', - }, - { - slug: 'naughtybookworms', - name: 'Naughty Bookworms', - alias: ['nb'], - url: 'https://www.naughtyamerica.com/site/naughty-bookworms', - network: 'naughtyamerica', - }, - { - slug: 'housewife1on1', - name: 'Housewife 1 on 1', - alias: ['h1o1'], - url: 'https://www.naughtyamerica.com/site/housewife-1-on-1', - network: 'naughtyamerica', - }, - { - slug: 'mywifeshotfriend', - name: "My Wife's Hot Friend", - alias: ['mwhf'], - url: 'https://www.naughtyamerica.com/site/my-wife-s-hot-friend', - network: 'naughtyamerica', - }, - { - slug: 'latinadultery', - name: 'Latin Adultery', - url: 'https://www.naughtyamerica.com/site/latin-adultery', - network: 'naughtyamerica', - }, - { - slug: 'assmasterpiece', - name: 'Ass Masterpiece', - alias: ['am'], - url: 'https://www.naughtyamerica.com/site/ass-masterpiece', - network: 'naughtyamerica', - }, - { - slug: '2chickssametime', - name: '2 Chicks Same Time', - alias: ['2cst'], - url: 'https://www.naughtyamerica.com/site/2-chicks-same-time', - network: 'naughtyamerica', - }, - { - slug: 'myfriendshotgirl', - name: "My Friend's Hot Girl", - alias: ['mfhg'], - url: 'https://www.naughtyamerica.com/site/my-friend-s-hot-girl', - network: 'naughtyamerica', - }, - { - slug: 'neighboraffair', - name: 'Neighbor Affair', - alias: ['naf'], - url: 'https://www.naughtyamerica.com/site/neighbor-affair', - network: 'naughtyamerica', - }, - { - slug: 'mygirlfriendsbustyfriend', - name: "My Girlfriend's Busty Friend", - alias: ['mgbf'], - url: 'https://www.naughtyamerica.com/site/my-girlfriend-s-busty-friend', - network: 'naughtyamerica', - }, - { - slug: 'naughtyathletics', - name: 'Naughty Athletics', - alias: ['na'], - url: 'https://www.naughtyamerica.com/site/naughty-athletics', - network: 'naughtyamerica', - }, - { - slug: 'mynaughtymassage', - name: 'My Naughty Massage', - alias: ['mnm'], - url: 'https://www.naughtyamerica.com/site/my-naughty-massage', - network: 'naughtyamerica', - }, - { - slug: 'fasttimes', - name: 'Fast Times', - url: 'https://www.naughtyamerica.com/site/fast-times', - network: 'naughtyamerica', - }, - { - slug: 'thepassenger', - name: 'The Passenger', - url: 'https://www.naughtyamerica.com/site/the-passenger', - network: 'naughtyamerica', - }, - { - slug: 'milfsugarbabesclassic', - name: 'Milf Sugar Babes Classic', - url: 'https://www.naughtyamerica.com/site/milf-sugar-babes-classic', - network: 'naughtyamerica', - }, - { - slug: 'perfectfuckingstrangersclassic', - name: 'Perfect Fucking Strangers Classic', - url: 'https://www.naughtyamerica.com/site/perfect-fucking-strangers-classic', - network: 'naughtyamerica', - }, - { - slug: 'asian1on1', - name: 'Asian 1 On 1', - url: 'https://www.naughtyamerica.com/site/asian-1-on-1', - network: 'naughtyamerica', - }, - { - slug: 'americandaydreams', - name: 'American Daydreams', - alias: ['ad'], - url: 'https://www.naughtyamerica.com/site/american-daydreams', - network: 'naughtyamerica', - }, - { - slug: 'socalcoeds', - name: 'Socal Coeds', - url: 'https://www.naughtyamerica.com/site/socal-coeds', - network: 'naughtyamerica', - }, - { - slug: 'naughtycountrygirls', - name: 'Naughty Country Girls', - url: 'https://www.naughtyamerica.com/site/naughty-country-girls', - network: 'naughtyamerica', - }, - { - slug: 'diaryofamilf', - name: 'Diary of a Milf', - url: 'https://www.naughtyamerica.com/site/diary-of-a-milf', - network: 'naughtyamerica', - }, - { - slug: 'naughtyrichgirls', - name: 'Naughty Rich Girls', - alias: ['nrg'], - url: 'https://www.naughtyamerica.com/site/naughty-rich-girls', - network: 'naughtyamerica', - }, - { - slug: 'mynaughtylatinmaid', - name: 'My Naughty Latin Maid', - url: 'https://www.naughtyamerica.com/site/my-naughty-latin-maid', - network: 'naughtyamerica', - }, - { - slug: 'naughtyamerica', - name: 'Naughty America', - alias: ['nam'], - url: 'https://www.naughtyamerica.com/site/naughty-america', - network: 'naughtyamerica', - }, - { - slug: 'diaryofananny', - name: 'Diary of a Nanny', - url: 'https://www.naughtyamerica.com/site/diary-of-a-nanny', - network: 'naughtyamerica', - }, - { - slug: 'naughtyflipside', - name: 'Naughty Flipside', - url: 'https://www.naughtyamerica.com/site/naughty-flipside', - network: 'naughtyamerica', - }, - { - slug: 'livepartygirl', - name: 'Live Party Girl', - url: 'https://www.naughtyamerica.com/site/live-party-girl', - network: 'naughtyamerica', - }, - { - slug: 'livenaughtystudent', - name: 'Live Naughty Student', - url: 'https://www.naughtyamerica.com/site/live-naughty-student', - network: 'naughtyamerica', - }, - { - slug: 'livenaughtysecretary', - name: 'Live Naughty Secretary', - url: 'https://www.naughtyamerica.com/site/live-naughty-secretary', - network: 'naughtyamerica', - }, - { - slug: 'livegymcam', - name: 'Live Gym Cam', - url: 'https://www.naughtyamerica.com/site/live-gym-cam', - network: 'naughtyamerica', - }, - { - slug: 'livenaughtyteacher', - name: 'Live Naughty Teacher', - url: 'https://www.naughtyamerica.com/site/live-naughty-teacher', - network: 'naughtyamerica', - }, - { - slug: 'livenaughtymilf', - name: 'Live Naughty Milf', - url: 'https://www.naughtyamerica.com/site/live-naughty-milf', - network: 'naughtyamerica', - }, - { - slug: 'livenaughtynurse', - name: 'Live Naughty Nurse', - url: 'https://www.naughtyamerica.com/site/live-naughty-nurse', - network: 'naughtyamerica', - }, - // NEW SENSATIONS - { - slug: 'hotwifexxx', - name: 'Hotwife XXX', - url: 'https://www.hotwifexxx.com', - network: 'newsensations', - parameters: { - siteId: 'hwxxx', - block: true, - }, - }, - { - slug: 'tabutales', - name: 'Tabu Tales', - url: 'https://www.thetabutales.com', - network: 'newsensations', - parameters: { siteId: 'tt' }, - }, - { - slug: 'nsfamilyxxx', - name: 'Family XXX', - url: 'https://www.familyxxx.com', - network: 'newsensations', - tags: ['family'], - parameters: { - siteId: 'famxxx', - block: true, - }, - }, - { - slug: 'thelesbianexperience', - name: 'The Lesbian Experience', - url: 'https://www.thelesbianexperience.com', - network: 'newsensations', - tags: ['lesbian'], - parameters: { siteId: 'tle' }, - }, - { - slug: 'theromanceseries', - name: 'The Romance Series', - url: 'https://www.theromanceseries.com', - network: 'newsensations', - parameters: { siteId: 'rs' }, - }, - { - slug: 'talesfromtheedge', - name: 'Tales From The Edge', - url: 'thetalesfromtheedge', - network: 'newsensations', - parameters: { siteId: 'ttfte' }, - }, - { - slug: 'parodypass', - name: 'Parody Pass', - url: 'https://www.parodypass.com', - network: 'newsensations', - parameters: { siteId: 'pp' }, - }, - { - slug: 'shanedieselsbangingbabes', - name: 'Shane Diesel\'s Banging Babes', - url: 'http://shanedieselsbangingbabes.com', - network: 'newsensations', - parameters: { siteId: 'sdbb' }, - }, - { - slug: 'unlimitedmilfs', - name: 'Unlimited MILFs', - url: 'https://www.unlimitedmilfs.com', - network: 'newsensations', - tags: ['milf'], - parameters: { siteId: 'um' }, - }, - { - slug: 'heavyhandfuls', - name: 'Heavy Handfuls', - url: 'https://www.heavyhandfuls.com', - network: 'newsensations', - parameters: { siteId: 'hh' }, - }, - { - slug: 'jizzbomb', - name: 'Jizz Bomb', - url: 'https://www.jizzbomb.com', - network: 'newsensations', - parameters: { siteId: 'jb' }, - }, - { - slug: 'stretchedoutsnatch', - name: 'Stretched Out Snatch', - url: 'https://www.stretchedoutsnatch.com', - network: 'newsensations', - parameters: { siteId: 'sos' }, - }, - { - slug: 'fourfingerclub', - name: 'Four Finger Club', - url: 'https://www.fourfingerclub.com', - network: 'newsensations', - parameters: { siteId: 'ffc' }, - }, - { - slug: 'ashlynnbrooke', - name: 'Ashlynn Brooke', - url: 'https://www.ashlynnbrooke.com', - network: 'newsensations', - parameters: { siteId: 'ab' }, - }, - { - slug: 'freshouttahighschool', - name: 'Fresh Outta High School', - url: 'https://www.freshouttahighschool.com', - network: 'newsensations', - parameters: { siteId: 'fohs' }, - }, - // NUBILES - { - slug: 'anilos', - name: 'Anilos', - url: 'https://www.anilos.com', - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'brattysis', - name: 'Bratty Sis', - url: 'https://www.brattysis.com', - tags: ['family'], - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'deeplush', - name: 'Deep Lush', - url: 'https://www.deeplush.com', - network: 'nubiles', - }, - { - slug: 'hotcrazymess', - name: 'Hot Crazy Mess', - alias: ['hcm'], - url: 'https://www.hotcrazymess.com', - network: 'nubiles', - }, - { - slug: 'nfbusty', - name: 'NF Busty', - url: 'https://www.nfbusty.com', - tags: ['big-boobs'], - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'nubilefilms', - name: 'Nubile Films', - alias: ['nf', 'nubilef'], - url: 'https://www.nubilefilms.com', - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'nubiles', - name: 'Nubiles', - url: 'https://www.nubiles.net', - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'nubilescasting', - name: 'Nubiles Casting', - url: 'https://www.nubiles-casting.com', - tags: ['casting'], - network: 'nubiles', - }, - { - slug: 'momsteachsex', - name: 'Moms Teach Sex', - alias: ['mts'], - url: 'https://www.momsteachsex.com', - tags: ['family', 'milf'], - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'petitehdporn', - name: 'Petite HD Porn', - alias: ['phdp'], - url: 'https://www.petitehdporn.com', - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'driverxxx', - name: 'Driver XXX', - url: 'https://www.driverxxx.com', - network: 'nubiles', - }, - { - slug: 'petiteballerinasfucked', - name: 'Petite Ballerinas Fucked', - alias: ['pbf'], - url: 'https://www.petiteballerinasfucked.com', - network: 'nubiles', - }, - { - slug: 'teacherfucksteens', - name: 'Teacher Fucks Teens', - alias: ['tft'], - url: 'https://www.teacherfucksteens.com', - tags: ['teacher'], - network: 'nubiles', - }, - { - slug: 'stepsiblingscaught', - name: 'Step Siblings Caught', - alias: ['ssc'], - url: 'https://www.stepsiblingscaught.com', - tags: ['family'], - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'princesscum', - name: 'Princess Cum', - alias: ['pc'], - url: 'https://www.princesscum.com', - network: 'nubiles', - }, - { - slug: 'badteenspunished', - name: 'Bad Teens Punished', - alias: ['btp'], - url: 'https://www.badteenspunished.com', - network: 'nubiles', - }, - { - slug: 'nubilesunscripted', - name: 'Nubiles Unscripted', - url: 'https://www.nubilesunscripted.com', - network: 'nubiles', - }, - { - slug: 'bountyhunterporn', - name: 'Bounty Hunter Porn', - url: 'https://www.bountyhunterporn.com', - network: 'nubiles', - }, - { - slug: 'daddyslilangel', - name: 'Daddy\'s Lil Angel', - alias: ['dlla'], - url: 'https://www.daddyslilangel.com', - tags: ['family', 'anal'], - network: 'nubiles', - }, - { - slug: 'myfamilypies', - name: 'My Family Pies', - alias: ['mfp'], - url: 'https://www.myfamilypies.com', - tags: ['family'], - network: 'nubiles', - parameters: { - upcoming: true, - }, - }, - { - slug: 'nubileset', - name: 'Nubiles Entertainment', - url: 'https://www.nubileset.com', - network: 'nubiles', - }, - { - slug: 'detentiongirls', - name: 'Detention Girls', - url: 'https://www.detentiongirls.com', - network: 'nubiles', - }, - { - slug: 'thatsitcomshow', - name: 'That Sitcom Show', - alias: ['tss'], - url: 'https://www.thatsitcomshow.com', - tags: ['parody'], - network: 'nubiles', - }, - // PERFECT GONZO - { - slug: 'allinternal', - name: 'All Internal', - url: 'https://allinternal.com', - network: 'perfectgonzo', - }, - { - slug: 'asstraffic', - name: 'Ass Traffic', - url: 'https://asstraffic.com', - network: 'perfectgonzo', - }, - { - slug: 'cumforcover', - name: 'Cum For Cover', - url: 'https://cumforcover.com', - network: 'perfectgonzo', - }, - { - slug: 'fistflush', - name: 'Fist Flush', - url: 'https://fistflush.com', - network: 'perfectgonzo', - }, - { - slug: 'givemepink', - name: 'Give Me Pink', - url: 'https://givemepink.com', - tags: ['solo', 'masturbation'], - network: 'perfectgonzo', - }, - { - slug: 'milfthing', - name: 'MILF Thing', - url: 'https://milfthing.com', - network: 'perfectgonzo', - }, - { - slug: 'primecups', - name: 'Prime Cups', - url: 'https://primecups.com', - network: 'perfectgonzo', - }, - { - slug: 'purepov', - name: 'Pure POV', - url: 'https://purepov.com', - network: 'perfectgonzo', - }, - { - slug: 'spermswap', - name: 'Sperm Swap', - url: 'https://spermswap.com', - tags: ['cum-swapping'], - network: 'perfectgonzo', - }, - { - slug: 'tamedteens', - name: 'Tamed Teens', - url: 'https://tamedteens.com', - network: 'perfectgonzo', - }, - // PERVCITY - { - slug: 'analoverdose', - name: 'Anal Overdose', - url: 'http://www.analoverdose.com', - description: 'Before proceeding, use caution: the stunning pornstars of Anal Overdose are so fiery that they cause heavy breathing, throbbing cocks and volcanic loads of cum. If you think you can handle the heat of smoking tits, sweltering pussy and red hot ass.', - network: 'pervcity', - parameters: { tourId: 3 }, - }, - { - slug: 'bangingbeauties', - name: 'Banging Beauties', - description: "Banging Beauties isn't just a porn site; it's the gateway to all your pussy-obsessed fantasies! Our members' area is flowing with beautiful pornstars anticipating big dick throbbing in their syrupy pink slits. These experienced babes love brutal vaginal pounding! Similarly, they're eager for anal switch-hitting to shake things up. However, it's not only about gorgeous sexperts filling their hungry holes. Sometimes, it's all about innocent rookies earning their pornstar status in first time threesomes and premier interracial scenes.", - url: 'http://www.bangingbeauties.com', - network: 'pervcity', - parameters: { tourId: 7 }, - }, - { - slug: 'oraloverdose', - name: 'Oral Overdose', - description: "Oral Overdose is the only site you need to live out every saliva soaked blowjob of your dreams in HD POV! We've got the most stunning cocksuckers in the world going to town on big dick. These babes not only love cock, they can't get enough of it! In fact, there is no prick too huge for our hungry girls' throats. You'll find gorgeous, big tits pornstars exercising their gag reflex in intense balls deep facefuck scenes. We also feature fresh, young newbies taking on the gagging deepthroat challenge.", - url: 'http://www.oraloverdose.com', - network: 'pervcity', - parameters: { tourId: 4 }, - }, - { - slug: 'chocolatebjs', - name: 'Chocolate BJs', - description: "You've just won the golden ticket to the best Chocolate BJs on the planet! We've sought far and wide to bring you the most beautiful black and ethnic pornstars. And they're in our members' area now! They can't wait to suck your white lollipop and lick the thick cream shooting from your big dick. Of course, no matter how sweet the booty or juicy the big tits, these brown foxes aren't all sugar and spice. In fact, when it comes to giving head, these big ass ebony babes know what they want: huge white cocks filling their throats!", - url: 'http://www.chocolatebjs.com', - network: 'pervcity', - parameters: { tourId: 6 }, - }, - { - slug: 'upherasshole', - name: 'Up Her Asshole', - description: "You don't need to travel the globe in search of the anal wonders of the world, because you get your own private tour right here on Up Her Asshole! Our stunning pornstars and rookie starlets welcome all ass fetish and anal sex fans, with their twerking bubble butts and winking assholes. However, big booty worship is just a slice of the fun. Combined with juicy tits (big and small, wet pussy (hairy and bald, these girls deliver a spectacular sensory experience in HD POV. Not only are you in danger of busting a nut before the going gets good, but also when the good turns remarkable with rimming, fingering and butt toys!", - url: 'http://www.upherasshole.com', - network: 'pervcity', - parameters: { tourId: 9 }, - }, - // PIMP XXX - { - slug: 'drilledxxx', - name: 'Drilled.XXX', - url: 'https://drilled.xxx', - tags: ['anal'], - network: 'pimpxxx', - }, - { - slug: 'cuckedxxx', - name: 'Cucked.XXX', - url: 'https://cucked.xxx', - tags: ['cuckold'], - network: 'pimpxxx', - }, - { - slug: 'familyxxx', - name: 'Family.XXX', - url: 'https://family.xxx', - tags: ['family'], - network: 'pimpxxx', - }, - { - slug: 'petitexxx', - name: 'Petite.XXX', - url: 'https://petite.xxx', - network: 'pimpxxx', - }, - { - slug: 'confessionsxxx', - name: 'Confessions.XXX', - url: 'https://confessions.xxx', - network: 'pimpxxx', - }, - { - slug: 'bcmxxx', - name: 'BCM.XXX', - url: 'https://bcm.xxx', - network: 'pimpxxx', - }, - // PORN PROS - { - name: 'Real Ex Girlfriends', - slug: 'realexgirlfriends', - alias: ['reg'], - url: 'https://pornpros.com/site/realexgirlfriends', - network: 'pornpros', - }, - { - name: '18 Years Old', - slug: 'eighteenyearsold', - alias: ['18yo'], - url: 'https://pornpros.com/site/18yearsold', - tags: ['teen'], - network: 'pornpros', - }, - { - name: 'Massage Creep', - slug: 'massagecreep', - alias: ['mc'], - url: 'https://pornpros.com/site/massagecreep', - tags: ['massage'], - network: 'pornpros', - }, - { - name: 'Deep Throat Love', - slug: 'deepthroatlove', - url: 'https://pornpros.com/site/deepthroatlove', - tags: ['blowjob', 'deepthroat'], - network: 'pornpros', - }, - { - name: 'Teen BFF', - slug: 'teenbff', - url: 'https://pornpros.com/site/teenbff', - tags: ['mff'], - network: 'pornpros', - }, - { - name: 'Shady P.I.', - slug: 'shadypi', - url: 'https://pornpros.com/site/shadypi', - network: 'pornpros', - }, - { - name: 'Cruelty Party', - slug: 'crueltyparty', - url: 'https://pornpros.com/site/crueltyparty', - network: 'pornpros', - }, - { - name: 'Disgraced 18', - slug: 'disgraced18', - url: 'https://pornpros.com/site/disgraced18', - network: 'pornpros', - }, - { - name: 'Cumshot Surprise', - slug: 'cumshotsurprise', - url: 'https://pornpros.com/site/cumshotsurprise', - network: 'pornpros', - }, - { - name: '40oz Bounce', - slug: 'fortyozbounce', - url: 'https://pornpros.com/site/40ozbounce', - network: 'pornpros', - }, - { - name: 'Jurassic Cock', - slug: 'jurassiccock', - url: 'https://pornpros.com/site/jurassiccock', - network: 'pornpros', - }, - { - name: 'Freaks Of Cock', - slug: 'freaksofcock', - url: 'https://pornpros.com/site/freaksofcock', - network: 'pornpros', - }, - { - name: 'Euro Humpers', - slug: 'eurohumpers', - url: 'https://pornpros.com/site/eurohumpers', - network: 'pornpros', - }, - { - name: 'Freaks Of Boobs', - slug: 'freaksofboobs', - url: 'https://pornpros.com/site/freaksofboobs', - network: 'pornpros', - }, - { - name: 'Cock Competition', - slug: 'cockcompetition', - url: 'https://pornpros.com/site/cockcompetition', - network: 'pornpros', - }, - { - name: 'Pimp Parade', - slug: 'pimpparade', - url: 'https://pornpros.com/site/pimpparade', - network: 'pornpros', - }, - { - name: 'MILF Humiliation', - slug: 'milfhumiliation', - url: 'https://milfhumiliation.com', - network: 'pornpros', - tags: ['milf'], - }, - { - name: 'Humiliated', - slug: 'humiliated', - url: 'https://humiliated.com', - network: 'pornpros', - }, - { - name: 'Flexible Positions', - slug: 'flexiblepositions', - url: 'https://flexiblepositions.com', - network: 'pornpros', - parameters: { - network: true, - }, - }, - { - name: 'Public Violations', - slug: 'publicviolations', - url: 'https://publicviolations.com', - network: 'pornpros', - parameters: { - network: true, - }, - }, - { - name: 'Amateur Violations', - slug: 'amateurviolations', - url: 'https://amateurviolations.com', - network: 'pornpros', - }, - { - name: 'Squirt Disgrace', - slug: 'squirtdisgrace', - url: 'https://squirtdisgrace.com', - network: 'pornpros', - }, - { - name: 'Cum Disgrace', - slug: 'cumdisgrace', - url: 'https://cumdisgrace.com', - network: 'pornpros', - }, - { - name: 'Webcam Hackers', - slug: 'webcamhackers', - url: 'https://webcamhackers.com', - network: 'pornpros', - }, - { - name: 'College Teens', - slug: 'collegeteens', - network: 'pornpros', - }, - // PRIVATE - { - slug: 'analintroductions', - name: 'Anal Introductions', - description: 'Private\'s Anal Introductions is all about ass. Watch these girls get their asses broken in by fat hard cocks! Hot double penetrations, gaping wide assholes and anal creampies are all standard in this exclusive site. Many of these girls have never had cock in their ass before, while others are real addicts and can only cum when being savagely sodomised. Watch which girls can take it... Private style.', - url: 'https://www.private.com/site/anal-introductions', - network: 'private', - }, - { - slug: 'iconfessfiles', - name: 'I Confess Files', - description: 'From the heart of the UK comes found footage exclusively provided to private.com which will shock and offend some viewers. Reality, perversion and unnatural lust come together perhaps as never before.', - url: 'https://www.private.com/site/i-confess-files', - network: 'private', - }, - { - slug: 'missionasspossible', - name: 'Mission: Ass Possible', - description: 'From the streets of Europe, Private\'s team of professionals find and exploit clueless young sluts, for some great sex and for your viewing pleasure. See what young hot chicks will do when their desire for easy fame or money makes them vulnerable to some of the craziest schemes and plots imaginable. Private\'s hung studs are on a mission for ass and hungry for fun. All part of the Private network of sites.', - url: 'https://www.private.com/site/mission-ass-possible', - network: 'private', - }, - { - slug: 'russianfakeagent', - name: 'Russian Fake Agent', - description: 'Direct from Russia, young naïve women pursue their dream of visas, Hollywood and fame. Eager to please and willing to do anything to get their break. Unfortunately, it’s a case of lies, sex and videotape as these gullible hotties perform for us. If these girls only knew the truth!', - url: 'https://www.private.com/site/russian-fake-agent', - network: 'private', - }, - { - slug: 'sexonthebeach', - name: 'Sex on the Beach', - description: 'Amazing locations and steamy sex in the sun, www.privatetropics.com is a celebration of tropical lust and sand covered sluts. From Private\'s exclusive line of scenes in exotic countries, watch what happens when our hot models go naked and native.', - url: 'https://www.private.com/site/sex-on-the-beach', - network: 'private', - }, - { - slug: 'tightandteen', - name: 'Tight and Teen', - description: 'Europe\'s number one teen offering and part of the Private network of sites, Tight and Teen takes you to the place that every father dreads, as 18+ teens discover just how much fun pleasing a hung stud can be. Fresh tight pussies, virgin anal initiations and teen face fuckings all brought to you exclusively by Europe\'s leading adult brand.', - url: 'https://www.private.com/site/tight-and-teen', - network: 'private', - }, - { - slug: 'blacksonsluts', - name: 'Blacks on Sluts', - description: 'See what happens when European women discover hung black studs looking to breed. Blacks on Sluts is 100% white slut and cheating wives versus huge black dicks giving them something they will never forget. Private puts its stamp on these women as our stallions stretch them to their limits.', - url: 'https://www.private.com/site/blacks-on-sluts', - network: 'private', - }, - { - slug: 'privateblack', - name: 'Private Black', - description: 'Private Black is number 1 for European Interracial Porn with exclusive interracial content in HD and Ultra 4K featuring the freshest young faces from Europe and the most popular European porn stars.', - url: 'https://www.privateblack.com', - network: 'private', - }, - { - slug: 'privatefetish', - name: 'Private Fetish', - description: 'www.privatefetish.com is here to give you that taste of dark desire that you secretly crave. Domination and Submission, Pleasure and Pain, are the drivers in this hardcore dungeon. What turns you on most? Being forced to beg for release from a sexy dominatrix or making that bitch next door beg for your cock? All part of Private\'s network of sites.', - url: 'https://www.private.com/site/private-fetish', - network: 'private', - }, - { - slug: 'privatemilfs', - name: 'Private MILFs', - description: 'Part of the awesome network of Private sites, Private MILFs is all about moms who are getting what they need when their limp dicked husbands can\'t perform. From their daughters\' stud boyfriends to their husbands\' hung black co-workers to their sons\' friends, no one is safe from their cravings for hard cock and salty cum.', - url: 'https://www.private.com/site/private-milfs', - network: 'private', - }, - { - slug: 'russianteenass', - name: 'Russian Teen Ass', - description: 'Many people say that Russian girls are the most beautiful in the world, and at www.russianteenass.com we show you why. These sexy Soviet newcomers are ready to work hard for their visa and their big shot at stardom. These barely 18+ girls know what they want and Private gives them an exclusive opportunity to get it. From Russia with lust, come see these girls in action!', - url: 'https://www.private.com/site/russian-teen-ass', - network: 'private', - }, - { - slug: 'privatestars', - name: 'Private Stars', - description: 'Welcome to Private Stars. The name speaks for itself as only top-model babes and perfect girls can join this select group of stars, all shot in HD. Part of the Private network of sites, Private Stars brings you a sneak peek into the life of seductive glamour girls who could have easily stepped from the catwalks of Paris or Milan and straight into a world of torrid sex.', - url: 'https://www.private.com/site/private-stars', - network: 'private', - }, - // PURE TABOO - { - name: 'Pure Taboo', - slug: 'puretaboo', - url: 'https://www.puretaboo.com', - description: 'PureTaboo.com is the ultimate site for family taboo porn, featuring submissive teens & virgins in rough sex videos in ultra 4k HD.', - network: 'puretaboo', - priority: 1, - parameters: { - independent: true, - mobile: 'https://m.dpfanatics.com/en/video', - }, - }, - { - name: 'Pretty Dirty', - slug: 'prettydirty', - alias: ['prdi'], - url: 'https://www.prettydirty.com', - network: 'puretaboo', - parameters: { - referer: 'https://www.puretaboo.com', - }, - }, - /* series, not sites, that appear on Pure Taboo itself + // BABES + { + name: 'Babes', + url: 'https://www.babes.com/scenes?site=213', + slug: 'babes', + network: 'babes', + }, + { + name: 'Babes Unleashed', + url: 'https://www.babes.com/scenes?site=218', + slug: 'babesunleashed', + network: 'babes', + }, + { + name: 'Black Is Better', + url: 'https://www.babes.com/scenes?site=217', + slug: 'blackisbetter', + network: 'babes', + }, + { + name: 'Elegant Anal', + url: 'https://www.babes.com/scenes?site=216', + slug: 'elegantanal', + network: 'babes', + }, + { + name: 'Office Obsession', + url: 'https://www.babes.com/scenes?site=214', + slug: 'officeobsession', + network: 'babes', + }, + { + name: 'Step Mom Lessons', + url: 'https://www.babes.com/scenes?site=215', + slug: 'stepmomlessons', + network: 'babes', + }, + // BAM VISIONS + { + slug: 'bamvisions', + name: 'BAM Visions', + url: 'https://tour.bamvisions.com', + parameters: { independent: true }, + network: 'bamvisions', + }, + // BANG + { + name: 'Trickery', + slug: 'bangtrickery', + url: 'https://www.bang.com/original/4800/bang-trickery', + parameters: { siteId: 4800 }, + network: 'bang', + }, + { + name: 'Yngr', + slug: 'yngrcom', + alias: ['byngr'], + // url: 'https://www.bang.com/original/5010/bang-yngr', + url: 'https://yngr.com', + parameters: { siteId: 5010 }, + network: 'bang', + }, + { + name: 'Roadside XXX', + slug: 'bangroadsidexxx', + // url: 'https://www.bang.com/original/4864/roadside-xxx', + url: 'https://roadsidexxx.com', + parameters: { siteId: 4864 }, + network: 'bang', + }, + { + name: 'Surprise', + slug: 'bangsurprise', + url: 'https://www.bang.com/original/5000/bang-surprise', + parameters: { siteId: 5000 }, + network: 'bang', + }, + { + name: 'Real Teens', + slug: 'bangrealteens', + alias: ['brealteens'], + url: 'https://www.bang.com/original/3366/bang-real-teens', + parameters: { siteId: 3366 }, + network: 'bang', + }, + { + name: 'FCK.news', + slug: 'bangfakenews', + // url: 'https://www.bang.com/original/4998/bang-fckNews', + url: 'https://fck.news', + parameters: { siteId: 4998 }, + network: 'bang', + }, + { + name: 'Pretty & Raw', + slug: 'prettyandraw', + // url: 'https://www.bang.com/original/4792/bang-pretty-and-raw', + url: 'https://prettyandraw.com', + parameters: { siteId: 4792 }, + network: 'bang', + }, + { + name: 'Japan', + slug: 'bangjapan', + url: 'https://www.bang.com/original/3079/bang-japan', + parameters: { siteId: 3079, ignore: true }, + network: 'bang', + }, + { + name: 'Rammed', + slug: 'bangrammed', + url: 'https://www.bang.com/original/4836/bang-rammed', + parameters: { siteId: 4836 }, + network: 'bang', + }, + { + name: 'Glamkore', + slug: 'bangglamkore', + alias: ['bglamkore'], + url: 'https://www.bang.com/original/4586/bang-glamkore', + parameters: { siteId: 4586 }, + network: 'bang', + }, + { + name: 'Screw The Cops', + slug: 'screwthecops', + url: 'https://www.bang.com/original/4710/bang-screw-cops', + parameters: { siteId: 4710 }, + network: 'bang', + }, + { + name: 'Real MILFs', + slug: 'bangrealmilfs', + alias: ['brealmilfs'], + url: 'https://www.bang.com/original/4448/bang-real-milfs', + parameters: { siteId: 4448 }, + network: 'bang', + }, + { + name: 'Confessions', + slug: 'bangconfessions', + alias: ['bconfessions'], + url: 'https://www.bang.com/original/4308/bang-confessions', + parameters: { siteId: 4308 }, + network: 'bang', + }, + { + name: 'Casting', + slug: 'bangcasting', + alias: ['bcasting'], + url: 'https://www.bang.com/original/3261/bang-casting', + parameters: { siteId: 3261 }, + network: 'bang', + }, + // BANGBROS + { + name: 'Ass Parade', + url: 'https://bangbros.com/websites/assparade', + slug: 'assparade', + description: null, + network: 'bangbros', + parameters: { code: 'ap' }, + }, + { + name: 'AvaSpice', + url: 'https://bangbros.com/websites/avaspice', + slug: 'avaspice', + description: null, + network: 'bangbros', + parameters: { code: 'av' }, + }, + { + name: 'Back Room Facials', + url: 'https://bangbros.com/websites/backroomfacials', + slug: 'backroomfacials', + description: null, + network: 'bangbros', + parameters: { code: 'brf' }, + }, + { + name: 'Backroom MILF', + url: 'https://bangbros.com/websites/backroommilf', + slug: 'backroommilf', + description: null, + network: 'bangbros', + parameters: { code: 'mf' }, + }, + { + name: 'Ball Honeys', + url: 'https://bangbros.com/websites/ballhoneys', + slug: 'ballhoneys', + description: null, + network: 'bangbros', + parameters: { code: 'es' }, + }, + { + name: 'BangBros 18', + url: 'https://bangbros.com/websites/bangbros18', + slug: 'bangbros18', + description: null, + network: 'bangbros', + parameters: { code: 'bbe' }, + }, + { + name: 'BangBros Angels', + url: 'https://bangbros.com/websites/bangbrosangels', + slug: 'bangbrosangels', + description: null, + network: 'bangbros', + parameters: { code: 'bng' }, + }, + { + name: 'Bangbros Clips', + url: 'https://bangbros.com/websites/bangbrosclips', + slug: 'bangbrosclips', + description: null, + network: 'bangbros', + parameters: { code: 'bbc' }, + }, + { + name: 'BangBros Remastered', + url: 'https://bangbros.com/websites/remaster', + slug: 'bangbrosremastered', + description: null, + network: 'bangbros', + parameters: { code: 'rm' }, + }, + { + name: 'Bang Bus', + url: 'https://bangbros.com/websites/bangbus', + slug: 'bangbus', + description: null, + network: 'bangbros', + parameters: { code: 'bb' }, + }, + { + name: 'Bang Casting', + url: 'https://bangbros.com/websites/bangcasting', + slug: 'bangbroscasting', + description: null, + network: 'bangbros', + parameters: { code: 'hih' }, + }, + { + name: 'Bang POV', + url: 'https://bangbros.com/websites/bangpov', + slug: 'bangpov', + description: null, + network: 'bangbros', + parameters: { code: 'bpov' }, + }, + { + name: 'Bang Tryouts', + url: 'https://bangbros.com/websites/bangtryouts', + slug: 'bangtryouts', + description: null, + network: 'bangbros', + parameters: { code: 'bto' }, + }, + { + name: 'Big Mouthfuls', + url: 'https://bangbros.com/websites/bigmouthfuls', + slug: 'bigmouthfuls', + description: null, + network: 'bangbros', + parameters: { code: 'bmf' }, + }, + { + name: 'Big Tit Cream Pie', + alias: ['btc'], + slug: 'bigtitcreampie', + url: 'https://bangbros.com/websites/bigtitcreampie', + description: null, + network: 'bangbros', + parameters: { code: 'btcp' }, + }, + { + name: 'Big Tits, Round Asses', + url: 'https://bangbros.com/websites/bigtitsroundasses', + alias: ['btra'], + slug: 'bigtitsroundasses', + description: null, + network: 'bangbros', + parameters: { code: 'btra' }, + }, + { + name: 'BlowJob Fridays', + url: 'https://bangbros.com/websites/blowjobfridays', + slug: 'blowjobfridays', + description: null, + network: 'bangbros', + parameters: { code: 'bj' }, + }, + { + name: 'Blowjob Ninjas', + url: 'https://bangbros.com/websites/blowjobninjas', + slug: 'blowjobninjas', + description: null, + network: 'bangbros', + parameters: { code: 'aa' }, + }, + { + name: 'Boob Squad', + url: 'https://bangbros.com/websites/boobsquad', + slug: 'boobsquad', + description: null, + network: 'bangbros', + parameters: { code: 'bs' }, + }, + { + name: 'Brown Bunnies', + url: 'https://bangbros.com/websites/brownbunnies', + slug: 'brownbunnies', + description: null, + network: 'bangbros', + parameters: { code: 'bkb' }, + }, + { + name: 'Can He Score?', + url: 'https://bangbros.com/websites/canhescore', + slug: 'canhescore', + description: null, + network: 'bangbros', + parameters: { code: 'bd' }, + }, + { + name: 'Casting', + url: 'https://bangbros.com/websites/casting', + slug: 'casting', + description: null, + network: 'bangbros', + parameters: { code: 'ca' }, + }, + { + name: 'Chongas', + url: 'https://bangbros.com/websites/chongas', + slug: 'chongas', + description: null, + network: 'bangbros', + parameters: { code: 'ch' }, + }, + { + name: 'Colombia Fuck Fest', + url: 'https://bangbros.com/websites/colombiafuckfest', + slug: 'colombiafuckfest', + description: null, + network: 'bangbros', + parameters: { code: 'cff' }, + }, + { + name: 'Dirty World Tour', + url: 'https://bangbros.com/websites/dirtyworldtour', + slug: 'dirtyworldtour', + description: null, + network: 'bangbros', + parameters: { code: 'bf' }, + }, + { + name: 'Dorm Invasion', + url: 'https://bangbros.com/websites/dorminvasion', + slug: 'dorminvasion', + description: null, + network: 'bangbros', + parameters: { code: 'di' }, + }, + { + name: 'Facial Fest', + url: 'https://bangbros.com/websites/facialfest', + slug: 'facialfest', + description: null, + network: 'bangbros', + parameters: { code: 'ff' }, + }, + { + name: 'Fuck Team Five', + url: 'https://bangbros.com/websites/fuckteamfive', + slug: 'fuckteamfive', + description: null, + network: 'bangbros', + parameters: { code: 'bbw' }, + }, + { + name: 'Glory Hole Loads', + url: 'https://bangbros.com/websites/gloryholeloads', + slug: 'gloryholeloads', + description: null, + network: 'bangbros', + parameters: { code: 'ghl' }, + }, + { + name: 'Latina Rampage', + url: 'https://bangbros.com/websites/latinarampage', + slug: 'latinarampage', + description: null, + network: 'bangbros', + parameters: { code: 'lrp' }, + }, + { + name: 'Living With Anna', + url: 'https://bangbros.com/websites/livingwithanna', + slug: 'livingwithanna', + description: null, + network: 'bangbros', + parameters: { code: 'lr' }, + }, + { + name: 'Magical Feet', + url: 'https://bangbros.com/websites/magicalfeet', + slug: 'magicalfeet', + description: null, + network: 'bangbros', + parameters: { code: 'fj' }, + }, + { + name: 'Milf Soup', + url: 'https://bangbros.com/websites/milfsoup', + slug: 'milfsoup', + description: null, + network: 'bangbros', + parameters: { code: 'ms' }, + }, + { + name: 'MomIsHorny', + url: 'https://bangbros.com/websites/momishorny', + slug: 'momishorny', + description: null, + network: 'bangbros', + parameters: { code: 'mih' }, + }, + { + name: 'Monsters of Cock', + url: 'https://bangbros.com/websites/monstersofcock', + slug: 'monstersofcock', + description: null, + network: 'bangbros', + parameters: { code: 'mc' }, + }, + { + name: 'Mr CamelToe', + url: 'https://bangbros.com/websites/mrcameltoe', + slug: 'mrcameltoe', + description: null, + network: 'bangbros', + parameters: { code: 'ct' }, + }, + { + name: 'My Dirty Maid', + slug: 'mydirtymaid', + alias: ['mdm'], + url: 'https://bangbros.com/websites/mydirtymaid', + description: null, + network: 'bangbros', + parameters: { code: 'mda' }, + }, + { + name: 'My Life In Brazil', + url: 'https://bangbros.com/websites/mylifeinbrazil', + slug: 'mylifeinbrazil', + description: null, + network: 'bangbros', + parameters: { code: 'mb' }, + }, + { + name: 'Newbie Black', + url: 'https://bangbros.com/websites/newbieblack', + slug: 'newbieblack', + description: null, + network: 'bangbros', + parameters: { code: 'blkg' }, + }, + { + name: 'Party of Three', + url: 'https://bangbros.com/websites/partyofthree', + slug: 'partyofthree', + description: null, + network: 'bangbros', + parameters: { code: 'ls' }, + }, + { + name: 'Pawg', + url: 'https://bangbros.com/websites/pawg', + slug: 'pawg', + description: null, + network: 'bangbros', + parameters: { code: 'pwg' }, + }, + { + name: 'Penny Show', + url: 'https://bangbros.com/websites/pennyshow', + slug: 'pennyshow', + description: null, + network: 'bangbros', + parameters: { code: 'ps' }, + }, + { + name: 'Porn Star Spa', + url: 'https://bangbros.com/websites/pornstarspa', + slug: 'pornstarspa', + description: null, + network: 'bangbros', + parameters: { code: 'pos' }, + }, + { + name: 'Power Munch', + url: 'https://bangbros.com/websites/powermunch', + slug: 'powermunch', + description: null, + network: 'bangbros', + parameters: { code: 'pm' }, + }, + { + name: 'Public Bang', + url: 'https://bangbros.com/websites/publicbang', + slug: 'publicbang', + description: null, + network: 'bangbros', + parameters: { code: 'pb' }, + }, + { + name: 'Slutty White Girls', + url: 'https://bangbros.com/websites/sluttywhitegirls', + slug: 'sluttywhitegirls', + description: null, + network: 'bangbros', + parameters: { code: 'swg' }, + }, + { + name: 'Stepmom Videos', + url: 'https://bangbros.com/websites/stepmomvideos', + slug: 'stepmomvideos', + description: null, + network: 'bangbros', + parameters: { code: 'smv' }, + }, + { + name: 'Street Ranger', + url: 'https://bangbros.com/websites/thewheeler', + slug: 'streetranger', + description: null, + network: 'bangbros', + parameters: { code: 'sg' }, + }, + { + name: 'Tugjobs', + url: 'https://bangbros.com/websites/tugjobs', + slug: 'tugjobs', + description: null, + network: 'bangbros', + parameters: { code: 'hj' }, + }, + { + name: 'Working Latinas', + url: 'https://bangbros.com/websites/workinglatinas', + slug: 'workinglatinas', + description: null, + network: 'bangbros', + parameters: { code: 'lw' }, + }, + { + name: 'MILF Lessons', + url: 'https://bangbros.com/websites/milflessons', + slug: 'milflessons', + description: null, + network: 'bangbros', + parameters: { code: 'ml' }, + }, + { + name: 'Mr. Anal', + url: 'https://bangbros.com/websites/mranal', + slug: 'mranal', + description: null, + network: 'bangbros', + parameters: { code: 'ma' }, + }, + // BLOWPASS + { + slug: '1000facials', + name: '1000 Facials', + alias: ['1kf'], + url: 'https://www.1000facials.com', + description: 'Welcome to 1000Facials.com, your source for the best facial porn with huge cumshots on your favorite teen and MILF pornstars. Watch all the blowjob action inside!', + network: 'blowpass', + parameters: { + latest: '/en/scenes/updates/%d/Category/0/Pornstar/0', + upcoming: '/en/scenes/upcoming', + }, + }, + { + slug: 'immorallive', + name: 'Immoral Live', + alias: ['il'], + url: 'https://www.immorallive.com', + description: 'Watch live sex shows and videos on ImmoralLive.com, featuring wild and crazy sex orgies, group sex, blowjob competitions and toy play from the famous Porno Dan. The hottest pornstars and amateur girls cum hard inside', + network: 'blowpass', + parameters: { + latest: '/en/videos/All-Categories/0/All-Pornstars/0/All/0/', + upcoming: '/en/videos/All-Categories/0/All-Pornstars/0/All/0/1/upcoming', + }, + }, + { + slug: 'mommyblowsbest', + name: 'Mommy Blows Best', + alias: ['mbb'], + url: 'https://www.mommyblowsbest.com', + description: 'Welcome to MommyBlowsBest.com. Home to thousands of MILF blowjobs and hot mom porn! Come see why experience counts, right here at MommyBlowsBest.com!', + network: 'blowpass', + parameters: { + latest: '/en/scenes/updates/0/Category/0/Actor/', + upcoming: '/en/scenes/upcoming', + }, + }, + { + slug: 'onlyteenblowjobs', + name: 'Only Teen Blowjobs', + alias: ['otb'], + url: 'https://www.onlyteenblowjobs.com', + description: 'OnlyTeenBlowjobs.com brings you the best teen blowjob porn featuring today\'s hottest young pornstars and amateurs. Watch as teens use their little mouths to suck and deepthroat the biggest of cocks!', + network: 'blowpass', + parameters: { + latest: '/en/scenes/updates/0/Category/0/Actor/', + upcoming: '/en/scenes/upcoming', + }, + }, + { + slug: 'throated', + name: 'Throated', + alias: ['ted'], + url: 'https://www.throated.com', + description: 'Throated.com is your portal for extreme throat fuck porn, face fucking videos and deepthroat gagging pornstars. Watch teens and MILFs go balls deep, swallowing cock in HD!', + network: 'blowpass', + parameters: { + latest: '/en/videos/latest/All-Categories/0/All-Pornstars/0/', + upcoming: '/en/videos/upcoming', + }, + }, + { + slug: 'sunlustxxx', + name: 'Sun Lust XXX', + url: 'https://www.sunlustxxx.com', + description: '', + network: 'blowpass', + show: true, // site offline, use only for indexing old scenes + }, + // BOOBPEDIA + { + slug: 'boobpedia', + name: 'Boobpedia', + url: 'https://www.boobpedia.com', + network: 'boobpedia', + }, + // BRAZZERS + { + slug: 'momsincontrol', + name: 'Moms in Control', + alias: ['mic'], + url: 'https://www.brazzers.com/sites/view/id/155/moms-in-control', + description: "There's nothing hotter than seeing a wholesome MILf get dirty, and that's exactly what MILFs in Control is all about: the hottest, sluttiest cougars in the business taking control of sexy situations to get exactly what they want. Feast your eyes as these mature beauties suck and fuck huge cocks, dominating big-dick studs and hot teen sluts until they get the cum that all MILFs crave!", + network: 'brazzers', + }, + { + slug: 'pornstarslikeitbig', + name: 'Pornstars Like It Big', + alias: ['plib'], + url: 'https://www.brazzers.com/sites/view/id/24/pornstars-like-it-big', + description: "A real big dick, that's what everyone wants. Porn-stars are no exception, all the biggest stars agree; BIG COCK is for them. Check out how it stretches their tiny pussies and cums on their round tits. We've got the best chicks jocking the biggest dicks.", + network: 'brazzers', + }, + { + slug: 'bigtitsatwork', + name: 'Big Tits at Work', + alias: ['btaw'], + url: 'https://www.brazzers.com/sites/view/id/15/big-tits-at-work', + description: 'Sitting at your desk, wishing you can fuck every busty coworker you have? Well, stop dreaming and step inside Big Tits At Work where you can watch real life work adventures caught on tape. Nothing But Big Breasted Work Professionals getting drilled all day long...', + network: 'brazzers', + }, + { + slug: 'bigtitsatschool', + name: 'Big Tits at School', + alias: ['btas'], + url: 'https://www.brazzers.com/sites/view/id/20/big-tits-at-school', + description: "The windows have been fogging up at Big Tits At School. Just take a peek inside one of our classrooms and you'll see our smoking hot busty students and big boobed dominant teachers getting their wet pussies stuffed with cock. Stay in your seat! you haven't been dismissed yet.", + network: 'brazzers', + }, + { + slug: 'babygotboobs', + name: 'Baby Got Boobs', + alias: ['bgb'], + url: 'https://www.brazzers.com/sites/view/id/9/baby-got-boobs', + description: "From fresh-faced teen to total slut, baby has boobs and she isn't afraid to show them. But does she know how to use them? These teens crave monster cock in their tight pussies, whether they're ready for a big dicking is another matter.", + network: 'brazzers', + }, + { + slug: 'realwifestories', + name: 'Real Wife Stories', + alias: ['rws'], + url: 'https://www.brazzers.com/sites/view/id/52/real-wife-stories', + description: "You might bring home the bacon, but your wife is still starving. That slut is hungry for cock, she can't get enough, and if you starve her any more she'll get it wherever she can. Better leave work early, or your big-titted wife might just have some giant cock getting squeezed into her waiting pussy, and it won't be yours.", + network: 'brazzers', + }, + { + slug: 'teenslikeitbig', + name: 'Teens Like It Big', + alias: ['tlib'], + url: 'https://www.brazzers.com/sites/view/id/51/teens-like-it-big', + description: "Whether they know it or not, teens love big stiff cocks in their tight pussies. Nothing goes better together than a tight, willing teen and a huge dick. In her bedroom or sneaking out to her boyfriend's, teens just want it all. Cum inside to see greedy sluts get more than they asked for", + network: 'brazzers', + }, + { + slug: 'zzseries', + name: 'ZZ Series', + alias: ['zzs'], + url: 'https://www.brazzers.com/sites/view/id/81/zz-series', + description: 'This is the spot for all our high-end content. ZZ series is exclusive footage that offers only the best in terms of story, stars and action. Check out the hottest porn-stars having the nastiest sex here at the ZZ series', + network: 'brazzers', + }, + { + slug: 'mommygotboobs', + name: 'Mommy Got Boobs', + alias: ['mgb'], + url: 'https://www.brazzers.com/sites/view/id/10/mommy-got-boobs', + description: "When hubby's away MILFS will play. Older women just crave cock, and they're experienced enough to know that only a young stud will do. Big-titted sluts everywhere are sucking and fucking in secret, giving it away to anybody they can. At Mommy Got Boobs, you can get some MILF of your own.", + network: 'brazzers', + }, + { + slug: 'milfslikeitbig', + name: 'MILFs Like It Big', + alias: ['mlib'], + url: 'https://www.brazzers.com/sites/view/id/36/milfs-like-it-big', + description: "When hubby's away milfy will play. These bored housewives want to get fucked and they want it now. They're experienced and know what they want. America's suburbs are full of these cum-de-sacs just waiting to get laid. Their round tits and thick asses are just begging for it. Cum inside, but don't park out front!", + network: 'brazzers', + }, + { + slug: 'bigtitsinuniform', + name: 'Big Tits In Uniform', + alias: ['btiu'], + url: 'https://www.brazzers.com/sites/view/id/73/big-tits-in-uniform', + description: "Big titted wonders are all around us, doing the toughest jobs in the tightest uniforms. Look at them just bursting out of that blouse, or over there, bulging under that nurse's uniform. You know when those tight uniforms come off these sluts go wild, sucking and fucking cocks left and right, their big tits just bouncing. I can't wait to punch the clock.", + network: 'brazzers', + }, + { + slug: 'doctoradventures', + name: 'Doctor Adventures', + alias: ['da'], + url: 'https://www.brazzers.com/sites/view/id/5/doctor-adventures', + description: 'Ever had fantasies about fucking your hot doctor? Live out your fantasies on doctoradventures.com. Countless doctor, patient scenarios come to life on this site with the sexiest and bustiest doctors imaginable! This is your one stop for the best in doctor porn in the world!', + network: 'brazzers', + }, + { + slug: 'brazzersexxtra', + name: 'Brazzers Exxtra', + alias: ['bex'], + url: 'https://www.brazzers.com/sites/view/id/152/brazzers-exxtra', + description: "\"Brazzers Exxtra\" is a doorway to new, unseen hardcore content! There are countless Brazzers videos that were not released throughout the years and we haven't been able to show them to you until now. Random videos staring the world's most popular pornstars, fresh new industry faces and a whole lot more! We'll even throw in an occasional free video from our friends at Mofos, Twisty's and Babes! Check it all out and let us know what you think. If you want more, we'll get it for you!", + network: 'brazzers', + }, + { + slug: 'bigtitsinsports', + name: 'Big Tits In Sports', + alias: ['btis'], + url: 'https://www.brazzers.com/sites/view/id/54/big-tits-in-sports', + description: 'Watch them bounce, watch them score and look at the way they handle those balls! Big tits in sports is here and so are the best big titted, athletic babes. Facials on the court and threesomes on the field, these busty sluts are ready for anything, even if it means playing dirty. Could you take them 1 on 1?', + network: 'brazzers', + }, + { + slug: 'brazzersvault', + name: 'Brazzers Vault', + url: 'https://www.brazzers.com/sites/view/id/56/brazzers-vault', + description: "We've got a whole super computer full of this stuff, technicians are working round the clock in the basement just to keep the thing from overheating. Yeah, this porno is hot. We need to get it out of before the whole thing melts down, that's why it's on the net, for you our loyal Brazzers Members. All the best scenes from all the best girls. In the World. Period.", + network: 'brazzers', + }, + { + slug: 'bigbuttslikeitbig', + name: 'Big Butts Like It Big', + alias: ['bblib'], + url: 'https://www.brazzers.com/sites/view/id/53/big-butts-like-it-big', + description: "You have to pair like with like. And big butts have to have big dicks to go with them. There's really no choice for these big round asses and the babes who fuck with them. Big assed bitches love it hard and deep, and won't have it any other way. Let the ass stuffing begin.", + network: 'brazzers', + }, + { + slug: 'bigwetbutts', + name: 'Big Wet Butts', + alias: ['bwb'], + url: 'https://www.brazzers.com/sites/view/id/8/big-wet-butts', + description: 'A nice, big, round butt is a special shape. Begging for powerful doggy style or straight anal penetration, cover a big butt in oil and it becomes a big wet butt, a true rarity. Watch these soft, tight asses get slathered and pounded like you only wish you could. Look at it bounce!', + network: 'brazzers', + }, + { + slug: 'daywithapornstar', + name: 'Day With A Pornstar', + alias: ['dwp'], + url: 'https://www.brazzers.com/sites/view/id/59/day-with-a-pornstar', + description: "We all know what our favorite stars can do on camera. We're familiar with the way they fuck and suck. What you don't get to see is what they do on their own time. Day With a Porn-star will show you everything, from crazy parties to total babe pals. Nobody else has access like this, it's the closest you get to living the dream.", + network: 'brazzers', + }, + { + slug: 'dirtymasseur', + name: 'Dirty Masseur', + alias: ['dm'], + url: 'https://www.brazzers.com/sites/view/id/150/dirty-masseur', + description: "Take a moment and unwind. Lay down, relax, and enjoy watching and wanking to these luscious Brazzers beauties getting good and greasy. Boobs, butts, and other lady-parts are at their prettiest when shimmering with slick oil. Book an appointment, and slide on in with a lubed babe. Believe me when I say, you'll have the happiest of endings...", + network: 'brazzers', + }, + { + slug: 'hotandmean', + name: 'Hot And Mean', + alias: ['ham'], + url: 'https://www.brazzers.com/sites/view/id/78/hot-and-mean', + description: "The hottest bitches run together. Hot, mean lesbians love to fuck each other and hate each other for being so beautiful. These lesbo sluts can't get enough pussy and love girl on girl action. Forget the dicks, these chicks don't need 'em. You can watch though, they love that.", + network: 'brazzers', + }, + { + slug: 'brazzersenespanol', + name: 'Brazzers en Español', + url: 'https://www.brazzers.com/sites/view/id/157/brazzers-en-espanol', + description: 'Brazzers en Español - El mejor sitio porno en alta definición del mundo ¡Ofreciéndole los vídeos para adultos en alta definición, descargables y en streaming, más exclusivos de Internet! Brazzers cuenta con las estrellas porno más sexys a través de los sitios más calientes en la red. Las estrellas porno y las escenas más calientes en internet. ¡Tendrá acceso a más sexo anal, tetas grandes y culos calientes de los que jamás soñó!', + network: 'brazzers', + }, + { + slug: 'brazzerslive', + name: 'Brazzers Live', + url: 'https://www.brazzers.com/sites/view/id/156/brazzers-live', + description: 'Brazzers is the industry leader for premium porn that breaks the mold. Pioneering its legendary LIVE SHOWS, ZZ is constantly redefining what hardcore erotica is about. Our wild fuck marathons are loaded with the steamiest improvised sex around. Catch a bevy of naked bodacious babes who ravage the biggest dicks with ease and in real-time. Our monster cock hunks rise to the occasion and feed these ravenous vixens who possess an insatiable appetite for cum.', + network: 'brazzers', + }, + { + slug: 'sexproadventures', + name: 'SexPro Adventures', + url: 'https://www.brazzers.com/sites/view/id/23/sexpro-adventures', + description: "Having trouble with your dick-style? The sex pros are here and they'll teach you everything you need to know to be a better man. At your place or theirs, these sluts just want to have a good time. Don't worry, she's a professional.", + network: 'brazzers', + }, + { + slug: 'shesgonnasquirt', + name: 'Shes Gonna Squirt', + url: 'https://www.brazzers.com/sites/view/id/151/shes-gonna-squirt', + description: "Enter the wet world of female ejaculation at shesgonnasquirt! Exclusive hardcore porn of your top pornstars squirting will excite you beyond belief. She's Gonna Squirt is home to the best in HD squirting sex videos. How to make a girl's pussy squirt is an art and should no longer remain a mystery, so join now to become a master.", + network: 'brazzers', + }, + { + slug: 'assesinpublic', + name: 'Asses In Public', + url: 'https://www.brazzers.com/sites/view/id/50/asses-in-public', + description: "Sex in public can present its challenges, never fear, we're willing to accept them. There's something hot about asses out in the street that we just can't deny. Porn-stars fucking on public or just hot girls showing their asses in the airport, we've got both and then some. Asses in Public has the roundest asses and the biggest tits just hanging out, where WILL we show up next?", + network: 'brazzers', + }, + { + slug: 'bustyz', + name: 'Bustyz', + url: 'https://www.brazzers.com/sites/view/id/6/bustyz', + description: "If the internet was a town we'd have the biggest tits around. We still do though, because Bustyz features only the best endowed porn stars in solo and group action. Watch these big-titted babes take cock, suck twat and show off their massive jugs. Real or fake, we don't judge, everyone's welcome under the big tit tent", + network: 'brazzers', + }, + { + slug: 'bustyandreal', + name: 'Busty & Real', + url: 'https://www.brazzers.com/sites/view/id/2/busty-real', + description: "Sometimes you need to take a break from the silicon football set. Busty and real has all the real jugs you need. Round. Soft. and as real as they come. These babes are rocking exactly what momma gave them. They've not afraid to show off their assets and get slammed with dick in the process.", + network: 'brazzers', + }, + { + slug: 'hotchicksbigasses', + name: 'Hot Chicks Big Asses', + url: 'https://www.brazzers.com/sites/view/id/7/hot-chicks-big-asses', + description: 'Everyone gather round; the giant ass. A babe can be hot in a lot of ways and having a big round ass is one of the best. All shapes, sizes and types these girls are the best of the best. Round, supple, jiggling asses taking on dicks and other pussies in equal measure.', + network: 'brazzers', + }, + { + slug: 'cfnm', + name: 'CFNM', + url: 'https://www.brazzers.com/sites/view/id/154/cfnm', + description: "Welcome to the world of clothed female sluts fucking, humiliating and dominating naked men, giving them a dose of what it feels like to be owned. If you love women with power dominating wimpy guys and showing them who's boss; women who crave for cock but get it exactly how they want it, that's what you'll find here. Simply put, the guys don't fuck the women, the women fuck the guys and make them feel like whores!", + network: 'brazzers', + }, + { + slug: 'jugfuckers', + name: 'JugFuckers', + url: 'https://www.brazzers.com/sites/view/id/12/jugfuckers', + description: "Like a sex hot-dog, a big dick fits nicely between two soft, round tits. Tit-fucking isn't easy and never will be. Our girls are pros and take big loads on their faces and tits with a smile. From DD to the smallest things going, we've got every type of tit- fuck around.", + network: 'brazzers', + }, + { + slug: 'teenslikeitblack', + name: 'Teens Like It Black', + url: 'https://www.brazzers.com/sites/view/id/57/teens-like-it-black', + description: "Teens just wanna piss their parents off; no rules, spring break, big black cocks. They love pushing things to the limit, how big and black can it be? Only teen girls know. Watch them get more than they bargained for, long black cocks drilling their tight, inexperienced pussies. It's an epic fuck when the biggest and the tightest meet.", + network: 'brazzers', + }, + { + slug: 'racksandblacks', + name: 'Racks & Blacks', + url: 'https://www.brazzers.com/sites/view/id/11/racks-blacks', + description: "All the interracial action you need is here. Big 'ol black cocks ramming and jamming pussies to the limit. All types of different girls fall prey to the venerable black dick. Wet pussies and fat asses? Bring it on. There's nothing our stable of asses can't handle, they'll keep cumming and cumming.", + network: 'brazzers', + }, + { + slug: 'buttsandblacks', + name: 'Butts & Blacks', + url: 'https://www.brazzers.com/sites/view/id/3/butts-blacks', + description: "Giant black dicks paired with round asses and garnished with the tightest pussies of all colors. Butts and Blacks delivers on its name sake, only the biggest dicks rocking the thickest chicks. These round honeys can take it all in and bounce around like it's a pogo stick. Come check out these soft round asses getting the attention they deserve.", + network: 'brazzers', + }, + // BURNING ANGEL + { + name: 'Burning Angel', + slug: 'burningangel', + alias: ['burna'], + url: 'https://www.burningangel.com', + network: 'burningangel', + parameters: { independent: true }, + }, + // CHERRY PIMPS + { + slug: 'cherrypimps', + name: 'Cherry Pimps', + alias: ['cps'], + url: 'https://cherrypimps.com', + description: 'CherryPimps your premium porn site to Download and Stream the hottest and most exclusive 4K HD videos and pictures on your phone, tablet, TV or console.', + network: 'cherrypimps', + parameters: { + extract: true, + }, + }, + { + slug: 'wildoncam', + name: 'Wild On Cam', + alias: ['woc'], + url: 'https://wildoncam.com', + tags: ['live'], + network: 'cherrypimps', + }, + { + slug: 'britneyamber', + name: 'Britney Amber', + url: 'https://www.britneyamber.com', + network: 'cherrypimps', + parameters: { + extract: true, + }, + }, + // DDF NETWORK + { + slug: 'ddfbusty', + name: 'DDF Busty', + alias: ['ddfb'], + url: 'https://ddfbusty.com', + description: 'Gorgeous Babes with big tits and Euro pornstars with huge natural boobs filmed in Exclusive Full HD, 4K, & VR porn videos.', + network: 'ddfnetwork', + }, + { + slug: 'handsonhardcore', + name: 'Hands on Hardcore', + alias: ['hoh'], + url: 'https://handsonhardcore.com', + description: 'Hardcore Sex & Anal Fucking Exclusive XXX Videos in VR, 4K and full HD with Hot European Pornstars', + network: 'ddfnetwork', + }, + { + slug: 'houseoftaboo', + name: 'House of Taboo', + alias: ['hotb', 'hotab'], + url: 'https://houseoftaboo.com', + description: 'Exclusive BDSM Porn & Extreme Sex Videos Produced in VR, 4K and full HD with The Hottest European Fetish Pornstars', + network: 'ddfnetwork', + }, + { + slug: 'ddfnetworkvr', + name: 'DDF Network VR', + alias: ['ddfvr'], + url: 'https://ddfnetworkvr.com', + description: 'VR Porn Videos shot Exclusively in 180 3D 4K Virtual Reality featuring the Hottest European & American VR Pornstar Babes', + network: 'ddfnetwork', + }, + { + slug: 'eurogirlsongirls', + name: 'Euro Girls on Girls', + url: 'https://eurogirlsongirls.com', + description: 'Hot Lesbian Sex & Glamour Lesbian Porn Videos and Photos Starring Gorgeous European Pornstars in 4K and Full HD VR.', + network: 'ddfnetwork', + }, + { + slug: '1byday', + name: '1By-Day', + url: 'https://1by-day.com', + description: 'Ultra Sexy Exclusive Solo Masturbation Videos in VR, 4K and full HD showcasing Glamour Babes & Intense Orgasms', + network: 'ddfnetwork', + }, + { + slug: 'euroteenerotica', + name: 'Euro Teen Erotica', + alias: ['ete'], + url: 'https://euroteenerotica.com', + description: 'Teen Threesomes & Barely Legal Porn Videos in 4K, VR and FULL HD with Hot Nymphomaniac Teen Babes', + network: 'ddfnetwork', + }, + { + slug: 'hotlegsandfeet', + name: 'Hot Legs and Feet', + url: 'https://hotlegsandfeet.com', + description: 'Foot Fetish & Sexy Legs Porn Videos with Hot and Sexy Euro Pornstars', + network: 'ddfnetwork', + }, + { + slug: 'onlyblowjob', + name: 'Only Blowjob', + alias: ['obj'], + url: 'https://onlyblowjob.com', + description: 'Fantasy Blowjobs & POV Cock Sucking Videos and Photos Produced in VR, 4K and full HD featuring Sexy European Pornstars', + network: 'ddfnetwork', + }, + { + slug: 'fuckinhd', + name: 'Fuck in HD', + url: 'https://fuckinhd.com', + description: 'HD Hardcore Sex & XXX Fantasy Porn Videos and Photos Produced in full HD featuring a Variety of Hardcore Porn Niches.', + network: 'ddfnetwork', + parameters: { native: true }, + }, + { + slug: 'bustylover', + name: 'Busty Lover', + url: 'https://bustylover.com', + network: 'ddfnetwork', + parameters: { native: true }, + }, + // DIGITAL PLAYGROUND + { + slug: 'digitalplayground', + name: 'Digital Playground', + url: 'https://www.digitalplayground.com/scenes', + description: '', + parameters: { extract: true }, + network: 'digitalplayground', + }, + { + slug: 'episodes', + name: 'Episodes', + url: 'https://www.digitalplayground.com/scenes?site=206', + description: '', + network: 'digitalplayground', + }, + { + slug: 'flixxx', + name: 'Flixxx', + url: 'https://www.digitalplayground.com/scenes?site=207', + description: '', + network: 'digitalplayground', + }, + { + slug: 'rawcut', + name: 'Raw Cut', + url: 'https://www.digitalplayground.com/scenes?site=208', + description: '', + network: 'digitalplayground', + }, + { + slug: 'dpstarepisodes', + name: 'DP Star Episodes', + url: 'https://www.digitalplayground.com/scenes?site=209', + description: '', + network: 'digitalplayground', + }, + { + slug: 'blockbuster', + name: 'Blockbuster', + url: 'https://www.digitalplayground.com/scenes?site=211', + description: '', + network: 'digitalplayground', + }, + { + slug: 'dpparodies', + name: 'DP Parodies', + url: 'https://www.digitalplayground.com/scenes?site=212', + description: '', + tags: ['parody'], + network: 'digitalplayground', + }, + // DOGFART NETWORK + { + slug: 'blacksonblondes', + name: 'Blacks On Blondes', + url: 'https://www.blacksonblondes.com/tour', + description: 'Blacks On Blondes is the Worlds Largest and Best Interracial Sex and Interracial Porn website. Black Men and White Women. BlacksOnBlondes has 23 years worth of Hardcore Interracial Content. Featuring the entire Legendary Dogfart Movie Archive', + network: 'dogfartnetwork', + }, + { + slug: 'cuckoldsessions', + name: 'Cuckold Sessions', + url: 'https://www.cuckoldsessions.com/tour', + description: 'Dogfart, the #1 Interracial Network in the World Presents CuckoldSessions.com/tour - Hardcore Cuckold Fetish Videos', + network: 'dogfartnetwork', + }, + { + slug: 'gloryhole', + name: 'Glory Hole', + url: 'https://www.gloryhole.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'blacksoncougars', + name: 'Blacks On Cougars', + url: 'https://www.blacksoncougars.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'wefuckblackgirls', + name: 'We Fuck Black Girls', + alias: ['wfbg'], + url: 'https://www.wefuckblackgirls.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'watchingmymomgoblack', + name: 'Watching My Mom Go Black', + url: 'https://www.watchingmymomgoblack.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'interracialblowbang', + name: 'Interracial Blowbang', + url: 'https://www.interracialblowbang.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'cumbang', + name: 'Cumbang', + url: 'https://www.cumbang.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'interracialpickups', + name: 'Interracial Pickups', + url: 'https://www.interracialpickups.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'watchingmydaughtergoblack', + name: 'Watching My Daughter Go Black', + url: 'https://www.watchingmydaughtergoblack.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'zebragirls', + name: 'Zebra Girls', + url: 'https://www.zebragirls.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'gloryholeinitiations', + name: 'Gloryhole Initiations', + url: 'https://www.gloryhole-initiations.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'dogfartbehindthescenes', + name: 'Dogfart Behind The Scenes', + url: 'https://www.dogfartbehindthescenes.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'blackmeatwhitefeet', + name: 'Black Meat White Feet', + url: 'https://www.blackmeatwhitefeet.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'springthomas', + name: 'Spring Thomas', + url: 'https://www.springthomas.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'katiethomas', + name: 'Katie Thomas', + url: 'https://www.katiethomas.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'ruthblackwell', + name: 'Ruth Blackwell', + url: 'https://www.ruthblackwell.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'candymonroe', + name: 'Candy Monroe', + url: 'https://www.candymonroe.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'wifewriting', + name: 'Wife Writing', + url: 'https://www.wifewriting.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'barbcummings', + name: 'Barb Cummings', + url: 'https://www.barbcummings.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'theminion', + name: 'The Minion', + url: 'https://www.theminion.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'blacksonboys', + name: 'Blacks On Boys', + url: 'https://www.blacksonboys.com/tour', + description: '', + network: 'dogfartnetwork', + }, + { + slug: 'gloryholesandhandjobs', + name: 'Gloryholes And Handjobs', + url: 'https://www.gloryholesandhandjobs.com/tour', + description: '', + network: 'dogfartnetwork', + }, + // EVIL ANGEL + { + slug: 'evilangel', + name: 'Evil Angel', + url: 'https://www.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: { independent: true }, + network: 'evilangel', + }, + // FAKE HUB + { + slug: 'fakeagentuk', + name: 'Fake Agent UK', + url: 'https://www.fakehub.com/scenes?site=277', + description: '', + network: 'fakehub', + }, + { + slug: 'fakecop', + name: 'Fake Cop', + url: 'https://www.fakehub.com/scenes?site=278', + description: '', + network: 'fakehub', + }, + { + slug: 'fakehospital', + name: 'Fake Hospital', + url: 'https://www.fakehub.com/scenes?site=279', + description: '', + network: 'fakehub', + }, + { + slug: 'fakeagent', + name: 'Fake Agent', + alias: ['fka'], + url: 'https://www.fakehub.com/scenes?site=280', + description: '', + network: 'fakehub', + }, + { + slug: 'faketaxi', + name: 'Fake Taxi', + alias: ['ftx'], + url: 'https://www.fakehub.com/scenes?site=281', + description: '', + network: 'fakehub', + }, + { + slug: 'publicagent', + name: 'Public Agent', + alias: ['pba'], + url: 'https://www.fakehub.com/scenes?site=282', + description: '', + network: 'fakehub', + }, + { + slug: 'femaleagent', + name: 'Female Agent', + url: 'https://www.fakehub.com/scenes?site=283', + description: '', + network: 'fakehub', + }, + { + slug: 'femalefaketaxi', + name: 'Female Fake Taxi', + alias: ['fft'], + url: 'https://www.fakehub.com/scenes?site=284', + description: '', + network: 'fakehub', + }, + { + slug: 'fakedrivingschool', + name: 'Fake Driving School', + alias: ['fds'], + url: 'https://www.fakehub.com/scenes?site=285', + description: '', + network: 'fakehub', + }, + { + slug: 'fakehuboriginals', + name: 'Fake Hub Originals', + alias: ['fho'], + url: 'https://www.fakehub.com/scenes?site=287', + description: '', + network: 'fakehub', + }, + { + slug: 'fakehostel', + name: 'Fake Hostel', + alias: ['fhl'], + url: 'https://www.fakehub.com/scenes?site=288', + description: '', + network: 'fakehub', + }, + // FAME DIGITAL + { + slug: 'devilsfilm', + name: 'Devil\'s Film', + url: 'https://www.devilsfilm.com', + description: 'Welcome to the best porn network, DevilsFilm.com, featuring teens, MILFs, trans and interracial porn with all of your favorite pornstars in 4k ultra HD!', + parameters: { api: true }, + network: 'famedigital', + }, + { + slug: 'lowartfilms', + name: 'Low Art Films', + url: 'https://www.lowartfilms.com', + description: 'Artistic Hardcore Porn Videos', + network: 'famedigital', + parameters: { + latest: '/en/All/scenes/0/latest/', + upcoming: '/en/All/scenes/0/upcoming', + }, + }, + { + slug: 'daringsex', + name: 'Daring Sex', + url: 'https://www.daringsexhd.com/', + description: 'Welcome the official Daring Sex site, home of high quality erotica, sensual porn and hardcore exploration of the darker side of sexuality. Here you will find a variety of videos for lovers looking for a bit of extra, or something darker with an element of control.', + network: 'famedigital', + parameters: { api: true }, + show: false, // no data sources + }, + { + slug: 'peternorth', + name: 'Peter North', + url: 'https://www.peternorth.com', + description: 'PeterNorth.com features hundreds of cumshots and deepthroat blowjob videos with the hottest teens & MILFs. Watch 25 years of Peter North inside!', + network: 'famedigital', + parameters: { + latest: '/en/videos/AllCategories/0/3/0/All-Dvds/0/latest/', + upcoming: '/en/videos/AllCategories/0/3/0/All-Dvds/0/upcoming', + }, + }, + { + slug: 'roccosiffredi', + name: 'Rocco Siffredi', + url: 'https://www.roccosiffredi.com', + description: 'Welcome to the official RoccoSiffredi.com, the Italian Stallion, with hardcore anal fucking and rough sex from the man himself who has coined the term hardcore.', + parameters: { api: true }, + network: 'famedigital', + }, + { + slug: 'silverstonedvd', + name: 'Silverstone DVD', + url: 'https://www.silverstonedvd.com', + description: 'Welcome to SilverStoneDVDs.com to enjoy unlimited streaming & downloads of teen porn, hot latina anal, young and dumb blowjob, DPs and hardcore porn.', + network: 'famedigital', + parameters: { + latest: '/en/All/scenes/0/latest/', + upcoming: '/en/All/scenes/0/upcoming', + }, + }, + { + slug: 'silviasaint', + name: 'Silvia Saint', + url: 'https://www.silviasaint.com', + description: 'Welcome to Silvia Saint official website. You can see Silvia Saint videos, pictures and blog!', + network: 'famedigital', + parameters: { + latest: '/en/scenes/All/0/', + upcoming: '/en/scenes/All/0/1/upcoming', + }, + }, + { + slug: 'whiteghetto', + name: 'White Ghetto', + url: 'https://www.whiteghetto.com', + description: 'Welcome to WhiteGhetto.com. Home of MILFs, GILFs, Midget porn, Indian babes, hairy pussies and more unusual and oddity porn!', + network: 'famedigital', + parameters: { + latest: '/en/scenes/All/0/superCat/0/latest/', + upcoming: '/en/scenes/All/0/superCat/0/upcoming', + }, + }, + // FANTASY MASSAGE + // Club Fantasy Massage is an aggregate site + { + slug: 'fantasymassage', + name: 'Fantasy Massage', + alias: ['fms'], + url: 'https://www.fantasymassage.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/fantasymassage/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/fantasymassage/AllCategories/0/Actor/0/upcoming/', + }, + }, + { + slug: 'allgirlmassage', + name: 'All Girl Massage', + alias: ['agm'], + url: 'https://www.allgirlmassage.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/allgirlmassage/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/allgirlmassage/AllCategories/0/Actor/0/upcoming/', + photos: 'https://www.fantasymassage.com/en/photo', + }, + }, + { + slug: 'nurumassage', + name: 'Nuru Massage', + alias: ['num'], + url: 'https://www.nurumassage.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/nurumassage/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/nurumassage/AllCategories/0/Actor/0/upcoming/', + photos: 'https://www.fantasymassage.com/en/photo', + }, + }, + { + slug: 'trickyspa', + name: 'Tricky Spa', + alias: ['tspa'], + url: 'https://www.trickyspa.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/trickyspa/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/trickyspa/AllCategories/0/Actor/0/upcoming/', + photos: 'https://www.fantasymassage.com/en/photo', + }, + }, + { + slug: 'soapymassage', + name: 'Soapy Massage', + url: 'https://www.soapymassage.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/soapymassage/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/soapymassage/AllCategories/0/Actor/0/upcoming/', + photos: 'https://www.fantasymassage.com/en/photo', + }, + }, + { + slug: 'milkingtable', + name: 'Milking Table', + url: 'https://www.milkingtable.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/milkingtable/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/milkingtable/AllCategories/0/Actor/0/upcoming/', + photos: 'https://www.fantasymassage.com/en/photo', + }, + }, + { + slug: 'massageparlor', + name: 'Massage Parlor', + url: 'https://www.massage-parlor.com', + network: 'fantasymassage', + parameters: { + latest: 'https://www.fantasymassage.com/en/allvideos/massage-parlor/AllCategories/0/AllPornstars/0/updates/', + upcoming: 'https://www.fantasymassage.com/en/allvideos/massage-parlor/AllCategories/0/Actor/0/upcoming/', + photos: 'https://www.fantasymassage.com/en/photo', + }, + }, + // FREEONES + { + slug: 'freeones', + name: 'FreeOnes', + url: 'https://www.freeones.com', + network: 'freeones', + }, + { + slug: 'freeoneslegacy', + name: 'FreeOnes (Legacy)', + url: 'https://www.freeones.com', + network: 'freeones', + }, + // FULL PORN NETWORK + { + slug: 'analbbc', + name: 'Anal BBC', + url: 'https://analbbc.com', + tags: ['anal', 'bbc'], + network: 'fullpornnetwork', + }, + { + slug: 'analviolation', + name: 'Anal Violation', + url: 'https://analviolation.com', + tags: ['anal'], + network: 'fullpornnetwork', + }, + { + slug: 'analized', + name: 'ANALIZED', + url: 'https://analized.com', + tags: ['anal'], + network: 'fullpornnetwork', + }, + { + slug: 'baddaddypov', + name: 'Bad Daddy POV', + alias: ['bdpov'], + url: 'https://baddaddypov.com', + tags: ['pov', 'family'], + network: 'fullpornnetwork', + }, + { + slug: 'dtfsluts', + name: 'DTF Sluts', + url: 'https://dtfsluts.com', + network: 'fullpornnetwork', + }, + { + slug: 'girlfaction', + name: 'Girlfaction', + url: 'https://girlfaction.com', + tags: ['lesbian'], + network: 'fullpornnetwork', + }, + { + slug: 'hergape', + name: 'Her Gape', + url: 'https://hergape.com', + tags: ['anal'], + network: 'fullpornnetwork', + }, + { + slug: 'homemadeanalwhores', + name: 'Homemade Anal Whores', + url: 'https://homemadeanalwhores.com', + tags: ['anal'], + network: 'fullpornnetwork', + }, + { + slug: 'jamesdeen', + name: 'James Deen', + url: 'https://jamesdeen.com', + network: 'fullpornnetwork', + }, + { + slug: 'onlyprince', + name: 'Only Prince', + url: 'https://onlyprince.com', + tags: ['bbc'], + network: 'fullpornnetwork', + }, + { + slug: 'pervertgallery', + name: 'Pervert Gallery', + url: 'http://pervertgallery.com', + network: 'fullpornnetwork', + }, + { + slug: 'povperverts', + name: 'POV Perverts', + url: 'http://povperverts.net', + tags: ['pov'], + network: 'fullpornnetwork', + }, + { + slug: 'teenageanalsluts', + name: 'Teenage Anal Sluts', + url: 'https://teenageanalsluts.com', + tags: ['anal'], + network: 'fullpornnetwork', + }, + { + slug: 'twistedvisual', + name: 'Twisted Visual', + url: 'https://twistedvisual.com', + network: 'fullpornnetwork', + }, + { + slug: 'yourmomdoesanal', + name: 'Your Mom Does Anal', + url: 'http://yourmomdoesanal.com', + tags: ['anal', 'milf'], + network: 'fullpornnetwork', + }, + { + slug: 'yourmomdoesporn', + name: 'Your Mom Does Porn', + url: 'https://yourmomdoesporn.com', + tags: ['milf'], + network: 'fullpornnetwork', + }, + { + slug: 'mugfucked', + name: 'Mugfucked', + url: 'https://mugfucked.com', + tags: ['facefucking', 'blowjob'], + network: 'fullpornnetwork', + }, + // GIRLSWAY + { + slug: 'girlsway', + name: 'Girlsway', + alias: ['gw'], + url: 'https://www.girlsway.com', + description: 'Girlsway.com has the best lesbian porn videos online! The hottest pornstars & first time lesbians in real girl on girl sex, tribbing, squirting & pussy licking action right HERE!', + tags: ['lesbian'], + network: 'girlsway', + parameters: { + scene: 'https://www.girlsway.com/en/video/girlsway', + }, + }, + { + slug: 'girlstryanal', + name: 'Girls Try Anal', + alias: ['gta'], + url: 'https://www.girlstryanal.com', + network: 'girlsway', + parameters: { + referer: 'https://www.girlsway.com', + mobile: 'https://m.dpfanatics.com/en/video', + }, + }, + { + slug: 'mommysgirl', + name: 'Mommy\'s Girl', + alias: ['mmgs'], + url: 'https://www.mommysgirl.com', + network: 'girlsway', + parameters: { + mobile: 'https://m.dpfanatics.com/en/video', + }, + }, + { + slug: 'webyoung', + name: 'Web Young', + url: 'https://www.webyoung.com', + network: 'girlsway', + parameters: { + referer: 'https://www.girlsway.com', + mobile: 'https://m.dpfanatics.com/en/video', + }, + }, + { + slug: 'sextapelesbians', + name: 'Sex Tape Lesbians', + url: 'https://www.sextapelesbians.com', + network: 'girlsway', + parameters: { + scene: 'https://www.girlsway.com/en/video/sextapelesbians', // sextapelesbians.com redirects to isthisreal.com + referer: 'https://www.girlsway.com', + }, + }, + { + slug: 'momsonmoms', + name: 'Moms On Moms', + url: 'https://www.girlsway.com/en/videos/momsonmoms', + network: 'girlsway', + parameters: { + scene: 'https://www.girlsway.com/en/video/sextapelesbians', + referer: 'https://www.girlsway.com', + }, + }, + // HUSSIE PASS + { + slug: 'hussiepass', + name: 'Hussie Pass', + url: 'https://www.hussiepass.com', + network: 'hussiepass', + }, + { + slug: 'eyeontheguy', + name: 'Eye On The Guy', + url: 'https://eyeontheguy.com', + tags: ['male-focus'], + network: 'hussiepass', + parameters: { + t1: true, + }, + }, + { + slug: 'seehimfuck', + name: 'See Him Fuck', + url: 'https://seehimfuck.com', + tags: ['male-focus'], + network: 'hussiepass', + parameters: { + tour: true, + }, + }, + { + slug: 'interracialpovs', + name: 'Interracial POVs', + url: 'https://www.interracialpovs.com', + tags: ['interracial', 'pov'], + network: 'hussiepass', + parameters: { + tour: true, + }, + }, + { + slug: 'povpornstars', + name: 'POV Pornstars', + url: 'http://www.povpornstars.com', + tags: ['pov'], + network: 'hussiepass', + parameters: { + latest: 'http://www.povpornstars.com/tour/categories/movies_%d_d.html', + profile: 'http://www.povpornstars.com/tour/models/%s.html', + tour: true, + }, + }, + // HUSH PASS + { + slug: 'shotherfirst', + name: 'Shot Her First', + url: 'https://shotherfirst.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/shot-her-first_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'whitezilla', + name: 'WhiteZilla', + url: 'https://whitezilla.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/whitezilla_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'frathousefuckfest', + name: 'Frat House Fuck Fest', + url: 'https://frathousefuckfest.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/frat-house-fuck-fest_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'freakyfirsttimers', + name: 'Freaky First Timers', + url: 'https://freakyfirsttimers.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/freaky-first-timers_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'milfinvaders', + name: 'MILF Invaders', + url: 'https://milfinvaders.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/milf-invaders_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'housewivesneedcash', + name: 'Housewives Need Cash', + url: 'https://housewivesneedcash.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/housewives-need-cash_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'bubblebuttbonanza', + name: 'Bubble Butt Bonanza', + url: 'https://bubblebuttbonanza.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/bubble-butt-bonanza_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'suburbansexparty', + name: 'Suburban Sex Party', + url: 'https://suburbansexparty.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/suburban-sex-party_%_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'buttnakedinthestreets', + name: 'Butt Naked In The Streets', + url: 'https://buttnakedinthestreets.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/ButtNakedInStreets_%d_d.html', + media: 'https://hushpass.com', + match: 'Butt Naked In Streets', + t1: true, + }, + }, + { + slug: 'muffbumperpatrol', + name: 'Muff Bumper Patrol', + url: 'https://muffbumperpatrol.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/muff-bumper-patrol_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'biggathananigga', + name: 'Bigga Than A Nigga', + url: 'https://biggathananigga.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/bigga-than-a-nigga_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'bachelorpartyfuckfest', + name: 'Bachelor Party Fuck Fest', + url: 'https://bachelorpartyfuckfest.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/bachelor-party-fuck-fest_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'teencumdumpsters', + name: 'Teen Cum Dumpsters', + url: 'https://teencumdumpsters.com', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/teen-cum-dumpsters_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'povhunnies', + name: 'POV Hunnies', + network: 'hushpass', + parameters: { + latest: 'https://hushpass.com/t1/categories/POVHunnies_%d_d.html', + media: 'https://hushpass.com', + t1: true, + }, + }, + { + slug: 'hushpass', + name: 'Hush Pass', + url: 'https://hushpass.com', + network: 'hushpass', + parameters: { + t1: true, + accFilter: true, + }, + }, + // INTERRACIAL PASS + { + slug: '2bigtobetrue', + name: '2 Big To Be True', + url: 'https://www.2bigtobetrue.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/2-big-to-be-true_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'abominableblackman', + name: 'Abominable Black Man', + url: 'https://www.abominableblackman.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/abominable-black-man_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'bootyannihilation', + name: 'Booty Annihilation', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/BootyAnnihilation_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'daddysworstnightmare', + name: 'Daddy\'s Worst Nightmare', + url: 'https://www.daddysworstnightmare.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/daddys-worst-nightmare_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'monstercockfuckfest', + name: 'Monster Cock Fuck Fest', + url: 'https://www.monstercockfuckfest.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/monster-cock-fuck-fest_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'mydaughtersfuckingablackdude', + name: 'My Daughter\'s Fucking A Black Dude', + url: 'https://www.mydaughtersfuckingablackdude.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/my-daughters-fucking-a-black-dude_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'mymomsfuckingblackzilla', + name: 'My Mom\'s Fucking Blackzilla', + url: 'https://www.mymomsfuckingblackzilla.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/my-moms-fucking-blackzilla_%d_d.html', + media: 'https://www.interracialpass.com', + t1: true, + }, + }, + { + slug: 'mywifesfirstmonstercock', + name: 'My Wife\'s First Monster Cock', + url: 'https://www.mywifesfirstmonstercock.com/', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + latest: 'https://www.interracialpass.com/t1/categories/my-wifes-first-monster-cock_%d_d.html', + media: 'https://www.interracialpass.com', + match: 'My Wifes First Monster Cock', + t1: true, + }, + }, + { + slug: 'interracialpass', + name: 'Interracial Pass', + url: 'https://www.interracialpass.com', + tags: ['interracial'], + network: 'interracialpass', + parameters: { + t1: true, + accFilter: true, + }, + }, + // INSEX + { + slug: 'sexuallybroken', + name: 'Sexually Broken', + alias: ['seb'], + url: 'https://www.sexuallybroken.com', + tags: ['bdsm'], + network: 'insex', + }, + { + slug: 'infernalrestraints', + name: 'Infernal Restraints', + alias: ['infr'], + url: 'https://www.infernalrestraints.com', + tags: ['bdsm'], + network: 'insex', + }, + { + slug: 'hardtied', + name: 'Hardtied', + url: 'https://www.hardtied.com', + tags: ['bdsm'], + network: 'insex', + }, + { + slug: 'realtimebondage', + name: 'Real Time Bondage', + alias: ['rtb'], + url: 'https://www.realtimebondage.com', + tags: ['bdsm', 'live'], + network: 'insex', + }, + { + slug: 'topgrl', + name: 'TopGrl', + alias: ['tg'], + url: 'https://www.topgrl.com', + tags: ['bdsm', 'femdom'], + network: 'insex', + }, + { + slug: 'paintoy', + name: 'Paintoy', + url: 'https://www.paintoy.com', + tags: ['bdsm'], + network: 'insex', + }, + { + slug: 'aganmedon', + name: 'Agan Medon', + url: 'https://www.aganmedon.com', + tags: ['bdsm', 'animated'], + network: 'insex', + }, + { + slug: 'sensualpain', + name: 'Sensual Pain', + url: 'https://www.sensualpain.com', + tags: ['bdsm'], + network: 'insex', + }, + // JAYS POV + { + slug: 'jayspov', + name: 'Jay\'s POV', + url: 'https://jayspov.net', + network: 'jayrock', + }, + { + slug: 'cospimps', + name: 'CosPimps', + url: 'https://cospimps.com', + network: 'jayrock', + }, + { + slug: 'blackforwife', + name: 'Black for Wife', + url: 'https://www.blackforwife.com', + network: 'jayrock', + parameters: { + referer: 'https://freetour.adulttime.com/en/blackforwife', + useGamma: true, + scene: false, + deep: 'https://21sextury.com/en/video', + photos: false, + }, + }, + // JESSE LOADS MONSTER FACIALS + { + slug: 'jesseloadsmonsterfacials', + name: 'Jesse Loads Monster Facials', + url: 'http://www.jesseloadsmonsterfacials.com', + network: 'jesseloadsmonsterfacials', + tags: ['facial', 'blowjob'], + parameters: { + independent: true, + }, + }, + // JULES JORDAN + { + slug: 'julesjordan', + name: 'Jules Jordan', + url: 'https://www.julesjordan.com', + description: 'Jules Jordan\'s Official Membership Site', + network: 'julesjordan', + }, + { + slug: 'theassfactory', + name: 'The Ass Factory', + url: 'https://www.theassfactory.com', + network: 'julesjordan', + }, + { + slug: 'spermswallowers', + name: 'Sperm Swallowers', + url: 'https://www.spermswallowers.com', + network: 'julesjordan', + }, + { + slug: 'manuelferrara', + name: 'Manuel Ferrara', + alias: ['mfa'], + url: 'https://www.manuelferrara.com', + network: 'julesjordan', + }, + { + slug: 'girlgirl', + name: 'Girl Girl', + url: 'https://www.girlgirl.com', + tags: ['lesbian'], + network: 'julesjordan', + }, + // KELLY MADISON MEDIA + { + slug: 'teenfidelity', + name: 'Teen Fidelity', + alias: ['tf'], + url: 'https://www.teenfidelity.com', + description: 'Home of Kelly Madison and Ryan Madison', + network: 'kellymadison', + }, + { + slug: 'pornfidelity', + name: 'Porn Fidelity', + alias: ['pf'], + url: 'https://www.pornfidelity.com', + description: 'Home of Kelly Madison and Ryan Madison', + network: 'kellymadison', + }, + { + slug: 'kellymadison', + name: 'Kelly Madison', + url: 'https://www.pornfidelity.com', + description: 'Home of Kelly Madison and Ryan Madison', + network: 'kellymadison', + }, + // KINK + { + slug: 'thirtyminutesoftorment', + name: '30 Minutes of Torment', + url: 'https://www.kink.com/channel/30minutesoftorment', + description: 'Thick-Muscled Men Endure 30 Minutes Of BDSM Torment By A Pain-Inducing Dom. Can they take 30 Minutes of Torment? Watch as top gay pornstars take on the challenge of a lifetime. Bondage, BDSM, punishment, huge insertions, & more!', + network: 'kink', + }, + { + slug: 'boundgangbangs', + name: 'Bound Gangbangs', + alias: ['bgb', 'bgbs'], + url: 'https://www.kink.com/channel/boundgangbangs', + description: 'Powerless whores tied in bondage and stuffed with a cock in every hole. At BoundGangbangs women get surprise extreme gangbangs, blindfolds, deepthroat blowjobs, sex punishment, bondage, double penetration and interracial sex.', + network: 'kink', + }, + { + slug: 'boundgods', + name: 'Bound Gods', + url: 'https://www.kink.com/channel/boundgods', + description: 'Muscle Studs Are Bound, Gagged & Spread For A Deep Cock Pounding. Not even the most rock hard muscled studs can escape punishment & submission on BoundGods.com Watch the hottest studs get tied down, fucked & submitted.', + tags: ['gay'], + network: 'kink', + }, + { + slug: 'boundinpublic', + name: 'Bound in Public', + url: 'https://www.kink.com/channel/boundinpublic', + description: 'Cum Starved Sluts Humiliated And Fucked Hard In Public By Hung Studs.', + network: 'kink', + }, + { + slug: 'brutalsessions', + name: 'Brutal Sessions', + url: 'https://www.kink.com/channel/brutalsessions', + description: "Hardcore BDSM jam packed with XXX fucking in bondage! We're taking dungeon sex beyond the castle!", + network: 'kink', + }, + { + slug: 'buttmachineboys', + name: 'Butt Machine Boys', + url: 'https://www.kink.com/channel/buttmachineboys', + description: 'Powerful Fucking Machines Pound Hot Men Hard & Deep.', + tags: ['gay'], + network: 'kink', + }, + { + slug: 'devicebondage', + name: 'Device Bondage', + alias: ['deb'], + url: 'https://www.kink.com/channel/devicebondage', + description: 'The Domination Of Sluts In Barbaric Metal Devices. Device Bondage takes BDSM porn to new levels with extreme restraints & unique devices with beautiful pornstars to huge, forced squirting orgasms.', + network: 'kink', + }, + { + slug: 'divinebitches', + name: 'Divine Bitches', + url: 'https://www.kink.com/channel/divinebitches', + description: 'Beautiful Women Dominate Submissive Men With Pain, Humiliation And Strap-On Fucking. The best in femdom and bondage. Men on Divine Bitches respond with obedience, ass worship, cunt worship, oral servitude, pantyhose worship, and foot worship.', + tags: ['femdom'], + network: 'kink', + }, + { + slug: 'electrosluts', + name: 'Electrosluts', + url: 'https://www.kink.com/channel/electrosluts', + description: 'Lezdoms Take Submissive Sluts To Their Limits, Shocking & Tormenting Their Wet Hot Pussies. Pornstars live out their electric bondage fantasies while dominatrixes use electrodes, paddles, caddle prods, & more to bring them to intense orgasms!', + network: 'kink', + }, + { + slug: 'everythingbutt', + name: 'Everything Butt', + url: 'https://www.kink.com/channel/everythingbutt', + description: 'Gaping Anal Holes Are Stuffed & Stretched To The Max. Anal Fisting, Enemas & Rimming Has Never Tasted So Good. EverythingButt.com explores the extreme limits of FemDom lesbian anal. Watch asses get destroyed by brutal fistings, huge insertions, double anal & more!', + network: 'kink', + }, + { + slug: 'filthyfemdom', + name: 'Filthy Femdom', + url: 'https://www.kink.com/channel/filthyfemdom', + description: 'Powerful women dominate your dirty dreams of sweet pain, seductive bondage, and sexual servitude.', + tags: ['femdom'], + network: 'kink', + }, + { + slug: 'familiestied', + name: 'Families Tied', + url: 'https://www.kink.com/channel/familiestied', + description: 'Intense BDSM family role play threesomes & more.', + network: 'kink', + }, + { + slug: 'footworship', + name: 'Foot Worship', + url: 'https://www.kink.com/channel/footworship', + description: 'Satisfy Your Foot Fetish With The Kinkiest Foot Action. Enjoy Trampling, Foot Jobs, High Heels, And Pantyhose.', + network: 'kink', + }, + { + slug: 'fuckedandbound', + name: 'Fucked and Bound', + alias: ['fab'], + url: 'https://www.kink.com/channel/fuckedandbound', + description: 'Extreme Anal, Rope Bondage, & Brutal Face Fucking.', + network: 'kink', + }, + { + slug: 'fuckingmachines', + name: 'Fucking Machines', + alias: ['fm', 'fum'], + url: 'https://www.kink.com/channel/fuckingmachines', + description: 'Machines Fucking Squirting Pussies With Extreme Insertions. Fucking Machines is the ultimate hardcore sex toy porn. Huge dildos strapped to sex machines relentlessly fucking pornstars to real squirting oragsms!', + network: 'kink', + }, + { + slug: 'hardcoregangbang', + name: 'Hardcore Gangbang', + url: 'https://www.kink.com/channel/hardcoregangbang', + description: "Where all women's hardcore gangbang fantasies come true. Watch extreme, brutal gangbangs with pornstars, models, & MILFs that crave cock in every hole. HardcoreGangbang.com has the best creampie gang bangs online.", + network: 'kink', + }, + { + slug: 'hogtied', + name: 'Hogtied', + alias: ['ht'], + url: 'https://www.kink.com/channel/hogtied', + description: 'Your favorite girls restrained with rope, punished & trained. Hogtied is the original extreme bondage porn website. Watch top pornstars and pain sluts in brutal bondage, getting tormented, and forced to orgasm!', + network: 'kink', + }, + { + slug: 'kinkfeatures', + name: 'Kink Features', + url: 'https://www.kink.com/channel/kinkfeatures', + description: 'Curated scenes by Kink\'s very best directors.', + network: 'kink', + }, + { + slug: 'kinkuniversity', + name: 'Kink University', + url: 'https://www.kink.com/channel/kinkuniversity', + description: 'Learn BDSM Technical Skills & Theories From Respected Teachers In The Kink Community. Learn BDSM skills and improve your sex techniques. Video tutorials feature top sex ed experts and hardcore demos on topics from bondage to relationships.', + network: 'kink', + }, + { + slug: 'meninpain', + name: 'Men In Pain', + url: 'https://www.kink.com/channel/meninpain', + description: 'Submissive Men Violated With Verbal Humiliation And Harsh Punishment By Beautiful Dominatrices.', + network: 'kink', + }, + { + slug: 'menonedge', + name: 'Men on Edge', + url: 'https://www.kink.com/channel/menonedge', + description: "Hot Guys Begging To Cum Are Brought To The Edge Of Complete Submission And Allowed To Blow Their Loads. Men on Edge has perfected the art of gay BDSM & edging porn. Watch straight men bound up & edged by dominant gay pornstars until they can't help but cum!", + tags: ['gay'], + network: 'kink', + }, + { + slug: 'nakedkombat', + name: 'Naked Kombat', + url: 'https://www.kink.com/channel/nakedkombat', + description: 'Fight Fit Studs Go Head To Head In A Battle For Dominance. The Loser Gets Pinned And Punish Fucked Without Mercy', + network: 'kink', + }, + { + slug: 'publicdisgrace', + name: 'Public Disgrace', + alias: ['pud'], + url: 'https://www.kink.com/channel/publicdisgrace', + description: 'Women Bound Stripped And Punished In Public Get Hardcore Fucked Where Everyone Can See. Unscripted public humiliation & punishment of submissive slaves in real life locations. PublicDisgrace features the best outdoor BDSM & voyeur porn!', + network: 'kink', + }, + { + slug: 'sadisticrope', + name: 'Sadistic Rope', + alias: ['sr'], + url: 'https://www.kink.com/channel/sadisticrope', + description: 'Innocence Taken By Extreme Rope Bondage, Hardcore BDSM And Pussy-Destroying Orgasms.', + network: 'kink', + }, + { + slug: 'sexandsubmission', + name: 'Sex and Submission', + alias: ['sas'], + url: 'https://www.kink.com/channel/sexandsubmission', + description: 'Submissive Sluts Are Dominated With Rough Sex And Bondage. Real pornstars, hardcore bondage, master & slave roles are what SexAndSubmission.com is all about. Watch submissive sluts give in to total domination!', + network: 'kink', + }, + { + slug: 'strugglingbabes', + name: 'Struggling Babes', + url: 'https://www.kink.com/channel/strugglingbabes', + description: 'Demystifying and celebrating alternative sexuality by providing the most authentic kinky videos. Experience the other side of porn.', + network: 'kink', + }, + { + slug: 'thetrainingofo', + name: 'The Training of O', + alias: ['tto'], + url: 'https://www.kink.com/channel/thetrainingofo', + description: 'Slaves Are Trained And Rewarded With Hardcore Bondage And Sex. Watch real pornstars undergo extreme slave training through hardcore bondage & BDSM porn. The Training of O is the ultimate slave / master experience!', + network: 'kink', + }, + { + slug: 'theupperfloor', + name: 'The Upper Floor', + alias: ['tuf'], + url: 'https://www.kink.com/channel/theupperfloor', + description: 'Trained slaves serve the house and their master in intense BDSM and kinky threesomes. The Upper Floor is a voyeuristic look into BDSM and fetish porn shoots with real submissive pornstars living out their kinky fantasies live on cam.', + network: 'kink', + }, + { + slug: 'tspussyhunters', + name: 'TS Pussy Hunters', + url: 'https://www.kink.com/channel/tspussyhunters', + description: 'Hot TS cocks prey on the wet pussies of submissive ladies who are fucked hard till they cum. Dominant TS femme fatales with the hardest dicks, the softest tits, and the worst intentions dominate, bind, and punish bitches on the ultimate transfucking porn site.', + tags: ['transsexual'], + network: 'kink', + }, + { + slug: 'tsseduction', + name: 'TS Seduction', + url: 'https://www.kink.com/channel/tsseduction', + description: 'Sexy TS Women With Huge Cocks Dominate The Holes Of Straight Boys. Real TS women who are drop-dead gorgeous from their pretty faces to their big tits to their hard TS cocks. TS Seduction is the ultimate in transsexual bondage porn.', + network: 'kink', + }, + { + slug: 'ultimatesurrender', + name: 'Ultimate Surrender', + url: 'https://www.kink.com/channel/ultimatesurrender', + description: 'Competitive Female Wrestling Where The Loser Gets Strap-On Punish Fucked. Ultimate Surrender features hardcore naked female wrestling porn videos where the winner gets to dominate the loser with some kinky lesbian FemDom!', + network: 'kink', + }, + { + slug: 'waterbondage', + name: 'Water Bondage', + url: 'https://www.kink.com/channel/waterbondage', + description: 'Helpless Bound Beauties Sprayed, Dunked And Tormented Until They Cum Hard & Wet.', + network: 'kink', + }, + { + slug: 'whippedass', + name: 'Whipped Ass', + alias: ['wpa', 'wa'], + url: 'https://www.kink.com/channel/whippedass', + description: 'Beautiful Submissive Sluts Take A Hard Fucking From Powerful Dominant Women. Watch brutal lesbian dominatrixes push submissive sluts to their orgasmic breaking points on WhippedAss! Hardcore fisting, huge strapons & face sitting!', + network: 'kink', + }, + { + slug: 'wiredpussy', + name: 'Wired Pussy', + url: 'https://www.kink.com/channel/wiredpussy', + description: 'Gorgeous Women Submit To Electricity, Are Zapped, Shocked & Prodded To Orgasm.', + network: 'kink', + }, + // LEGALPORNO + { + slug: 'legalporno', + name: 'LegalPorno', + alias: ['clip'], + url: 'https://www.legalporno.com', + description: 'The Best HD Porn For You!', + parameters: { independent: true }, + network: 'legalporno', + }, + // METRO HD + { + slug: 'devianthardcore', + name: 'Deviant Hardcore', + url: 'https://www.devianthardcore.com', + tags: ['bdsm'], + parameters: { + siteId: 305, + native: true, + }, + network: 'metrohd', + }, + { + slug: 'shewillcheat', + name: 'She Will Cheat', + url: 'https://www.shewillcheat.com', + parameters: { + siteId: 306, + native: true, + }, + network: 'metrohd', + }, + { + slug: 'familyhookups', + name: 'Family Hookups', + url: 'https://www.familyhookups.com', + tags: ['family'], + parameters: { + siteId: 307, + native: true, + }, + network: 'metrohd', + }, + { + slug: 'kinkyspa', + name: 'Kinky Spa', + url: 'https://www.kinkyspa.com', + tags: ['massage'], + parameters: { + siteId: 308, + native: true, + }, + network: 'metrohd', + }, + { + slug: 'girlgrind', + name: 'Girl Grind', + url: 'https://www.girlgrind.com', + tags: ['lesbian'], + parameters: { + siteId: 309, + native: true, + }, + network: 'metrohd', + }, + // MEN + { + slug: 'bigdicksatschool', + name: 'Big Dicks At School', + url: 'https://www.bigdicksatschool.com', + description: '', + parameters: { siteId: 252 }, + tags: ['gay'], + network: 'men', + }, + { + slug: 'drillmyhole', + name: 'Drill My Hole', + url: 'https://www.drillmyhole.com', + description: '', + parameters: { siteId: 253 }, + tags: ['gay'], + network: 'men', + }, + { + slug: 'str8togay', + name: 'Str8 to Gay', + url: 'https://www.str8togay.com', + tags: ['gay'], + parameters: { siteId: 254 }, + network: 'men', + }, + { + slug: 'thegayoffice', + name: 'The Gay Office', + url: 'https://www.thegayoffice.com', + tags: ['gay'], + parameters: { siteId: 255 }, + network: 'men', + }, + { + slug: 'jizzorgy', + name: 'Jizz Orgy', + url: 'https://www.jizzorgy.com', + tags: ['gay'], + parameters: { siteId: 256 }, + network: 'men', + }, + { + slug: 'menofuk', + name: 'Men of UK', + url: 'https://www.menofuk.com', + tags: ['gay'], + parameters: { siteId: 258 }, + network: 'men', + }, + { + slug: 'toptobottom', + name: 'Top to Bottom', + url: 'https://www.toptobottom.com', + tags: ['gay'], + parameters: { siteId: 259 }, + network: 'men', + }, + { + slug: 'godsofmen', + name: 'Gods of Men', + url: 'https://www.godsofmen.com', + tags: ['gay'], + parameters: { siteId: 260 }, + network: 'men', + }, + // MINDGEEK + { + slug: 'pornhub', + name: 'PornHub', + url: 'https://www.pornhub.com', + description: '', + network: 'mindgeek', + }, + { + slug: 'tube8vip', + name: 'Tube8Vip', + url: 'https://www.tube8vip.com', + description: '', + parameters: { native: true }, + network: 'mindgeek', + }, + { + slug: 'transangels', + name: 'TransAngels', + url: 'https://www.transangels.com', + tags: ['transsexual'], + parameters: { native: true }, + network: 'mindgeek', + }, + { + slug: 'trueamateurs', + name: 'True Amateurs', + url: 'https://www.trueamateurs.com', + description: 'TrueAmateurs.com is the best homemade porn from real amateurs. Watch these real hot couples in our exclusive scenes.', + parameters: { native: true }, + network: 'mindgeek', + }, + // MIKE ADRIANO + { + slug: 'trueanal', + name: 'True Anal', + url: 'https://trueanal.com', + description: 'TrueAnal is the hottest site with all hardcore Anal content and only the most popular pornstars getting their asses pounded and gapped with huge cock and more!', + tags: ['anal'], + network: 'mikeadriano', + }, + { + slug: 'allanal', + name: 'All Anal', + url: 'https://allanal.com', + description: 'Popular babes getting their tight asses filled with cock! Pure anal fucking only at AllAnal!', + tags: ['anal', 'mff'], + network: 'mikeadriano', + }, + { + slug: 'nympho', + name: 'Nympho', + url: 'https://nympho.com', + description: 'These Babes have an appetite for nasty, sloppy fucking!', + network: 'mikeadriano', + }, + { + slug: 'swallowed', + name: 'Swallowed', + url: 'https://swallowed.com', + description: 'Swallowed is a Premium adult website for the hottest Blowjobs content online with only the most popular pornstars swallowing cock!', + tags: ['blowjob', 'deepthroat', 'facefucking'], + network: 'mikeadriano', + }, + // MILE HIGH MEDIA + { + slug: 'doghousedigital', + name: 'Doghouse Digital', + url: 'https://www.doghousedigital.com', + parameters: { siteId: 321 }, + network: 'milehighmedia', + }, + { + slug: 'milehighmedia', + name: 'Mile High Media', + url: 'https://www.milehighmedia.com/scenes?site=323', + network: 'milehighmedia', + }, + { + slug: 'realityjunkies', + name: 'Reality Junkies', + url: 'https://www.realityjunkies.com', + parameters: { siteId: 324 }, + network: 'milehighmedia', + }, + { + slug: 'sweetheartvideo', + name: 'Sweetheart Video', + url: 'https://www.sweetheartvideo.com', + parameters: { siteId: 325 }, + network: 'milehighmedia', + }, + { + slug: 'sweetsinner', + name: 'Sweet Sinner', + url: 'https://www.sweetsinner.com', + parameters: { siteId: 326 }, + network: 'milehighmedia', + }, + { + slug: 'iconmale', + name: 'Icon Male', + url: 'https://www.iconmale.com', + tags: ['gay'], + parameters: { native: true }, + network: 'milehighmedia', + }, + // MOFOS + { + slug: 'girlsgonepink', + name: 'Girls Gone Pink', + url: 'https://www.mofos.com/scenes?site=204', + description: "There comes a point in every woman's life when she gets a little curious about what some hot girl on girl sex could be like. Whether they're lesbian or just straight and incredibly daring and open-minded, the end result is the same. GirlsGonePink.com is full of soft lips, long flowing hair, and sensual feminine figures that are enough to get any horny minx's blood pumping. Premium full-length lesbian porn videos await you full of perfect boobs, pointy nipples, round butts, and luscious legs that usually stay separated!", + network: 'mofos', + }, + { + slug: 'ebonysextapes', + name: 'Ebony Sex Tapes', + url: 'https://www.mofos.com/scenes?site=202', + description: 'Once you go black, you never go back! Did you think that was only how white women feel about black men? Well if you did, that can only mean you never had a stacked, curvy, big ass, beautiful black teen riding your hard white cock. Watch these lucky guys fuck their Ebony Girlfriends at EbonySexTapes.com.', + network: 'mofos', + }, + { + slug: 'sharemybf', + name: 'Share My BF', + alias: ['smb'], + url: 'https://www.mofos.com/scenes?site=201', + description: 'Would your cock be able to handle 2 wet pussies at the same time? One hot teen riding your face while the other deepthroats you. You know your GF tells all her friends how big your dick is. Imagine if you can fuck her and her friend at the same time? Live the fantasy at ShareMyBF.com.', + network: 'mofos', + }, + { + slug: 'dontbreakme', + name: "Don't Break Me", + alias: ['dbm'], + url: 'https://www.mofos.com/scenes?site=198', + description: 'DontBreakMe.com is about tiny spinners fucking big guys with massive dicks! Most of these chicks are shorter than 5 feet tall and weigh less than 100lbs. Meanwhile, these girls are paired up with guys who tower over them by at least 1.5 feet and have 9" dicks!! The look on their faces when they see that huge dick pop out of his pants is priceless. While it turns them on they usually get a bit nervous: "how will I squeeze that huge cock inside?" Ouch! Check it out.', + network: 'mofos', + }, + { + slug: 'iknowthatgirl', + name: 'I Know That Girl', + alias: ['iktg'], + url: 'https://www.mofos.com/scenes?site=183', + description: 'Every single gorgeous girl you see on this site is 100% Real! They are all part of the biggest user submitted, amateur video site in the world...IKnowThatGirl.com! Hot young girlfriends getting kinky on camera, sucking and fucking, even stuffing dildos up their tight pussies, all filmed on home video and leaked to us by some lowlife, soon to be ex-boyfriend or former best friend! Oh well... Enjoy!', + network: 'mofos', + }, + { + slug: 'letstryanal', + name: 'Lets Try Anal', + alias: ['lta'], + url: 'https://www.mofos.com/scenes?site=189', + description: "This isn't just another anal site! Letstryanal.com features the hottest real footage of amateur girls and their first time ass fucking experiences. Watch it all... innocent girlfriends being convinced to try anal, their faces of pain and screaming as they beg their boyfriend to \"please go slower\" while a large cock penetrates their tight asses for the first time! Let's face it, there is nothing like seeing a cock disappear in a virgin asshole. It's so hot!", + network: 'mofos', + }, + { + slug: 'latinasextapes', + name: 'Latina Sex Tapes', + alias: ['lst'], + url: 'https://www.mofos.com/scenes?site=188', + description: "100% Real Latina Girls getting fucked by their boyfriends, filmed and submitted to us for Big $$$! Watch amazing real footage and private videos of these beautiful amateur girls, their perfectly tanned bodies, mouth-watering curves, luscious round asses, and mind blowing accents! We've only kept the best, most outstanding sex videos and uploaded them for you to watch. You'll be amazed with what we received, and more is on the way!", + network: 'mofos', + }, + { + slug: 'publicpickups', + name: 'Public Pickups', + alias: ['ppu'], + url: 'https://www.mofos.com/scenes?site=190', + description: "Check out the hottest REAL footage of young girls getting picked up and fucked in public! The girls are usually shy around guys approaching them with a video camera, but that's the fun part. Besides their shyness slowly disappears after they're offered money to get dirty. While it's a real turn on seeing the girls flash and get fondled in public... the hottest part is watching them get fucked everywhere...in cars, parks, clubs, even the library!", + network: 'mofos', + }, + { + slug: 'pervsonpatrol', + name: 'Pervs On Patrol', + alias: ['pop'], + url: 'https://www.mofos.com/scenes?site=185', + description: "A while back, this beautiful girl who lived next door use to always undress with her window opened. This girl had no fucking clue that I was jerking off over her from across the yard. One day I decided to grab my dad's camera and start filming her. It was amazing... until she finally caught me. Fuck, this girl was PISSED!..., but could you fucking believe that once she calmed down she was actually a little turned on by the whole situation,... and what happened next changed my life!", + network: 'mofos', + }, + { + slug: 'strandedteens', + name: 'Stranded Teens', + alias: ['sts'], + url: 'https://www.mofos.com/scenes?site=192', + description: "Watch videos on StrandedTeens.com and you will never look at a hitchhiker the same way again! Some of these girls will do anything for a ride or simply to make a friend - even the shy ones. From giving road head to getting ass-fucked on the hood of the car, you can watch it all. Check it out now, you won't be disappointed!", + network: 'mofos', + }, + { + slug: 'realslutparty', + name: 'Real Slut Party', + url: 'https://www.mofos.com/scenes?site=184', + description: "Wanna see the most mind blowing college sex parties from across the country? It's the real deal, all caught on video and submitted by you! Insane college craziness, pussy packed house parties, holiday orgies, backyard BBQ's gone wrong and hundreds of tight, young girls getting crazy, stripped down, and on the prowl for all the cock they can find!", + network: 'mofos', + }, + { + slug: 'mofoslab', + name: 'MOFOS Lab', + url: 'https://www.mofos.com/scenes?site=205', + description: "We've received your feedback and are experimenting with turning your wildest fantasies into the ultimate POV experience; this is Mofos Lab! Featuring today's hottest and freshest talent, immerse yourself in an exciting Mofos venture that brings you the edgiest new content!", + network: 'mofos', + }, + { + slug: 'mofosbsides', + name: 'Mofos B Sides', + url: 'https://www.mofos.com/scenes?site=191', + description: "Mofos B-Sides is a doorway to new, unseen amateur video! Hundreds of clips have been submitted to Mofos through the years and we've never shown them to you until now. We'll give you a little bit at a time, from random girls in random scenario\\’s, and maybe even an occasional free video from our friends at Brazzers, Twisty's and Babes! Check it all out and let us know what you think.", + network: 'mofos', + }, + { + slug: 'shesafreak', + name: "She's A Freak", + alias: ['saf'], + url: 'https://www.mofos.com/scenes?site=187', + description: "Fresh, young amateur girls with beautiful tight bodies, pushing themselves to the limit! It's just another great way that today's hottest new models are choosing to showcase their stunning bodies and show all of us that they're ready for more! Soaking wet masturbation, fisting, squirting, double penetration and anal toys are just some of the things they do to show us how freaky they can be and how ready they are to graduate from toys to thick, fat cock!", + network: 'mofos', + }, + // NAUGHTY AMERICA + { + slug: 'myfriendshotmom', + name: 'My Friend\'s Hot Mom', + alias: ['mfhm'], + url: 'https://www.naughtyamerica.com/site/my-friend-s-hot-mom', + network: 'naughtyamerica', + }, + { + slug: 'slutstepmom', + name: 'Slut Step Mom', + url: 'https://www.naughtyamerica.com/site/slut-step-mom', + network: 'naughtyamerica', + }, + { + slug: 'openfamily', + name: 'Open Family', + url: 'https://www.naughtyamerica.com/site/open-family', + network: 'naughtyamerica', + }, + { + slug: 'sleazystepdad', + name: 'Sleazy Stepdad', + url: 'https://www.naughtyamerica.com/site/sleazy-stepdad', + network: 'naughtyamerica', + }, + { + slug: 'watchyourmom', + name: 'Watch Your Mom', + url: 'https://www.naughtyamerica.com/site/watch-your-mom', + network: 'naughtyamerica', + }, + { + slug: 'bigcockbully', + name: 'Big Cock Bully', + alias: ['bcb'], + url: 'https://www.naughtyamerica.com/site/big-cock-bully', + network: 'naughtyamerica', + }, + { + slug: 'bigcockhero', + name: 'Big Cock Hero', + alias: ['bch'], + url: 'https://www.naughtyamerica.com/site/big-cock-hero', + network: 'naughtyamerica', + }, + { + slug: 'mysistershotfriend', + name: "My Sister's Hot Friend", + alias: ['mshf'], + url: 'https://www.naughtyamerica.com/site/my-sister-s-hot-friend', + network: 'naughtyamerica', + }, + { + slug: 'myfirstsexteacher', + name: 'My First Sex Teacher', + alias: ['mfst'], + url: 'https://www.naughtyamerica.com/site/my-first-sex-teacher', + network: 'naughtyamerica', + }, + { + slug: 'slutstepsister', + name: 'Slut Step Sister', + url: 'https://www.naughtyamerica.com/site/slut-step-sister', + network: 'naughtyamerica', + }, + { + slug: 'teenslovecream', + name: 'Teens Love Cream', + url: 'https://www.naughtyamerica.com/site/teens-love-cream', + network: 'naughtyamerica', + }, + { + slug: 'latinastepmom', + name: 'Latina Step Mom', + url: 'https://www.naughtyamerica.com/site/latina-step-mom', + network: 'naughtyamerica', + }, + { + slug: 'seducedbyacougar', + name: 'Seduced By A Cougar', + url: 'https://www.naughtyamerica.com/site/seduced-by-a-cougar', + network: 'naughtyamerica', + }, + { + slug: 'showmybf', + name: 'Show My BF', + url: 'https://www.naughtyamerica.com/site/show-my-bf', + network: 'naughtyamerica', + }, + { + slug: 'mydaughtershotfriend', + name: "My Daughter's Hot Friend", + alias: ['mdhf'], + url: 'https://www.naughtyamerica.com/site/my-daughter-s-hot-friend', + network: 'naughtyamerica', + }, + { + slug: 'lasluts', + name: 'LA Sluts', + url: 'https://www.naughtyamerica.com/site/la-sluts', + network: 'naughtyamerica', + }, + { + slug: 'mywifeismypornstar', + name: 'My Wife Is My Pornstar', + url: 'https://www.naughtyamerica.com/site/my-wife-is-my-pornstar', + network: 'naughtyamerica', + }, + { + slug: 'watchyourwife', + name: 'Watch Your Wife', + url: 'https://www.naughtyamerica.com/site/watch-your-wife', + network: 'naughtyamerica', + }, + { + slug: 'tonightsgirlfriendclassic', + alias: ['togc'], + name: "Tonight's Girlfriend", + url: 'https://www.naughtyamerica.com/site/tonight-s-girlfriend-classic', + network: 'naughtyamerica', + }, + { + slug: 'wivesonvacation', + name: 'Wives on Vacation', + alias: ['wov'], + url: 'https://www.naughtyamerica.com/site/wives-on-vacation', + network: 'naughtyamerica', + }, + { + slug: 'naughtyweddings', + name: 'Naughty Weddings', + alias: ['nw'], + url: 'https://www.naughtyamerica.com/site/naughty-weddings', + network: 'naughtyamerica', + }, + { + slug: 'dirtywivesclub', + name: 'Dirty Wives Club', + alias: ['dwc'], + url: 'https://www.naughtyamerica.com/site/dirty-wives-club', + network: 'naughtyamerica', + }, + { + slug: 'mydadshotgirlfriend', + name: "My Dad's Hot Girlfriend", + alias: ['mdhg'], + url: 'https://www.naughtyamerica.com/site/my-dad-s-hot-girlfriend', + network: 'naughtyamerica', + }, + { + slug: 'mygirllovesanal', + name: 'My Girl Loves Anal', + url: 'https://www.naughtyamerica.com/site/my-girl-loves-anal', + network: 'naughtyamerica', + }, + { + slug: 'analcollege', + name: 'Anal College', + url: 'https://www.naughtyamerica.com/site/anal-college', + network: 'naughtyamerica', + }, + { + slug: 'lesbiangirlongirl', + name: 'Lesbian Girl on Girl', + url: 'https://www.naughtyamerica.com/site/lesbian-girl-on-girl', + network: 'naughtyamerica', + }, + { + slug: 'naughtyoffice', + name: 'Naughty Office', + alias: ['no'], + url: 'https://www.naughtyamerica.com/site/naughty-office', + network: 'naughtyamerica', + }, + { + slug: 'ihaveawife', + name: 'I Have a Wife', + alias: ['ihaw'], + url: 'https://www.naughtyamerica.com/site/i-have-a-wife', + network: 'naughtyamerica', + }, + { + slug: 'naughtybookworms', + name: 'Naughty Bookworms', + alias: ['nb'], + url: 'https://www.naughtyamerica.com/site/naughty-bookworms', + network: 'naughtyamerica', + }, + { + slug: 'housewife1on1', + name: 'Housewife 1 on 1', + alias: ['h1o1'], + url: 'https://www.naughtyamerica.com/site/housewife-1-on-1', + network: 'naughtyamerica', + }, + { + slug: 'mywifeshotfriend', + name: "My Wife's Hot Friend", + alias: ['mwhf'], + url: 'https://www.naughtyamerica.com/site/my-wife-s-hot-friend', + network: 'naughtyamerica', + }, + { + slug: 'latinadultery', + name: 'Latin Adultery', + url: 'https://www.naughtyamerica.com/site/latin-adultery', + network: 'naughtyamerica', + }, + { + slug: 'assmasterpiece', + name: 'Ass Masterpiece', + alias: ['am'], + url: 'https://www.naughtyamerica.com/site/ass-masterpiece', + network: 'naughtyamerica', + }, + { + slug: '2chickssametime', + name: '2 Chicks Same Time', + alias: ['2cst'], + url: 'https://www.naughtyamerica.com/site/2-chicks-same-time', + network: 'naughtyamerica', + }, + { + slug: 'myfriendshotgirl', + name: "My Friend's Hot Girl", + alias: ['mfhg'], + url: 'https://www.naughtyamerica.com/site/my-friend-s-hot-girl', + network: 'naughtyamerica', + }, + { + slug: 'neighboraffair', + name: 'Neighbor Affair', + alias: ['naf'], + url: 'https://www.naughtyamerica.com/site/neighbor-affair', + network: 'naughtyamerica', + }, + { + slug: 'mygirlfriendsbustyfriend', + name: "My Girlfriend's Busty Friend", + alias: ['mgbf'], + url: 'https://www.naughtyamerica.com/site/my-girlfriend-s-busty-friend', + network: 'naughtyamerica', + }, + { + slug: 'naughtyathletics', + name: 'Naughty Athletics', + alias: ['na'], + url: 'https://www.naughtyamerica.com/site/naughty-athletics', + network: 'naughtyamerica', + }, + { + slug: 'mynaughtymassage', + name: 'My Naughty Massage', + alias: ['mnm'], + url: 'https://www.naughtyamerica.com/site/my-naughty-massage', + network: 'naughtyamerica', + }, + { + slug: 'fasttimes', + name: 'Fast Times', + url: 'https://www.naughtyamerica.com/site/fast-times', + network: 'naughtyamerica', + }, + { + slug: 'thepassenger', + name: 'The Passenger', + url: 'https://www.naughtyamerica.com/site/the-passenger', + network: 'naughtyamerica', + }, + { + slug: 'milfsugarbabesclassic', + name: 'Milf Sugar Babes Classic', + url: 'https://www.naughtyamerica.com/site/milf-sugar-babes-classic', + network: 'naughtyamerica', + }, + { + slug: 'perfectfuckingstrangersclassic', + name: 'Perfect Fucking Strangers Classic', + url: 'https://www.naughtyamerica.com/site/perfect-fucking-strangers-classic', + network: 'naughtyamerica', + }, + { + slug: 'asian1on1', + name: 'Asian 1 On 1', + url: 'https://www.naughtyamerica.com/site/asian-1-on-1', + network: 'naughtyamerica', + }, + { + slug: 'americandaydreams', + name: 'American Daydreams', + alias: ['ad'], + url: 'https://www.naughtyamerica.com/site/american-daydreams', + network: 'naughtyamerica', + }, + { + slug: 'socalcoeds', + name: 'Socal Coeds', + url: 'https://www.naughtyamerica.com/site/socal-coeds', + network: 'naughtyamerica', + }, + { + slug: 'naughtycountrygirls', + name: 'Naughty Country Girls', + url: 'https://www.naughtyamerica.com/site/naughty-country-girls', + network: 'naughtyamerica', + }, + { + slug: 'diaryofamilf', + name: 'Diary of a Milf', + url: 'https://www.naughtyamerica.com/site/diary-of-a-milf', + network: 'naughtyamerica', + }, + { + slug: 'naughtyrichgirls', + name: 'Naughty Rich Girls', + alias: ['nrg'], + url: 'https://www.naughtyamerica.com/site/naughty-rich-girls', + network: 'naughtyamerica', + }, + { + slug: 'mynaughtylatinmaid', + name: 'My Naughty Latin Maid', + url: 'https://www.naughtyamerica.com/site/my-naughty-latin-maid', + network: 'naughtyamerica', + }, + { + slug: 'naughtyamerica', + name: 'Naughty America', + alias: ['nam'], + url: 'https://www.naughtyamerica.com/site/naughty-america', + network: 'naughtyamerica', + }, + { + slug: 'diaryofananny', + name: 'Diary of a Nanny', + url: 'https://www.naughtyamerica.com/site/diary-of-a-nanny', + network: 'naughtyamerica', + }, + { + slug: 'naughtyflipside', + name: 'Naughty Flipside', + url: 'https://www.naughtyamerica.com/site/naughty-flipside', + network: 'naughtyamerica', + }, + { + slug: 'livepartygirl', + name: 'Live Party Girl', + url: 'https://www.naughtyamerica.com/site/live-party-girl', + network: 'naughtyamerica', + }, + { + slug: 'livenaughtystudent', + name: 'Live Naughty Student', + url: 'https://www.naughtyamerica.com/site/live-naughty-student', + network: 'naughtyamerica', + }, + { + slug: 'livenaughtysecretary', + name: 'Live Naughty Secretary', + url: 'https://www.naughtyamerica.com/site/live-naughty-secretary', + network: 'naughtyamerica', + }, + { + slug: 'livegymcam', + name: 'Live Gym Cam', + url: 'https://www.naughtyamerica.com/site/live-gym-cam', + network: 'naughtyamerica', + }, + { + slug: 'livenaughtyteacher', + name: 'Live Naughty Teacher', + url: 'https://www.naughtyamerica.com/site/live-naughty-teacher', + network: 'naughtyamerica', + }, + { + slug: 'livenaughtymilf', + name: 'Live Naughty Milf', + url: 'https://www.naughtyamerica.com/site/live-naughty-milf', + network: 'naughtyamerica', + }, + { + slug: 'livenaughtynurse', + name: 'Live Naughty Nurse', + url: 'https://www.naughtyamerica.com/site/live-naughty-nurse', + network: 'naughtyamerica', + }, + // NEW SENSATIONS + { + slug: 'hotwifexxx', + name: 'Hotwife XXX', + url: 'https://www.hotwifexxx.com', + network: 'newsensations', + parameters: { + siteId: 'hwxxx', + block: true, + }, + }, + { + slug: 'tabutales', + name: 'Tabu Tales', + url: 'https://www.thetabutales.com', + network: 'newsensations', + parameters: { siteId: 'tt' }, + }, + { + slug: 'nsfamilyxxx', + name: 'Family XXX', + url: 'https://www.familyxxx.com', + network: 'newsensations', + tags: ['family'], + parameters: { + siteId: 'famxxx', + block: true, + }, + }, + { + slug: 'thelesbianexperience', + name: 'The Lesbian Experience', + url: 'https://www.thelesbianexperience.com', + network: 'newsensations', + tags: ['lesbian'], + parameters: { siteId: 'tle' }, + }, + { + slug: 'theromanceseries', + name: 'The Romance Series', + url: 'https://www.theromanceseries.com', + network: 'newsensations', + parameters: { siteId: 'rs' }, + }, + { + slug: 'talesfromtheedge', + name: 'Tales From The Edge', + url: 'thetalesfromtheedge', + network: 'newsensations', + parameters: { siteId: 'ttfte' }, + }, + { + slug: 'parodypass', + name: 'Parody Pass', + url: 'https://www.parodypass.com', + network: 'newsensations', + parameters: { siteId: 'pp' }, + }, + { + slug: 'shanedieselsbangingbabes', + name: 'Shane Diesel\'s Banging Babes', + url: 'http://shanedieselsbangingbabes.com', + network: 'newsensations', + parameters: { siteId: 'sdbb' }, + }, + { + slug: 'unlimitedmilfs', + name: 'Unlimited MILFs', + url: 'https://www.unlimitedmilfs.com', + network: 'newsensations', + tags: ['milf'], + parameters: { siteId: 'um' }, + }, + { + slug: 'heavyhandfuls', + name: 'Heavy Handfuls', + url: 'https://www.heavyhandfuls.com', + network: 'newsensations', + parameters: { siteId: 'hh' }, + }, + { + slug: 'jizzbomb', + name: 'Jizz Bomb', + url: 'https://www.jizzbomb.com', + network: 'newsensations', + parameters: { siteId: 'jb' }, + }, + { + slug: 'stretchedoutsnatch', + name: 'Stretched Out Snatch', + url: 'https://www.stretchedoutsnatch.com', + network: 'newsensations', + parameters: { siteId: 'sos' }, + }, + { + slug: 'fourfingerclub', + name: 'Four Finger Club', + url: 'https://www.fourfingerclub.com', + network: 'newsensations', + parameters: { siteId: 'ffc' }, + }, + { + slug: 'ashlynnbrooke', + name: 'Ashlynn Brooke', + url: 'https://www.ashlynnbrooke.com', + network: 'newsensations', + parameters: { siteId: 'ab' }, + }, + { + slug: 'freshouttahighschool', + name: 'Fresh Outta High School', + url: 'https://www.freshouttahighschool.com', + network: 'newsensations', + parameters: { siteId: 'fohs' }, + }, + // NUBILES + { + slug: 'anilos', + name: 'Anilos', + url: 'https://www.anilos.com', + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'brattysis', + name: 'Bratty Sis', + url: 'https://www.brattysis.com', + tags: ['family'], + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'deeplush', + name: 'Deep Lush', + url: 'https://www.deeplush.com', + network: 'nubiles', + }, + { + slug: 'hotcrazymess', + name: 'Hot Crazy Mess', + alias: ['hcm'], + url: 'https://www.hotcrazymess.com', + network: 'nubiles', + }, + { + slug: 'nfbusty', + name: 'NF Busty', + url: 'https://www.nfbusty.com', + tags: ['big-boobs'], + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'nubilefilms', + name: 'Nubile Films', + alias: ['nf', 'nubilef'], + url: 'https://www.nubilefilms.com', + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'nubiles', + name: 'Nubiles', + url: 'https://www.nubiles.net', + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'nubilescasting', + name: 'Nubiles Casting', + url: 'https://www.nubiles-casting.com', + tags: ['casting'], + network: 'nubiles', + }, + { + slug: 'momsteachsex', + name: 'Moms Teach Sex', + alias: ['mts'], + url: 'https://www.momsteachsex.com', + tags: ['family', 'milf'], + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'petitehdporn', + name: 'Petite HD Porn', + alias: ['phdp'], + url: 'https://www.petitehdporn.com', + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'driverxxx', + name: 'Driver XXX', + url: 'https://www.driverxxx.com', + network: 'nubiles', + }, + { + slug: 'petiteballerinasfucked', + name: 'Petite Ballerinas Fucked', + alias: ['pbf'], + url: 'https://www.petiteballerinasfucked.com', + network: 'nubiles', + }, + { + slug: 'teacherfucksteens', + name: 'Teacher Fucks Teens', + alias: ['tft'], + url: 'https://www.teacherfucksteens.com', + tags: ['teacher'], + network: 'nubiles', + }, + { + slug: 'stepsiblingscaught', + name: 'Step Siblings Caught', + alias: ['ssc'], + url: 'https://www.stepsiblingscaught.com', + tags: ['family'], + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'princesscum', + name: 'Princess Cum', + alias: ['pc'], + url: 'https://www.princesscum.com', + network: 'nubiles', + }, + { + slug: 'badteenspunished', + name: 'Bad Teens Punished', + alias: ['btp'], + url: 'https://www.badteenspunished.com', + network: 'nubiles', + }, + { + slug: 'nubilesunscripted', + name: 'Nubiles Unscripted', + url: 'https://www.nubilesunscripted.com', + network: 'nubiles', + }, + { + slug: 'bountyhunterporn', + name: 'Bounty Hunter Porn', + url: 'https://www.bountyhunterporn.com', + network: 'nubiles', + }, + { + slug: 'daddyslilangel', + name: 'Daddy\'s Lil Angel', + alias: ['dlla'], + url: 'https://www.daddyslilangel.com', + tags: ['family', 'anal'], + network: 'nubiles', + }, + { + slug: 'myfamilypies', + name: 'My Family Pies', + alias: ['mfp'], + url: 'https://www.myfamilypies.com', + tags: ['family'], + network: 'nubiles', + parameters: { + upcoming: true, + }, + }, + { + slug: 'nubileset', + name: 'Nubiles Entertainment', + url: 'https://www.nubileset.com', + network: 'nubiles', + }, + { + slug: 'detentiongirls', + name: 'Detention Girls', + url: 'https://www.detentiongirls.com', + network: 'nubiles', + }, + { + slug: 'thatsitcomshow', + name: 'That Sitcom Show', + alias: ['tss'], + url: 'https://www.thatsitcomshow.com', + tags: ['parody'], + network: 'nubiles', + }, + // PERFECT GONZO + { + slug: 'allinternal', + name: 'All Internal', + url: 'https://allinternal.com', + network: 'perfectgonzo', + }, + { + slug: 'asstraffic', + name: 'Ass Traffic', + url: 'https://asstraffic.com', + network: 'perfectgonzo', + }, + { + slug: 'cumforcover', + name: 'Cum For Cover', + url: 'https://cumforcover.com', + network: 'perfectgonzo', + }, + { + slug: 'fistflush', + name: 'Fist Flush', + url: 'https://fistflush.com', + network: 'perfectgonzo', + }, + { + slug: 'givemepink', + name: 'Give Me Pink', + url: 'https://givemepink.com', + tags: ['solo', 'masturbation'], + network: 'perfectgonzo', + }, + { + slug: 'milfthing', + name: 'MILF Thing', + url: 'https://milfthing.com', + network: 'perfectgonzo', + }, + { + slug: 'primecups', + name: 'Prime Cups', + url: 'https://primecups.com', + network: 'perfectgonzo', + }, + { + slug: 'purepov', + name: 'Pure POV', + url: 'https://purepov.com', + network: 'perfectgonzo', + }, + { + slug: 'spermswap', + name: 'Sperm Swap', + url: 'https://spermswap.com', + tags: ['cum-swapping'], + network: 'perfectgonzo', + }, + { + slug: 'tamedteens', + name: 'Tamed Teens', + url: 'https://tamedteens.com', + network: 'perfectgonzo', + }, + // PERVCITY + { + slug: 'analoverdose', + name: 'Anal Overdose', + url: 'http://www.analoverdose.com', + description: 'Before proceeding, use caution: the stunning pornstars of Anal Overdose are so fiery that they cause heavy breathing, throbbing cocks and volcanic loads of cum. If you think you can handle the heat of smoking tits, sweltering pussy and red hot ass.', + network: 'pervcity', + parameters: { tourId: 3 }, + }, + { + slug: 'bangingbeauties', + name: 'Banging Beauties', + description: "Banging Beauties isn't just a porn site; it's the gateway to all your pussy-obsessed fantasies! Our members' area is flowing with beautiful pornstars anticipating big dick throbbing in their syrupy pink slits. These experienced babes love brutal vaginal pounding! Similarly, they're eager for anal switch-hitting to shake things up. However, it's not only about gorgeous sexperts filling their hungry holes. Sometimes, it's all about innocent rookies earning their pornstar status in first time threesomes and premier interracial scenes.", + url: 'http://www.bangingbeauties.com', + network: 'pervcity', + parameters: { tourId: 7 }, + }, + { + slug: 'oraloverdose', + name: 'Oral Overdose', + description: "Oral Overdose is the only site you need to live out every saliva soaked blowjob of your dreams in HD POV! We've got the most stunning cocksuckers in the world going to town on big dick. These babes not only love cock, they can't get enough of it! In fact, there is no prick too huge for our hungry girls' throats. You'll find gorgeous, big tits pornstars exercising their gag reflex in intense balls deep facefuck scenes. We also feature fresh, young newbies taking on the gagging deepthroat challenge.", + url: 'http://www.oraloverdose.com', + network: 'pervcity', + parameters: { tourId: 4 }, + }, + { + slug: 'chocolatebjs', + name: 'Chocolate BJs', + description: "You've just won the golden ticket to the best Chocolate BJs on the planet! We've sought far and wide to bring you the most beautiful black and ethnic pornstars. And they're in our members' area now! They can't wait to suck your white lollipop and lick the thick cream shooting from your big dick. Of course, no matter how sweet the booty or juicy the big tits, these brown foxes aren't all sugar and spice. In fact, when it comes to giving head, these big ass ebony babes know what they want: huge white cocks filling their throats!", + url: 'http://www.chocolatebjs.com', + network: 'pervcity', + parameters: { tourId: 6 }, + }, + { + slug: 'upherasshole', + name: 'Up Her Asshole', + description: "You don't need to travel the globe in search of the anal wonders of the world, because you get your own private tour right here on Up Her Asshole! Our stunning pornstars and rookie starlets welcome all ass fetish and anal sex fans, with their twerking bubble butts and winking assholes. However, big booty worship is just a slice of the fun. Combined with juicy tits (big and small, wet pussy (hairy and bald, these girls deliver a spectacular sensory experience in HD POV. Not only are you in danger of busting a nut before the going gets good, but also when the good turns remarkable with rimming, fingering and butt toys!", + url: 'http://www.upherasshole.com', + network: 'pervcity', + parameters: { tourId: 9 }, + }, + // PIMP XXX + { + slug: 'drilledxxx', + name: 'Drilled.XXX', + url: 'https://drilled.xxx', + tags: ['anal'], + network: 'pimpxxx', + }, + { + slug: 'cuckedxxx', + name: 'Cucked.XXX', + url: 'https://cucked.xxx', + tags: ['cuckold'], + network: 'pimpxxx', + }, + { + slug: 'familyxxx', + name: 'Family.XXX', + url: 'https://family.xxx', + tags: ['family'], + network: 'pimpxxx', + }, + { + slug: 'petitexxx', + name: 'Petite.XXX', + url: 'https://petite.xxx', + network: 'pimpxxx', + }, + { + slug: 'confessionsxxx', + name: 'Confessions.XXX', + url: 'https://confessions.xxx', + network: 'pimpxxx', + }, + { + slug: 'bcmxxx', + name: 'BCM.XXX', + url: 'https://bcm.xxx', + network: 'pimpxxx', + }, + // PORN PROS + { + name: 'Real Ex Girlfriends', + slug: 'realexgirlfriends', + alias: ['reg'], + url: 'https://pornpros.com/site/realexgirlfriends', + network: 'pornpros', + }, + { + name: '18 Years Old', + slug: 'eighteenyearsold', + alias: ['18yo'], + url: 'https://pornpros.com/site/18yearsold', + tags: ['teen'], + network: 'pornpros', + }, + { + name: 'Massage Creep', + slug: 'massagecreep', + alias: ['mc'], + url: 'https://pornpros.com/site/massagecreep', + tags: ['massage'], + network: 'pornpros', + }, + { + name: 'Deep Throat Love', + slug: 'deepthroatlove', + url: 'https://pornpros.com/site/deepthroatlove', + tags: ['blowjob', 'deepthroat'], + network: 'pornpros', + }, + { + name: 'Teen BFF', + slug: 'teenbff', + url: 'https://pornpros.com/site/teenbff', + tags: ['mff'], + network: 'pornpros', + }, + { + name: 'Shady P.I.', + slug: 'shadypi', + url: 'https://pornpros.com/site/shadypi', + network: 'pornpros', + }, + { + name: 'Cruelty Party', + slug: 'crueltyparty', + url: 'https://pornpros.com/site/crueltyparty', + network: 'pornpros', + }, + { + name: 'Disgraced 18', + slug: 'disgraced18', + url: 'https://pornpros.com/site/disgraced18', + network: 'pornpros', + }, + { + name: 'Cumshot Surprise', + slug: 'cumshotsurprise', + url: 'https://pornpros.com/site/cumshotsurprise', + network: 'pornpros', + }, + { + name: '40oz Bounce', + slug: 'fortyozbounce', + url: 'https://pornpros.com/site/40ozbounce', + network: 'pornpros', + }, + { + name: 'Jurassic Cock', + slug: 'jurassiccock', + url: 'https://pornpros.com/site/jurassiccock', + network: 'pornpros', + }, + { + name: 'Freaks Of Cock', + slug: 'freaksofcock', + url: 'https://pornpros.com/site/freaksofcock', + network: 'pornpros', + }, + { + name: 'Euro Humpers', + slug: 'eurohumpers', + url: 'https://pornpros.com/site/eurohumpers', + network: 'pornpros', + }, + { + name: 'Freaks Of Boobs', + slug: 'freaksofboobs', + url: 'https://pornpros.com/site/freaksofboobs', + network: 'pornpros', + }, + { + name: 'Cock Competition', + slug: 'cockcompetition', + url: 'https://pornpros.com/site/cockcompetition', + network: 'pornpros', + }, + { + name: 'Pimp Parade', + slug: 'pimpparade', + url: 'https://pornpros.com/site/pimpparade', + network: 'pornpros', + }, + { + name: 'MILF Humiliation', + slug: 'milfhumiliation', + url: 'https://milfhumiliation.com', + network: 'pornpros', + tags: ['milf'], + }, + { + name: 'Humiliated', + slug: 'humiliated', + url: 'https://humiliated.com', + network: 'pornpros', + }, + { + name: 'Flexible Positions', + slug: 'flexiblepositions', + url: 'https://flexiblepositions.com', + network: 'pornpros', + parameters: { + network: true, + }, + }, + { + name: 'Public Violations', + slug: 'publicviolations', + url: 'https://publicviolations.com', + network: 'pornpros', + parameters: { + network: true, + }, + }, + { + name: 'Amateur Violations', + slug: 'amateurviolations', + url: 'https://amateurviolations.com', + network: 'pornpros', + }, + { + name: 'Squirt Disgrace', + slug: 'squirtdisgrace', + url: 'https://squirtdisgrace.com', + network: 'pornpros', + }, + { + name: 'Cum Disgrace', + slug: 'cumdisgrace', + url: 'https://cumdisgrace.com', + network: 'pornpros', + }, + { + name: 'Webcam Hackers', + slug: 'webcamhackers', + url: 'https://webcamhackers.com', + network: 'pornpros', + }, + { + name: 'College Teens', + slug: 'collegeteens', + network: 'pornpros', + }, + // PRIVATE + { + slug: 'analintroductions', + name: 'Anal Introductions', + description: 'Private\'s Anal Introductions is all about ass. Watch these girls get their asses broken in by fat hard cocks! Hot double penetrations, gaping wide assholes and anal creampies are all standard in this exclusive site. Many of these girls have never had cock in their ass before, while others are real addicts and can only cum when being savagely sodomised. Watch which girls can take it... Private style.', + url: 'https://www.private.com/site/anal-introductions', + network: 'private', + }, + { + slug: 'iconfessfiles', + name: 'I Confess Files', + description: 'From the heart of the UK comes found footage exclusively provided to private.com which will shock and offend some viewers. Reality, perversion and unnatural lust come together perhaps as never before.', + url: 'https://www.private.com/site/i-confess-files', + network: 'private', + }, + { + slug: 'missionasspossible', + name: 'Mission: Ass Possible', + description: 'From the streets of Europe, Private\'s team of professionals find and exploit clueless young sluts, for some great sex and for your viewing pleasure. See what young hot chicks will do when their desire for easy fame or money makes them vulnerable to some of the craziest schemes and plots imaginable. Private\'s hung studs are on a mission for ass and hungry for fun. All part of the Private network of sites.', + url: 'https://www.private.com/site/mission-ass-possible', + network: 'private', + }, + { + slug: 'russianfakeagent', + name: 'Russian Fake Agent', + description: 'Direct from Russia, young naïve women pursue their dream of visas, Hollywood and fame. Eager to please and willing to do anything to get their break. Unfortunately, it’s a case of lies, sex and videotape as these gullible hotties perform for us. If these girls only knew the truth!', + url: 'https://www.private.com/site/russian-fake-agent', + network: 'private', + }, + { + slug: 'sexonthebeach', + name: 'Sex on the Beach', + description: 'Amazing locations and steamy sex in the sun, www.privatetropics.com is a celebration of tropical lust and sand covered sluts. From Private\'s exclusive line of scenes in exotic countries, watch what happens when our hot models go naked and native.', + url: 'https://www.private.com/site/sex-on-the-beach', + network: 'private', + }, + { + slug: 'tightandteen', + name: 'Tight and Teen', + description: 'Europe\'s number one teen offering and part of the Private network of sites, Tight and Teen takes you to the place that every father dreads, as 18+ teens discover just how much fun pleasing a hung stud can be. Fresh tight pussies, virgin anal initiations and teen face fuckings all brought to you exclusively by Europe\'s leading adult brand.', + url: 'https://www.private.com/site/tight-and-teen', + network: 'private', + }, + { + slug: 'blacksonsluts', + name: 'Blacks on Sluts', + description: 'See what happens when European women discover hung black studs looking to breed. Blacks on Sluts is 100% white slut and cheating wives versus huge black dicks giving them something they will never forget. Private puts its stamp on these women as our stallions stretch them to their limits.', + url: 'https://www.private.com/site/blacks-on-sluts', + network: 'private', + }, + { + slug: 'privateblack', + name: 'Private Black', + description: 'Private Black is number 1 for European Interracial Porn with exclusive interracial content in HD and Ultra 4K featuring the freshest young faces from Europe and the most popular European porn stars.', + url: 'https://www.privateblack.com', + network: 'private', + }, + { + slug: 'privatefetish', + name: 'Private Fetish', + description: 'www.privatefetish.com is here to give you that taste of dark desire that you secretly crave. Domination and Submission, Pleasure and Pain, are the drivers in this hardcore dungeon. What turns you on most? Being forced to beg for release from a sexy dominatrix or making that bitch next door beg for your cock? All part of Private\'s network of sites.', + url: 'https://www.private.com/site/private-fetish', + network: 'private', + }, + { + slug: 'privatemilfs', + name: 'Private MILFs', + description: 'Part of the awesome network of Private sites, Private MILFs is all about moms who are getting what they need when their limp dicked husbands can\'t perform. From their daughters\' stud boyfriends to their husbands\' hung black co-workers to their sons\' friends, no one is safe from their cravings for hard cock and salty cum.', + url: 'https://www.private.com/site/private-milfs', + network: 'private', + }, + { + slug: 'russianteenass', + name: 'Russian Teen Ass', + description: 'Many people say that Russian girls are the most beautiful in the world, and at www.russianteenass.com we show you why. These sexy Soviet newcomers are ready to work hard for their visa and their big shot at stardom. These barely 18+ girls know what they want and Private gives them an exclusive opportunity to get it. From Russia with lust, come see these girls in action!', + url: 'https://www.private.com/site/russian-teen-ass', + network: 'private', + }, + { + slug: 'privatestars', + name: 'Private Stars', + description: 'Welcome to Private Stars. The name speaks for itself as only top-model babes and perfect girls can join this select group of stars, all shot in HD. Part of the Private network of sites, Private Stars brings you a sneak peek into the life of seductive glamour girls who could have easily stepped from the catwalks of Paris or Milan and straight into a world of torrid sex.', + url: 'https://www.private.com/site/private-stars', + network: 'private', + }, + // PURE TABOO + { + name: 'Pure Taboo', + slug: 'puretaboo', + url: 'https://www.puretaboo.com', + description: 'PureTaboo.com is the ultimate site for family taboo porn, featuring submissive teens & virgins in rough sex videos in ultra 4k HD.', + network: 'puretaboo', + priority: 1, + parameters: { + independent: true, + mobile: 'https://m.dpfanatics.com/en/video', + }, + }, + { + name: 'Pretty Dirty', + slug: 'prettydirty', + alias: ['prdi'], + url: 'https://www.prettydirty.com', + network: 'puretaboo', + parameters: { + referer: 'https://www.puretaboo.com', + }, + }, + /* series, not sites, that appear on Pure Taboo itself { name: 'Under The Bed', slug: 'underthebed', @@ -4409,1789 +4416,1789 @@ const sites = [ network: 'puretaboo', }, */ - // REALITY KINGS - { - name: 'Look At Her Now', - url: 'https://www.lookathernow.com', - description: 'Look At Her Now brings you best HD reality porn videos every week. Check out these girls before and after they get some rough pounding.', - parameters: { native: true }, - // parameters: { siteId: 300 }, - slug: 'lookathernow', - network: 'realitykings', - }, - { - name: 'We Live Together', - slug: 'welivetogether', - alias: ['wlt'], - url: 'https://www.welivetogether.com', - description: "We are girls that love to eat pussy and We Live Together! Every week we go out on the streets, bars, parties, malls... wherever and we pick up the cutest lesbians and invite them to come over and party at our apartment. From our girl friends at college, to roommates, and friends of friends.. we're always looking for the hottest lesbian girls around! We Live Together has hundreds of lesbian videos for you to download right from Reality Kings... it's the sexiest lesbian porn anywhere guys and gals! :-) Come watch us eat pussy and work our dildo magic on gorgeous, sexy girls. We love to get together and get off in steamy hot threesome and foursome lesbian movies! We promise you're going to love our amazing collection of lesbian porn. Thanks for dropping in to the We Live Together Apartment, hope you enjoy your visit! Love xoxo Brittney, Taylor, Nicole & All the Girls", - parameters: { siteId: 3 }, - network: 'realitykings', - }, - { - name: 'Black GFs', - slug: 'blackgfs', - alias: ['bgfs'], - url: 'https://www.realitykings.com/scenes?site=47', - description: '', - parameters: null, - network: 'realitykings', - }, - { - name: 'Dare Dorm', - url: 'https://www.daredorm.com', - description: '', - parameters: { siteId: 48 }, - slug: 'daredorm', - network: 'realitykings', - }, - { - name: 'GF Revenge', - slug: 'gfrevenge', - alias: ['gfr'], - url: 'https://www.gfrevenge.com', - description: '', - parameters: { siteId: 49 }, - network: 'realitykings', - }, - { - name: 'Horny Birds', - url: 'https://www.realitykings.com/scenes?site=50', - description: '', - parameters: null, - slug: 'hornybirds', - network: 'realitykings', - }, - { - name: 'Crazy College GFs', - url: 'https://www.realitykings.com/scenes?site=51', - description: '', - parameters: null, - slug: 'crazycollegegfs', - network: 'realitykings', - }, - { - name: 'Crazy Asian GFs', - url: 'https://www.crazyasiangfs.com', - description: '', - parameters: { siteId: 52 }, - slug: 'crazyasiangfs', - network: 'realitykings', - }, - { - name: 'Teens Love Huge Cocks', - url: 'https://www.teenslovehugecocks.com', - alias: ['tlhc'], - description: "Teens Love Big Cocks is dedicated to providing you the hottest teens getting fucked by the biggest cocks! Every week Reality Kings introduces another teen to a big hot meat rod! When these girls see a big throbbing penis they can't resist shoving it in their hot teen mouths. These girl next door types are no slouches when it comes to oral sex! Watch them deepthroat & gag on a mouth full of cock before taking big hot loads all over their pretty faces. The fun doesn't stop there! These girls love getting their tight teen pussy & asses spread wide and pounded by massive dicks! These girls won't settle for less & there is no dick too large. Start downloading TeensLoveBigCock porn videos & HD quality pictures now and watch teen pussy get fucked like you've never seen before!", - parameters: { siteId: 42 }, - slug: 'teenslovehugecocks', - network: 'realitykings', - }, - { - name: 'Big Naturals', - url: 'https://www.bignaturals.com', - alias: ['bin'], - description: "If you think there is nothing like big natural breasts, Big Naturals welcomes you home. Reality Kings brings you nothing but the hottest amateur big tit women. We're talking about some seriously big boobs. Sexy women with big bouncy tits who love to get it on. These women don't hesitate to let their big natural tits get fucked and let those massive juggs bounce! Big Naturals has hundreds of high quality videos available for download. If into tits, this is the place to be. There's no plastic parts here, only big natural boobs! There's thousands of high resolution pics available to download as well. Check out any of our top rated scenes for the biggest, huge natural tits. Hooters, fun bags, juggs... whatever you want to call them Reality Kings and Big Naturals have the hottest big boobs you'll find anywhere. Sit back, relax, and watch the titties bounce... Reality Kings style!", - parameters: { siteId: 5 }, - slug: 'bignaturals', - network: 'realitykings', - }, - { - name: 'Money Talks', - slug: 'moneytalks', - alias: ['mot'], - url: 'https://www.moneytalks.com', - description: "Money Talks... bullshit walks. We all know the saying, but at Reality Kings we like to prove it! Just watch us approach everyday people on the street and ask them what they will do for some real American Greenbacks! Check out smokin' hot amateurs preform in porn videos or watch crazy college kids preform insane stunts on film... all in exchange for cold hard cash. People will do anything when Money Talks! Watch as we offer cash in exchange for one, AMAZING blow job! From crazy Spring Breakers to the girl next door, we find some amazing sluts and see just what they'll do for the loot--girls that give up the booty, for the booty! Arrr! Reality Kings has every high quality Money Talks episode available for download. We're talking about some seriously hot videos here. You won't find this crazy porn content anywhere else! Remember, Money Talks... bullshit walks!", - parameters: { siteId: 28 }, - network: 'realitykings', - }, - { - name: 'Moms Lick Teens', - url: 'https://www.momslickteens.com', - alias: ['momslickteens'], - description: 'Hot moms know how to fuck, especially when they have a lot of pent up energy. MomsLickTeens.com is where all the magic happens between lustful milf minxes and curious 18+ teen bombshells in HD porn videos. Mature horny women love to sample a fresh batch of pussy and ass whenever possible here at Reality Kings. They love teaching the carnal arts to eager younger women who crave a deeper understanding of the female body. Our bodacious mommies love exploring the anatomy of their fresh-faced lesbian lovers and engage in cunnilingus and anilingus within seconds. Naked women licking, sucking, scissoring, and toying their gaping pussy and assholes with a plethora of adult toys is absolutely riveting to watch. You’ll be aroused by RK girls of different ages rolling around together in sweaty sex scenes. Moms Lick Teens features limber tongues exploring the deepest recesses of female erogenous zones often eliciting projectile squirt orgasms. The phenomenon of female ejaculation occurs regularly in our premium erotica so get a load of it while blowing your own load to our buxom mommies today!', - parameters: { siteId: 43 }, - slug: 'momslickteens', - network: 'realitykings', - }, - { - name: 'RK Prime', - slug: 'rkprime', - alias: ['rkp'], - url: 'https://www.realitykings.com/scenes?site=45', - parameters: null, - network: 'realitykings', - }, - { - name: 'Milf Hunter', - url: 'https://www.milfhunter.com', - description: "Reality Kings presents MILF Hunter the ORIGINAL reality porn site dedicated to MILFs and mature sex content. If you don't know what a MILF is, allow us to explain... we're talking about sex starved, smokin' hot moms that are in need of a little attention--a MILF, a Mother I'd Like to Fuck! We've all seen these moms at the mall, the beach, and around town. Watch every week as the Hunter captures another hottie on film and gives them what they've been craving... some dick! These moms are seriously hot MILFs and they appear in the most incredible high quality pics and movies! We have hundreds of mature porn videos available for you to download. Or if you're looking for photos we have thousands of high resolution MILF porn pics directly from the MILF Hunter! Reality Kings brings you the best mature sex scenes around so why not join the MILF Hunter hunt down mature moms across America...", - parameters: { siteId: 2 }, - slug: 'milfhunter', - network: 'realitykings', - }, - { - name: 'Pure 18', - url: 'https://www.realitykings.com/scenes?site=31', - description: 'There\'s a lot of stuff out there that claims to be "pure", from spring water to gold chains, who knows what\'s actually legit$2 Reality Kings presents Pure 18, legit, 100% verified 18 year old sex scenes--no bullshit, only incredible 18 year old girls! These hot girls are the real deal, barely legal, smokin\' hot babes looking for some fun. Don\'t let their age fool you, these chicks know how to work a cock. Tight pussies and tight asses, the finest sex scenes around, that\'s what Pure 18 is all about! If you love watching amazing blow jobs, you\'re going to love this content. Download hundreds of high quality videos and pics featuring 100% verified 18 year old sex! These cuties are not shy about sex. Watch them take on monster cocks and love every minute of it. Pure 18, legit, verified, real 18 year old girls hungry for a cock meat sandwich!', - parameters: null, - slug: 'pure18', - network: 'realitykings', - }, - { - name: 'Bad Tow Truck', - url: 'https://www.realitykings.com/scenes?site=44', - description: 'Driving a tow truck is hard work. Especially when clients have a tough time paying! At BadTowTruck.com we alleviate that problem for sexy female damsels in distress by offering them different “payment” options. When big tit babes need a boost but are tight on cash, our drivers are more than happy to boost their tight asses up and give them a deep dicking. The chance to unleash a creamy internal cumshot should not be missed here at Reality Kings! Enjoy HD porn videos full of stranded sirens who are all too happy to get naked and oblige their rescuer with a gagging BJ. Anal riding is not far behind as our buxom RK divas love to get their bumpers shined. Bad Tow Truck is home to monster cocks getting gobbled up by very appreciative clients whose cars have broken down. They love swallowing every drop of jizz in the front or back seat of the tow truck. Anything goes here as our tantalizing teasers can contort their agile bodies into any number of sexual positions in tight spaces. They require maximal torque and horsepower in their erotic escapades so watch them get out of a jam with the help of a helpful towing guy, and then get jammed by him!', - parameters: null, - slug: 'badtowtruck', - network: 'realitykings', - }, - { - name: 'Cum Fiesta', - url: 'https://www.cumfiesta.com', - alias: ['cuf'], - description: '"Is this the fiesta $4 " Of course, welcome to the Cum Fiesta! Every week Reality Kings welcomes another hottie to the party... the dick sucking party! :-) When the girl (or girls!) arrive they show us the super secret password (watch a trailer to find out) and the party begins! As their clothes come off, these babes begin to show off their amazing oral skills. Amateur chicks taking the biggest facial cumshots, that\'s what Cum Fiesta is about! There are no pansy blow jobs here, these chicks give the best head around, and take huge cum shots to their face at the same time. Download hundreds of high quality videos and pics of semen swallowing hotties. Join the party that features amateurs, newbies, and even the girl next door! These babes love to suck cock and take incredible cum shots you\'ll find no where else! This is no siesta folks, it\'s a Cum Fiesta!', - parameters: { siteId: 10 }, - slug: 'cumfiesta', - network: 'realitykings', - }, - { - name: '8th Street Latinas', - url: 'https://www.8thstreetlatinas.com', - description: "Scientists say that the sun is what makes the temperature rise south of the equator, but we think its the women! If you haven't seen the chicas (women) from 8th Street Latinas, you're in for a spicy treat. From the famous Calle 8 (8th Street) in Miami, Reality Kings brings you the some incredibly hot latinas! We're talking Cubans, Dominicans, Panamanians, and other sexy latinas from South and Central America. These babes aim to please with their tanned bodies and deliciously round asses. Damn, we can't get enough! 8th Street Latinas has some caliente (hot) content for you to download, featuring the hottest latina sex scenes around. These are not tanned white chicks, these are REAL, hot latinas who know how to get your blood pumping. If you think you can handle the heat, grab your pair of shorts and flip-flops and let's head to Miami... 8th Street Latinas has some incredible latina porn for you!", - parameters: { siteId: 1 }, - slug: '8thstreetlatinas', - network: 'realitykings', - }, - { - name: "Mike's Apartment", - url: 'http://www.mikesapartment.com', - description: 'There\'s a room for rent in Mikes Apartment and Mike has found another hot chick to fill the vacancy! Join Mike on his search for roommates to help pay the bills. If these hot euro babes don\'t have the money, that\'s alright, Mike offers them the room in exchange for a few hours of masturbation and fun! And if the girl is traveling with a companion, thats not a problem... Mike just creates another steamy euro sex film for his "private" collection. Seriously, these babes are Europe\'s finest and Mike is your connoisseur of European booty! From their tight bodies, to their thick accents, these ladies know how to please and excite. Reality Kings offers hundreds of our incredible european porn movies and pics for you to download, and you don\'t have to travel to Moscow to get them. If you\'re looking for original, hot, European porn content, welcome to your new home: Mikes Apartment.', - parameters: { siteId: 25 }, - slug: 'mikesapartment', - network: 'realitykings', - }, - { - name: 'In the VIP', - url: 'http://www.inthevip.com', - description: "Ever wonder what happens In The VIP$3 Reality Kings takes you to the hottest night clubs in the country to show you exactly what goes on in the VIP room. When the club is packed, the music is pumpin', and the ladies are looking this HOT, you know it's going to be a damn good time! Grab a drink and step into the VIP room. Check out these gorgeous babes shaking their asses and flashing the camera. You will never see ladies like this in some whack ass bar, only in the most exclusive VIP rooms. As the party gets going, the clothes come off, and the panties drop! Watch some amazing free VIP porn movies, featuring these hotties having sex VIP style. We're talking about some down and dirty club sex featuring smokin' hot sluts. These chicks came for a good time and they've found it! Join the exclusive party In The VIP.", - parameters: { siteId: 22 }, - slug: 'inthevip', - network: 'realitykings', - }, - { - name: 'CFNM Secret', - slug: 'cfnmsecret', - alias: ['cfnms'], - url: 'https://www.realitykings.com/scenes?site=9', - description: "Shhh, keep your voice down! At Reality Kings we have a secret to share, the CFNM Secret! What's this secret all about$5 Clothed Females and Nude Males (CFNM)! Beautiful women dressed to impress and an unsuspecting male who is about to discover the secret for himself! These voyeurs are interested in every inch of the male body, touching and grabbing, they won't stop until they've had enough. Who wouldn't mind being these ladies play things$6 Gorgeous babes teasing and embarrassing men for their own fun and pleasure. Vulnerable guys being inspected and scrutinized in amazing high quality voyeur sex scenes. From CFNM handjobs to CFNM party scenes Reality Kings offers the hottest women enjoying the male body like never before. Browse our free CFNM videos below to download high quality pics and trailers. You're going to tell your friends about this secret, the CFNM Secret!", - parameters: null, - network: 'realitykings', - }, - { - name: 'Street BlowJobs', - url: 'https://www.realitykings.com/scenes?site=36', - description: "Street Blowjobs is one man's hunt for down on their luck ladies. Ladies who will blow your creamy wad, while they win a nice greedy wad of cash for their outstanding efforts. Horny honeys captured on hidden spy camera, giving amazing blowjobs for some good ol' American greenbacks. Can you imagine any of these smokin' hot babes giving blowjobs to you for just a little bit of moolah$11 Well we've got the content for you! Street Blowjobs has hundreds of hot blowjobs for you to download in high quality movies and pics. Watch these hotties use their magnificent dick sucking lips to get exactly what they want--your cock and your money! Reality Kings brings you a new episode every week, full of the best blowjobs and public blowjobs around. They say money can't buy happiness, but we beg to differ... money can chose your type of pleasure at Street Blowjobs.", - parameters: null, - slug: 'streetblowjobs', - network: 'realitykings', - }, - { - name: 'Hot Bush', - url: 'https://www.realitykings.com/scenes?site=21', - description: 'Forget bald vaginas, at Reality Kings we love a Hot Bush! Women with some grass on the field. These hairy beavers belong to some of the hottest women around. From brunette babes to red heads, every episode features a stunning natural beauty being worshiped for her beautiful bush! Browse some of our free hairy pussy videos below, and see for yourself if the curtains match the drapes. From the landing strip to the Bermuda triangle, these magnificent muff mounds are sure to please! Natural unshaved pussy movies and pics, thats what Hot Bush is all about. Grab your snorkel and get ready for some serious muff diving! Join the Reality Kings crew for some *very* Hot Bush.', - parameters: null, - slug: 'hotbush', - network: 'realitykings', - }, - { - name: 'Team Squirt', - url: 'https://www.realitykings.com/scenes?site=37', - description: "There's no denying it, at Reality Kings we love all kinds of pussy! Ask us what we really love however, and you'll get one answer: hot wet pussy! Team Squirt invites you to strap on your snorkel and fins, because we're going diving in some of the wettest pussy around! This is NOT pee ladies and gentlemen, this is real female ejaculation. Watch these beautiful ladies experience pleasure beyond belief as they try to control their squirting pussy on camera. Masturbation, fucking, whatever it takes, these babes will do anything for a squirting orgasm! Team Squirt has tons of high quality videos of girls squirting available for you to download right now. Be prepared, this is some serious female squirting content! From the girl, to the camera... everything is drenched when these super soakers take aim. These babes all pack a loaded, squirting pussy, and they know exactly how to use it! Grab your eye protection and join the team... Team Squirt.", - parameters: null, - slug: 'teamsquirt', - network: 'realitykings', - }, - { - name: 'Milf Next Door', - url: 'https://www.realitykings.com/scenes?site=26', - description: "We all love them, from the sexy mom at the grocery store, to the mature hottie down the block... we're talking about the MILF Next Door! There is nothing that these hot MILFs need more than a good pounding. If you don't know what a MILF is, allow us to explain... a Mother I'd Like to Fuck, a MILF! Watch as these sex starved sluts and their girlfriends search for a lucky dude to satisfy their craving for cock. MILF Next Door offers lesbian threesomes, amazing foursomes, and more mature sex movies featuring the hottest mature women! Start downloading some of this incredible content right now from our free pics and videos below. Every episode features another stunningly hot MILF finally getting the attention she deserves. If you love everyday mom's and can't wait to see these ladies get off, join Reality Kings and the MILF Next Door.", - parameters: null, - slug: 'milfnextdoor', - network: 'realitykings', - }, - { - name: 'Captain Stabbin', - url: 'https://www.captainstabbin.com', - description: "Hop aboard the S.S. Stabbin and join Captain Stabbin on his trip to analize the seven seas! What's better than a girl with a hot pussy$8 A girl with a hot pussy getting poked in the ass! Reality Kings invites you to the worlds greatest anal sex adventure. These babes don't need a boarding pass to climb aboard this ship, only a hot body and a gorgeous ass. Watch as the Captain sets course for the anal islands in search of the best anal sex scenes around! Download hundreds of incredible anal sex movies and pics in stunning high quality formats. Captain Stabbin brings you the very best booty content, from her first anal sex scene, to amazing boat sex scenes at sea, every episode is sure to please! These girls are ready for a stern spanking (pun intended)! Raise the main sail, set course, and join Captain Stabbin on his anal adventure! Arrr!", - parameters: { siteId: 8 }, - slug: 'captainstabbin', - network: 'realitykings', - }, - { - name: 'Big Tits Boss', - url: 'https://www.realitykings.com/scenes?site=6', - description: "Reality Kings presents Big Tits Boss! Have you been checking out that smokin' hot female executive around the office$10 Damn she's fine! She wears those short skirts and tight tops that make her huge tits pop out! Time to stop slackin' off fellas, because these ladies have been watching you closely. We're talking about the sexy women with the big tits at work. CEOs, Lawyers, CIOs, CFOs, these babes don't take any bullshit and they'll gladly use you like a toy whenever they please! Big Tits Boss has amazing high quality videos and pics available for download, featuring some very powerful women with awesome big natural tits. You won't mind being called into the office for a little discipline this time around! It's all business when these hotties are in the office... so fill out your TPS reports and be on your best behavior if you're looking for the promotion from the Big Tits Boss!", - parameters: null, - slug: 'bigtitsboss', - network: 'realitykings', - }, - { - name: 'Euro Sex Parties', - slug: 'eurosexparties', - alias: ['esp'], - url: 'https://www.realitykings.com/scenes?site=13', - description: "Pack your bags folks, we're headed to Europe! It's time to join two best friends as they travel across Europe and throw some amazing Euro Sex Parties. Forget about boring 1on1 sex scenes. Get ready for hardcore threesomes, foursomes, and fivesomes! Hot European porn directly from the source, just the way we like it. Euro babes with tight bodies taking on multiple cocks... what could be better$9 How about watching them eat pussy as well! Now that's a group sex party we'd love to attend. From hardcore group sex to hardcore gangbangs, this is the hottest content anywhere. Euro Sex Parties offers hundreds of European porn videos and pics to download in stunning high quality formats. Don't pack your bags yet, Reality Kings has tons of free movies and pics for you to download right here! Join us on our European vacation, and we'll throw a few Euro Sex Parties along the way.", - parameters: null, - network: 'realitykings', - }, - { - name: 'Dangerous Dongs', - url: 'https://www.realitykings.com/scenes?site=12', - description: "Reality Kings presents the Dangerous Dongs porn site, which bring you highlights from the featured huge cock and big dick porn on the entire network. If you enjoy watching sexy girls having fat cock stuffed deep into their tight pussies while they moan in pleasure, Dangerous Dongs has thousands of high resolution big dick porn pics and videos waiting for you inside. See tons of hot Latina, MILF, college, blond, teen and ebony babes taking big cock balls deep. You'll be able to download tons of big cock videos with hardcore sex scenes where cute girls with big tits and sexy, round asses take fat cock from every angle including doggy style and reverse cowgirl. So, for true fans of the big dick porn genre, look no further, we've gathered the best huge cock videos Reality Kings has to offer on the internet.", - parameters: null, - slug: 'dangerousdongs', - network: 'realitykings', - }, - { - name: 'Extreme Asses', - url: 'https://www.realitykings.com/scenes?site=14', - description: "Extreme Asses brings you a slew of big ass babes exclusively from Reality Kings. We're talking serious highlights from featured RK models like Jayden James big ass pictures and even some Jenny Hendrix ass sex videos. These sexy babes have nothing but big bouncy tits and porn ass that is perfect whether being viewed in doggy style action or riding cock in big ass videos. Watch tons of free ass porn trailers to get a taste of the kind of porn ass and ass sex that awaits you inside the Extreme Asses website. No matter what you like whether it be Latina, MILF, college, blond, teen or ebony big ass, we've got all the best extreme asses you can handle. This is big ass heaven, so be sure to check out this collection of big ass porn and ass sex gathered from Reality Kings best for your viewing pleasure.", - parameters: null, - slug: 'extremeasses', - network: 'realitykings', - }, - { - name: '40 Inch Plus', - url: 'https://www.realitykings.com/scenes?site=4', - description: "We have three words for you: deliciously round asses. Are you searching for ladies of the thicker variety$15 Beautiful women with hips measuring over 40 Inch Plus$16 Reality Kings presents the finest collection of booty around! Hot babes that love to have their big asses pinched and smacked. Grab 'em, squeeze 'em, bite 'em, these girls love to have their round asses played with. 40 Inch Plus is a tribute to ladies with a shape, there's no skinny chicks here! All of these women have a perfect ass and their tits are not too bad either! Download hundreds of movies and pics featuring women with big round asses. Check out our trailers and pics below to get a free sample of this incredible content. If you're looking for beautiful women with nice round asses look no further! Reality Kings and 40 Inch Plus are your source for gorgeous women with big asses.", - parameters: null, - slug: '40inchplus', - network: 'realitykings', - }, - { - name: 'Happy Tugs', - url: 'https://www.happytugs.com', - description: "Come on dudes, who doesn't like a happy ending$13 We've all seen those hole in the wall Asian massage parlors! Finally there is a site that celebrates the hand job. Asian beauties rubbing massage oil all over, what could be better$14 These babes know how to work out the kinks, seriously amazing rub 'n tug jobs. Happy Tugs captures hidden camera footage from inside one of the country's best sexual massage parlors. The dudes come in looking for a little rub down and, for a few dollars more, get a full servicing. It doesn't get any better than this, hand jobs and hot Asian babes. Check out our amazing happy ending videos, with babes rubbing their oil soaked breasts all over their favorite customers. Strip down, jump on the massage table and get your wallet out, Happy Tugs will ensure you get a very happy ending!", - parameters: { siteId: 19 }, - slug: 'happytugs', - network: 'realitykings', - }, - { - name: 'Reckless In Miami', - url: 'https://www.realitykings.com/scenes?site=303', - description: '', - parameters: null, - slug: 'recklessinmiami', - network: 'realitykings', - }, - { - name: 'HD Love', - url: 'https://www.realitykings.com/scenes?site=20', - description: 'Looking for incredibly hot porn videos in HD$12 Reality Kings showcases it all in hardcore erotica here at HDLove.com. Our premium adult content will satisfy your deepest carnal desires in stunning high-definition sex scenes. Feast your eyes on bodacious naked babes who love nothing more than to fuck on camera for you. Our hi-def movies capture every inch of their voluptuous bodies in vivid detail. Perfect round boobs and perky nipples are just the start. These jezebels proudly display their killer asses and dripping wet pussies before ravaging huge cocks like their lives depended on it. Whether you’re in the mood for horny 18+ teen nymphos or seasoned mature women the raunchiest scenes will keep you cumming back to HD Love. Our eager to please divas love to disrobe and spread as much love around as they can offering up deepthroat gagging blowjobs to anyone daring enough. They yearn for deep anal penetrations and are always up for a sweaty orgy so witness it all in crystal clear resolution. Catch every microscopic detail and blow your load repeatedly with the best that RK has in store!', - parameters: null, - slug: 'hdlove', - network: 'realitykings', - }, - { - name: 'Bikini Crashers', - url: 'https://www.realitykings.com/scenes?site=7', - description: "What's better than a babe in a scantily clad bikini$22 A party full of babes in scantily clad bikinis of course! Welcome to the Bikini Crashers! Reality Kings invites you to join our party with the hottest swimsuit models and bikini babes anywhere. We're talking about smokin' hot beauties throwing a naked pool party. Could it get any better than this$23 From perfectly round asses to amazing tan lines, these girls know how to party. We're not talking about your average swimsuit model either, these chicks are wild, crazy, and ready to get it on. Every party is loaded with 1on1, girl on girl, and group sex scenes. Gorgeous swimsuit girls getting it on by the pool, the beach, or anywhere they can show off their amazing bikinis! So grab a cold one, your shades, and kick back... you're invited to the Bikini Crashers party!", - parameters: null, - slug: 'bikinicrashers', - network: 'realitykings', - }, - { - name: 'Wives in Pantyhose', - url: 'https://www.realitykings.com/scenes?site=41', - description: 'Wives in Pantyhose features all kinds of real wives in sexy lingerie fingering their pussies with sex toys while they squeeze their big mature tits and moan. This Reality Kings network site has collected tons of pantyhose pics of hot wives and presented them to you for your viewing pleasure. No matter whether you prefer Latinas, MILFs, redheads, blondes or ebony babes, Wives in Pantyhose has all the sexiest nylon wives masturbating. There are even pantyhose lesbians playing with each other using dildos while they orgasm in smoking hot pantyhose videos. Wives in Pantyhose is easily one the best collection of real wives engaging in pantyhose porn ever put together on the net. So if you have a housewife pantyhose fetish, the the website Wives in Pantyhose is sure to deliver for you all the best models and porn the Reality Kings network has to offer.', - parameters: null, - slug: 'wivesinpantyhose', - network: 'realitykings', - }, - { - name: 'No Faces', - url: 'https://www.realitykings.com/scenes?site=30', - description: 'Isn’t it arousing to watch porn with a little mystery thrown in$19 That’s what Nofaces.com is about. The scrumptious porn stars at Reality Kings like to keep you guessing sometimes, so enjoy a wide array of HD porn videos where the faces of our horny minxes are not shown. A little sensual secrecy never hurt anyone so have some fun trying to figure out which titillating temptress is getting reamed in our torrid sex clips. Are her ample breast, perfect round ass, and wet pussy enough of a giveaway$20 What about her tattoos, piercings, or birth marks$21 Play the role of an X-rated detective and enjoy an endless sea of hardcore erotica with 18+ teen foxes and mature naked temptresses in graphic films depicting a covert cum sucker gobbling up monster cocks with glee. Our enigmatic nymphos relish in getting fucked in multiple holes simultaneously knowing that their identity is not revealed on camera. No Faces respects an RK girl’s desire to remain anonymous and only show more arousing parts of her luscious body.', - parameters: null, - slug: 'nofaces', - network: 'realitykings', - }, - { - name: 'Saturday Night Latinas', - url: 'https://www.realitykings.com/scenes?site=34', - description: "What's better than a Saturday Night out partying$18 Taking home a beautiful chick at the end of the night to fuck and have your way with! Reality Kings presents Saturday Night Latinas, gorgeous babes from the steamy night clubs and streets of Brazil. These hotties may have left the club, but the real party is about to begin! Real latina girls sucking and fucking after a night of partying! From deliciously round asses to amazing tan lines, these Brazilian bombshells are sure to please. Browse our videos below to download free latina porn movies and pictures. We have hundreds of latina sex scenes available for you to download. Grab your bags and get ready to head to Brazil, Reality Kings invites you to take home a Saturday Night Latina of your very own. Hot latina babes who love to party, join us today for a steamy Saturday Night out!", - parameters: null, - slug: 'saturdaynightlatinas', - network: 'realitykings', - }, - { - name: 'Extreme Naturals', - url: 'https://www.realitykings.com/scenes?site=15', - description: 'There are big natural breasts, then there are Extreme Naturals. On this site, we say, "Go big or go home!" That\'s why we only deliver massive naturals straight from the best Reality Kings has to offer. Extreme Naturals has painstakingly combed the RK network for the best giant naturals models and the hottest big naturals videos with the most hardcore XXX. These sexy babes have giant naturals that bounce while they ride cock and while they get stroked from behind doggy style in their perfect porn asses. For true fans of huge natural breasts, be sure to watch tons of free big naturals videos exclusively available as Extreme Naturals trailers on the website. Whether you like your giant naturals to be on Latinas, MILFs, college babes, blondes, teens or ebony babes, Extreme Naturals has the best collection of massive naturals straight from the vaults of Reality Kings.', - parameters: null, - slug: 'extremenaturals', - network: 'realitykings', - }, - { - name: 'Cum Girls', - url: 'https://www.realitykings.com/scenes?site=11', - description: "Reality Kings presents the Cum Girls porn site, which is dedicated solely to XXX cum shots and cum videos. If you like seeing hot girls with cum in their mouth, or face cum pictures, Cum Girls has thousands of high resolution cum porn pics and videos waiting for your viewing pleasure. There are smoking hot Latina, MILF, college, blond, teen and ebony babes with cum shots not only on their face, but also cum ass and cum tits pictures too. You'll be able to download tons of cum porn videos with hardcore sex scenes that all end with sticky and gooey cum in the mouth, the face, boobs, pussy or ass. Cum Girls has got cum porn and cum videos for true fans of the genre, and they all come straight to you from Reality Kings, so you know you're getting nothing less than the best on the internet.", - parameters: null, - slug: 'cumgirls', - network: 'realitykings', - }, - { - name: 'VIP Crew', - url: 'https://www.realitykings.com/scenes?site=40', - description: "Party animals rejoice! The VIP Crew is your guide to the hottest and wildest VIP parties in the world! We're not talking about ordinary house parties here, we're talking about the biggest, most badass sex parties around. When you combine loads of fun, some fine looking women, and a few lucky dudes you have the recipe for one amazing fucking party. Best of all, you're invited! From huge orgy sex parties to private sex parties, the VIP Crew brings in the hottest women--all ready to bare their VIP pussies for you. Babes that aren't afraid of a little pole dancing, foam dancing, or strip tease! These girls will do anything to join these wild sex parties and have a good time. Reality Kings has hundreds of high quality videos and pics available for you to download. So what the hell are you waiting for$17 Join the VIP Crew and get your freak on!", - parameters: null, - slug: 'vipcrew', - network: 'realitykings', - }, - { - name: 'Moms Bang Teens', - slug: 'momsbangteens', - alias: ['mbt'], - url: 'https://www.momsbangteens.com', - description: "Reality Kings presents the first website dedicated to hot moms who love to bang 18+ teens. Moms Bang Teens features the sexiest MILFs on the web, and these MILFs are all about fucking young guys on camera. If you remember lusting after one of your friend's hot moms back in grade school, then you know exactly what Moms Bang Teens is all about. Imagine if instead of just fantasizing about that sexy mother, you actually got to bang her. These are the same hot moms you see at your local supermarket and shopping at your neighborhood mall. Some of them are married and never get the attention they need. While others are just horny and sexy moms who never got tied down with a husband. Instead they like to go out and find hot young studs that know how to fuck them right. These are experienced and mature women who know what they want; young 18+ teens that can give them that rock hard cock.", - parameters: { siteId: 27 }, - network: 'realitykings', - }, - { - name: 'Sneaky Sex', - url: 'https://www.sneakysex.com', - description: 'Sneaky dirty sex! They are fucking and nobody can see, otherwise they will have a HUGE problem. When no one is watching, these horny MILFs and Teens are having sneaky sex!', - parameters: { siteId: 46 }, - slug: 'sneakysex', - network: 'realitykings', - }, - { - name: 'See My Wife', - url: 'https://www.realitykings.com/scenes?site=35', - description: 'Have you been spying on that hot couple next door$26 See My Wife invites you to view the private porn collection of horny amateurs everywhere! We\'re talking about 100% user submitted movies and pictures. Real women appearing in the hottest wife sex scenes around, that is what See My Wife is about. Our users have a chance to make 0 for pics and 00 for videos when they submit their homemade content. If you\'ve ever said "I wish I could bang my wife on film and get paid for it," look no further! Reality Kings considers every submission when we post new episodes. Check out some of our free pics and trailers below, this is one amazing collection of girlfriend and wife sex scenes. Every week we post a new episode crammed with four incredible babes showing off in front of the camera. No need to spy on the couple next door when you come See My Wife!', - parameters: null, - slug: 'seemywife', - network: 'realitykings', - }, - { - name: 'Girls of Naked', - url: 'https://www.realitykings.com/scenes?site=18', - description: 'Nothing is hotter than voluptuous minxes who love getting naked. Girlsofnaked.com is home to a bevy of bodacious beauties who are all about showing as much skin to whomever is willing to satisfy their sexual desires. Our 18+ pornstars are daring and always curious for new carnal adventures in HD porn videos. Reality Kings has compiled an incredible assortment of erotica with big boob naughty nymphos. Watch them squeeze their perky nipples before rubbing their ticklish clits in steamy scenes. Our deviant divas need their juicy pussies stuffed 24/7 by the biggest cocks in the adult biz and will stop at nothing to devour as much man meat as they can fit into every hungry orifice. Girls of Naked celebrate nudity and hardcore sex in all its glory. Fetishes, orgies, bukkake, anal creampies and much more are their favorite pastimes. RK has full-length premium porno movies bursting with our luscious babes bursting out of their clothes just for you!', - parameters: null, - slug: 'girlsofnaked', - network: 'realitykings', - }, - { - name: 'Lil Humpers', - url: 'https://lilhumpers.com', - description: '', - parameters: { siteId: 310 }, - slug: 'lilhumpers', - network: 'realitykings', - }, - { - name: 'Mike in Brazil', - url: 'https://www.realitykings.com/scenes?site=24', - description: "Are you ready for the never ending booty vacation$24 Join Mike In Brazil as he explores the wild, the exotic, and the gorgeous women of South America! If you have never been to Brazil, don't worry... Mike will give you a crash course on the most amazing ASSet of their native women. We're talking about deliciously tanned, round, thong clad Brazilian ass! These booties will not disappoint. Mike exports nothing but the finest, Grade A, Brazilian porn directly to your computer screen. Check out the hottest Brazilian ass around, wearing nothing but bikinis and thongs that are sure to get your blood pumping! These hotties spend hours working on their amazing tans to show off their bodies... look at those incredible tan lines! Mike In Brazil features some amazing hardcore sex, from anal to Brazilian facials, we're sure you're going to be planning a trip to Brazil soon. What are you waiting for$25 Join the never ending booty vacation with Mike In Brazil!", - parameters: null, - slug: 'mikeinbrazil', - network: 'realitykings', - }, - { - name: 'Real Orgasms', - url: 'https://www.realitykings.com/scenes?site=32', - description: "Real Orgasms features all kinds of sexy women playing with their pussies and stimulating their clits with sex toys and big dildos until they have real orgasms. This Reality Kings network site has collected tons of real orgasm videos and masturbation videos and concentrated them down to only the best real female orgasms that you will ever witness on the net. Whether you're really into Latinas, MILFs, college babes, blondes, teens or ebony babes, Real Orgasms has every kind of the most beautiful women masturbating. Watch as they play with themselves using sex toys and dildos while they moan, shake and their pussies convulse, as they have real orgasm on video for your pleasure. By far, this is the best collection of real orgasm porn ever put together on the net. Thanks to Reality Kings, Real Orgasms only delivers 100% real female orgasms and masturbation videos.", - parameters: null, - slug: 'realorgasms', - network: 'realitykings', - }, - { - name: 'Tranny Surprise', - url: 'https://www.trannysurprise.com', - description: 'If you’re in the mood for graphic tranny porn, look no further than TrannySurprise.com. A sexy shemale is a thing of beauty, often possessing a voracious appetite for sex. Reality Kings is home to some of the most incredible transsexual pornstars on the net. Watch them stroke their huge dicks and massage their voluminous ball sacks in our full-length HD videos. All these goddesses want to do is suck dick until it erupts in their wide open mouths. Cum swallowing is their specialty so enjoy our scenes full of creamy jizz loads overflowing onto their giant tits and firm stomachs. These nude RK shemales live to get rimjobs before getting drilled by gigantic dicks. Messy creampies are usually how their nights end so witness the torrid fuck marathons leading up to juicy orgasms. Tranny Surprise features sensual ladyboys that know just how to please anyone looking to take a walk on the wild side. Premium porno is what you deserve so eat it all up with our luscious, busty trannies. Long legs, tight asses, toned physiques, and a healthy dose of raw animal passion is what our “chicks with dicks” deliver in every one of our erotic films.', - parameters: { native: true }, - slug: 'trannysurprise', - network: 'realitykings', - }, - { - name: 'Flower Tucci', - url: 'https://www.realitykings.com/scenes?site=17', - description: 'Reality Kings presents Flower Tucci, and this is what she has to say: "My name is Flower, and I live, eat, breathe, sleep, and worship SEX! You have never met a girl like me! My ass is for worshiping. I can squirt over and over again when my pussy cums. I search out the biggest cocks and take them in my mouth, pussy, and ass! I milk those cocks until my pussy squirts everywhere. This site is dedicated to all my fantasies. Watch me search for the ultimate orgasm." Damn folks! I don\'t know about you, but this babe sounds perfect. Squirting pussy, amazing ass, gorgeous tits... the full package! If you\'re like us, you can\'t wait another moment to download these amazing videos of Flower Tucci squirting. Reality Kings is the one and only home to Flower Tucci pics and Flower Tucci movies--this content is absolutely incredible! Join us, Flower, and her friends in search of the ultimate orgasm.', - parameters: null, - slug: 'flowertucci', - network: 'realitykings', - }, - { - name: 'First Time Auditions', - url: 'https://www.realitykings.com/scenes?site=16', - description: "Forget about the next big music idol, we're looking for the next big porn star! Reality Kings presents First Time Auditions, featuring the hottest amateur chicks, searching for fame and fortune. These sluts will do anything to break into the business, from blowjobs to amateur sex scenes, these are their first porn auditions caught on film. Do you think they have what it takes$7 Download hundreds of amateur porn movies and pics, and you be the judge. First Time Auditions places ads in local and college newspapers seeking the hottest models around. When these babes arrive, we are never disappointed. They show off their perfect bodies and their many, amazing talents! These are the hottest amateur auditions around. Trying to get their careers started, these girls give the porn auditions of a lifetime! If you're ready to be the judge, to put these girls to the test, watch them on their First Time Auditions.", - parameters: null, - slug: 'firsttimeauditions', - network: 'realitykings', - }, - { - name: 'Top Shelf Pussy', - url: 'https://www.realitykings.com/scenes?site=38', - description: 'Top Shelf Pussy features nothing but the best pussy on the net. If pussy is like Johnny Walker, consider Top Shelf Pussy the Blue Label of the bunch. Whether you like shaved pussy, teen pussy, hairy pussy, wet pussy, mature pussy, black pussy or fat pussy, Top Shelf Pussy has got the hottest models and the best pussy videos on the net. Watch tons of free pussy trailers and see as these gorgeous girls play with their pussies using sex toys and dildos. Then see them take a deep stroking to their wet pussy while they moan, shake and their pussies convulse in some amazing pussy porn. No doubt, Top Shelf Pussy has got tons of pictures of sexy ladies spread eagle and more pussy videos than you could ever possibly watch. If you are a fan of the pussy porn genre, then Top Shelf Pussy is the site for you.', - parameters: null, - slug: 'topshelfpussy', - network: 'realitykings', - }, - { - name: 'Round and Brown', - url: 'https://www.roundandbrown.com', - alias: ['rab'], - description: 'Chocolate lovers out there, Reality Kings presents to you... Round And Brown, the porn site that caters to horny dudes who have a mighty craving for fine "sistah" booties. The ladies featured in these ebony porn movies are SIZZLING HOT, like a newly melted chocolate fondue! We\'re talking about some damn fine black booties! If it\'s Round And Brown, it gets the special lube treatment, no exceptions! Think you can handle this collection of premium ebony ass$1 There\'s no skinny white girls here, only gorgeous black beauties with deliciously round booties, featured in the best hardcore ebony sex videos around! Reality Kings is the only one who can bring you this amazing collection of black girl porn. If you love big round asses, gorgeous black babes, and amazing tits we have the videos and pics you\'re looking for. Warning: This chocolate may melt in your hand and your mouth... but who cares, if it\'s Round And Brown!', - parameters: { siteId: 33 }, - slug: 'roundandbrown', - network: 'realitykings', - }, - { - name: 'Monster Curves', - slug: 'monstercurves', - alias: ['mcu'], - url: 'https://www.realitykings.com/scenes?site=29', - description: "Forget about those toothpick size runway models, give us some ladies with curves-- Monster Curves! If you love your women round and juicy, ladies with some meat on their bones... then we have the content for you! We're talking about women with hips that don't quit. Incredibly round asses that will make your mouth water! Big booty girls with big round asses. Only people as obsessed as us could bring you this many pairs of perfects hips and asses! Download hundreds of movies and pics featuring gorgeous girls with amazing curves (we call them Monster Curves). Check out some of our free trailers below, these girls and their round butts and perfect hips are sure to wet your appetite! Every week, Reality Kings brings you nothing but the finest butts, the sexy round asses that jiggle when you grab 'em, the women with the Monster Curves!", - parameters: null, - network: 'realitykings', - }, - // SCORE - { - name: '18 Eighteen', - slug: '18eighteen', - url: 'https://www.18eighteen.com', - network: 'score', - parameters: { path: '/xxx-teen-videos' }, - }, - { - name: '40 Something Mag', - slug: '40somethingmag', - url: 'https://www.40somethingmag.com', - parameters: { path: '/xxx-mature-videos' }, - network: 'score', - }, - { - name: '50 Plus MILFs', - slug: '50plusmilfs', - url: 'https://www.50plusmilfs.com', - parameters: { path: '/xxx-milf-videos' }, - network: 'score', - }, - { - name: '60 Plus MILFs', - slug: '60plusmilfs', - url: 'https://www.60plusmilfs.com', - parameters: { path: '/xxx-granny-videos' }, - network: 'score', - }, - { - name: 'Ashley Sage Ellison', - slug: 'ashleysageellison', - url: 'https://www.bigboobbundle.com/ashleysageellison', - parameters: { path: '/videos', actors: ['Ashley Sage Ellison'] }, - network: 'score', - }, - { - name: 'Autumn Jade', - slug: 'autumnjade', - url: 'https://www.bigboobbundle.com/autumn-jade', - network: 'score', - parameters: { path: '/videos', actors: ['Autumn Jade'] }, - }, - { - name: 'Big Boob Bundle', - slug: 'bigboobbundle', - url: 'https://www.bigboobbundle.com', - network: 'score', - show: false, // all content appears to be on subsites - }, - { - name: 'Big Boobs POV', - slug: 'bigboobspov', - url: 'https://www.scorepass.com/bigboobspov', - network: 'score', - }, - { - name: 'Big Tit Angela White', - slug: 'bigtitangelawhite', - url: 'https://www.bigboobbundle.com/bigtitangelawhite', - parameters: { path: '/videos', actors: ['Angela White'] }, // no dates available - network: 'score', - }, - { - name: 'Big Tit Hitomi', - slug: 'bigtithitomi', - url: 'https://www.bigboobbundle.com/bigtithitomi', - parameters: { path: '/videos', actors: ['Hitomi'] }, - network: 'score', - }, - { - name: 'Big Tit Hooker', - slug: 'bigtithooker', - url: 'https://www.scorepass.com/bigtithooker', - network: 'score', - }, - { - name: 'Big Tit Terry Nova', - slug: 'bigtitterrynova', - url: 'https://www.bigboobbundle.com/bigtitterrynova', - parameters: { path: '/videos', actors: ['Terry Nova'] }, - network: 'score', - }, - { - name: 'Big Tit Venera', - slug: 'bigtitvenera', - url: 'https://www.bigboobbundle.com/bigtitvenera', - network: 'score', - }, - { - name: 'Black And Stacked', - slug: 'blackandstacked', - url: 'https://www.scorepass.com/blackandstacked', - network: 'score', - }, - { - name: 'Boned At Home', - slug: 'bonedathome', - url: 'https://www.scorepass.com/bonedathome', - network: 'score', - }, - { - name: 'Bootylicious Mag', - slug: 'bootyliciousmag', - url: 'https://www.bootyliciousmag.com', - network: 'score', - }, - { - name: 'Busty Angelique', - slug: 'bustyangelique', - url: 'https://www.bigboobbundle.com/bustyangelique', - network: 'score', - }, - { - name: 'Busty Arianna', - slug: 'bustyarianna', - url: 'https://www.bigboobbundle.com/bustyarianna', - network: 'score', - }, - { - name: 'Busty Danni Ashe', - slug: 'bustydanniashe', - url: 'https://www.bigboobbundle.com/bustydanniashe', - network: 'score', - }, - { - name: 'Busty Dusty Stash', - slug: 'bustydustystash', - url: 'https://www.bigboobbundle.com/bustydustystash', - network: 'score', - }, - { - name: 'Busty Ines Cudna', - slug: 'bustyinescudna', - url: 'https://www.bigboobbundle.com/bustyinescudna', - network: 'score', - }, - { - name: 'Busty Kelly Kay', - slug: 'bustykellykay', - url: 'https://www.bigboobbundle.com/bustykellykay', - network: 'score', - }, - { - name: 'Busty Kerry Marie', - slug: 'bustykerrymarie', - url: 'https://www.bigboobbundle.com/bustykerrymarie', - network: 'score', - }, - { - name: 'Busty Lorna Morgan', - slug: 'bustylornamorgan', - url: 'https://www.bigboobbundle.com/bustylornamorgan', - network: 'score', - }, - { - name: 'Busty Merilyn', - slug: 'bustymerilyn', - url: 'https://www.scorepass.com/bustymerilyn', - network: 'score', - }, - { - name: 'Busty Old Sluts', - slug: 'bustyoldsluts', - url: 'https://www.milfbundle.com/bustyoldsluts', - network: 'score', - }, - { - name: 'Busty Sammie Black', - slug: 'bustysammieblack', - url: 'https://www.bigboobbundle.com/bustysammieblack', - network: 'score', - }, - { - name: 'Cherry Brady', - slug: 'cherrybrady', - url: 'https://www.bigboobbundle.com/cherrybrady', - network: 'score', - }, - { - name: 'Chloes World', - slug: 'chloesworld', - url: 'https://www.scorepass.com/chloesworld', - network: 'score', - }, - { - name: 'Christy Marks', - slug: 'christymarks', - url: 'https://www.scorepass.com/christymarks', - network: 'score', - }, - { - name: 'Creampie for Granny', - slug: 'creampieforgranny', - url: 'https://www.milfbundle.com/creampieforgranny', - network: 'score', - }, - { - name: 'Crystal Gunns World', - slug: 'crystalgunnsworld', - url: 'https://www.bigboobbundle.com/crystalgunnsworld', - network: 'score', - }, - { - name: 'Daylene Rio', - slug: 'daylenerio', - url: 'https://www.bigboobbundle.com/daylenerio', - network: 'score', - }, - { - name: 'Desiraes World', - slug: 'desiraesworld', - url: 'https://www.bigboobbundle.com/desiraesworld', - network: 'score', - }, - { - name: 'Diane Poppos', - slug: 'dianepoppos', - url: 'https://www.bigboobbundle.com/dianepoppos', - network: 'score', - }, - { - name: 'Eva Notty Videos', - slug: 'evanottyvideos', - url: 'https://www.bigboobbundle.com/evanottyvideos', - network: 'score', - }, - { - name: 'Feed Her Fuck Her', - slug: 'feedherfuckher', - url: 'https://www.scorepass.com/feedherfuckher', - network: 'score', - }, - { - name: 'Flat And Fucked MILFs', - slug: 'flatandfuckedmilfs', - url: 'https://www.milfbundle.com/flatandfuckedmilfs', - network: 'score', - }, - { - name: 'Granny Gets A Facial', - slug: 'grannygetsafacial', - url: 'https://www.milfbundle.com/grannygetsafacial', - network: 'score', - }, - { - name: 'Granny Loves BBC', - slug: 'grannylovesbbc', - url: 'https://www.milfbundle.com/grannylovesbbc', - network: 'score', - }, - { - name: 'Granny Loves Young Cock', - slug: 'grannylovesyoungcock', - url: 'https://www.milfbundle.com/grannylovesyoungcock', - network: 'score', - }, - { - name: 'Home Alone MILFs', - slug: 'homealonemilfs', - url: 'https://www.milfbundle.com/homealonemilfs', - network: 'score', - }, - { - name: 'I Boned Your Mom', - slug: 'ibonedyourmom', - url: 'https://www.milfbundle.com/ibonedyourmom', - network: 'score', - }, - { - name: 'I Fucked the Boss', - slug: 'ifuckedtheboss', - url: 'https://www.milfbundle.com/ifuckedtheboss', - network: 'score', - }, - { - name: 'Jessica Turner', - slug: 'jessicaturner', - url: 'https://www.bigboobbundle.com/jessicaturner', - network: 'score', - }, - { - name: 'Joana Bliss', - slug: 'joanabliss', - url: 'https://www.bigboobbundle.com/joanabliss', - network: 'score', - }, - { - name: 'Julia Miles', - slug: 'juliamiles', - url: 'https://www.bigboobbundle.com/juliamiles', - network: 'score', - }, - { - name: 'Karina Hart', - slug: 'karinahart', - url: 'https://www.scorepass.com/karinahart', - network: 'score', - }, - { - name: 'Karla James', - slug: 'karlajames', - url: 'https://www.bigboobbundle.com/karlajames', - network: 'score', - }, - { - name: 'Leanne Crow Videos', - slug: 'leannecrowvideos', - url: 'https://www.bigboobbundle.com/leannecrowvideos', - network: 'score', - }, - { - name: 'Leg Sex', - slug: 'legsex', - url: 'https://www.legsex.com', - network: 'score', - }, - { - name: 'Linseys World', - slug: 'linseysworld', - url: 'https://www.scorepass.com/linseysworld', - network: 'score', - }, - { - name: 'Mega Tits Minka', - slug: 'megatitsminka', - url: 'https://www.bigboobbundle.com/megatitsminka', - network: 'score', - }, - { - name: 'Micky Bells', - slug: 'mickybells', - url: 'https://www.bigboobbundle.com/mickybells', - network: 'score', - }, - { - name: 'MILF Bundle', - slug: 'milfbundle', - url: 'https://www.milfbundle.com', - network: 'score', - show: false, - }, - { - name: 'Teaming Cock', - slug: 'milfthreesomes', - url: 'https://www.milfbundle.com/milfthreesomes', - network: 'score', - }, - { - name: 'MILF Tugs', - slug: 'milftugs', - url: 'https://www.milfbundle.com/milftugs', - network: 'score', - }, - { - name: 'Natalie Fiore', - slug: 'nataliefiore', - url: 'https://www.bigboobbundle.com/nataliefiore', - network: 'score', - }, - { - name: 'Naughty Footjobs', - slug: 'naughtyfootjobs', - url: 'https://www.scorepass.com/naughtyfootjobs', - network: 'score', - }, - { - name: 'Naughty Mag', - slug: 'naughtymag', - url: 'https://www.naughtymag.com', - network: 'score', - }, - { - name: 'Naughty Tugs', - slug: 'naughtytugs', - url: 'https://www.scorepass.com/naughtytugs', - network: 'score', - }, - { - name: 'Nicole Peters', - slug: 'nicolepeters', - url: 'https://www.bigboobbundle.com/nicolepeters', - network: 'score', - }, - { - name: 'Old Horny MILFs', - slug: 'oldhornymilfs', - url: 'https://www.milfbundle.com/oldhornymilfs', - network: 'score', - }, - { - name: 'Picking Up Pussy', - slug: 'pickinguppussy', - url: 'https://www.scorepass.com/pickinguppussy', - network: 'score', - }, - { - name: 'Porn Loser', - slug: 'pornloser', - url: 'https://www.scorepass.com/pornloser', - network: 'score', - }, - { - name: 'Porn Mega Load', - slug: 'pornmegaload', - url: 'https://www.pornmegaload.com', - network: 'score', - show: false, - }, - { - name: 'SaRennas World', - slug: 'sarennasworld', - url: 'https://www.bigboobbundle.com/sarennasworld', - network: 'score', - }, - { - name: 'Scoreland', - slug: 'scoreland', - url: 'https://www.scoreland.com', - network: 'score', - parameters: { path: '/big-boob-videos' }, - priority: 3, - }, - { - name: 'Scoreland2', - slug: 'scoreland2', - url: 'https://www.scoreland2.com', - network: 'score', - parameters: { path: '/big-boob-scenes' }, - priority: 1, - }, - { - name: 'Score Classics', - slug: 'scoreclassics', - url: 'https://www.scoreclassics.com', - network: 'score', - parameters: { path: '/classic-boob-videos' }, - priority: 1, - }, - { - name: 'Scoreland TV', - slug: 'scorelandtv', - url: 'https://www.scorepass.com/scorelandtv', - network: 'score', - priority: 1, - show: false, // appears to be streaming service for other sites - }, - { - name: 'ScoreTV', - slug: 'scoretv', - url: 'https://www.scoretv.tv', - network: 'score', - priority: 1, - show: false, // similar to or same as Scoreland TV - }, - { - name: 'Score Videos', - slug: 'scorevideos', - url: 'https://www.scorevideos.com', - network: 'score', - parameters: { path: '/porn-videos' }, - priority: 2, - }, - { - name: 'Sha Rizel Videos', - slug: 'sharizelvideos', - url: 'https://www.bigboobbundle.com/sharizelvideos', - network: 'score', - }, - { - name: 'Silver Sluts', - slug: 'silversluts', - url: 'https://www.milfbundle.com/silversluts', - network: 'score', - }, - { - name: 'Stacy Vandenberg Boobs', - slug: 'stacyvandenbergboobs', - url: 'https://www.bigboobbundle.com/stacyvandenbergboobs', - network: 'score', - }, - { - name: 'Susie Wildin', - slug: 'susiewildin', - url: 'https://www.bigboobbundle.com/susiewildin', - network: 'score', - }, - { - name: 'Tawny Peaks', - slug: 'tawnypeaks', - url: 'https://www.bigboobbundle.com/tawny-peaks', - network: 'score', - }, - { - name: 'Tiffany Towers', - slug: 'tiffanytowers', - url: 'https://www.bigboobbundle.com/tiffany-towers', - network: 'score', - }, - { - name: 'Tits And Tugs', - slug: 'titsandtugs', - url: 'https://www.scorepass.com/titsandtugs', - network: 'score', - }, - { - name: 'TNA Tryouts', - slug: 'tnatryouts', - url: 'https://www.scorepass.com/tnatryouts', - network: 'score', - }, - { - name: 'Valory Irene', - slug: 'valoryirene', - url: 'https://www.bigboobbundle.com/valoryirene', - network: 'score', - }, - { - name: 'XL Girls', - slug: 'xlgirls', - url: 'https://www.xlgirls.com', - network: 'score', - }, - { - name: 'Your Mom Loves Anal', - slug: 'yourmomlovesanal', - url: 'https://www.milfbundle.com/yourmomlovesanal', - network: 'score', - }, - { - name: 'Your Mom\'s Got Big Tits', - slug: 'yourmomsgotbigtits', - url: 'https://www.milfbundle.com/yourmomsgotbigtits', - network: 'score', - }, - { - name: 'Your Wife My Meat', - slug: 'yourwifemymeat', - url: 'https://www.milfbundle.com/yourwifemymeat', - network: 'score', - }, - // SEXY HUB - { - slug: 'danejones', - name: 'Dane Jones', - alias: ['dnj'], - url: 'https://www.danejones.com/', - parameters: { siteId: 290 }, - network: 'sexyhub', - }, - { - slug: 'lesbea', - name: 'Lesbea', - alias: ['lsb'], - url: 'https://www.lesbea.com', - parameters: { siteId: 291 }, - tags: ['lesbian'], - network: 'sexyhub', - }, - { - slug: 'massagerooms', - name: 'Massage Rooms', - alias: ['mrs'], - url: 'https://www.sexyhub.com/scenes?site=292', - tags: ['massage'], - network: 'sexyhub', - }, - { - slug: 'momxxx', - name: 'Mom XXX', - alias: ['mom'], - url: 'https://www.sexyhub.com/scenes?site=293', - tags: ['milf'], - network: 'sexyhub', - }, - { - slug: 'fitnessrooms', - name: 'Fitness Rooms', - alias: ['frs'], - url: 'https://www.sexyhub.com/scenes?site=294', - network: 'sexyhub', - }, - { - slug: 'girlfriends', - name: 'Girlfriends', - url: 'https://www.sexyhub.com/scenes?site=289', - tags: ['lesbian'], - network: 'sexyhub', - }, - // TEAM SKEET - { - slug: 'exxxtrasmall', - name: 'Exxxtra Small', - alias: ['ext'], - description: '', - url: 'https://www.exxxtrasmall.com', - parameters: { id: 'exs' }, - network: 'teamskeet', - }, - { - slug: 'teenpies', - name: 'Teen Pies', - description: '', - url: 'https://www.teenpies.com', - parameters: { id: 'tp' }, - network: 'teamskeet', - }, - { - slug: 'innocenthigh', - name: 'Innocent High', - alias: ['inh'], - description: '', - url: 'https://www.innocenthigh.com', - parameters: { id: 'ih' }, - network: 'teamskeet', - }, - { - slug: 'teencurves', - name: 'Teen Curves', - description: '', - url: 'https://www.teencurves.com', - parameters: { id: 'tc' }, - network: 'teamskeet', - }, - { - slug: 'cfnmteens', - name: 'CFNM Teens', - alias: ['cfnmt'], - url: 'https://www.cfnmteens.com', - parameters: { id: 'cfnm' }, - network: 'teamskeet', - }, - { - slug: 'teensloveanal', - name: 'Teens Love Anal', - alias: ['tla'], - url: 'https://www.teensloveanal.com', - tags: ['anal'], - parameters: { id: 'tla' }, - network: 'teamskeet', - }, - { - slug: 'mybabysittersclub', - name: 'My Babysitters Club', - description: '', - url: 'https://www.mybabysittersclub.com', - parameters: { id: 'bsc' }, - network: 'teamskeet', - }, - { - slug: 'shesnew', - name: 'She\'s New', - alias: ['ssn'], - url: 'https://www.shesnew.com', - parameters: { id: 'bsc' }, - network: 'teamskeet', - }, - { - slug: 'teensdoporn', - name: 'Teens Do Porn', - alias: ['tdp'], - url: 'https://www.teensdoporn.com', - parameters: { id: 'tdp' }, - network: 'teamskeet', - }, - { - slug: 'povlife', - name: 'POV Life', - description: '', - url: 'https://www.povlife.com', - parameters: { id: 'pov' }, - network: 'teamskeet', - }, - { - slug: 'therealworkout', - name: 'The Real Workout', - description: '', - url: 'https://www.therealworkout.com', - parameters: { id: 'trw' }, - network: 'teamskeet', - }, - { - slug: 'thisgirlsucks', - name: 'This Girl Sucks', - alias: ['tgs'], - description: '', - url: 'https://www.thisgirlsucks.com', - parameters: { id: 'tgs' }, - network: 'teamskeet', - }, - { - slug: 'teenslovemoney', - name: 'Teens Love Money', - alias: ['tlm'], - description: '', - url: 'https://www.teenslovemoney.com', - parameters: { id: 'tlm' }, - network: 'teamskeet', - }, - { - slug: 'oyeloca', - name: 'Oye Loca', - description: '', - url: 'https://www.oyeloca.com', - parameters: { id: 'ol' }, - network: 'teamskeet', - }, - { - slug: 'tittyattack', - name: 'Titty Attack', - description: '', - url: 'https://www.tittyattack.com', - parameters: { id: 'ta' }, - network: 'teamskeet', - }, - { - slug: 'teenyblack', - name: 'Teeny Black', - description: '', - url: 'https://www.teenyblack.com', - parameters: { id: 'tb' }, - network: 'teamskeet', - }, - { - slug: 'lusthd', - name: 'Lust HD', - description: '', - url: 'https://www.lusthd.com', - parameters: { id: 'lhd' }, - network: 'teamskeet', - }, - { - slug: 'rubateen', - name: 'Rub A Teen', - description: '', - url: 'https://www.rubateen.com', - parameters: { id: 'rat' }, - network: 'teamskeet', - }, - { - slug: 'herfreshmanyear', - name: 'Her Freshman Year', - description: '', - url: 'https://www.exxxtrasmall.com', - parameters: { id: 'hfy' }, - network: 'teamskeet', - }, - { - slug: 'selfdesire', - name: 'Self Desire', - description: '', - url: 'https://www.selfdesire.com', - parameters: { id: 'sd' }, - network: 'teamskeet', - }, - { - slug: 'solointerviews', - name: 'Solo Interviews', - description: '', - url: 'https://www.solointerviews.com', - parameters: { id: 'si' }, - network: 'teamskeet', - }, - { - slug: 'teamskeetextras', - name: 'Team Skeet Extras', - description: '', - url: 'https://www.teamskeetextras.com', - parameters: { id: 'tse' }, - network: 'teamskeet', - }, - { - slug: 'dyked', - name: 'Dyked', - description: '', - url: 'https://www.dyked.com', - parameters: { id: 'dyk' }, - network: 'teamskeet', - }, - { - slug: 'badmilfs', - name: 'Bad MILFs', - description: '', - url: 'https://www.badmilfs.com', - parameters: { id: 'bad' }, - network: 'teamskeet', - }, - { - slug: 'gingerpatch', - name: 'Ginger Patch', - description: '', - url: 'https://www.gingerpatch.com', - parameters: { id: 'gp' }, - network: 'teamskeet', - }, - { - slug: 'bracefaced', - name: 'Brace Faced', - description: '', - url: 'https://www.bracefaced.com', - parameters: { id: 'bfd' }, - network: 'teamskeet', - }, - { - slug: 'teenjoi', - name: 'Teen JOI', - description: '', - url: 'https://www.teenjoi.com', - parameters: { id: 'joi' }, - network: 'teamskeet', - }, - { - slug: 'stepsiblings', - name: 'Step Siblings', - alias: ['steps'], - url: 'https://www.stepsiblings.com', - parameters: { id: 'sss' }, - network: 'teamskeet', - }, - { - slug: 'submissived', - name: 'Submissived', - description: '', - url: 'https://www.submissived.com', - tags: ['bdsm'], - parameters: { scraper: 'A' }, - network: 'teamskeet', - }, - { - slug: 'familystrokes', - name: 'Family Strokes', - alias: ['fams'], - url: 'https://www.familystrokes.com', - parameters: { scraper: 'A' }, - tags: ['family'], - network: 'teamskeet', - }, - // TWISTYS - { - name: 'Twistys', - slug: 'twistys', - url: 'https://www.twistys.com/scenes?site=219', - description: 'Twistys.com is the #1 ranked babe site on the web! With over 10 years of photos and videos, updated daily with 3 HQ photo sets and 2 HD videos, Twistys also awards one hot babe a Treat of the Month title, complete with exclusive photo & video sets! Weekly updates!', - network: 'twistys', - priority: 1, - }, - { - name: 'When Girls Play', - slug: 'whengirlsplay', - alias: ['wgp'], - url: 'https://www.whengirlsplay.com', - description: 'Watch hot girls seducing other girls in steamy lesbian play. These sluts finger, use dildos, strap-ons and squirt their pink pussies in lesbian porn by WhenGirlsPlay.com. Get Access to the Hottest Lesbian videos on the web!', - parameters: { siteId: 227 }, - network: 'twistys', - priority: 1, - }, - { - name: 'Turning Twistys', - slug: 'turningtwistys', - url: 'https://www.twistys.com/scenes?site=302', - description: 'Where straight curious cuties explore their sexuality, and get seduced by sexy butch girls. These sneaky lesbians lure the innocent looking beauties away from their boyfriends and into their beds... Be careful or they might just steal your girl!', - network: 'twistys', - priority: 1, - }, - { - name: 'Mom Knows Best', - slug: 'momknowsbest', - url: 'https://www.momknowsbest.com', - description: 'The world’s tightest teens and most elegant MILFs get together at Mom Knows Best, where horny teen girls learn the ins and outs of lesbian sex from confident and beautiful older women. These MILFs know what they like and exactly how to get it, and lucky for them, these tasty teens are eager to learn, and always very eager to please!', - parameters: { siteId: 234 }, - network: 'twistys', - priority: 1, - }, - { - name: 'Twistys Hard', - slug: 'twistyshard', - alias: ['th'], - url: 'https://www.twistyshard.com', - description: 'Watch horny nymphos get stuffed with stiff, bulging cocks. Hot sluts eager to spread their legs, bend over on all fours, or mount a big rock-hard erection. They want their needs fulfilled, and love to show off how they do it. Get into Twistys Hard and see just how hard things can get!', - parameters: { siteId: 225 }, - network: 'twistys', - priority: 1, - }, - { - name: 'Feature Films', - slug: 'featurefilms', - url: 'https://www.twistys.com/scenes?site=233', - description: 'Prepare yourself for a night at the movies you\'ll never forget with Feature Films from Twistys. High-end cinematic productions featuring in-depth storylines and industry-leading visuals, erotic adventures with the most beautiful women in the world are now at your fingertips. Lesbian, Hardcore Bonus Updates!', - network: 'twistys', - }, - { - name: 'Nicole Graves', - slug: 'nicolegraves', - url: 'https://www.twistys.com/scenes?site=223', - description: 'NicoleGraves.com is the only official web site of Nicole Graves with 100% EXCLUSIVE content! Here you\'ll find Nicole Graves videos and photos of her shaved pussy and Nicole Graves fucking and giving a blowjob!', - network: 'twistys', - }, - { - name: 'Anette Dawn', - slug: 'anettedawn', - url: 'https://www.twistys.com/scenes?site=221', - description: 'Hey guys! Its me Anette Dawn, if you\'ve seen me on other sites, like Twistys I have been doing internet modeling for a while now. This is however my first and only official site. I recieved so many requests for more so I finally got this site together! I can\'t wait for you to join me inside!', - network: 'twistys', - }, - { - name: 'Twistys Teasers', - slug: 'twistysteasers', - url: 'https://www.twistys.com/scenes?site=232', - description: 'Twistys Teasers is a doorway to new exciting content, opened exclusively to you, our loyal members. See it here first while it’s fresh and hot, and be sure to let us know exactly how much you like it before you leave. Be tempted. Be tantalized. Be teased. Solo, Lesbian, Hardcore Bonus Updates!', - network: 'twistys', - }, - { - name: 'Euro Foxes', - slug: 'eurofoxes', - url: 'https://www.twistys.com/scenes?site=226', - description: 'EuroFoxes.com: the worlds Number One European Babe site! EuroFoxes is dedicated to bringing you the very best european babes!', - network: 'twistys', - }, - { - name: 'Blue Fantasies', - slug: 'bluefantasies', - url: 'https://www.twistys.com/scenes?site=220', - description: 'BlueFantasies.com prides itself on getting the most beautiful women in the world to show off their hot bodies, 100% exclusively for you.', - network: 'twistys', - }, - { - name: 'Busty Ones', - slug: 'bustyones', - url: 'https://www.twistys.com/scenes?site=229', - description: 'BustyOnes.com bringing you the most beautiful big breasts in the world! The hottest women alive showcasing their fantastic tits.', - network: 'twistys', - }, - // VIVID - { - slug: 'vividceleb', - name: 'Vivid Celeb', - url: 'https://www.vividceleb.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2018-03-25'), - }, - }, - { - slug: 'thebrats', - name: 'The Brats', - url: 'https://www.thebrats.com', - network: 'vivid', - }, - { - slug: 'wheretheboysarent', - name: 'Where The Boys Aren\'t', - url: 'https://www.wheretheboysarent.com', - network: 'vivid', - }, - { - slug: 'nineteen', - name: 'Nineteen', - url: 'http://www.nineteen.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-01-23'), - }, - }, - { - slug: 'nastystepfamily', - name: 'Nasty Step Family', - url: 'http://www.nastystepfamily.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-01-29'), - }, - }, - { - slug: 'girlswhofuckgirls', - name: 'Girls Who Fuck Girls', - url: 'http://www.girlswhofuckgirls.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-05-21'), - }, - }, - { - slug: 'petited', - name: 'Petited', - url: 'http://www.petited.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-01-28'), - }, - }, - { - slug: 'orgytrain', - name: 'Orgy Train', - url: 'http://www.orgytrain.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-01-09'), - }, - }, - { - slug: 'momisamilf', - name: 'Mom Is A MILF', - url: 'http://www.momisamilf.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-01-25'), - }, - }, - { - slug: 'blackwhitefuckfest', - name: 'Black White Fuck Fest', - url: 'http://www.blackwhitefuckfest.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-01-30'), - }, - }, - { - slug: '65inchhugeasses', - name: '65 Inch Huge Asses', - url: 'http://www.65inchhugeasses.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2019-05-18'), - }, - }, - { - slug: 'brandnewfaces', - name: 'Brand New Faces', - url: 'http://www.brandnewfaces.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2018-02-28'), - }, - }, - { - slug: 'vividclassic', - name: 'Vivid Classic', - url: 'http://www.vividclassic.com', - network: 'vivid', - parameters: { - referer: 'https://www.thebrats.com', - deep: 'https://www.thebrats.com/en/video', - scene: false, - lastNative: new Date('2016-06-29'), - }, - }, - // VIXEN - { - slug: 'vixen', - name: 'Vixen', - description: 'Vixen.com features the world’s finest cinematic adult films with 4K quality and high-end erotic photography.', - url: 'https://www.vixen.com', - network: 'vixen', - }, - { - slug: 'blacked', - name: 'Blacked', - description: 'Porn videos of beautiful girls in first time interracial porn videos. BLACKED has the hottest pornstars in HD sex videos.', - url: 'https://www.blacked.com', - tags: ['interracial', 'bbc'], - network: 'vixen', - }, - { - slug: 'tushy', - name: 'Tushy', - description: 'Watch the world\'s best HD Anal videos! Featuring beautiful, never before seen girls in first time anal. Exclusively on Tushy.com', - url: 'https://www.tushy.com', - tags: ['anal'], - network: 'vixen', - }, - { - slug: 'blackedraw', - name: 'Blacked Raw', - description: 'Experience real women in interracial sex videos. Passionate sex with beautiful pornstars. No photoshop just the highest quality porn. Everything you see is real.', - url: 'https://www.blackedraw.com', - tags: ['interracial', 'bbc'], - network: 'vixen', - }, - { - slug: 'tushyraw', - name: 'Tushy Raw', - description: 'Anal sex videos with beautiful models and pornstars being fucked in the ass. TUSHY RAW features famous pornstars in high quality anal porn videos.', - url: 'https://www.tushyraw.com', - tags: ['anal'], - network: 'vixen', - }, - { - slug: 'deeper', - name: 'Deeper', - description: 'Porn videos from DEEPER.com featuring Passionate sex, light kink and BDSM with plenty of erotic sex videos featuring beautiful models.', - url: 'https://www.deeper.com', - network: 'vixen', - }, - // VOGOV - { - slug: 'vogov', - name: 'VogoV', - url: 'https://www.vogov.com', - description: 'Top rated models. Graceful locations. Best gonzo scenes. 4K UHD 60 FPS. So, in general Vogov is a website that is worth visiting and exploring carefully. It gives a chance to spend a fantastic night with gorgeous girls ready to experiment and to full around with their lovers.', - network: 'vogov', - }, - // WHALE MEMBER - { - name: 'Cum 4K', - slug: 'cum4k', - url: 'https://cum4k.com', - tags: ['fake-cum', 'creampie', '4k'], - network: 'whalemember', - }, - { - name: 'Tiny 4K', - slug: 'tiny4k', - url: 'https://tiny4k.com', - tags: ['4k'], - network: 'whalemember', - }, - { - name: 'POVD', - slug: 'povd', - url: 'https://povd.com', - tags: ['pov'], - network: 'whalemember', - }, - { - name: 'Lubed', - slug: 'lubed', - url: 'https://lubed.com', - tags: ['oil'], - network: 'whalemember', - }, - { - name: 'Casting Couch X', - slug: 'castingcouchx', - alias: ['castingcouch x', 'castingcouch-x', 'casting couch-x'], - url: 'https://castingcouch-x.com', - network: 'whalemember', - }, - { - name: 'Passion HD', - slug: 'passionhd', - alias: ['phd', 'passion-hd'], - url: 'https://passion-hd.com', - network: 'whalemember', - }, - { - name: 'Nanny Spy', - slug: 'nannyspy', - url: 'https://nannyspy.com', - network: 'whalemember', - }, - { - name: 'Girl Cum', - slug: 'girlcum', - url: 'https://girlcum.com', - network: 'whalemember', - }, - { - name: 'Pure Mature', - slug: 'puremature', - url: 'https://puremature.com', - tags: ['milf'], - network: 'whalemember', - }, - { - name: 'Fantasy HD', - slug: 'fantasyhd', - alias: ['fhd'], - url: 'https://fantasyhd.com', - network: 'whalemember', - }, - { - name: 'Spy Fam', - slug: 'spyfam', - url: 'https://spyfam.com', - tags: ['family'], - network: 'whalemember', - }, - { - name: 'Holed', - slug: 'holed', - url: 'https://holed.com', - tags: ['anal'], - network: 'whalemember', - }, - { - name: 'BBC Pie', - slug: 'bbcpie', - url: 'https://bbcpie.com', - tags: ['bbc', 'interracial'], - network: 'whalemember', - }, - { - name: 'Wet VR', - slug: 'wetvr', - url: 'https://wetvr.com', - tags: ['virtual-reality'], - network: 'whalemember', - }, - { - name: 'Exotic 4K', - slug: 'exotic4k', - url: 'https://exotic4k.com', - tags: ['4k'], - network: 'whalemember', - }, - { - name: 'My Very First Time', - slug: 'myveryfirsttime', - alias: ['mvft'], - url: 'https://myveryfirsttime.com', - network: 'whalemember', - }, - { - name: 'Baeb', - slug: 'baeb', - alias: ['baebz'], - url: 'https://baeb.com', - network: 'whalemember', - }, - // WICKED - { - slug: 'wicked', - name: 'Wicked', - alias: ['wkp'], - url: 'https://www.wicked.com', - description: 'Welcome to the new Wicked.com! Watch over 25 years of Wicked Pictures\' brand of award-winning porn for couples and women in 4k HD movies & xxx videos', - parameters: { independent: true }, - network: 'wicked', - }, - // XEMPIRE - { - slug: 'hardx', - name: 'HardX', - description: "Welcome to HardX.com, home of exclusive hardcore gonzo porn and first time anal scenes, DP, blowbangs and gangbangs from today's hottest porn stars!", - url: 'https://www.hardx.com', - network: 'xempire', - }, - { - slug: 'eroticax', - name: 'EroticaX', - description: 'EroticaX.com features intimate scenes of passionate, erotic sex. Watch the sensual side of hardcore porn as your favorite pornstars have real, intense orgasms.', - url: 'https://www.eroticax.com', - network: 'xempire', - }, - { - slug: 'darkx', - name: 'DarkX', - description: 'Watch interracial BBC porn videos on DarkX.com, featuring the best pornstars taking big black cock in exclusive scenes. The best black on white porn inside!', - url: 'https://www.darkx.com', - tags: ['interracial'], - network: 'xempire', - }, - { - slug: 'allblackx', - name: 'AllBlackX', - description: 'AllBlackX.com features the hottest ebony pornstar beauties in hardcore black on black gonzo porn. From director Mason, watch 4k ultra HD videos inside', - url: 'https://www.allblackx.com', - network: 'xempire', - tags: ['ebony', 'bbc'], - }, - { - slug: 'lesbianx', - name: 'LesbianX', - description: "LesbianX.com features today's top pornstars in hardcore lesbian porn. Watch passionate & intense girl on girl sex videos, from erotic kissing to pussy licking.", - url: 'https://www.lesbianx.com', - tags: ['lesbian'], - network: 'xempire', - }, + // REALITY KINGS + { + name: 'Look At Her Now', + url: 'https://www.lookathernow.com', + description: 'Look At Her Now brings you best HD reality porn videos every week. Check out these girls before and after they get some rough pounding.', + parameters: { native: true }, + // parameters: { siteId: 300 }, + slug: 'lookathernow', + network: 'realitykings', + }, + { + name: 'We Live Together', + slug: 'welivetogether', + alias: ['wlt'], + url: 'https://www.welivetogether.com', + description: "We are girls that love to eat pussy and We Live Together! Every week we go out on the streets, bars, parties, malls... wherever and we pick up the cutest lesbians and invite them to come over and party at our apartment. From our girl friends at college, to roommates, and friends of friends.. we're always looking for the hottest lesbian girls around! We Live Together has hundreds of lesbian videos for you to download right from Reality Kings... it's the sexiest lesbian porn anywhere guys and gals! :-) Come watch us eat pussy and work our dildo magic on gorgeous, sexy girls. We love to get together and get off in steamy hot threesome and foursome lesbian movies! We promise you're going to love our amazing collection of lesbian porn. Thanks for dropping in to the We Live Together Apartment, hope you enjoy your visit! Love xoxo Brittney, Taylor, Nicole & All the Girls", + parameters: { siteId: 3 }, + network: 'realitykings', + }, + { + name: 'Black GFs', + slug: 'blackgfs', + alias: ['bgfs'], + url: 'https://www.realitykings.com/scenes?site=47', + description: '', + parameters: null, + network: 'realitykings', + }, + { + name: 'Dare Dorm', + url: 'https://www.daredorm.com', + description: '', + parameters: { siteId: 48 }, + slug: 'daredorm', + network: 'realitykings', + }, + { + name: 'GF Revenge', + slug: 'gfrevenge', + alias: ['gfr'], + url: 'https://www.gfrevenge.com', + description: '', + parameters: { siteId: 49 }, + network: 'realitykings', + }, + { + name: 'Horny Birds', + url: 'https://www.realitykings.com/scenes?site=50', + description: '', + parameters: null, + slug: 'hornybirds', + network: 'realitykings', + }, + { + name: 'Crazy College GFs', + url: 'https://www.realitykings.com/scenes?site=51', + description: '', + parameters: null, + slug: 'crazycollegegfs', + network: 'realitykings', + }, + { + name: 'Crazy Asian GFs', + url: 'https://www.crazyasiangfs.com', + description: '', + parameters: { siteId: 52 }, + slug: 'crazyasiangfs', + network: 'realitykings', + }, + { + name: 'Teens Love Huge Cocks', + url: 'https://www.teenslovehugecocks.com', + alias: ['tlhc'], + description: "Teens Love Big Cocks is dedicated to providing you the hottest teens getting fucked by the biggest cocks! Every week Reality Kings introduces another teen to a big hot meat rod! When these girls see a big throbbing penis they can't resist shoving it in their hot teen mouths. These girl next door types are no slouches when it comes to oral sex! Watch them deepthroat & gag on a mouth full of cock before taking big hot loads all over their pretty faces. The fun doesn't stop there! These girls love getting their tight teen pussy & asses spread wide and pounded by massive dicks! These girls won't settle for less & there is no dick too large. Start downloading TeensLoveBigCock porn videos & HD quality pictures now and watch teen pussy get fucked like you've never seen before!", + parameters: { siteId: 42 }, + slug: 'teenslovehugecocks', + network: 'realitykings', + }, + { + name: 'Big Naturals', + url: 'https://www.bignaturals.com', + alias: ['bin'], + description: "If you think there is nothing like big natural breasts, Big Naturals welcomes you home. Reality Kings brings you nothing but the hottest amateur big tit women. We're talking about some seriously big boobs. Sexy women with big bouncy tits who love to get it on. These women don't hesitate to let their big natural tits get fucked and let those massive juggs bounce! Big Naturals has hundreds of high quality videos available for download. If into tits, this is the place to be. There's no plastic parts here, only big natural boobs! There's thousands of high resolution pics available to download as well. Check out any of our top rated scenes for the biggest, huge natural tits. Hooters, fun bags, juggs... whatever you want to call them Reality Kings and Big Naturals have the hottest big boobs you'll find anywhere. Sit back, relax, and watch the titties bounce... Reality Kings style!", + parameters: { siteId: 5 }, + slug: 'bignaturals', + network: 'realitykings', + }, + { + name: 'Money Talks', + slug: 'moneytalks', + alias: ['mot'], + url: 'https://www.moneytalks.com', + description: "Money Talks... bullshit walks. We all know the saying, but at Reality Kings we like to prove it! Just watch us approach everyday people on the street and ask them what they will do for some real American Greenbacks! Check out smokin' hot amateurs preform in porn videos or watch crazy college kids preform insane stunts on film... all in exchange for cold hard cash. People will do anything when Money Talks! Watch as we offer cash in exchange for one, AMAZING blow job! From crazy Spring Breakers to the girl next door, we find some amazing sluts and see just what they'll do for the loot--girls that give up the booty, for the booty! Arrr! Reality Kings has every high quality Money Talks episode available for download. We're talking about some seriously hot videos here. You won't find this crazy porn content anywhere else! Remember, Money Talks... bullshit walks!", + parameters: { siteId: 28 }, + network: 'realitykings', + }, + { + name: 'Moms Lick Teens', + url: 'https://www.momslickteens.com', + alias: ['momslickteens'], + description: 'Hot moms know how to fuck, especially when they have a lot of pent up energy. MomsLickTeens.com is where all the magic happens between lustful milf minxes and curious 18+ teen bombshells in HD porn videos. Mature horny women love to sample a fresh batch of pussy and ass whenever possible here at Reality Kings. They love teaching the carnal arts to eager younger women who crave a deeper understanding of the female body. Our bodacious mommies love exploring the anatomy of their fresh-faced lesbian lovers and engage in cunnilingus and anilingus within seconds. Naked women licking, sucking, scissoring, and toying their gaping pussy and assholes with a plethora of adult toys is absolutely riveting to watch. You’ll be aroused by RK girls of different ages rolling around together in sweaty sex scenes. Moms Lick Teens features limber tongues exploring the deepest recesses of female erogenous zones often eliciting projectile squirt orgasms. The phenomenon of female ejaculation occurs regularly in our premium erotica so get a load of it while blowing your own load to our buxom mommies today!', + parameters: { siteId: 43 }, + slug: 'momslickteens', + network: 'realitykings', + }, + { + name: 'RK Prime', + slug: 'rkprime', + alias: ['rkp'], + url: 'https://www.realitykings.com/scenes?site=45', + parameters: null, + network: 'realitykings', + }, + { + name: 'Milf Hunter', + url: 'https://www.milfhunter.com', + description: "Reality Kings presents MILF Hunter the ORIGINAL reality porn site dedicated to MILFs and mature sex content. If you don't know what a MILF is, allow us to explain... we're talking about sex starved, smokin' hot moms that are in need of a little attention--a MILF, a Mother I'd Like to Fuck! We've all seen these moms at the mall, the beach, and around town. Watch every week as the Hunter captures another hottie on film and gives them what they've been craving... some dick! These moms are seriously hot MILFs and they appear in the most incredible high quality pics and movies! We have hundreds of mature porn videos available for you to download. Or if you're looking for photos we have thousands of high resolution MILF porn pics directly from the MILF Hunter! Reality Kings brings you the best mature sex scenes around so why not join the MILF Hunter hunt down mature moms across America...", + parameters: { siteId: 2 }, + slug: 'milfhunter', + network: 'realitykings', + }, + { + name: 'Pure 18', + url: 'https://www.realitykings.com/scenes?site=31', + description: 'There\'s a lot of stuff out there that claims to be "pure", from spring water to gold chains, who knows what\'s actually legit$2 Reality Kings presents Pure 18, legit, 100% verified 18 year old sex scenes--no bullshit, only incredible 18 year old girls! These hot girls are the real deal, barely legal, smokin\' hot babes looking for some fun. Don\'t let their age fool you, these chicks know how to work a cock. Tight pussies and tight asses, the finest sex scenes around, that\'s what Pure 18 is all about! If you love watching amazing blow jobs, you\'re going to love this content. Download hundreds of high quality videos and pics featuring 100% verified 18 year old sex! These cuties are not shy about sex. Watch them take on monster cocks and love every minute of it. Pure 18, legit, verified, real 18 year old girls hungry for a cock meat sandwich!', + parameters: null, + slug: 'pure18', + network: 'realitykings', + }, + { + name: 'Bad Tow Truck', + url: 'https://www.realitykings.com/scenes?site=44', + description: 'Driving a tow truck is hard work. Especially when clients have a tough time paying! At BadTowTruck.com we alleviate that problem for sexy female damsels in distress by offering them different “payment” options. When big tit babes need a boost but are tight on cash, our drivers are more than happy to boost their tight asses up and give them a deep dicking. The chance to unleash a creamy internal cumshot should not be missed here at Reality Kings! Enjoy HD porn videos full of stranded sirens who are all too happy to get naked and oblige their rescuer with a gagging BJ. Anal riding is not far behind as our buxom RK divas love to get their bumpers shined. Bad Tow Truck is home to monster cocks getting gobbled up by very appreciative clients whose cars have broken down. They love swallowing every drop of jizz in the front or back seat of the tow truck. Anything goes here as our tantalizing teasers can contort their agile bodies into any number of sexual positions in tight spaces. They require maximal torque and horsepower in their erotic escapades so watch them get out of a jam with the help of a helpful towing guy, and then get jammed by him!', + parameters: null, + slug: 'badtowtruck', + network: 'realitykings', + }, + { + name: 'Cum Fiesta', + url: 'https://www.cumfiesta.com', + alias: ['cuf'], + description: '"Is this the fiesta $4 " Of course, welcome to the Cum Fiesta! Every week Reality Kings welcomes another hottie to the party... the dick sucking party! :-) When the girl (or girls!) arrive they show us the super secret password (watch a trailer to find out) and the party begins! As their clothes come off, these babes begin to show off their amazing oral skills. Amateur chicks taking the biggest facial cumshots, that\'s what Cum Fiesta is about! There are no pansy blow jobs here, these chicks give the best head around, and take huge cum shots to their face at the same time. Download hundreds of high quality videos and pics of semen swallowing hotties. Join the party that features amateurs, newbies, and even the girl next door! These babes love to suck cock and take incredible cum shots you\'ll find no where else! This is no siesta folks, it\'s a Cum Fiesta!', + parameters: { siteId: 10 }, + slug: 'cumfiesta', + network: 'realitykings', + }, + { + name: '8th Street Latinas', + url: 'https://www.8thstreetlatinas.com', + description: "Scientists say that the sun is what makes the temperature rise south of the equator, but we think its the women! If you haven't seen the chicas (women) from 8th Street Latinas, you're in for a spicy treat. From the famous Calle 8 (8th Street) in Miami, Reality Kings brings you the some incredibly hot latinas! We're talking Cubans, Dominicans, Panamanians, and other sexy latinas from South and Central America. These babes aim to please with their tanned bodies and deliciously round asses. Damn, we can't get enough! 8th Street Latinas has some caliente (hot) content for you to download, featuring the hottest latina sex scenes around. These are not tanned white chicks, these are REAL, hot latinas who know how to get your blood pumping. If you think you can handle the heat, grab your pair of shorts and flip-flops and let's head to Miami... 8th Street Latinas has some incredible latina porn for you!", + parameters: { siteId: 1 }, + slug: '8thstreetlatinas', + network: 'realitykings', + }, + { + name: "Mike's Apartment", + url: 'http://www.mikesapartment.com', + description: 'There\'s a room for rent in Mikes Apartment and Mike has found another hot chick to fill the vacancy! Join Mike on his search for roommates to help pay the bills. If these hot euro babes don\'t have the money, that\'s alright, Mike offers them the room in exchange for a few hours of masturbation and fun! And if the girl is traveling with a companion, thats not a problem... Mike just creates another steamy euro sex film for his "private" collection. Seriously, these babes are Europe\'s finest and Mike is your connoisseur of European booty! From their tight bodies, to their thick accents, these ladies know how to please and excite. Reality Kings offers hundreds of our incredible european porn movies and pics for you to download, and you don\'t have to travel to Moscow to get them. If you\'re looking for original, hot, European porn content, welcome to your new home: Mikes Apartment.', + parameters: { siteId: 25 }, + slug: 'mikesapartment', + network: 'realitykings', + }, + { + name: 'In the VIP', + url: 'http://www.inthevip.com', + description: "Ever wonder what happens In The VIP$3 Reality Kings takes you to the hottest night clubs in the country to show you exactly what goes on in the VIP room. When the club is packed, the music is pumpin', and the ladies are looking this HOT, you know it's going to be a damn good time! Grab a drink and step into the VIP room. Check out these gorgeous babes shaking their asses and flashing the camera. You will never see ladies like this in some whack ass bar, only in the most exclusive VIP rooms. As the party gets going, the clothes come off, and the panties drop! Watch some amazing free VIP porn movies, featuring these hotties having sex VIP style. We're talking about some down and dirty club sex featuring smokin' hot sluts. These chicks came for a good time and they've found it! Join the exclusive party In The VIP.", + parameters: { siteId: 22 }, + slug: 'inthevip', + network: 'realitykings', + }, + { + name: 'CFNM Secret', + slug: 'cfnmsecret', + alias: ['cfnms'], + url: 'https://www.realitykings.com/scenes?site=9', + description: "Shhh, keep your voice down! At Reality Kings we have a secret to share, the CFNM Secret! What's this secret all about$5 Clothed Females and Nude Males (CFNM)! Beautiful women dressed to impress and an unsuspecting male who is about to discover the secret for himself! These voyeurs are interested in every inch of the male body, touching and grabbing, they won't stop until they've had enough. Who wouldn't mind being these ladies play things$6 Gorgeous babes teasing and embarrassing men for their own fun and pleasure. Vulnerable guys being inspected and scrutinized in amazing high quality voyeur sex scenes. From CFNM handjobs to CFNM party scenes Reality Kings offers the hottest women enjoying the male body like never before. Browse our free CFNM videos below to download high quality pics and trailers. You're going to tell your friends about this secret, the CFNM Secret!", + parameters: null, + network: 'realitykings', + }, + { + name: 'Street BlowJobs', + url: 'https://www.realitykings.com/scenes?site=36', + description: "Street Blowjobs is one man's hunt for down on their luck ladies. Ladies who will blow your creamy wad, while they win a nice greedy wad of cash for their outstanding efforts. Horny honeys captured on hidden spy camera, giving amazing blowjobs for some good ol' American greenbacks. Can you imagine any of these smokin' hot babes giving blowjobs to you for just a little bit of moolah$11 Well we've got the content for you! Street Blowjobs has hundreds of hot blowjobs for you to download in high quality movies and pics. Watch these hotties use their magnificent dick sucking lips to get exactly what they want--your cock and your money! Reality Kings brings you a new episode every week, full of the best blowjobs and public blowjobs around. They say money can't buy happiness, but we beg to differ... money can chose your type of pleasure at Street Blowjobs.", + parameters: null, + slug: 'streetblowjobs', + network: 'realitykings', + }, + { + name: 'Hot Bush', + url: 'https://www.realitykings.com/scenes?site=21', + description: 'Forget bald vaginas, at Reality Kings we love a Hot Bush! Women with some grass on the field. These hairy beavers belong to some of the hottest women around. From brunette babes to red heads, every episode features a stunning natural beauty being worshiped for her beautiful bush! Browse some of our free hairy pussy videos below, and see for yourself if the curtains match the drapes. From the landing strip to the Bermuda triangle, these magnificent muff mounds are sure to please! Natural unshaved pussy movies and pics, thats what Hot Bush is all about. Grab your snorkel and get ready for some serious muff diving! Join the Reality Kings crew for some *very* Hot Bush.', + parameters: null, + slug: 'hotbush', + network: 'realitykings', + }, + { + name: 'Team Squirt', + url: 'https://www.realitykings.com/scenes?site=37', + description: "There's no denying it, at Reality Kings we love all kinds of pussy! Ask us what we really love however, and you'll get one answer: hot wet pussy! Team Squirt invites you to strap on your snorkel and fins, because we're going diving in some of the wettest pussy around! This is NOT pee ladies and gentlemen, this is real female ejaculation. Watch these beautiful ladies experience pleasure beyond belief as they try to control their squirting pussy on camera. Masturbation, fucking, whatever it takes, these babes will do anything for a squirting orgasm! Team Squirt has tons of high quality videos of girls squirting available for you to download right now. Be prepared, this is some serious female squirting content! From the girl, to the camera... everything is drenched when these super soakers take aim. These babes all pack a loaded, squirting pussy, and they know exactly how to use it! Grab your eye protection and join the team... Team Squirt.", + parameters: null, + slug: 'teamsquirt', + network: 'realitykings', + }, + { + name: 'Milf Next Door', + url: 'https://www.realitykings.com/scenes?site=26', + description: "We all love them, from the sexy mom at the grocery store, to the mature hottie down the block... we're talking about the MILF Next Door! There is nothing that these hot MILFs need more than a good pounding. If you don't know what a MILF is, allow us to explain... a Mother I'd Like to Fuck, a MILF! Watch as these sex starved sluts and their girlfriends search for a lucky dude to satisfy their craving for cock. MILF Next Door offers lesbian threesomes, amazing foursomes, and more mature sex movies featuring the hottest mature women! Start downloading some of this incredible content right now from our free pics and videos below. Every episode features another stunningly hot MILF finally getting the attention she deserves. If you love everyday mom's and can't wait to see these ladies get off, join Reality Kings and the MILF Next Door.", + parameters: null, + slug: 'milfnextdoor', + network: 'realitykings', + }, + { + name: 'Captain Stabbin', + url: 'https://www.captainstabbin.com', + description: "Hop aboard the S.S. Stabbin and join Captain Stabbin on his trip to analize the seven seas! What's better than a girl with a hot pussy$8 A girl with a hot pussy getting poked in the ass! Reality Kings invites you to the worlds greatest anal sex adventure. These babes don't need a boarding pass to climb aboard this ship, only a hot body and a gorgeous ass. Watch as the Captain sets course for the anal islands in search of the best anal sex scenes around! Download hundreds of incredible anal sex movies and pics in stunning high quality formats. Captain Stabbin brings you the very best booty content, from her first anal sex scene, to amazing boat sex scenes at sea, every episode is sure to please! These girls are ready for a stern spanking (pun intended)! Raise the main sail, set course, and join Captain Stabbin on his anal adventure! Arrr!", + parameters: { siteId: 8 }, + slug: 'captainstabbin', + network: 'realitykings', + }, + { + name: 'Big Tits Boss', + url: 'https://www.realitykings.com/scenes?site=6', + description: "Reality Kings presents Big Tits Boss! Have you been checking out that smokin' hot female executive around the office$10 Damn she's fine! She wears those short skirts and tight tops that make her huge tits pop out! Time to stop slackin' off fellas, because these ladies have been watching you closely. We're talking about the sexy women with the big tits at work. CEOs, Lawyers, CIOs, CFOs, these babes don't take any bullshit and they'll gladly use you like a toy whenever they please! Big Tits Boss has amazing high quality videos and pics available for download, featuring some very powerful women with awesome big natural tits. You won't mind being called into the office for a little discipline this time around! It's all business when these hotties are in the office... so fill out your TPS reports and be on your best behavior if you're looking for the promotion from the Big Tits Boss!", + parameters: null, + slug: 'bigtitsboss', + network: 'realitykings', + }, + { + name: 'Euro Sex Parties', + slug: 'eurosexparties', + alias: ['esp'], + url: 'https://www.realitykings.com/scenes?site=13', + description: "Pack your bags folks, we're headed to Europe! It's time to join two best friends as they travel across Europe and throw some amazing Euro Sex Parties. Forget about boring 1on1 sex scenes. Get ready for hardcore threesomes, foursomes, and fivesomes! Hot European porn directly from the source, just the way we like it. Euro babes with tight bodies taking on multiple cocks... what could be better$9 How about watching them eat pussy as well! Now that's a group sex party we'd love to attend. From hardcore group sex to hardcore gangbangs, this is the hottest content anywhere. Euro Sex Parties offers hundreds of European porn videos and pics to download in stunning high quality formats. Don't pack your bags yet, Reality Kings has tons of free movies and pics for you to download right here! Join us on our European vacation, and we'll throw a few Euro Sex Parties along the way.", + parameters: null, + network: 'realitykings', + }, + { + name: 'Dangerous Dongs', + url: 'https://www.realitykings.com/scenes?site=12', + description: "Reality Kings presents the Dangerous Dongs porn site, which bring you highlights from the featured huge cock and big dick porn on the entire network. If you enjoy watching sexy girls having fat cock stuffed deep into their tight pussies while they moan in pleasure, Dangerous Dongs has thousands of high resolution big dick porn pics and videos waiting for you inside. See tons of hot Latina, MILF, college, blond, teen and ebony babes taking big cock balls deep. You'll be able to download tons of big cock videos with hardcore sex scenes where cute girls with big tits and sexy, round asses take fat cock from every angle including doggy style and reverse cowgirl. So, for true fans of the big dick porn genre, look no further, we've gathered the best huge cock videos Reality Kings has to offer on the internet.", + parameters: null, + slug: 'dangerousdongs', + network: 'realitykings', + }, + { + name: 'Extreme Asses', + url: 'https://www.realitykings.com/scenes?site=14', + description: "Extreme Asses brings you a slew of big ass babes exclusively from Reality Kings. We're talking serious highlights from featured RK models like Jayden James big ass pictures and even some Jenny Hendrix ass sex videos. These sexy babes have nothing but big bouncy tits and porn ass that is perfect whether being viewed in doggy style action or riding cock in big ass videos. Watch tons of free ass porn trailers to get a taste of the kind of porn ass and ass sex that awaits you inside the Extreme Asses website. No matter what you like whether it be Latina, MILF, college, blond, teen or ebony big ass, we've got all the best extreme asses you can handle. This is big ass heaven, so be sure to check out this collection of big ass porn and ass sex gathered from Reality Kings best for your viewing pleasure.", + parameters: null, + slug: 'extremeasses', + network: 'realitykings', + }, + { + name: '40 Inch Plus', + url: 'https://www.realitykings.com/scenes?site=4', + description: "We have three words for you: deliciously round asses. Are you searching for ladies of the thicker variety$15 Beautiful women with hips measuring over 40 Inch Plus$16 Reality Kings presents the finest collection of booty around! Hot babes that love to have their big asses pinched and smacked. Grab 'em, squeeze 'em, bite 'em, these girls love to have their round asses played with. 40 Inch Plus is a tribute to ladies with a shape, there's no skinny chicks here! All of these women have a perfect ass and their tits are not too bad either! Download hundreds of movies and pics featuring women with big round asses. Check out our trailers and pics below to get a free sample of this incredible content. If you're looking for beautiful women with nice round asses look no further! Reality Kings and 40 Inch Plus are your source for gorgeous women with big asses.", + parameters: null, + slug: '40inchplus', + network: 'realitykings', + }, + { + name: 'Happy Tugs', + url: 'https://www.happytugs.com', + description: "Come on dudes, who doesn't like a happy ending$13 We've all seen those hole in the wall Asian massage parlors! Finally there is a site that celebrates the hand job. Asian beauties rubbing massage oil all over, what could be better$14 These babes know how to work out the kinks, seriously amazing rub 'n tug jobs. Happy Tugs captures hidden camera footage from inside one of the country's best sexual massage parlors. The dudes come in looking for a little rub down and, for a few dollars more, get a full servicing. It doesn't get any better than this, hand jobs and hot Asian babes. Check out our amazing happy ending videos, with babes rubbing their oil soaked breasts all over their favorite customers. Strip down, jump on the massage table and get your wallet out, Happy Tugs will ensure you get a very happy ending!", + parameters: { siteId: 19 }, + slug: 'happytugs', + network: 'realitykings', + }, + { + name: 'Reckless In Miami', + url: 'https://www.realitykings.com/scenes?site=303', + description: '', + parameters: null, + slug: 'recklessinmiami', + network: 'realitykings', + }, + { + name: 'HD Love', + url: 'https://www.realitykings.com/scenes?site=20', + description: 'Looking for incredibly hot porn videos in HD$12 Reality Kings showcases it all in hardcore erotica here at HDLove.com. Our premium adult content will satisfy your deepest carnal desires in stunning high-definition sex scenes. Feast your eyes on bodacious naked babes who love nothing more than to fuck on camera for you. Our hi-def movies capture every inch of their voluptuous bodies in vivid detail. Perfect round boobs and perky nipples are just the start. These jezebels proudly display their killer asses and dripping wet pussies before ravaging huge cocks like their lives depended on it. Whether you’re in the mood for horny 18+ teen nymphos or seasoned mature women the raunchiest scenes will keep you cumming back to HD Love. Our eager to please divas love to disrobe and spread as much love around as they can offering up deepthroat gagging blowjobs to anyone daring enough. They yearn for deep anal penetrations and are always up for a sweaty orgy so witness it all in crystal clear resolution. Catch every microscopic detail and blow your load repeatedly with the best that RK has in store!', + parameters: null, + slug: 'hdlove', + network: 'realitykings', + }, + { + name: 'Bikini Crashers', + url: 'https://www.realitykings.com/scenes?site=7', + description: "What's better than a babe in a scantily clad bikini$22 A party full of babes in scantily clad bikinis of course! Welcome to the Bikini Crashers! Reality Kings invites you to join our party with the hottest swimsuit models and bikini babes anywhere. We're talking about smokin' hot beauties throwing a naked pool party. Could it get any better than this$23 From perfectly round asses to amazing tan lines, these girls know how to party. We're not talking about your average swimsuit model either, these chicks are wild, crazy, and ready to get it on. Every party is loaded with 1on1, girl on girl, and group sex scenes. Gorgeous swimsuit girls getting it on by the pool, the beach, or anywhere they can show off their amazing bikinis! So grab a cold one, your shades, and kick back... you're invited to the Bikini Crashers party!", + parameters: null, + slug: 'bikinicrashers', + network: 'realitykings', + }, + { + name: 'Wives in Pantyhose', + url: 'https://www.realitykings.com/scenes?site=41', + description: 'Wives in Pantyhose features all kinds of real wives in sexy lingerie fingering their pussies with sex toys while they squeeze their big mature tits and moan. This Reality Kings network site has collected tons of pantyhose pics of hot wives and presented them to you for your viewing pleasure. No matter whether you prefer Latinas, MILFs, redheads, blondes or ebony babes, Wives in Pantyhose has all the sexiest nylon wives masturbating. There are even pantyhose lesbians playing with each other using dildos while they orgasm in smoking hot pantyhose videos. Wives in Pantyhose is easily one the best collection of real wives engaging in pantyhose porn ever put together on the net. So if you have a housewife pantyhose fetish, the the website Wives in Pantyhose is sure to deliver for you all the best models and porn the Reality Kings network has to offer.', + parameters: null, + slug: 'wivesinpantyhose', + network: 'realitykings', + }, + { + name: 'No Faces', + url: 'https://www.realitykings.com/scenes?site=30', + description: 'Isn’t it arousing to watch porn with a little mystery thrown in$19 That’s what Nofaces.com is about. The scrumptious porn stars at Reality Kings like to keep you guessing sometimes, so enjoy a wide array of HD porn videos where the faces of our horny minxes are not shown. A little sensual secrecy never hurt anyone so have some fun trying to figure out which titillating temptress is getting reamed in our torrid sex clips. Are her ample breast, perfect round ass, and wet pussy enough of a giveaway$20 What about her tattoos, piercings, or birth marks$21 Play the role of an X-rated detective and enjoy an endless sea of hardcore erotica with 18+ teen foxes and mature naked temptresses in graphic films depicting a covert cum sucker gobbling up monster cocks with glee. Our enigmatic nymphos relish in getting fucked in multiple holes simultaneously knowing that their identity is not revealed on camera. No Faces respects an RK girl’s desire to remain anonymous and only show more arousing parts of her luscious body.', + parameters: null, + slug: 'nofaces', + network: 'realitykings', + }, + { + name: 'Saturday Night Latinas', + url: 'https://www.realitykings.com/scenes?site=34', + description: "What's better than a Saturday Night out partying$18 Taking home a beautiful chick at the end of the night to fuck and have your way with! Reality Kings presents Saturday Night Latinas, gorgeous babes from the steamy night clubs and streets of Brazil. These hotties may have left the club, but the real party is about to begin! Real latina girls sucking and fucking after a night of partying! From deliciously round asses to amazing tan lines, these Brazilian bombshells are sure to please. Browse our videos below to download free latina porn movies and pictures. We have hundreds of latina sex scenes available for you to download. Grab your bags and get ready to head to Brazil, Reality Kings invites you to take home a Saturday Night Latina of your very own. Hot latina babes who love to party, join us today for a steamy Saturday Night out!", + parameters: null, + slug: 'saturdaynightlatinas', + network: 'realitykings', + }, + { + name: 'Extreme Naturals', + url: 'https://www.realitykings.com/scenes?site=15', + description: 'There are big natural breasts, then there are Extreme Naturals. On this site, we say, "Go big or go home!" That\'s why we only deliver massive naturals straight from the best Reality Kings has to offer. Extreme Naturals has painstakingly combed the RK network for the best giant naturals models and the hottest big naturals videos with the most hardcore XXX. These sexy babes have giant naturals that bounce while they ride cock and while they get stroked from behind doggy style in their perfect porn asses. For true fans of huge natural breasts, be sure to watch tons of free big naturals videos exclusively available as Extreme Naturals trailers on the website. Whether you like your giant naturals to be on Latinas, MILFs, college babes, blondes, teens or ebony babes, Extreme Naturals has the best collection of massive naturals straight from the vaults of Reality Kings.', + parameters: null, + slug: 'extremenaturals', + network: 'realitykings', + }, + { + name: 'Cum Girls', + url: 'https://www.realitykings.com/scenes?site=11', + description: "Reality Kings presents the Cum Girls porn site, which is dedicated solely to XXX cum shots and cum videos. If you like seeing hot girls with cum in their mouth, or face cum pictures, Cum Girls has thousands of high resolution cum porn pics and videos waiting for your viewing pleasure. There are smoking hot Latina, MILF, college, blond, teen and ebony babes with cum shots not only on their face, but also cum ass and cum tits pictures too. You'll be able to download tons of cum porn videos with hardcore sex scenes that all end with sticky and gooey cum in the mouth, the face, boobs, pussy or ass. Cum Girls has got cum porn and cum videos for true fans of the genre, and they all come straight to you from Reality Kings, so you know you're getting nothing less than the best on the internet.", + parameters: null, + slug: 'cumgirls', + network: 'realitykings', + }, + { + name: 'VIP Crew', + url: 'https://www.realitykings.com/scenes?site=40', + description: "Party animals rejoice! The VIP Crew is your guide to the hottest and wildest VIP parties in the world! We're not talking about ordinary house parties here, we're talking about the biggest, most badass sex parties around. When you combine loads of fun, some fine looking women, and a few lucky dudes you have the recipe for one amazing fucking party. Best of all, you're invited! From huge orgy sex parties to private sex parties, the VIP Crew brings in the hottest women--all ready to bare their VIP pussies for you. Babes that aren't afraid of a little pole dancing, foam dancing, or strip tease! These girls will do anything to join these wild sex parties and have a good time. Reality Kings has hundreds of high quality videos and pics available for you to download. So what the hell are you waiting for$17 Join the VIP Crew and get your freak on!", + parameters: null, + slug: 'vipcrew', + network: 'realitykings', + }, + { + name: 'Moms Bang Teens', + slug: 'momsbangteens', + alias: ['mbt'], + url: 'https://www.momsbangteens.com', + description: "Reality Kings presents the first website dedicated to hot moms who love to bang 18+ teens. Moms Bang Teens features the sexiest MILFs on the web, and these MILFs are all about fucking young guys on camera. If you remember lusting after one of your friend's hot moms back in grade school, then you know exactly what Moms Bang Teens is all about. Imagine if instead of just fantasizing about that sexy mother, you actually got to bang her. These are the same hot moms you see at your local supermarket and shopping at your neighborhood mall. Some of them are married and never get the attention they need. While others are just horny and sexy moms who never got tied down with a husband. Instead they like to go out and find hot young studs that know how to fuck them right. These are experienced and mature women who know what they want; young 18+ teens that can give them that rock hard cock.", + parameters: { siteId: 27 }, + network: 'realitykings', + }, + { + name: 'Sneaky Sex', + url: 'https://www.sneakysex.com', + description: 'Sneaky dirty sex! They are fucking and nobody can see, otherwise they will have a HUGE problem. When no one is watching, these horny MILFs and Teens are having sneaky sex!', + parameters: { siteId: 46 }, + slug: 'sneakysex', + network: 'realitykings', + }, + { + name: 'See My Wife', + url: 'https://www.realitykings.com/scenes?site=35', + description: 'Have you been spying on that hot couple next door$26 See My Wife invites you to view the private porn collection of horny amateurs everywhere! We\'re talking about 100% user submitted movies and pictures. Real women appearing in the hottest wife sex scenes around, that is what See My Wife is about. Our users have a chance to make 0 for pics and 00 for videos when they submit their homemade content. If you\'ve ever said "I wish I could bang my wife on film and get paid for it," look no further! Reality Kings considers every submission when we post new episodes. Check out some of our free pics and trailers below, this is one amazing collection of girlfriend and wife sex scenes. Every week we post a new episode crammed with four incredible babes showing off in front of the camera. No need to spy on the couple next door when you come See My Wife!', + parameters: null, + slug: 'seemywife', + network: 'realitykings', + }, + { + name: 'Girls of Naked', + url: 'https://www.realitykings.com/scenes?site=18', + description: 'Nothing is hotter than voluptuous minxes who love getting naked. Girlsofnaked.com is home to a bevy of bodacious beauties who are all about showing as much skin to whomever is willing to satisfy their sexual desires. Our 18+ pornstars are daring and always curious for new carnal adventures in HD porn videos. Reality Kings has compiled an incredible assortment of erotica with big boob naughty nymphos. Watch them squeeze their perky nipples before rubbing their ticklish clits in steamy scenes. Our deviant divas need their juicy pussies stuffed 24/7 by the biggest cocks in the adult biz and will stop at nothing to devour as much man meat as they can fit into every hungry orifice. Girls of Naked celebrate nudity and hardcore sex in all its glory. Fetishes, orgies, bukkake, anal creampies and much more are their favorite pastimes. RK has full-length premium porno movies bursting with our luscious babes bursting out of their clothes just for you!', + parameters: null, + slug: 'girlsofnaked', + network: 'realitykings', + }, + { + name: 'Lil Humpers', + url: 'https://lilhumpers.com', + description: '', + parameters: { siteId: 310 }, + slug: 'lilhumpers', + network: 'realitykings', + }, + { + name: 'Mike in Brazil', + url: 'https://www.realitykings.com/scenes?site=24', + description: "Are you ready for the never ending booty vacation$24 Join Mike In Brazil as he explores the wild, the exotic, and the gorgeous women of South America! If you have never been to Brazil, don't worry... Mike will give you a crash course on the most amazing ASSet of their native women. We're talking about deliciously tanned, round, thong clad Brazilian ass! These booties will not disappoint. Mike exports nothing but the finest, Grade A, Brazilian porn directly to your computer screen. Check out the hottest Brazilian ass around, wearing nothing but bikinis and thongs that are sure to get your blood pumping! These hotties spend hours working on their amazing tans to show off their bodies... look at those incredible tan lines! Mike In Brazil features some amazing hardcore sex, from anal to Brazilian facials, we're sure you're going to be planning a trip to Brazil soon. What are you waiting for$25 Join the never ending booty vacation with Mike In Brazil!", + parameters: null, + slug: 'mikeinbrazil', + network: 'realitykings', + }, + { + name: 'Real Orgasms', + url: 'https://www.realitykings.com/scenes?site=32', + description: "Real Orgasms features all kinds of sexy women playing with their pussies and stimulating their clits with sex toys and big dildos until they have real orgasms. This Reality Kings network site has collected tons of real orgasm videos and masturbation videos and concentrated them down to only the best real female orgasms that you will ever witness on the net. Whether you're really into Latinas, MILFs, college babes, blondes, teens or ebony babes, Real Orgasms has every kind of the most beautiful women masturbating. Watch as they play with themselves using sex toys and dildos while they moan, shake and their pussies convulse, as they have real orgasm on video for your pleasure. By far, this is the best collection of real orgasm porn ever put together on the net. Thanks to Reality Kings, Real Orgasms only delivers 100% real female orgasms and masturbation videos.", + parameters: null, + slug: 'realorgasms', + network: 'realitykings', + }, + { + name: 'Tranny Surprise', + url: 'https://www.trannysurprise.com', + description: 'If you’re in the mood for graphic tranny porn, look no further than TrannySurprise.com. A sexy shemale is a thing of beauty, often possessing a voracious appetite for sex. Reality Kings is home to some of the most incredible transsexual pornstars on the net. Watch them stroke their huge dicks and massage their voluminous ball sacks in our full-length HD videos. All these goddesses want to do is suck dick until it erupts in their wide open mouths. Cum swallowing is their specialty so enjoy our scenes full of creamy jizz loads overflowing onto their giant tits and firm stomachs. These nude RK shemales live to get rimjobs before getting drilled by gigantic dicks. Messy creampies are usually how their nights end so witness the torrid fuck marathons leading up to juicy orgasms. Tranny Surprise features sensual ladyboys that know just how to please anyone looking to take a walk on the wild side. Premium porno is what you deserve so eat it all up with our luscious, busty trannies. Long legs, tight asses, toned physiques, and a healthy dose of raw animal passion is what our “chicks with dicks” deliver in every one of our erotic films.', + parameters: { native: true }, + slug: 'trannysurprise', + network: 'realitykings', + }, + { + name: 'Flower Tucci', + url: 'https://www.realitykings.com/scenes?site=17', + description: 'Reality Kings presents Flower Tucci, and this is what she has to say: "My name is Flower, and I live, eat, breathe, sleep, and worship SEX! You have never met a girl like me! My ass is for worshiping. I can squirt over and over again when my pussy cums. I search out the biggest cocks and take them in my mouth, pussy, and ass! I milk those cocks until my pussy squirts everywhere. This site is dedicated to all my fantasies. Watch me search for the ultimate orgasm." Damn folks! I don\'t know about you, but this babe sounds perfect. Squirting pussy, amazing ass, gorgeous tits... the full package! If you\'re like us, you can\'t wait another moment to download these amazing videos of Flower Tucci squirting. Reality Kings is the one and only home to Flower Tucci pics and Flower Tucci movies--this content is absolutely incredible! Join us, Flower, and her friends in search of the ultimate orgasm.', + parameters: null, + slug: 'flowertucci', + network: 'realitykings', + }, + { + name: 'First Time Auditions', + url: 'https://www.realitykings.com/scenes?site=16', + description: "Forget about the next big music idol, we're looking for the next big porn star! Reality Kings presents First Time Auditions, featuring the hottest amateur chicks, searching for fame and fortune. These sluts will do anything to break into the business, from blowjobs to amateur sex scenes, these are their first porn auditions caught on film. Do you think they have what it takes$7 Download hundreds of amateur porn movies and pics, and you be the judge. First Time Auditions places ads in local and college newspapers seeking the hottest models around. When these babes arrive, we are never disappointed. They show off their perfect bodies and their many, amazing talents! These are the hottest amateur auditions around. Trying to get their careers started, these girls give the porn auditions of a lifetime! If you're ready to be the judge, to put these girls to the test, watch them on their First Time Auditions.", + parameters: null, + slug: 'firsttimeauditions', + network: 'realitykings', + }, + { + name: 'Top Shelf Pussy', + url: 'https://www.realitykings.com/scenes?site=38', + description: 'Top Shelf Pussy features nothing but the best pussy on the net. If pussy is like Johnny Walker, consider Top Shelf Pussy the Blue Label of the bunch. Whether you like shaved pussy, teen pussy, hairy pussy, wet pussy, mature pussy, black pussy or fat pussy, Top Shelf Pussy has got the hottest models and the best pussy videos on the net. Watch tons of free pussy trailers and see as these gorgeous girls play with their pussies using sex toys and dildos. Then see them take a deep stroking to their wet pussy while they moan, shake and their pussies convulse in some amazing pussy porn. No doubt, Top Shelf Pussy has got tons of pictures of sexy ladies spread eagle and more pussy videos than you could ever possibly watch. If you are a fan of the pussy porn genre, then Top Shelf Pussy is the site for you.', + parameters: null, + slug: 'topshelfpussy', + network: 'realitykings', + }, + { + name: 'Round and Brown', + url: 'https://www.roundandbrown.com', + alias: ['rab'], + description: 'Chocolate lovers out there, Reality Kings presents to you... Round And Brown, the porn site that caters to horny dudes who have a mighty craving for fine "sistah" booties. The ladies featured in these ebony porn movies are SIZZLING HOT, like a newly melted chocolate fondue! We\'re talking about some damn fine black booties! If it\'s Round And Brown, it gets the special lube treatment, no exceptions! Think you can handle this collection of premium ebony ass$1 There\'s no skinny white girls here, only gorgeous black beauties with deliciously round booties, featured in the best hardcore ebony sex videos around! Reality Kings is the only one who can bring you this amazing collection of black girl porn. If you love big round asses, gorgeous black babes, and amazing tits we have the videos and pics you\'re looking for. Warning: This chocolate may melt in your hand and your mouth... but who cares, if it\'s Round And Brown!', + parameters: { siteId: 33 }, + slug: 'roundandbrown', + network: 'realitykings', + }, + { + name: 'Monster Curves', + slug: 'monstercurves', + alias: ['mcu'], + url: 'https://www.realitykings.com/scenes?site=29', + description: "Forget about those toothpick size runway models, give us some ladies with curves-- Monster Curves! If you love your women round and juicy, ladies with some meat on their bones... then we have the content for you! We're talking about women with hips that don't quit. Incredibly round asses that will make your mouth water! Big booty girls with big round asses. Only people as obsessed as us could bring you this many pairs of perfects hips and asses! Download hundreds of movies and pics featuring gorgeous girls with amazing curves (we call them Monster Curves). Check out some of our free trailers below, these girls and their round butts and perfect hips are sure to wet your appetite! Every week, Reality Kings brings you nothing but the finest butts, the sexy round asses that jiggle when you grab 'em, the women with the Monster Curves!", + parameters: null, + network: 'realitykings', + }, + // SCORE + { + name: '18 Eighteen', + slug: '18eighteen', + url: 'https://www.18eighteen.com', + network: 'score', + parameters: { path: '/xxx-teen-videos' }, + }, + { + name: '40 Something Mag', + slug: '40somethingmag', + url: 'https://www.40somethingmag.com', + parameters: { path: '/xxx-mature-videos' }, + network: 'score', + }, + { + name: '50 Plus MILFs', + slug: '50plusmilfs', + url: 'https://www.50plusmilfs.com', + parameters: { path: '/xxx-milf-videos' }, + network: 'score', + }, + { + name: '60 Plus MILFs', + slug: '60plusmilfs', + url: 'https://www.60plusmilfs.com', + parameters: { path: '/xxx-granny-videos' }, + network: 'score', + }, + { + name: 'Ashley Sage Ellison', + slug: 'ashleysageellison', + url: 'https://www.bigboobbundle.com/ashleysageellison', + parameters: { path: '/videos', actors: ['Ashley Sage Ellison'] }, + network: 'score', + }, + { + name: 'Autumn Jade', + slug: 'autumnjade', + url: 'https://www.bigboobbundle.com/autumn-jade', + network: 'score', + parameters: { path: '/videos', actors: ['Autumn Jade'] }, + }, + { + name: 'Big Boob Bundle', + slug: 'bigboobbundle', + url: 'https://www.bigboobbundle.com', + network: 'score', + show: false, // all content appears to be on subsites + }, + { + name: 'Big Boobs POV', + slug: 'bigboobspov', + url: 'https://www.scorepass.com/bigboobspov', + network: 'score', + }, + { + name: 'Big Tit Angela White', + slug: 'bigtitangelawhite', + url: 'https://www.bigboobbundle.com/bigtitangelawhite', + parameters: { path: '/videos', actors: ['Angela White'] }, // no dates available + network: 'score', + }, + { + name: 'Big Tit Hitomi', + slug: 'bigtithitomi', + url: 'https://www.bigboobbundle.com/bigtithitomi', + parameters: { path: '/videos', actors: ['Hitomi'] }, + network: 'score', + }, + { + name: 'Big Tit Hooker', + slug: 'bigtithooker', + url: 'https://www.scorepass.com/bigtithooker', + network: 'score', + }, + { + name: 'Big Tit Terry Nova', + slug: 'bigtitterrynova', + url: 'https://www.bigboobbundle.com/bigtitterrynova', + parameters: { path: '/videos', actors: ['Terry Nova'] }, + network: 'score', + }, + { + name: 'Big Tit Venera', + slug: 'bigtitvenera', + url: 'https://www.bigboobbundle.com/bigtitvenera', + network: 'score', + }, + { + name: 'Black And Stacked', + slug: 'blackandstacked', + url: 'https://www.scorepass.com/blackandstacked', + network: 'score', + }, + { + name: 'Boned At Home', + slug: 'bonedathome', + url: 'https://www.scorepass.com/bonedathome', + network: 'score', + }, + { + name: 'Bootylicious Mag', + slug: 'bootyliciousmag', + url: 'https://www.bootyliciousmag.com', + network: 'score', + }, + { + name: 'Busty Angelique', + slug: 'bustyangelique', + url: 'https://www.bigboobbundle.com/bustyangelique', + network: 'score', + }, + { + name: 'Busty Arianna', + slug: 'bustyarianna', + url: 'https://www.bigboobbundle.com/bustyarianna', + network: 'score', + }, + { + name: 'Busty Danni Ashe', + slug: 'bustydanniashe', + url: 'https://www.bigboobbundle.com/bustydanniashe', + network: 'score', + }, + { + name: 'Busty Dusty Stash', + slug: 'bustydustystash', + url: 'https://www.bigboobbundle.com/bustydustystash', + network: 'score', + }, + { + name: 'Busty Ines Cudna', + slug: 'bustyinescudna', + url: 'https://www.bigboobbundle.com/bustyinescudna', + network: 'score', + }, + { + name: 'Busty Kelly Kay', + slug: 'bustykellykay', + url: 'https://www.bigboobbundle.com/bustykellykay', + network: 'score', + }, + { + name: 'Busty Kerry Marie', + slug: 'bustykerrymarie', + url: 'https://www.bigboobbundle.com/bustykerrymarie', + network: 'score', + }, + { + name: 'Busty Lorna Morgan', + slug: 'bustylornamorgan', + url: 'https://www.bigboobbundle.com/bustylornamorgan', + network: 'score', + }, + { + name: 'Busty Merilyn', + slug: 'bustymerilyn', + url: 'https://www.scorepass.com/bustymerilyn', + network: 'score', + }, + { + name: 'Busty Old Sluts', + slug: 'bustyoldsluts', + url: 'https://www.milfbundle.com/bustyoldsluts', + network: 'score', + }, + { + name: 'Busty Sammie Black', + slug: 'bustysammieblack', + url: 'https://www.bigboobbundle.com/bustysammieblack', + network: 'score', + }, + { + name: 'Cherry Brady', + slug: 'cherrybrady', + url: 'https://www.bigboobbundle.com/cherrybrady', + network: 'score', + }, + { + name: 'Chloes World', + slug: 'chloesworld', + url: 'https://www.scorepass.com/chloesworld', + network: 'score', + }, + { + name: 'Christy Marks', + slug: 'christymarks', + url: 'https://www.scorepass.com/christymarks', + network: 'score', + }, + { + name: 'Creampie for Granny', + slug: 'creampieforgranny', + url: 'https://www.milfbundle.com/creampieforgranny', + network: 'score', + }, + { + name: 'Crystal Gunns World', + slug: 'crystalgunnsworld', + url: 'https://www.bigboobbundle.com/crystalgunnsworld', + network: 'score', + }, + { + name: 'Daylene Rio', + slug: 'daylenerio', + url: 'https://www.bigboobbundle.com/daylenerio', + network: 'score', + }, + { + name: 'Desiraes World', + slug: 'desiraesworld', + url: 'https://www.bigboobbundle.com/desiraesworld', + network: 'score', + }, + { + name: 'Diane Poppos', + slug: 'dianepoppos', + url: 'https://www.bigboobbundle.com/dianepoppos', + network: 'score', + }, + { + name: 'Eva Notty Videos', + slug: 'evanottyvideos', + url: 'https://www.bigboobbundle.com/evanottyvideos', + network: 'score', + }, + { + name: 'Feed Her Fuck Her', + slug: 'feedherfuckher', + url: 'https://www.scorepass.com/feedherfuckher', + network: 'score', + }, + { + name: 'Flat And Fucked MILFs', + slug: 'flatandfuckedmilfs', + url: 'https://www.milfbundle.com/flatandfuckedmilfs', + network: 'score', + }, + { + name: 'Granny Gets A Facial', + slug: 'grannygetsafacial', + url: 'https://www.milfbundle.com/grannygetsafacial', + network: 'score', + }, + { + name: 'Granny Loves BBC', + slug: 'grannylovesbbc', + url: 'https://www.milfbundle.com/grannylovesbbc', + network: 'score', + }, + { + name: 'Granny Loves Young Cock', + slug: 'grannylovesyoungcock', + url: 'https://www.milfbundle.com/grannylovesyoungcock', + network: 'score', + }, + { + name: 'Home Alone MILFs', + slug: 'homealonemilfs', + url: 'https://www.milfbundle.com/homealonemilfs', + network: 'score', + }, + { + name: 'I Boned Your Mom', + slug: 'ibonedyourmom', + url: 'https://www.milfbundle.com/ibonedyourmom', + network: 'score', + }, + { + name: 'I Fucked the Boss', + slug: 'ifuckedtheboss', + url: 'https://www.milfbundle.com/ifuckedtheboss', + network: 'score', + }, + { + name: 'Jessica Turner', + slug: 'jessicaturner', + url: 'https://www.bigboobbundle.com/jessicaturner', + network: 'score', + }, + { + name: 'Joana Bliss', + slug: 'joanabliss', + url: 'https://www.bigboobbundle.com/joanabliss', + network: 'score', + }, + { + name: 'Julia Miles', + slug: 'juliamiles', + url: 'https://www.bigboobbundle.com/juliamiles', + network: 'score', + }, + { + name: 'Karina Hart', + slug: 'karinahart', + url: 'https://www.scorepass.com/karinahart', + network: 'score', + }, + { + name: 'Karla James', + slug: 'karlajames', + url: 'https://www.bigboobbundle.com/karlajames', + network: 'score', + }, + { + name: 'Leanne Crow Videos', + slug: 'leannecrowvideos', + url: 'https://www.bigboobbundle.com/leannecrowvideos', + network: 'score', + }, + { + name: 'Leg Sex', + slug: 'legsex', + url: 'https://www.legsex.com', + network: 'score', + }, + { + name: 'Linseys World', + slug: 'linseysworld', + url: 'https://www.scorepass.com/linseysworld', + network: 'score', + }, + { + name: 'Mega Tits Minka', + slug: 'megatitsminka', + url: 'https://www.bigboobbundle.com/megatitsminka', + network: 'score', + }, + { + name: 'Micky Bells', + slug: 'mickybells', + url: 'https://www.bigboobbundle.com/mickybells', + network: 'score', + }, + { + name: 'MILF Bundle', + slug: 'milfbundle', + url: 'https://www.milfbundle.com', + network: 'score', + show: false, + }, + { + name: 'Teaming Cock', + slug: 'milfthreesomes', + url: 'https://www.milfbundle.com/milfthreesomes', + network: 'score', + }, + { + name: 'MILF Tugs', + slug: 'milftugs', + url: 'https://www.milfbundle.com/milftugs', + network: 'score', + }, + { + name: 'Natalie Fiore', + slug: 'nataliefiore', + url: 'https://www.bigboobbundle.com/nataliefiore', + network: 'score', + }, + { + name: 'Naughty Footjobs', + slug: 'naughtyfootjobs', + url: 'https://www.scorepass.com/naughtyfootjobs', + network: 'score', + }, + { + name: 'Naughty Mag', + slug: 'naughtymag', + url: 'https://www.naughtymag.com', + network: 'score', + }, + { + name: 'Naughty Tugs', + slug: 'naughtytugs', + url: 'https://www.scorepass.com/naughtytugs', + network: 'score', + }, + { + name: 'Nicole Peters', + slug: 'nicolepeters', + url: 'https://www.bigboobbundle.com/nicolepeters', + network: 'score', + }, + { + name: 'Old Horny MILFs', + slug: 'oldhornymilfs', + url: 'https://www.milfbundle.com/oldhornymilfs', + network: 'score', + }, + { + name: 'Picking Up Pussy', + slug: 'pickinguppussy', + url: 'https://www.scorepass.com/pickinguppussy', + network: 'score', + }, + { + name: 'Porn Loser', + slug: 'pornloser', + url: 'https://www.scorepass.com/pornloser', + network: 'score', + }, + { + name: 'Porn Mega Load', + slug: 'pornmegaload', + url: 'https://www.pornmegaload.com', + network: 'score', + show: false, + }, + { + name: 'SaRennas World', + slug: 'sarennasworld', + url: 'https://www.bigboobbundle.com/sarennasworld', + network: 'score', + }, + { + name: 'Scoreland', + slug: 'scoreland', + url: 'https://www.scoreland.com', + network: 'score', + parameters: { path: '/big-boob-videos' }, + priority: 3, + }, + { + name: 'Scoreland2', + slug: 'scoreland2', + url: 'https://www.scoreland2.com', + network: 'score', + parameters: { path: '/big-boob-scenes' }, + priority: 1, + }, + { + name: 'Score Classics', + slug: 'scoreclassics', + url: 'https://www.scoreclassics.com', + network: 'score', + parameters: { path: '/classic-boob-videos' }, + priority: 1, + }, + { + name: 'Scoreland TV', + slug: 'scorelandtv', + url: 'https://www.scorepass.com/scorelandtv', + network: 'score', + priority: 1, + show: false, // appears to be streaming service for other sites + }, + { + name: 'ScoreTV', + slug: 'scoretv', + url: 'https://www.scoretv.tv', + network: 'score', + priority: 1, + show: false, // similar to or same as Scoreland TV + }, + { + name: 'Score Videos', + slug: 'scorevideos', + url: 'https://www.scorevideos.com', + network: 'score', + parameters: { path: '/porn-videos' }, + priority: 2, + }, + { + name: 'Sha Rizel Videos', + slug: 'sharizelvideos', + url: 'https://www.bigboobbundle.com/sharizelvideos', + network: 'score', + }, + { + name: 'Silver Sluts', + slug: 'silversluts', + url: 'https://www.milfbundle.com/silversluts', + network: 'score', + }, + { + name: 'Stacy Vandenberg Boobs', + slug: 'stacyvandenbergboobs', + url: 'https://www.bigboobbundle.com/stacyvandenbergboobs', + network: 'score', + }, + { + name: 'Susie Wildin', + slug: 'susiewildin', + url: 'https://www.bigboobbundle.com/susiewildin', + network: 'score', + }, + { + name: 'Tawny Peaks', + slug: 'tawnypeaks', + url: 'https://www.bigboobbundle.com/tawny-peaks', + network: 'score', + }, + { + name: 'Tiffany Towers', + slug: 'tiffanytowers', + url: 'https://www.bigboobbundle.com/tiffany-towers', + network: 'score', + }, + { + name: 'Tits And Tugs', + slug: 'titsandtugs', + url: 'https://www.scorepass.com/titsandtugs', + network: 'score', + }, + { + name: 'TNA Tryouts', + slug: 'tnatryouts', + url: 'https://www.scorepass.com/tnatryouts', + network: 'score', + }, + { + name: 'Valory Irene', + slug: 'valoryirene', + url: 'https://www.bigboobbundle.com/valoryirene', + network: 'score', + }, + { + name: 'XL Girls', + slug: 'xlgirls', + url: 'https://www.xlgirls.com', + network: 'score', + }, + { + name: 'Your Mom Loves Anal', + slug: 'yourmomlovesanal', + url: 'https://www.milfbundle.com/yourmomlovesanal', + network: 'score', + }, + { + name: 'Your Mom\'s Got Big Tits', + slug: 'yourmomsgotbigtits', + url: 'https://www.milfbundle.com/yourmomsgotbigtits', + network: 'score', + }, + { + name: 'Your Wife My Meat', + slug: 'yourwifemymeat', + url: 'https://www.milfbundle.com/yourwifemymeat', + network: 'score', + }, + // SEXY HUB + { + slug: 'danejones', + name: 'Dane Jones', + alias: ['dnj'], + url: 'https://www.danejones.com/', + parameters: { siteId: 290 }, + network: 'sexyhub', + }, + { + slug: 'lesbea', + name: 'Lesbea', + alias: ['lsb'], + url: 'https://www.lesbea.com', + parameters: { siteId: 291 }, + tags: ['lesbian'], + network: 'sexyhub', + }, + { + slug: 'massagerooms', + name: 'Massage Rooms', + alias: ['mrs'], + url: 'https://www.sexyhub.com/scenes?site=292', + tags: ['massage'], + network: 'sexyhub', + }, + { + slug: 'momxxx', + name: 'Mom XXX', + alias: ['mom'], + url: 'https://www.sexyhub.com/scenes?site=293', + tags: ['milf'], + network: 'sexyhub', + }, + { + slug: 'fitnessrooms', + name: 'Fitness Rooms', + alias: ['frs'], + url: 'https://www.sexyhub.com/scenes?site=294', + network: 'sexyhub', + }, + { + slug: 'girlfriends', + name: 'Girlfriends', + url: 'https://www.sexyhub.com/scenes?site=289', + tags: ['lesbian'], + network: 'sexyhub', + }, + // TEAM SKEET + { + slug: 'exxxtrasmall', + name: 'Exxxtra Small', + alias: ['ext'], + description: '', + url: 'https://www.exxxtrasmall.com', + parameters: { id: 'exs' }, + network: 'teamskeet', + }, + { + slug: 'teenpies', + name: 'Teen Pies', + description: '', + url: 'https://www.teenpies.com', + parameters: { id: 'tp' }, + network: 'teamskeet', + }, + { + slug: 'innocenthigh', + name: 'Innocent High', + alias: ['inh'], + description: '', + url: 'https://www.innocenthigh.com', + parameters: { id: 'ih' }, + network: 'teamskeet', + }, + { + slug: 'teencurves', + name: 'Teen Curves', + description: '', + url: 'https://www.teencurves.com', + parameters: { id: 'tc' }, + network: 'teamskeet', + }, + { + slug: 'cfnmteens', + name: 'CFNM Teens', + alias: ['cfnmt'], + url: 'https://www.cfnmteens.com', + parameters: { id: 'cfnm' }, + network: 'teamskeet', + }, + { + slug: 'teensloveanal', + name: 'Teens Love Anal', + alias: ['tla'], + url: 'https://www.teensloveanal.com', + tags: ['anal'], + parameters: { id: 'tla' }, + network: 'teamskeet', + }, + { + slug: 'mybabysittersclub', + name: 'My Babysitters Club', + description: '', + url: 'https://www.mybabysittersclub.com', + parameters: { id: 'bsc' }, + network: 'teamskeet', + }, + { + slug: 'shesnew', + name: 'She\'s New', + alias: ['ssn'], + url: 'https://www.shesnew.com', + parameters: { id: 'bsc' }, + network: 'teamskeet', + }, + { + slug: 'teensdoporn', + name: 'Teens Do Porn', + alias: ['tdp'], + url: 'https://www.teensdoporn.com', + parameters: { id: 'tdp' }, + network: 'teamskeet', + }, + { + slug: 'povlife', + name: 'POV Life', + description: '', + url: 'https://www.povlife.com', + parameters: { id: 'pov' }, + network: 'teamskeet', + }, + { + slug: 'therealworkout', + name: 'The Real Workout', + description: '', + url: 'https://www.therealworkout.com', + parameters: { id: 'trw' }, + network: 'teamskeet', + }, + { + slug: 'thisgirlsucks', + name: 'This Girl Sucks', + alias: ['tgs'], + description: '', + url: 'https://www.thisgirlsucks.com', + parameters: { id: 'tgs' }, + network: 'teamskeet', + }, + { + slug: 'teenslovemoney', + name: 'Teens Love Money', + alias: ['tlm'], + description: '', + url: 'https://www.teenslovemoney.com', + parameters: { id: 'tlm' }, + network: 'teamskeet', + }, + { + slug: 'oyeloca', + name: 'Oye Loca', + description: '', + url: 'https://www.oyeloca.com', + parameters: { id: 'ol' }, + network: 'teamskeet', + }, + { + slug: 'tittyattack', + name: 'Titty Attack', + description: '', + url: 'https://www.tittyattack.com', + parameters: { id: 'ta' }, + network: 'teamskeet', + }, + { + slug: 'teenyblack', + name: 'Teeny Black', + description: '', + url: 'https://www.teenyblack.com', + parameters: { id: 'tb' }, + network: 'teamskeet', + }, + { + slug: 'lusthd', + name: 'Lust HD', + description: '', + url: 'https://www.lusthd.com', + parameters: { id: 'lhd' }, + network: 'teamskeet', + }, + { + slug: 'rubateen', + name: 'Rub A Teen', + description: '', + url: 'https://www.rubateen.com', + parameters: { id: 'rat' }, + network: 'teamskeet', + }, + { + slug: 'herfreshmanyear', + name: 'Her Freshman Year', + description: '', + url: 'https://www.exxxtrasmall.com', + parameters: { id: 'hfy' }, + network: 'teamskeet', + }, + { + slug: 'selfdesire', + name: 'Self Desire', + description: '', + url: 'https://www.selfdesire.com', + parameters: { id: 'sd' }, + network: 'teamskeet', + }, + { + slug: 'solointerviews', + name: 'Solo Interviews', + description: '', + url: 'https://www.solointerviews.com', + parameters: { id: 'si' }, + network: 'teamskeet', + }, + { + slug: 'teamskeetextras', + name: 'Team Skeet Extras', + description: '', + url: 'https://www.teamskeetextras.com', + parameters: { id: 'tse' }, + network: 'teamskeet', + }, + { + slug: 'dyked', + name: 'Dyked', + description: '', + url: 'https://www.dyked.com', + parameters: { id: 'dyk' }, + network: 'teamskeet', + }, + { + slug: 'badmilfs', + name: 'Bad MILFs', + description: '', + url: 'https://www.badmilfs.com', + parameters: { id: 'bad' }, + network: 'teamskeet', + }, + { + slug: 'gingerpatch', + name: 'Ginger Patch', + description: '', + url: 'https://www.gingerpatch.com', + parameters: { id: 'gp' }, + network: 'teamskeet', + }, + { + slug: 'bracefaced', + name: 'Brace Faced', + description: '', + url: 'https://www.bracefaced.com', + parameters: { id: 'bfd' }, + network: 'teamskeet', + }, + { + slug: 'teenjoi', + name: 'Teen JOI', + description: '', + url: 'https://www.teenjoi.com', + parameters: { id: 'joi' }, + network: 'teamskeet', + }, + { + slug: 'stepsiblings', + name: 'Step Siblings', + alias: ['steps'], + url: 'https://www.stepsiblings.com', + parameters: { id: 'sss' }, + network: 'teamskeet', + }, + { + slug: 'submissived', + name: 'Submissived', + description: '', + url: 'https://www.submissived.com', + tags: ['bdsm'], + parameters: { scraper: 'A' }, + network: 'teamskeet', + }, + { + slug: 'familystrokes', + name: 'Family Strokes', + alias: ['fams'], + url: 'https://www.familystrokes.com', + parameters: { scraper: 'A' }, + tags: ['family'], + network: 'teamskeet', + }, + // TWISTYS + { + name: 'Twistys', + slug: 'twistys', + url: 'https://www.twistys.com/scenes?site=219', + description: 'Twistys.com is the #1 ranked babe site on the web! With over 10 years of photos and videos, updated daily with 3 HQ photo sets and 2 HD videos, Twistys also awards one hot babe a Treat of the Month title, complete with exclusive photo & video sets! Weekly updates!', + network: 'twistys', + priority: 1, + }, + { + name: 'When Girls Play', + slug: 'whengirlsplay', + alias: ['wgp'], + url: 'https://www.whengirlsplay.com', + description: 'Watch hot girls seducing other girls in steamy lesbian play. These sluts finger, use dildos, strap-ons and squirt their pink pussies in lesbian porn by WhenGirlsPlay.com. Get Access to the Hottest Lesbian videos on the web!', + parameters: { siteId: 227 }, + network: 'twistys', + priority: 1, + }, + { + name: 'Turning Twistys', + slug: 'turningtwistys', + url: 'https://www.twistys.com/scenes?site=302', + description: 'Where straight curious cuties explore their sexuality, and get seduced by sexy butch girls. These sneaky lesbians lure the innocent looking beauties away from their boyfriends and into their beds... Be careful or they might just steal your girl!', + network: 'twistys', + priority: 1, + }, + { + name: 'Mom Knows Best', + slug: 'momknowsbest', + url: 'https://www.momknowsbest.com', + description: 'The world’s tightest teens and most elegant MILFs get together at Mom Knows Best, where horny teen girls learn the ins and outs of lesbian sex from confident and beautiful older women. These MILFs know what they like and exactly how to get it, and lucky for them, these tasty teens are eager to learn, and always very eager to please!', + parameters: { siteId: 234 }, + network: 'twistys', + priority: 1, + }, + { + name: 'Twistys Hard', + slug: 'twistyshard', + alias: ['th'], + url: 'https://www.twistyshard.com', + description: 'Watch horny nymphos get stuffed with stiff, bulging cocks. Hot sluts eager to spread their legs, bend over on all fours, or mount a big rock-hard erection. They want their needs fulfilled, and love to show off how they do it. Get into Twistys Hard and see just how hard things can get!', + parameters: { siteId: 225 }, + network: 'twistys', + priority: 1, + }, + { + name: 'Feature Films', + slug: 'featurefilms', + url: 'https://www.twistys.com/scenes?site=233', + description: 'Prepare yourself for a night at the movies you\'ll never forget with Feature Films from Twistys. High-end cinematic productions featuring in-depth storylines and industry-leading visuals, erotic adventures with the most beautiful women in the world are now at your fingertips. Lesbian, Hardcore Bonus Updates!', + network: 'twistys', + }, + { + name: 'Nicole Graves', + slug: 'nicolegraves', + url: 'https://www.twistys.com/scenes?site=223', + description: 'NicoleGraves.com is the only official web site of Nicole Graves with 100% EXCLUSIVE content! Here you\'ll find Nicole Graves videos and photos of her shaved pussy and Nicole Graves fucking and giving a blowjob!', + network: 'twistys', + }, + { + name: 'Anette Dawn', + slug: 'anettedawn', + url: 'https://www.twistys.com/scenes?site=221', + description: 'Hey guys! Its me Anette Dawn, if you\'ve seen me on other sites, like Twistys I have been doing internet modeling for a while now. This is however my first and only official site. I recieved so many requests for more so I finally got this site together! I can\'t wait for you to join me inside!', + network: 'twistys', + }, + { + name: 'Twistys Teasers', + slug: 'twistysteasers', + url: 'https://www.twistys.com/scenes?site=232', + description: 'Twistys Teasers is a doorway to new exciting content, opened exclusively to you, our loyal members. See it here first while it’s fresh and hot, and be sure to let us know exactly how much you like it before you leave. Be tempted. Be tantalized. Be teased. Solo, Lesbian, Hardcore Bonus Updates!', + network: 'twistys', + }, + { + name: 'Euro Foxes', + slug: 'eurofoxes', + url: 'https://www.twistys.com/scenes?site=226', + description: 'EuroFoxes.com: the worlds Number One European Babe site! EuroFoxes is dedicated to bringing you the very best european babes!', + network: 'twistys', + }, + { + name: 'Blue Fantasies', + slug: 'bluefantasies', + url: 'https://www.twistys.com/scenes?site=220', + description: 'BlueFantasies.com prides itself on getting the most beautiful women in the world to show off their hot bodies, 100% exclusively for you.', + network: 'twistys', + }, + { + name: 'Busty Ones', + slug: 'bustyones', + url: 'https://www.twistys.com/scenes?site=229', + description: 'BustyOnes.com bringing you the most beautiful big breasts in the world! The hottest women alive showcasing their fantastic tits.', + network: 'twistys', + }, + // VIVID + { + slug: 'vividceleb', + name: 'Vivid Celeb', + url: 'https://www.vividceleb.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2018-03-25'), + }, + }, + { + slug: 'thebrats', + name: 'The Brats', + url: 'https://www.thebrats.com', + network: 'vivid', + }, + { + slug: 'wheretheboysarent', + name: 'Where The Boys Aren\'t', + url: 'https://www.wheretheboysarent.com', + network: 'vivid', + }, + { + slug: 'nineteen', + name: 'Nineteen', + url: 'http://www.nineteen.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-01-23'), + }, + }, + { + slug: 'nastystepfamily', + name: 'Nasty Step Family', + url: 'http://www.nastystepfamily.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-01-29'), + }, + }, + { + slug: 'girlswhofuckgirls', + name: 'Girls Who Fuck Girls', + url: 'http://www.girlswhofuckgirls.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-05-21'), + }, + }, + { + slug: 'petited', + name: 'Petited', + url: 'http://www.petited.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-01-28'), + }, + }, + { + slug: 'orgytrain', + name: 'Orgy Train', + url: 'http://www.orgytrain.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-01-09'), + }, + }, + { + slug: 'momisamilf', + name: 'Mom Is A MILF', + url: 'http://www.momisamilf.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-01-25'), + }, + }, + { + slug: 'blackwhitefuckfest', + name: 'Black White Fuck Fest', + url: 'http://www.blackwhitefuckfest.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-01-30'), + }, + }, + { + slug: '65inchhugeasses', + name: '65 Inch Huge Asses', + url: 'http://www.65inchhugeasses.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2019-05-18'), + }, + }, + { + slug: 'brandnewfaces', + name: 'Brand New Faces', + url: 'http://www.brandnewfaces.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2018-02-28'), + }, + }, + { + slug: 'vividclassic', + name: 'Vivid Classic', + url: 'http://www.vividclassic.com', + network: 'vivid', + parameters: { + referer: 'https://www.thebrats.com', + deep: 'https://www.thebrats.com/en/video', + scene: false, + lastNative: new Date('2016-06-29'), + }, + }, + // VIXEN + { + slug: 'vixen', + name: 'Vixen', + description: 'Vixen.com features the world’s finest cinematic adult films with 4K quality and high-end erotic photography.', + url: 'https://www.vixen.com', + network: 'vixen', + }, + { + slug: 'blacked', + name: 'Blacked', + description: 'Porn videos of beautiful girls in first time interracial porn videos. BLACKED has the hottest pornstars in HD sex videos.', + url: 'https://www.blacked.com', + tags: ['interracial', 'bbc'], + network: 'vixen', + }, + { + slug: 'tushy', + name: 'Tushy', + description: 'Watch the world\'s best HD Anal videos! Featuring beautiful, never before seen girls in first time anal. Exclusively on Tushy.com', + url: 'https://www.tushy.com', + tags: ['anal'], + network: 'vixen', + }, + { + slug: 'blackedraw', + name: 'Blacked Raw', + description: 'Experience real women in interracial sex videos. Passionate sex with beautiful pornstars. No photoshop just the highest quality porn. Everything you see is real.', + url: 'https://www.blackedraw.com', + tags: ['interracial', 'bbc'], + network: 'vixen', + }, + { + slug: 'tushyraw', + name: 'Tushy Raw', + description: 'Anal sex videos with beautiful models and pornstars being fucked in the ass. TUSHY RAW features famous pornstars in high quality anal porn videos.', + url: 'https://www.tushyraw.com', + tags: ['anal'], + network: 'vixen', + }, + { + slug: 'deeper', + name: 'Deeper', + description: 'Porn videos from DEEPER.com featuring Passionate sex, light kink and BDSM with plenty of erotic sex videos featuring beautiful models.', + url: 'https://www.deeper.com', + network: 'vixen', + }, + // VOGOV + { + slug: 'vogov', + name: 'VogoV', + url: 'https://www.vogov.com', + description: 'Top rated models. Graceful locations. Best gonzo scenes. 4K UHD 60 FPS. So, in general Vogov is a website that is worth visiting and exploring carefully. It gives a chance to spend a fantastic night with gorgeous girls ready to experiment and to full around with their lovers.', + network: 'vogov', + }, + // WHALE MEMBER + { + name: 'Cum 4K', + slug: 'cum4k', + url: 'https://cum4k.com', + tags: ['fake-cum', 'creampie', '4k'], + network: 'whalemember', + }, + { + name: 'Tiny 4K', + slug: 'tiny4k', + url: 'https://tiny4k.com', + tags: ['4k'], + network: 'whalemember', + }, + { + name: 'POVD', + slug: 'povd', + url: 'https://povd.com', + tags: ['pov'], + network: 'whalemember', + }, + { + name: 'Lubed', + slug: 'lubed', + url: 'https://lubed.com', + tags: ['oil'], + network: 'whalemember', + }, + { + name: 'Casting Couch X', + slug: 'castingcouchx', + alias: ['castingcouch x', 'castingcouch-x', 'casting couch-x'], + url: 'https://castingcouch-x.com', + network: 'whalemember', + }, + { + name: 'Passion HD', + slug: 'passionhd', + alias: ['phd', 'passion-hd'], + url: 'https://passion-hd.com', + network: 'whalemember', + }, + { + name: 'Nanny Spy', + slug: 'nannyspy', + url: 'https://nannyspy.com', + network: 'whalemember', + }, + { + name: 'Girl Cum', + slug: 'girlcum', + url: 'https://girlcum.com', + network: 'whalemember', + }, + { + name: 'Pure Mature', + slug: 'puremature', + url: 'https://puremature.com', + tags: ['milf'], + network: 'whalemember', + }, + { + name: 'Fantasy HD', + slug: 'fantasyhd', + alias: ['fhd'], + url: 'https://fantasyhd.com', + network: 'whalemember', + }, + { + name: 'Spy Fam', + slug: 'spyfam', + url: 'https://spyfam.com', + tags: ['family'], + network: 'whalemember', + }, + { + name: 'Holed', + slug: 'holed', + url: 'https://holed.com', + tags: ['anal'], + network: 'whalemember', + }, + { + name: 'BBC Pie', + slug: 'bbcpie', + url: 'https://bbcpie.com', + tags: ['bbc', 'interracial'], + network: 'whalemember', + }, + { + name: 'Wet VR', + slug: 'wetvr', + url: 'https://wetvr.com', + tags: ['virtual-reality'], + network: 'whalemember', + }, + { + name: 'Exotic 4K', + slug: 'exotic4k', + url: 'https://exotic4k.com', + tags: ['4k'], + network: 'whalemember', + }, + { + name: 'My Very First Time', + slug: 'myveryfirsttime', + alias: ['mvft'], + url: 'https://myveryfirsttime.com', + network: 'whalemember', + }, + { + name: 'Baeb', + slug: 'baeb', + alias: ['baebz'], + url: 'https://baeb.com', + network: 'whalemember', + }, + // WICKED + { + slug: 'wicked', + name: 'Wicked', + alias: ['wkp'], + url: 'https://www.wicked.com', + description: 'Welcome to the new Wicked.com! Watch over 25 years of Wicked Pictures\' brand of award-winning porn for couples and women in 4k HD movies & xxx videos', + parameters: { independent: true }, + network: 'wicked', + }, + // XEMPIRE + { + slug: 'hardx', + name: 'HardX', + description: "Welcome to HardX.com, home of exclusive hardcore gonzo porn and first time anal scenes, DP, blowbangs and gangbangs from today's hottest porn stars!", + url: 'https://www.hardx.com', + network: 'xempire', + }, + { + slug: 'eroticax', + name: 'EroticaX', + description: 'EroticaX.com features intimate scenes of passionate, erotic sex. Watch the sensual side of hardcore porn as your favorite pornstars have real, intense orgasms.', + url: 'https://www.eroticax.com', + network: 'xempire', + }, + { + slug: 'darkx', + name: 'DarkX', + description: 'Watch interracial BBC porn videos on DarkX.com, featuring the best pornstars taking big black cock in exclusive scenes. The best black on white porn inside!', + url: 'https://www.darkx.com', + tags: ['interracial'], + network: 'xempire', + }, + { + slug: 'allblackx', + name: 'AllBlackX', + description: 'AllBlackX.com features the hottest ebony pornstar beauties in hardcore black on black gonzo porn. From director Mason, watch 4k ultra HD videos inside', + url: 'https://www.allblackx.com', + network: 'xempire', + tags: ['ebony', 'bbc'], + }, + { + slug: 'lesbianx', + name: 'LesbianX', + description: "LesbianX.com features today's top pornstars in hardcore lesbian porn. Watch passionate & intense girl on girl sex videos, from erotic kissing to pussy licking.", + url: 'https://www.lesbianx.com', + tags: ['lesbian'], + network: 'xempire', + }, ]; /* eslint-disable max-len */ exports.seed = knex => Promise.resolve() - .then(async () => { - const networks = await knex('networks').select('*'); - const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); + .then(async () => { + const networks = await knex('networks').select('*'); + const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); - const tags = await knex('tags').select('*').where('alias_for', null); - const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); + const tags = await knex('tags').select('*').where('alias_for', null); + const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); - const sitesWithNetworks = sites.map(site => ({ - slug: site.slug, - name: site.name, - alias: (site.alias || []).join(','), - description: site.description, - url: site.url, - parameters: site.parameters, - network_id: networksMap[site.network], - priority: site.priority, - show: site.show, - })); + const sitesWithNetworks = sites.map(site => ({ + slug: site.slug, + name: site.name, + alias: (site.alias || []).join(','), + description: site.description, + url: site.url, + parameters: site.parameters, + network_id: networksMap[site.network], + priority: site.priority, + show: site.show, + })); - const { inserted, updated } = await upsert('sites', sitesWithNetworks, 'slug', knex); - const sitesMap = [].concat(inserted, updated).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); + const { inserted, updated } = await upsert('sites', sitesWithNetworks, 'slug', knex); + const sitesMap = [].concat(inserted, updated).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); - const tagAssociations = sites.map(site => (site.tags - ? site.tags.map(tagSlug => ({ - site_id: sitesMap[site.slug], - tag_id: tagsMap[tagSlug], - inherit: true, - })) - : [] - )).flat(); + const tagAssociations = sites.map(site => (site.tags + ? site.tags.map(tagSlug => ({ + site_id: sitesMap[site.slug], + tag_id: tagsMap[tagSlug], + inherit: true, + })) + : [] + )).flat(); - return upsert('sites_tags', tagAssociations, ['site_id', 'tag_id'], knex); - }); + return upsert('sites_tags', tagAssociations, ['site_id', 'tag_id'], knex); + }); /* 'X-Art' => 'xart', diff --git a/seeds/04_media.js b/seeds/04_media.js index 852c0556d..e38931e4c 100644 --- a/seeds/04_media.js +++ b/seeds/04_media.js @@ -2,785 +2,786 @@ const nanoid = require('nanoid/non-secure'); const upsert = require('../src/utils/upsert'); const tagPosters = [ - ['airtight', 6, 'Remy Lacroix in "Ass Worship 14" for Jules Jordan'], - ['anal', 0, 'Adriana Chechik in "Manuel Creampies Their Asses 3" for Jules Jordan'], - ['anal-creampie', 0, 'Gina Valentina and Jane Wilde in "A Very Special Anniversary" for Tushy'], - ['ass-eating', 0, 'Kendra Sunderland and Ana Foxxx in "Kendra\'s Obsession, Part 3" for Blacked'], - ['asian', 0, 'Alina Li in "Slut Puppies 8" for Jules Jordan'], - ['ass-to-mouth', 'poster', 'Alysa Gap and Logan in "Anal Buffet 4" for Evil Angel'], - ['bdsm', 0, 'Dani Daniels in "The Traning of Dani Daniels, Day 2" for The Training of O at Kink'], - ['behind-the-scenes', 0, 'Janice Griffith in "Day With A Pornstar: Janice" for Brazzers'], - ['blonde', 0, 'Anikka Albrite and Lena Nicole or Cherie DeVille in the BTS of "New Zealand Holiday" for In The Crack'], - ['blowbang', 'poster', 'Marsha May in "Feeding Frenzy 12" for Jules Jordan'], - ['blowjob', 0, 'Adriana Chechik in "The Dinner Party" for Real Wife Stories (Brazzers)'], - ['brunette', 0, 'Nicole Black in GIO971 for LegalPorno'], - ['bukkake', 'poster', 'Mia Malkova in "Facialized 2" for HardX'], - ['caucasian', 0, 'Remy Lacroix for HardX'], - ['creampie', 'poster', 'ALina Lopez in "Making Yourself Unforgettable" for Blacked'], - ['cum-in-mouth', 1, 'Sarah Vandella in "Blow Bang Vandella" for HardX'], - ['da-tp', 0, 'Natasha Teen in LegalPorno SZ2164'], - ['deepthroat', 0, 'Chanel Grey in "Deepthroating Is Fun" for Throated'], - ['double-anal', 7, 'Adriana Chechik in "DP Masters 6" for Jules Jordan'], - ['double-blowjob', 1, 'Veronica Rodriguez and Penny Pax in "Fucking Older Guys 5" for Penthouse'], - ['double-dildo-blowjob', 0, 'Adriana Chechik and Vicki Chase in "Anal Savages 1" for Jules Jordan'], - ['double-penetration', 2, 'Megan Rain in "DP Masters 4" for Jules Jordan'], - ['double-vaginal', 'poster', 'Riley Reid in "Pizza That Ass" for Reid My Lips'], - ['dv-tp', 'poster', 'Juelz Ventura in "Gangbanged 5" for Elegant Angel'], - ['ebony', 1, 'Ana Foxxx in "DP Me 4" for HardX'], - ['facefucking', 2, 'Jynx Maze for Throated'], - ['facial', 0, 'Brooklyn Gray in "All About Ass 4" for Evil Angel'], - ['fake-boobs', 1, 'Lela Star in "Thick" for Jules Jordan'], - ['family', 0, 'Teanna Trump in "A Family Appear: Part One" for Brazzers'], - ['gangbang', 5, 'Carter Cruise\'s first gangbang in "Slut Puppies 9" for Jules Jordan'], - ['gaping', 1, 'Vina Sky in "Vina Sky Does Anal" for HardX'], - ['interracial', 0, 'Jaye Summers and Prince Yahshua in "Platinum Pussy 3" for Jules Jordan'], - ['latina', 'poster', 'Alexis Love for Penthouse'], - ['lesbian', 0, 'Jenna Sativa and Alina Lopez in "Opposites Attract" for Girl Girl'], - ['maid', 0, 'Whitney Wright in "Dredd Up Your Ass 2" for Jules Jordan'], - ['milf', 0, 'Olivia Austin in "Dredd 3" for Jules Jordan'], - ['mff', 0, 'Madison Ivy, Adriana Chechik and Keiran Lee in "Day With A Pornstar" for Brazzers'], - ['mfm', 5, 'Vina Sky in "Slut Puppies 15" for Jules Jordan'], - ['natural-boobs', 0, 'Autumn Falls in "Manuel Ferrara\'s Ripe 7" for Jules Jordan'], - ['nurse', 0, 'Sarah Vandella in "Cum For Nurse Sarah" for Brazzers'], - ['orgy', 1, 'Megan Rain (DP), Morgan Lee (anal), Jessa Rhodes, Melissa Moore and Kimmy Granger in "Orgy Masters 8" for Jules Jordan'], - ['pussy-eating', 0, 'Kali Roses licking Emily Willis\' pussy in "Peeping On My Neighbor" for Girl Girl'], - ['redhead', 0, 'Penny Pax in "The Submission of Emma Marx: Boundaries" for New Sensations'], - ['schoolgirl', 1, 'Eliza Ibarra for Brazzers'], - ['swallowing', 'poster'], - ['teen', 0, 'Eva Elfie in "Fresh New Talent" for Club Seventeen'], - ['tattoo', 'poster', 'Kali Roses in "Goes All In For Anal" for Hussie Pass'], - ['trainbang', 'poster', 'Kali Roses in "Passing Me Around" for Blacked'], - ['triple-anal', 'poster', 'Kristy Black in SZ1986 for LegalPorno'], + ['airtight', 6, 'Remy Lacroix in "Ass Worship 14" for Jules Jordan'], + ['anal', 0, 'Adriana Chechik in "Manuel Creampies Their Asses 3" for Jules Jordan'], + ['anal-creampie', 0, 'Gina Valentina and Jane Wilde in "A Very Special Anniversary" for Tushy'], + ['ass-eating', 0, 'Kendra Sunderland and Ana Foxxx in "Kendra\'s Obsession, Part 3" for Blacked'], + ['asian', 0, 'Alina Li in "Slut Puppies 8" for Jules Jordan'], + ['ass-to-mouth', 'poster', 'Alysa Gap and Logan in "Anal Buffet 4" for Evil Angel'], + ['bdsm', 0, 'Dani Daniels in "The Traning of Dani Daniels, Day 2" for The Training of O at Kink'], + ['behind-the-scenes', 0, 'Janice Griffith in "Day With A Pornstar: Janice" for Brazzers'], + ['blonde', 0, 'Anikka Albrite and Lena Nicole or Cherie DeVille in the BTS of "New Zealand Holiday" for In The Crack'], + ['blowbang', 'poster', 'Marsha May in "Feeding Frenzy 12" for Jules Jordan'], + ['blowjob', 0, 'Adriana Chechik in "The Dinner Party" for Real Wife Stories (Brazzers)'], + ['brunette', 0, 'Nicole Black in GIO971 for LegalPorno'], + ['bukkake', 0, 'Jaye Summers in "Facialized 5" for HardX'], + ['caucasian', 0, 'Remy Lacroix for HardX'], + ['creampie', 'poster', 'ALina Lopez in "Making Yourself Unforgettable" for Blacked'], + ['cum-in-mouth', 1, 'Sarah Vandella in "Blow Bang Vandella" for HardX'], + ['da-tp', 0, 'Natasha Teen in LegalPorno SZ2164'], + ['deepthroat', 0, 'Chanel Grey in "Deepthroating Is Fun" for Throated'], + ['double-anal', 7, 'Adriana Chechik in "DP Masters 6" for Jules Jordan'], + ['double-blowjob', 1, 'Veronica Rodriguez and Penny Pax in "Fucking Older Guys 5" for Penthouse'], + ['double-dildo-blowjob', 0, 'Adriana Chechik and Vicki Chase in "Anal Savages 1" for Jules Jordan'], + ['double-penetration', 2, 'Megan Rain in "DP Masters 4" for Jules Jordan'], + ['double-vaginal', 'poster', 'Riley Reid in "Pizza That Ass" for Reid My Lips'], + ['dv-tp', 'poster', 'Juelz Ventura in "Gangbanged 5" for Elegant Angel'], + ['ebony', 1, 'Ana Foxxx in "DP Me 4" for HardX'], + ['facefucking', 2, 'Jynx Maze for Throated'], + ['facial', 0, 'Brooklyn Gray in "All About Ass 4" for Evil Angel'], + ['fake-boobs', 1, 'Lela Star in "Thick" for Jules Jordan'], + ['family', 0, 'Teanna Trump in "A Family Appear: Part One" for Brazzers'], + ['gangbang', 5, 'Carter Cruise\'s first gangbang in "Slut Puppies 9" for Jules Jordan'], + ['gaping', 1, 'Vina Sky in "Vina Sky Does Anal" for HardX'], + ['interracial', 0, 'Jaye Summers and Prince Yahshua in "Platinum Pussy 3" for Jules Jordan'], + ['latina', 'poster', 'Alexis Love for Penthouse'], + ['lesbian', 0, 'Jenna Sativa and Alina Lopez in "Opposites Attract" for Girl Girl'], + ['maid', 0, 'Whitney Wright in "Dredd Up Your Ass 2" for Jules Jordan'], + ['milf', 0, 'Olivia Austin in "Dredd 3" for Jules Jordan'], + ['mff', 0, 'Madison Ivy, Adriana Chechik and Keiran Lee in "Day With A Pornstar" for Brazzers'], + ['mfm', 5, 'Vina Sky in "Slut Puppies 15" for Jules Jordan'], + ['natural-boobs', 0, 'Autumn Falls in "Manuel Ferrara\'s Ripe 7" for Jules Jordan'], + ['nurse', 0, 'Sarah Vandella in "Cum For Nurse Sarah" for Brazzers'], + ['orgy', 1, 'Megan Rain (DP), Morgan Lee (anal), Jessa Rhodes, Melissa Moore and Kimmy Granger in "Orgy Masters 8" for Jules Jordan'], + ['pussy-eating', 0, 'Kali Roses licking Emily Willis\' pussy in "Peeping On My Neighbor" for Girl Girl'], + ['redhead', 0, 'Penny Pax in "The Submission of Emma Marx: Boundaries" for New Sensations'], + ['schoolgirl', 1, 'Eliza Ibarra for Brazzers'], + ['swallowing', 'poster'], + ['teen', 0, 'Eva Elfie in "Fresh New Talent" for Club Seventeen'], + ['tattoo', 'poster', 'Kali Roses in "Goes All In For Anal" for Hussie Pass'], + ['trainbang', 'poster', 'Kali Roses in "Passing Me Around" for Blacked'], + ['triple-anal', 'poster', 'Kristy Black in SZ1986 for LegalPorno'], ] - .map(([slug, filename, comment], index) => ({ - id: nanoid(), - tagSlug: slug, - path: `tags/${slug}/${filename}.jpeg`, - thumbnail: `tags/${slug}/thumbs/${filename}.jpeg`, - lazy: `tags/${slug}/lazy/${filename}.jpeg`, - mime: 'image/jpeg', - index, - comment, - })); + .map(([slug, filename, comment], index) => ({ + id: nanoid(), + tagSlug: slug, + path: `tags/${slug}/${filename}.jpeg`, + thumbnail: `tags/${slug}/thumbs/${filename}.jpeg`, + lazy: `tags/${slug}/lazy/${filename}.jpeg`, + mime: 'image/jpeg', + index, + comment, + })); const tagPhotos = [ - ['airtight', 5, 'Chloe Amour in "DP Masters 4" for Jules Jordan'], - ['airtight', 1, 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan'], - ['airtight', 2, 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel'], - ['airtight', 3, 'Anita Bellini in "Triple Dick Gangbang" for Hands On Hardcore (DDF Network)'], - ['asian', 'poster', 'Vina Sky in "Slut Puppies 15" for Jules Jordan'], - // ['asian', 1, 'Alina Li in "Oil Overload 11" for Jules Jordan'], - // ['anal', 'poster', 'Jynx Maze in "Anal Buffet 6" for Evil Angel'], - ['anal', 4, 'Lana Roy in "Anal In The Club" for 21Naturals'], - ['anal', 3, 'Dakota Skye for Brazzers'], - // ['anal', 1, 'Veronica Leal and Tina Kay in "Agents On Anal Mission" for Asshole Fever'], - // ['anal', 0, 'Veronica Leal'], - ['behind-the-scenes', 1, 'Madison Ivy in "Day With A Pornstar" for Brazzers'], - ['caucasian', 1, 'Sheena Shaw for Brazzers'], - ['da-tp', 1, 'Francys Belle in SZ1702 for LegalPorno'], - ['da-tp', 2, 'Angel Smalls in GIO408 for LegalPorno'], - ['da-tp', 3, 'Evelina Darling in GIO294'], - ['da-tp', 4, 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno'], - ['double-anal', 2, 'Lana Rhoades in "Lana Rhoades Unleashed" for HardX'], - ['double-anal', 6, 'Sheena Shaw in "Ass Worship 14" for Jules Jordan'], - ['double-anal', 5, 'Riley Reid in "The Gangbang of Riley Reid" for Jules Jordan'], - ['double-anal', 'poster', 'Haley Reed in "Young Hot Ass" for Evil Angel'], - ['double-anal', 0, 'Nicole Black doing double anal during a gangbang in GIO971 for LegalPorno'], - ['double-anal', 1, 'Ria Sunn in SZ1801 for LegalPorno'], - ['double-blowjob', 0, 'Kira Noir and Kali Roses for Brazzers'], - ['double-dildo-blowjob', 1, 'Aidra Fox and Reena Sky in "Reena\'s Got A Staring Problem" for Brazzers'], - ['double-dildo-dp', 0, 'u/LacyCrow "Sometimes you have to do it yourself"'], - ['double-penetration', 'poster', 'Mia Malkova in "DP Me 8" for HardX'], - ['double-penetration', 0, 'Zoey Monroe in "Slut Puppies 7" for Jules Jordan'], - ['double-penetration', 1, 'Jynx Maze in "Don\'t Make Me Beg 4" for Evil Angel'], - ['double-vaginal', 0, 'Aaliyah Hadid in "Squirting From Double Penetration With Anal" for Bang Bros'], - ['dv-tp', 1, 'Adriana Chechik in "Adriana\'s Triple Anal Penetration!"'], - ['dv-tp', 0, 'Luna Rival in LegalPorno SZ1490'], - ['facial', 1, 'Ella Knox in "Mr Saltys Adult Emporium Adventure 2" for Aziani'], - ['facial', 'poster', 'Jynx Maze'], - ['facefucking', 1, 'Carrie for Young Throats'], - // ['fake-boobs', 0, 'Marsha May in "Once You Go Black 7" for Jules Jordan'], - ['gangbang', 'poster', 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan'], - ['gangbang', 0, '"4 On 1 Gangbangs" for Doghouse Digital'], - ['gangbang', 4, 'Marley Brinx in "The Gangbang of Marley Brinx" for Jules Jordan'], - ['gangbang', 1, 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.'], - ['gaping', 'poster', 'Zoey Monroe in "Manuel DPs Them All 5" for Jules Jordan'], - ['gaping', 2, 'Alex Grey in "DP Masters 5" for Jules Jordan'], - ['latina', 0, 'Abby Lee Brazil for Bang Bros'], - // ['mfm', 0, 'Vina Sky in "Jules Jordan\'s Three Ways" for Jules Jordan'], - ['mfm', 1, 'Jynx Maze in "Don\'t Make Me Beg 4" for Evil Angel'], - ['orgy', 'poster', 'Zoey Mornoe (DP), Jillian Janson (sex), Frida Sante, Katerina Kay and Natasha Starr in "Orgy Masters 6" for Jules Jordan'], - ['trainbang', 0, 'Nicole Black in GIO971 for LegalPorno'], - ['triple-anal', 1, 'Natasha Teen in SZ2098 for LegalPorno'], - ['triple-anal', 2, 'Kira Thorn in GIO1018 for LegalPorno'], - ['cum-in-mouth', 'poster', 'Khloe Kapri'], + ['airtight', 5, 'Chloe Amour in "DP Masters 4" for Jules Jordan'], + ['airtight', 1, 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan'], + ['airtight', 2, 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel'], + ['airtight', 3, 'Anita Bellini in "Triple Dick Gangbang" for Hands On Hardcore (DDF Network)'], + ['asian', 'poster', 'Vina Sky in "Slut Puppies 15" for Jules Jordan'], + // ['asian', 1, 'Alina Li in "Oil Overload 11" for Jules Jordan'], + // ['anal', 'poster', 'Jynx Maze in "Anal Buffet 6" for Evil Angel'], + ['anal', 4, 'Lana Roy in "Anal In The Club" for 21Naturals'], + ['anal', 3, 'Dakota Skye for Brazzers'], + // ['anal', 1, 'Veronica Leal and Tina Kay in "Agents On Anal Mission" for Asshole Fever'], + // ['anal', 0, 'Veronica Leal'], + ['behind-the-scenes', 1, 'Madison Ivy in "Day With A Pornstar" for Brazzers'], + ['bukkake', 'poster', 'Mia Malkova in "Facialized 2" for HardX'], + ['caucasian', 1, 'Sheena Shaw for Brazzers'], + ['da-tp', 1, 'Francys Belle in SZ1702 for LegalPorno'], + ['da-tp', 2, 'Angel Smalls in GIO408 for LegalPorno'], + ['da-tp', 3, 'Evelina Darling in GIO294'], + ['da-tp', 4, 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno'], + ['double-anal', 2, 'Lana Rhoades in "Lana Rhoades Unleashed" for HardX'], + ['double-anal', 6, 'Sheena Shaw in "Ass Worship 14" for Jules Jordan'], + ['double-anal', 5, 'Riley Reid in "The Gangbang of Riley Reid" for Jules Jordan'], + ['double-anal', 'poster', 'Haley Reed in "Young Hot Ass" for Evil Angel'], + ['double-anal', 0, 'Nicole Black doing double anal during a gangbang in GIO971 for LegalPorno'], + ['double-anal', 1, 'Ria Sunn in SZ1801 for LegalPorno'], + ['double-blowjob', 0, 'Kira Noir and Kali Roses for Brazzers'], + ['double-dildo-blowjob', 1, 'Aidra Fox and Reena Sky in "Reena\'s Got A Staring Problem" for Brazzers'], + ['double-dildo-dp', 0, 'u/LacyCrow "Sometimes you have to do it yourself"'], + ['double-penetration', 'poster', 'Mia Malkova in "DP Me 8" for HardX'], + ['double-penetration', 0, 'Zoey Monroe in "Slut Puppies 7" for Jules Jordan'], + ['double-penetration', 1, 'Jynx Maze in "Don\'t Make Me Beg 4" for Evil Angel'], + ['double-vaginal', 0, 'Aaliyah Hadid in "Squirting From Double Penetration With Anal" for Bang Bros'], + ['dv-tp', 1, 'Adriana Chechik in "Adriana\'s Triple Anal Penetration!"'], + ['dv-tp', 0, 'Luna Rival in LegalPorno SZ1490'], + ['facial', 1, 'Ella Knox in "Mr Saltys Adult Emporium Adventure 2" for Aziani'], + ['facial', 'poster', 'Jynx Maze'], + ['facefucking', 1, 'Carrie for Young Throats'], + // ['fake-boobs', 0, 'Marsha May in "Once You Go Black 7" for Jules Jordan'], + ['gangbang', 'poster', 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan'], + ['gangbang', 0, '"4 On 1 Gangbangs" for Doghouse Digital'], + ['gangbang', 4, 'Marley Brinx in "The Gangbang of Marley Brinx" for Jules Jordan'], + ['gangbang', 1, 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.'], + ['gaping', 'poster', 'Zoey Monroe in "Manuel DPs Them All 5" for Jules Jordan'], + ['gaping', 2, 'Alex Grey in "DP Masters 5" for Jules Jordan'], + ['latina', 0, 'Abby Lee Brazil for Bang Bros'], + // ['mfm', 0, 'Vina Sky in "Jules Jordan\'s Three Ways" for Jules Jordan'], + ['mfm', 1, 'Jynx Maze in "Don\'t Make Me Beg 4" for Evil Angel'], + ['orgy', 'poster', 'Zoey Mornoe (DP), Jillian Janson (sex), Frida Sante, Katerina Kay and Natasha Starr in "Orgy Masters 6" for Jules Jordan'], + ['trainbang', 0, 'Nicole Black in GIO971 for LegalPorno'], + ['triple-anal', 1, 'Natasha Teen in SZ2098 for LegalPorno'], + ['triple-anal', 2, 'Kira Thorn in GIO1018 for LegalPorno'], + ['cum-in-mouth', 'poster', 'Khloe Kapri'], ] - .map(([slug, fileIndex, comment], index) => ({ - id: nanoid(), - tagSlug: slug, - path: `tags/${slug}/${fileIndex}.jpeg`, - thumbnail: `tags/${slug}/thumbs/${fileIndex}.jpeg`, - lazy: `tags/${slug}/lazy/${fileIndex}.jpeg`, - mime: 'image/jpeg', - index, - comment, - })); + .map(([slug, fileIndex, comment], index) => ({ + id: nanoid(), + tagSlug: slug, + path: `tags/${slug}/${fileIndex}.jpeg`, + thumbnail: `tags/${slug}/thumbs/${fileIndex}.jpeg`, + lazy: `tags/${slug}/lazy/${fileIndex}.jpeg`, + mime: 'image/jpeg', + index, + comment, + })); const sfw = Object.entries({ - animals: [ - ['7WXfIIxVlNo', 'David Boca'], - ['aMBhrrveocw', 'Ivan Diaz'], - ['LTzbD8lj0kw', 'Lionel HESRY'], - ['t2I1PTZEx4E', 'Mohamed Nanabhai'], - ['0S1qMPpHbkE', 'Thomas Giotopoulos'], - ['UnLcVKt7d7I', 'Fernando @cferdo'], - ['reXwAuEdkVM', 'Ryan Al Bishri'], - ['Hq5N_iPRkOs', 'Axel Holen'], - ['GtXsMqOR3ik', 'Sophie Dale'], - ['FYJNmIQk7JA', 'Anastasiya Romanova'], - ['pQmu6_4VDaI', 'joel herzog'], - ['j0iiByCxGfA', 'Jordan Opel'], - ['67rtZAf_Jgw', 'Sean Thoman'], - ['2puB0ahKDag', 'K. Mitch Hodge'], - ['AM9ZtoUss68', 'Erica Nilsson'], - ['7r58deUCN4Q', 'Zuriela Benitez'], - ['PQJN_Po0O1U', 'Quaid Lagan'], - ['rCkstBJusv4', 'Martin Woortman'], - ['Tj4lLm49hhM', 'Saketh Upadhya'], - ['SjtR_cCEh0w', 'Patti Black'], - ['hmn3ZiTDep4', 'Portuguese Gravity'], - ['ZC3LibyQxZQ', 'Narges Pms'], - ['l-OYOwN2gpQ', 'Pablo Guerrero'], - ['E8LxrpUjW7Q', 'Tamara Bellis'], - ['xYVRzube0iM', 'Ricardo Braham'], - ['7AzUTGOBeJI', 'Adam Thomas'], - ['9Ha6VNNsKAA', 'Thomas Evans'], - ['GNvCMZzljr0', 'Hari Nandakumar'], - ['51u3WqwzXnQ', 'Grace Evans'], - ['X8Sck0T0y8s', 'Mélody P'], - ], - kittens: [ - ['fEK4jvgnApg', 'Kim Davies'], - ['FCx5h0erwnA', 'Lorraine Steriopol'], - ['RcGUiP9dphM', 'Kym Ellis'], - ['gAPXLS1LRVE', 'Olya Kuzovkina'], - ['JHf_O0inuHg', 'Prasad Panchakshari'], - ['CKsDMYPDgCs', 'Jacalyn Beales'], - ['mQquoOszMRM', 'Dimitri Houtteman'], - ['MNju0A6EeE0', 'Amy Baugess'], - ['emI7VUcvLi0', 'The Lucky Neko'], - ['Aq4e2-v7UuQ', 'Mario Peter'], - ['6vY_jbV12kQ', 'Kazuky Akayashi'], - ['1BfCps2-XjQ', 'Ramiz Dedaković'], - ['iNEXVlX-RLs', 'Lucija Ros'], - ['b8g-ywrrl5Y', 'Leighann Blackwood'], - ['Y3gjv5x38Wc', 'Agustin Fernandez'], - ['TYXh7h4QxX0', 'Leighann Blackwood'], - ['6EchiwVg7C4', 'Jesse Borovnica'], - ['fGh_mgAcKYY', 'Šárka Jonášová'], - ['V8o2n1GbMWc', 'Hunt Han'], - ['TEnrxLKakgI', 'Diver Zhang'], - ['XZuTLRfxwcU', 'Diana Parkhouse'], - ['rpvS2T2Tl0c', 'Luiza Braun'], - ['1S08ciB6Fy4', 'Екатерина Балабанова'], - ['iuBVMDnwaho', 'Shaun Meintjes'], - ['02kGh5lGsb8', 'Kym Ellis'], - ['8ELEgu78IbU', 'Kristin Wilson'], - ['TQ0XD_mGC8c', 'Kamal Bilal'], - ['zgvb8Knw_AU', 'Zoë Gayah Jonker'], - ['GGb-AyBZhjY', 'Brett Jordan'], - ['DyD9hx5lvpg', 'James Pond'], - ], - dogs: [ - ['f7-aL0xQd6A', 'Mark Galer'], - ['kOI1RlSGsfo', 'Mark Zamora'], - ['7c8z4KEvgds', 'William Daigneault'], - ['oWrZoAVOBS0', 'William Daigneault'], - ['xwpF_Hts7jA', 'Carolinie Cavalli'], - ['72mXoMwCoq4', 'Yuki Dog'], - ['7EKROB9iGFc', 'Yuki Dog'], - ['cX-KEISwDIw', 'Cole Keister'], - ['6v-lTFEoO6E', 'Camilo Fierro'], - ['1-sM8xqPFTM', 'Dimitri Bong'], - ['uTRtIcwkbGM', 'Lenin Estrada'], - ['dpn6K9e1vzY', 'Evan Clark'], - ['M8AKGLnbi90', 'Tatiana Rodriguez'], - ['sxGJv1SUlew', 'Jon Tyson'], - ['pT9TTuuzivQ', 'William Daigneault'], - ['KKNxCHLesGc', 'Julio Arcadio Santamaría Reyes'], - ['e_Du6fAT5dI', 'Thomas Fryatt'], - ['1IqHMmYaQJE', 'Mitchell Orr'], - ['BHh-jKrTIoU', 'NICOLAS TESSARI'], - ['l6GlfPH-8y4', 'Tatiana Rodriguez'], - ['d7TLLuiwm_c', 'Brett Jordan'], - ['I_PBuIa3Bm8', 'Karl Bewick'], - ['d2hWXEV8J-8', 'Harrison Kugler'], - ['AhKXmqa_7yU', 'Yuki Dog'], - ['aH79Nc7Npas', 'Irene Garcia'], - ['v3-zcCWMjgM', 'James Barker'], - ], - architecture: [ - ['jBanV-D3T-Q', 'Dimitry Anikin'], - ['VoQ35NRfZro', 'Dabbas'], - ['_u_wI4LaT7o', 'Alex Hudson'], - ['oSmn4cbhl8w', 'Jorge Gardner'], - ['c8GdokJMjWU', 'Mert Kahveci'], - ['nfPguKj20Ac', 'Ilya Panasenko'], - ['GGxUyCgfORg', 'photo_comments'], - ['oLUPaceKme0', 'Jack Schwartz'], - ['bo6oz4m4OXY', 'Vinicius Henrique'], - ['7HYbCXD2GSA', 'Theme Inn'], - ['6K4hh4VX3T0', 'SaiKrishna Saketh'], - ['lTxOZBNZ9yM', '[2Ni]'], - ['H3mL3kocOQ4', 'Artur Matosyan'], - ['ijxxeMO3c8E', 'Larry Teo'], - ['WMrd7-CjyF0', 'Anna Claire Schellenberg'], - ['FJIFiUCOTfc', 'Kirsten Drew'], - ['9daKXiWx5Eg', 'Anastasia Dulgier'], - ['47QjuZBn5dQ', 'Murugavel Oli'], - ['yjR2ne1gtAA', 'Marius'], - ['y9vO3FWDZb0', 'bckfwd'], - ['Ro6CB6x-VUg', 'Andreas NextVoyagePL'], - ['2td44mctvmI', 'Cameron Venti'], - ['M1uoNRrNrkE', 'Willian Justen de Vasconcellos'], - ['L-2jRW74fPY', 'Daryan Shamkhali'], - ['Qr5pi1_GlvY', 'Benno Klandt'], - ['twruXW0M2Mw', 'sk'], - ['B8vwUO2NM9Y', 'Stuart Frisby'], - ['p9jBrqMSU6Q', 'Han Leentvaar'], - ['4rGlazYAV3I', 'Dmitry Bayer'], - ], - flowers: [ - ['4QLKuXKAy7k', 'BEAUFIGEAU CELINE'], - ['DlYzHwAl32g', 'HISANARI KUNIMOTO'], - ['3TXuFNun-5Q', 'Erik Andres Reynoso'], - ['Rrhfeq9yeQ0', 'NeONBRAND'], - ['YaQdJyulJdU', 'Natasha V'], - ['wt4u1wNiT9I', 'Teo Zac'], - ['HG5RcKEawLA', 'Varshesh Joshi'], - ['VBtdWGCQ2yw', 'Vanessa Ochotorena'], - ['a-lIQzpvbHs', 'Thomas AE'], - ['D5RScffd8WU', 'NeONBRAND'], - ['mXQyEcINwa8', 'Thought Catalog'], - ['REczfcmwooE', 'Steve Harvey'], - ['7pGehyH7o64', 'Leonardo Wong'], - ['wvGQYtZ_c08', 'chuttersnap'], - ['jjqZ0gvHLYc', 'Suresh Purohit'], - ['9wQ-aGu0gBs', 'Joël de Vriend'], - ['8ANiHTtHbAQ', 'Nick Karvounis'], - ['Ehko8EbURbk', 'Chris Barbalis'], - ['Rlxfn__azLQ', 'Annie Spratt'], - ['q8I3Jeph4uU', 'Jake Dela Concepcion'], - ['wQjjYp8_a0Y', 'Henry Lorenzatto'], - ['GnwWHUXBfYI', 'Will Svec'], - ['pwFNVBlj5XU', 'Danijela Froki'], - ['RWz313DUECo', 'Laura Baker'], - ['lffwXgsqChg', 'Nils Schirmer'], - ['e1O6NCmhUt4', 'Brian McGowan'], - ], - food: [ - ['XPSXhLx143g', 'Wouter Meijering'], - ['VpxavZd4S-I', 'Mr Lemon'], - ['9vHOhKoNlNw', 'Edrece Stansberry'], - ['uScYRjZ2ol8', 'donald modeste'], - ['fCE-pTmFrPI', 'Valeriu Bondarenco'], - ['1ZTccDpF71k', 'Austin Paquette'], - ['Au-LzDMd_Cw', 'Stephanie McCabe'], - ['9cfHdC2Asak', 'cindy fernandez'], - ['4-rmvQRL2nY', 'Annie Spratt'], - ['8Yvwy4Kbd1g', 'Alex Kondratiev'], - ['4PtChDuxsDI', 'Dronile Hiraldo'], - ['Mi1SNlsyWAk', 'Louis Hansel @shotsoflouis'], - ['nn0whk6nzv4', 'Yulia Chinato'], - ['8JIIoSFtEbo', 'Massimo Virgilio'], - ['44eKcVXzFoc', 'Alonso Romero'], - ['JpbtAb-f3JA', 'Dollar Gill'], - ['Hvrm3efPYIA', 'Delaney Van'], - ['4E0dknSrQVU', 'Matteo Maretto'], - ['krNP2ESq-54', 'Joshua Bedford'], - ['DPNrBT1WCMs', 'Egor Lyfar'], - ['Yy-dHQP-Ax0', 'Markus Spiske'], - ['BKSntHf8oiU', 'Melissa Walker Horn'], - ['asaGSZEyltQ', 'Noora AlHammadi'], - ['U3hCd1S7FQ4', 'Louis Hansel @shotsoflouis'], - ['VOpJTnP6S9g', 'Liana Mikah'], - ['9TWavGempJc', 'Ashleigh Robertson'], - ['I2tgHl69Dco', 'Louis Hansel @shotsoflouis'], - ['fyQr1T3GE34', 'Petr Sevcovic'], - ['pGe5mc4Eip8', 'Daniel Park'], - ['gZsgKrNc8es', 'Dan Gold'], - ['XPSXhLx143g', 'Wouter Meijering'], - ['VpxavZd4S-I', 'Mr Lemon'], - ['9vHOhKoNlNw', 'Edrece Stansberry'], - ['uScYRjZ2ol8', 'donald modeste'], - ['fCE-pTmFrPI', 'Valeriu Bondarenco'], - ['1ZTccDpF71k', 'Austin Paquette'], - ['Au-LzDMd_Cw', 'Stephanie McCabe'], - ['9cfHdC2Asak', 'cindy fernandez'], - ['4-rmvQRL2nY', 'Annie Spratt'], - ['8Yvwy4Kbd1g', 'Alex Kondratiev'], - ['4PtChDuxsDI', 'Dronile Hiraldo'], - ['Mi1SNlsyWAk', 'Louis Hansel @shotsoflouis'], - ['nn0whk6nzv4', 'Yulia Chinato'], - ['8JIIoSFtEbo', 'Massimo Virgilio'], - ['44eKcVXzFoc', 'Alonso Romero'], - ['JpbtAb-f3JA', 'Dollar Gill'], - ['Hvrm3efPYIA', 'Delaney Van'], - ['4E0dknSrQVU', 'Matteo Maretto'], - ['krNP2ESq-54', 'Joshua Bedford'], - ['DPNrBT1WCMs', 'Egor Lyfar'], - ['Yy-dHQP-Ax0', 'Markus Spiske'], - ['BKSntHf8oiU', 'Melissa Walker Horn'], - ['asaGSZEyltQ', 'Noora AlHammadi'], - ['U3hCd1S7FQ4', 'Louis Hansel @shotsoflouis'], - ['VOpJTnP6S9g', 'Liana Mikah'], - ['9TWavGempJc', 'Ashleigh Robertson'], - ['I2tgHl69Dco', 'Louis Hansel @shotsoflouis'], - ['fyQr1T3GE34', 'Petr Sevcovic'], - ['pGe5mc4Eip8', 'Daniel Park'], - ['gZsgKrNc8es', 'Dan Gold'], - ], - candy: [ - ['51AhxwkYyHo', 'Viktor Forgacs'], - ['rS1GogPLVHk', 'Eaters Collective'], - ['9m6NQHyxk-s', 'Brooke Lark'], - ['DOmaKmeCp_8', 'Sarah Takforyan'], - ['aCPOKOb4qis', 'Jamie Street'], - ['On8Ov1TItnU', 'QooQee'], - ['KN7tpVQCmWA', 'Nick Fewings'], - ['9lmFbtJ2QzM', 'Tim Gouw'], - ['54hUU5pNSvo', 'Erol Ahmed'], - ['zYH4SubXCWY', 'Наталья Горох'], - ['wJHJY7PcDcg', 'Andrew Itaga'], - ['BnusUP5jydc', 'Marc Markstein'], - ['NI_fJ15rIfI', 'Szabo Viktor'], - ['LymVMRIUwPQ', 'Happy Films'], - ['mrNVnLEphdo', 'Greg Nunes'], - ['FKvoEKSV2LY', 'zhou yu'], - ['CKLF34baCTQ', 'Willian Justen de Vasconcellos'], - ['7uGCN9qshsY', 'Siora Photography'], - ['xBTnaTgleQE', 'Glen Carrie'], - ['sC_HExGwbhI', 'Erik Mclean'], - ['1gViVVlEaPc', 'Sérgio André'], - ['k9yY0XZTSnI', 'Fernando Hernandez'], - ['h1_R9-o9an0', 'emrecan arık'], - ['XB0ha-DSGoU', 'Laura Briedis'], - ['ONn4OfAnxZY', 'Monique Carrati'], - ['24p9dPeXdFA', 'Frederic Köberl'], - ['GL6J_sFYHLw', 'Yuiizaa September'], - ['LU_fCezP9-o', 'Amit Lahav'], - ['CSkAj_XqOVc', 'Mockaroon'], - ['xLvIcAYuuMQ', 'Luis Aguila'], - ], - fruit: [ - ['ZLc9yTIFzNk', 'Kelly Sikkema'], - ['DoxGtpAsdYY', 'Sanni Sahil'], - ['lyEkpuuIrg0', 'kaouther djouada'], - ['fDx4zHpnlOM', 'Nanxi wei'], - ['0AXNt5SdeXU', 'Sara Cervera'], - ['s05XKB6jK2c', 'Markus Spiske'], - ['bPMyJzKhCyA', 'Louis Hansel @shotsoflouis'], - ['4jeWN4puDrw', 'Carolyn Leber'], - ['8EScigZC6AU', 'MF Evelyn'], - ['1QbosWMxOx0', 'elCarito'], - ['NWA3s8r-1bc', 'Fli Hi'], - ['TfNUmbaIjj8', 'Sophie Dale'], - ['duskNBVv420', 'Louis Hansel @shotsoflouis'], - ['QYupKZjYDbw', 'Moritz Kindler'], - ['ozcHSq3XfVs', 'Callum Blacoe'], - ['ucY_-U1dM8U', 'Louis Hansel @shotsoflouis'], - ['4WLc_dWE-kc', 'Dmitry Mishin'], - ['BTqDasyX62E', 'Priscilla Du Preez'], - ['SD5dOSkhQdU', 'Alexander Mils'], - ['4pJekgmSmPM', 'Rinck Content Studio'], - ['0XGWys_GaFo', 'Toa Heftiba'], - ['uTZvsJsylYc', 'Louis Hansel @shotsoflouis'], - ['vAHaYh5s_Sc', 'RAPHAEL MAKSIAN'], - ['0N4A1c5tJSs', 'Miguel Vaz'], - ['VAhUq30sW0c', 'Cody Berg'], - ['qbO7Mlhq8PQ', 'JOSHUA COLEMAN'], - ['dLB32q_bRs0', 'Jason Leung'], - ['66itnKdGlC4', 'Fateme Azimi'], - ['_DYRZHbCIq0', 'Will Mcmahon'], - ], - landscapes: [ - ['FuaNmJPLAHg', 'MINSUN KIM'], - ['8flZ753v87Q', 'Ken Schlabach'], - ['6qdO_nFhYoI', 'Clay Banks'], - ['VMmPxFmfwfQ', 'Xiaopeng Ma'], - ['DAufF3R8B5Q', 'Sangga Rima Roman Selia'], - ['5U9n9gip7VY', 'Annie Spratt'], - ['Tot7FLHWotI', 'elCarito'], - ['vph4L_OjhuI', 'Marek Piwnicki'], - ['Nd8h38tRIlc', 'Kevin Horvat'], - ['nACRxCEAut8', 'ALEKSEY KUPRIKOV'], - ['PjyfZ-Ujut8', 'Denys Nevozhai'], - ['X6utHocVX8w', 'Ian Williams'], - ['cq6g1WssaJI', 'Tyler Casey'], - ['U1ad7OjdHx8', 'Dave Herring'], - ['si5673B4PIE', 'Annie Spratt'], - ['shv8H0Dv5iE', 'Aditya Chinchure'], - ['hjVrWFVyOqE', 'chuttersnap'], - ['_skrrHIcsEk', 'Edward Ma'], - ['GMNtcgo-KdA', 'Yoav Hornung'], - ['313dueuxHls', 'Jamie Street'], - ['SncOlKOY7NY', 'Ward Mercer'], - ['7R11NCmOY6k', 'Ivan Bandura'], - ['YFwOiSisSLM', 'Krzysztof Kotkowicz'], - ['gbO7zCOUJIg', 'Brian Kyed'], - ['5iwG4xlUgJ4', 'Jason Leung'], - ['G_PSim-dsvU', 'Alexandra Jitariuc'], - ['gVl6Hsi_pxo', 'Jean-Philippe Delberghe'], - ['p8h0_0pyW9k', 'Bence Balla-Schottner'], - ], - waterfalls: [ - ['Cwd0zYOIClY', 'Thomas Ensley'], - ['dGCHAo7mb2Q', 'Jamie Melville'], - ['bukitR21NO8', 'Jen Theodore'], - ['IKIY5bpd9eM', 'Bruno Kelzer'], - ['rrVm3b-uxkk', 'Jamie Melville'], - ['UegnUIW76gQ', 'Jamie Melville'], - ['d0cvao4fC6Q', 'Forest Simon'], - ['N8r2hH1siEY', 'Lester Hine'], - ['oRmeWoJx_nY', 'Michael Olsen'], - ['jVjwlfyJAkA', 'Tom Wheatley'], - ['ar6PYxBFgis', 'Gerald Berliner'], - ['9fpK7fPw6Is', 'Jamie Melville'], - ['jeXkw2HR1SU', 'Julia Caesar'], - ['Evs5MnlmUXY', 'Damian Kamp'], - ['phstcH4QKJc', 'Jakob Owens'], - ['7dmDlBfB9Vk', 'Spencer Watson'], - ['SHhaNnNR1xo', 'SaiKrishna Saketh'], - ['_6ghImrDiVU', 'Daniel H. Tong'], - ['gfaXzDmMY7M', 'RADIN AHMAD MUJAHID'], - ['rFbK1PP9LEA', 'Daniel Hernandez'], - ['s6Tv7b4SAoo', 'Miltiadis Fragkidis'], - ['ir9RUDjVpPo', 'Clay Banks'], - ['7emiteIwfuk', 'yunchuan luo'], - ['9tqrKo4B98I', 'Austin Farrington'], - ['dfazzUCjMro', 'oakie'], - ['4HEuHmA7WfM', 'Ketan Morris'], - ['eG1_rPekhTk', 'Jack B'], - ['_up7EDGdTqU', 'Hari Nandakumar'], - ['MhIPEJmVwaA', 'Karthik Chandran'], - ], - travel: [ - ['gC2Q_Tfub6c', 'James Lee'], - ['1Tcu61Qz7c0', 'Payas'], - ['eLMJ2x7s9ak', 'Alexandre Chambon'], - ['aNrRsB2wLDk', 'Ahmet Yalçınkaya'], - ['xu2WYJek5AI', 'Anastasia Petrova'], - ['xRoexKKv8fs', 'Robson Hatsukami Morgan'], - ['qdIer4A0afE', 'Camille Brodard'], - ['0lPZBa6-1J8', 'Torbjorn Sandbakk'], - ['CLm3pWXrS9Q', 'Tim Trad'], - ['D1fs2x11_jk', 'Raul Taciu'], - ['BZ3aE3ouAfc', 'Petr Sevcovic'], - ['_OUvt8kLf0s', 'GAWN AUSTRALIA'], - ['T_6yJJQ_-wA', 'Square Lab'], - ['1Z_mX3zzEBc', 'Andy Pearce'], - ['lWHJwoSZf7M', 'Joseph Costa'], - ['O6Euhw7NMbQ', 'Cosmic Timetraveler'], - ['bs1eqd6zSiU', 'Harley-Davidson'], - ['_qkuc1V9Gbg', 'Ken Cheung'], - ], - cars: [ - ['w5SZe8hoqlk', 'Rachel Lynette French'], - ['Nwk0ye_Y_As', 'Lex Valishvili'], - ['P9yqMy_9ZaY', 'Anastasia Dulgier'], - ['aGwBtbncMWo', 'Sandra Kaas'], - ['jV_QaRjbhWE', 'Clem Onojeghuo'], - ['SwmaJDvasuU', 'Gunnar Ridderström'], - ['BsJovWrQ7vE', 'Caroline'], - ['3t0Q-GYXE0U', 'Austin Park'], - ['tlCzxI2RQAc', 'Vinícius Henrique'], - ['fecFQcxsUok', 'Zachary Spears'], - ['upttrzCo8-U', 'takahiro taguchi'], - ['zoHustBfNxA', 'Lukas Werner'], - ['UZH69YA50qY', 'Austin Park'], - ['tJwY80NMkhk', 'Hanny Naibaho'], - ['esfNTaHsR1c', 'Willian Justen de Vasconcellos'], - ['Tas1kF-6aNA', 'Tyler Casey'], - ['7nrsVjvALnA', 'Denys Nevozhai'], - ['CRf3KYexpCA', 'Will Truettner'], - ['B8JpogxOnyw', 'Mike Von'], - ['mU5vnpJW_CQ', 'Luke Tanis'], - ['GaRPxo9Z86M', 'Court Cook'], - ['Orp-VAQ_gNA', 'Angello Lopez'], - ['q4UZ53rYYy0', 'Ali Moharami'], - ['XA2OEcvrrP8', 'Alexander Pidgeon'], - ['hXMv04v_py0', 'Patrick Schöpflin'], - ['G7sWGEF8pRc', 'Julian Hochgesang'], - ['PUnARRf-rE8', 'jean wimmerlin'], - ['UIk-rF4Df60', 'Igor Bumba'], - ['0dW-eQVL0WQ', 'Michael Heuser'], - ], - aviation: [ - ['bw1hXT_okL4', 'sayhitobel'], - ['ecOXN5jGtaU', 'Alexander Lobanov'], - ['rFujmwrNv1w', 'Abe Drzycimski'], - ['EKYsla2fER8', 'Paola Aguilar'], - ['1uiXp6fXd3w', 'Kevin Hackert'], - ['tVzGTraJ4T0', 'Samantha Gades'], - ['5h1-cHxJz1I', 'Nikli Nodin'], - ['u-1cYIua_aI', 'John Torcasio'], - ['l2OusPPMLxs', 'yvette Wynne'], - ['SUIvWFHBZas', 'Timothy Newman'], - ['Tvrnezn1N6g', 'Daniel Eledut'], - ['ZN0469D3v98', 'Jason Hafso'], - ['LNmKCmZ2pcI', 'Ricardo Resende'], - ['P_ExwnnPGyM', 'Tudose Alexandru'], - ['1vyWYdyUDGU', 'Ian Cumming'], - ['k102QVrpitQ', 'Jake Blucker'], - ['eB8-XtMtqZI', 'Miguel Ángel Sanz'], - ['1XDZavWyows', 'Samuel Sianipar'], - ['7WHD-pdECGU', 'Miguel Ángel Sanz'], - ['CN8HsCUCjUQ', 'Cameron Kitson'], - ['q_rNy9pRe78', 'Walter Walraven'], - ['a9SA6Zs1L9g', 'Tim Dennert'], - ['JW50PRr5UbI', 'Roland O'], - ['um_6nrOnPZ4', 'Pascal Meier'], - ['EpxT58kpBhc', 'Terence Burke'], - ['3fbjHj2k0vE', 'Dušan Smetana'], - ['9o4p3eCcRBE', 'Jeremy Bishop'], - ['sbfLwfoVX7E', 'Nour Betar'], - ['4VCJBAtlnNI', 'sippakorn yamkasikorn'], - ['LcbsrFbqwGk', 'Angel Barnes'], - ], - nature: [ - ['io0umElGQwU', 'Fabrizio Forte'], - ['lh2sQ_L3GaA', 'Daniel Lord'], - ['Gyr7o3BMsSc', 'Dave Francis'], - ['lRipDukRpd4', 'Kevin'], - ['GxymWkdnl4Y', 'Fernando Jorge'], - ['rWnw7JL0LPE', 'Rahul Gupta'], - ['YKuYS3ChMyw', 'Yuliya Kosolapova'], - ['ZsUjLYqbNz8', 'Pau Morfín'], - ['x2o3HX6RKG8', 'Dave'], - ['Few073pm6aQ', 'Dennis Bertuch'], - ['jP-wvLA7uyg', 'Gary Butterfield'], - ['euhs3wanL-I', 'Andrew Stapleton'], - ['UHt2TG_CHO0', 'Wolfgang Hasselmann'], - ['SAIb5NkbAaE', 'Alexis Gethin'], - ['tdNjdIW-_OY', 'Lachlan'], - ['0NtjFr0-DwM', 'Jane Carmona'], - ['fhg_nm1sufU', 'Pascal Debrunner'], - ['HJUDECvtxZE', 'jose alfonso sierra'], - ['8vS1CwHIEH8', 'Serge Sustavov'], - ['oTTnZcwQ2t4', 'Daniel Radford'], - ['TNhs9udas8Q', 'Migsar Navarro'], - ['zT9g3crTPcE', 'Devin L'], - ['hYuNG1zptM4', 'Roberto Gamito'], - ['DfOQRep1LKU', 'Wolfgang Rottmann'], - ['2sT_iJhIcEg', 'Anamika Pokharel'], - ['Zjcl2vaeGdo', 'Lucas Mitchell'], - ['b2T6zgFAW1E', 'Theme Inn'], - ['EnFQmcTtsjo', 'Yang Jing'], - ['PWI-CDVynhw', 'Thomas Galler'], - ['gTvLsDi-Uzo', 'Arun Raj'], - ], - trees: [ - ['aSCx7M1E4Vo', 'Markos Mant'], - ['3B9MYDjQ-EQ', 'Firasat Durrani'], - ['39oaRqfUacc', 'Nick'], - ['Y0NLa_KmEgU', 'Michael Ankes'], - ['i6nGVkbbUT4', 'Henry McIntosh'], - ['_WITb4g1ImY', 'Mark Basarab'], - ['prd5CXMsD68', 'JOHN TOWNER'], - ['avxSevmCKfw', 'Marta Pawlik'], - ['LZf0HMPg-3I', 'Max Brinton'], - ['Ovjx9FsXX9c', 'Andy Fitzsimon'], - ['YPtfMXRMUe0', 'Corey Agopian'], - ['h3y3XPXKoeY', 'Elke Karin Lugert'], - ['qeGC1zrsG8U', 'Tycho Atsma'], - ['dAkd3xaSDTA', 'Nick West'], - ['Prd-KB7CKo4', 'Austin Neill'], - ['iqu9ZTwTfVM', 'Clem Onojeghuo'], - ['klNpWLkgezo', 'Wil Stewart'], - ['Pm4U5IqI4dM', 'Lena Derevianko'], - ['5i664o1oY4c', 'Yannick Pulver'], - ['uoMFPm5_Xg8', 'Yoni Kozminsi'], - ['Qr2dZRNdf_M', 'Afifi Zulkifle'], - ['jxljuYqn8uU', 'Sebastian Pichler'], - ['H3mO3zNlvNU', 'Jonathan Knepper'], - ['pEb-Xf_qM0s', 'Martin Reisch'], - ['prSogOoFmkw', 'Braden Jarvis'], - ['y9csmronT3s', 'Alberto Restifo'], - ['R8R9H_xuvBY', 'Andrew Pons'], - ['fuT-m1yzUG4', 'Ales Krivec'], - ], - wildlife: [ - ['Tot7FLHWotI', 'elCarito'], - ['-I0EgcZdV0E', 'Maarten van den Heuvel'], - ['F21zt7_icpo', 'Tevin Trinh'], - ['gMxgp-MwCyQ', 'James Wainscoat'], - ['FFlb5Uj3vhc', 'Sigmund'], - ['hglSMjdh83M', 'Srinivasan Venkataraman'], - ['UgWcvfl4q5I', 'NOAA'], - ['PZ1nEPFNBJQ', 'Third Idea'], - ['f4yYs5P5GbA', 'rigel'], - ['e94T5ag-9x0', 'Jayden Brand'], - ['6Fcllk7ze_Q', 'Vivian Arcidiacono'], - ['O7G3II8E2Eo', 'Erik-Jan Leusink'], - ['-6UNL6Ghn_c', 'Silvio Kundt'], - ['EyZMGYn1Uj4', 'AGL Fotos'], - ['nxR7gvSokH8', 'Waldemar Brandt'], - ['sAGXVK6bNFc', 'Amar Yashlaha'], - ['8zLCXDWETEg', 'Clément ROY'], - ['4nPFQ2sUhUE', 'Justin Porter'], - ['mUNDTQrfnSk', 'Zahrin Lukman'], - ['VXcX0Joa09k', 'Max Rovensky'], - ['F_HycxA2lwc', 'Marthijn Brinks'], - ['ss01halnU4I', 'Dušan Smetana'], - ['lktWv61WoNI', 'Rory Lindholm'], - ['myeQ2RH1PX0', 'Alexander Ross'], - ['dhIAyAmfjz8', 'Third Idea'], - ['AGprQpF4STo', 'Jeff Lemond'], - ['P8pBJQVt4UA', 'Christer Gundersen'], - ['nt8Ek7sRgdA', 'Mikell Darling'], - ['YHv0BDThVOw', 'Nicholas Doherty'], - ], - interior: [ - ['9wGKENQ-qTI', 'Kunj Parekh'], - ['x3BCSWCAtrY', 'yann maignan'], - ['EF6z_6R94zQ', 'Orlova Maria'], - ['dC8NC2QBFyQ', 'Kari Shea'], - ['A_AuirVquQY', 'Abbie Bernet'], - ['IJf2v-StB4Y', 'yann maignan'], - ['frsN89Tf-1Q', 'Joshua Eckstein'], - ['s95oB2n9jng', 'Aaron Huber'], - ['n_vdmdtNh6M', 'Tiplada Mekvisan'], - ['GbiVL6t4T-o', 'gdtography'], - ['xx0oSB1YxRE', 'Joseph Albanese'], - ['McaNtoPEEcg', 'Brooke Cagle'], - ['s65VlkIYSDw', 'Robert Bye'], - ['pEaBEqXXk-M', 'Aw Creative'], - ['GaX7QTgs8pg', 'Waldemar Brandt'], - ['b75FBg97dU0', 'Bao Menglong'], - ['5i0GnoTTjSE', 'Philipp Berndt'], - ['buhmhprfo3g', 'Kara Eads'], - ['L7EwHkq1B2s', 'Kara Eads'], - ['gBdirnalxcQ', 'Nick Fewings'], - ['ILgaxpiQu0', 'Matteo Maretto'], - ['ydcMwcfY5E0', 'Clark Street Mercantile'], - ['zLT3VqWEgOQ', 'Sidekix Media'], - ['Yg0Rds6_TsY', 'Sergiu Cindea'], - ['gbS_fhrFo10', 'Bekah Russom'], - ['o3c-euNd_ZM', 'Alessandra Caretto'], - ['IEkMMvdZFc0', 'Nick Hillier'], - ['hnhE83RhAWg', 'Stefan Steinbauer'], - ], - statues: [ - ['ig8E7Mlrl7Y', 'Vidar Nordli-Mathisen'], - ['1ak3Z7ZmtQA', 'Ryan Lum'], - ['ntPF02wcTY', 'Gigi'], - ['5_i4OPeOAZU', 'Viktor Forgacs'], - ['iRON0g6iO0k', 'Alexandre Chambon'], - ['PhQ4CpXLEX4', 'Daniels Joffe'], - ['EfHqouvZU2Y', 'Bettina Otott Kovács'], - ['kaEhf0eZme8', 'Nils'], - ['4-4IDc21Gto', 'K. Mitch Hodge'], - ['Y8Xh7ZJFU5A', 'Vidar Nordli-Mathisen'], - ['fmawALmMLSA', 'Gabriel TRESCH'], - ['DUp4B6M0AMc', 'Sebastien'], - ['MFZUY4gqvA4', 'James Yarema'], - ['50vvwcNFFzU', 'Robert Anasch'], - ['TMRi8cD2umM', 'Frank Eiffert'], - ['wLx_WCkWvHg', 'Chris A. Tweten'], - ['Twoj21Av-so', 'Arthur Reeder'], - ['EiGDn8cwU4Y', 'Tessa Rampersad'], - ['fVY6UxZuECA', 'Christine Wehrmeier'], - ['uJdTBTJ9rbo', 'Christine Wehrmeier'], - ['4eEBFTBKx5E', 'Ralph Spandl'], - ['HtQRGemW_40', 'Ivan Bertona'], - ['uHBcinxOLhQ', 'K. Mitch Hodge'], - ['2TmsyZXMNTE', 'Emma Fabbri'], - ['9KkPloRgOUY', 'Matteo Maretto'], - ['KzPefInJW58', 'JOSHUA COLEMAN'], - ['szVTIkisN1M', 'David Siglin'], - ['iRzEPkYSETQ', 'Francois Olwage'], - ], - technology: [ - ['QpTCSHzhWuo', 'Joshua Hoehne'], - ['0lMpQaXfOCg', 'Barrett Ward'], - ['w33-zg-dNL4', 'Rami Al-zayat'], - ['MC5WbGo_bZM', 'Tom Pumford'], - ['iHJ7xouUyXs', 'Amith Nair'], - ['E3I9thV98kQ', 'Tatiana Lapina'], - ['JuUK7Er9nR4', 'Mohamed Boumaiza'], - ['Dei5oAC_wJc', 'Kenny Luo'], - ['ltwEbf_G9bs', 'Mario Caruso'], - ['gWdlDR4WpV4', 'Zarak Khan'], - ['0Um6Yr1cyx0', 'Antoine Beauvillain'], - ['_8S9nEmCZK0', 'Oliur'], - ['etFrnBJS1qc', 'NeONBRAND'], - ['ZMVtx_KJtOk', 'Thought Catalog'], - ['JNuKyKXLh8U', 'Noiseporn'], - ['5gzr-RM-rZM', 'Kenny Luo'], - ['eWaXmZsXKDs', 'Zane Lee'], - ['4qGbMEZb56c', 'Thomas William'], - ['hwqWxHoH2wk', 'Markus Spiske'], - ['vZJdYl5JVXY', 'Kaitlyn Baker'], - ['Lg8xTZjs6Lg', 'Marc Mueller'], - ['M5HQPjXrjlQ', 'Matt Hoffman'], - ['A-b37b-CrYE', 'Kenny Luo'], - ['Kj2SaNHG-hg', 'Christopher Burns'], - ['A1v0-iH3T5A', 'Patrick Hendry'], - ['iFBIdX54BOk', 'Keagan Henman'], - ], + animals: [ + ['7WXfIIxVlNo', 'David Boca'], + ['aMBhrrveocw', 'Ivan Diaz'], + ['LTzbD8lj0kw', 'Lionel HESRY'], + ['t2I1PTZEx4E', 'Mohamed Nanabhai'], + ['0S1qMPpHbkE', 'Thomas Giotopoulos'], + ['UnLcVKt7d7I', 'Fernando @cferdo'], + ['reXwAuEdkVM', 'Ryan Al Bishri'], + ['Hq5N_iPRkOs', 'Axel Holen'], + ['GtXsMqOR3ik', 'Sophie Dale'], + ['FYJNmIQk7JA', 'Anastasiya Romanova'], + ['pQmu6_4VDaI', 'joel herzog'], + ['j0iiByCxGfA', 'Jordan Opel'], + ['67rtZAf_Jgw', 'Sean Thoman'], + ['2puB0ahKDag', 'K. Mitch Hodge'], + ['AM9ZtoUss68', 'Erica Nilsson'], + ['7r58deUCN4Q', 'Zuriela Benitez'], + ['PQJN_Po0O1U', 'Quaid Lagan'], + ['rCkstBJusv4', 'Martin Woortman'], + ['Tj4lLm49hhM', 'Saketh Upadhya'], + ['SjtR_cCEh0w', 'Patti Black'], + ['hmn3ZiTDep4', 'Portuguese Gravity'], + ['ZC3LibyQxZQ', 'Narges Pms'], + ['l-OYOwN2gpQ', 'Pablo Guerrero'], + ['E8LxrpUjW7Q', 'Tamara Bellis'], + ['xYVRzube0iM', 'Ricardo Braham'], + ['7AzUTGOBeJI', 'Adam Thomas'], + ['9Ha6VNNsKAA', 'Thomas Evans'], + ['GNvCMZzljr0', 'Hari Nandakumar'], + ['51u3WqwzXnQ', 'Grace Evans'], + ['X8Sck0T0y8s', 'Mélody P'], + ], + kittens: [ + ['fEK4jvgnApg', 'Kim Davies'], + ['FCx5h0erwnA', 'Lorraine Steriopol'], + ['RcGUiP9dphM', 'Kym Ellis'], + ['gAPXLS1LRVE', 'Olya Kuzovkina'], + ['JHf_O0inuHg', 'Prasad Panchakshari'], + ['CKsDMYPDgCs', 'Jacalyn Beales'], + ['mQquoOszMRM', 'Dimitri Houtteman'], + ['MNju0A6EeE0', 'Amy Baugess'], + ['emI7VUcvLi0', 'The Lucky Neko'], + ['Aq4e2-v7UuQ', 'Mario Peter'], + ['6vY_jbV12kQ', 'Kazuky Akayashi'], + ['1BfCps2-XjQ', 'Ramiz Dedaković'], + ['iNEXVlX-RLs', 'Lucija Ros'], + ['b8g-ywrrl5Y', 'Leighann Blackwood'], + ['Y3gjv5x38Wc', 'Agustin Fernandez'], + ['TYXh7h4QxX0', 'Leighann Blackwood'], + ['6EchiwVg7C4', 'Jesse Borovnica'], + ['fGh_mgAcKYY', 'Šárka Jonášová'], + ['V8o2n1GbMWc', 'Hunt Han'], + ['TEnrxLKakgI', 'Diver Zhang'], + ['XZuTLRfxwcU', 'Diana Parkhouse'], + ['rpvS2T2Tl0c', 'Luiza Braun'], + ['1S08ciB6Fy4', 'Екатерина Балабанова'], + ['iuBVMDnwaho', 'Shaun Meintjes'], + ['02kGh5lGsb8', 'Kym Ellis'], + ['8ELEgu78IbU', 'Kristin Wilson'], + ['TQ0XD_mGC8c', 'Kamal Bilal'], + ['zgvb8Knw_AU', 'Zoë Gayah Jonker'], + ['GGb-AyBZhjY', 'Brett Jordan'], + ['DyD9hx5lvpg', 'James Pond'], + ], + dogs: [ + ['f7-aL0xQd6A', 'Mark Galer'], + ['kOI1RlSGsfo', 'Mark Zamora'], + ['7c8z4KEvgds', 'William Daigneault'], + ['oWrZoAVOBS0', 'William Daigneault'], + ['xwpF_Hts7jA', 'Carolinie Cavalli'], + ['72mXoMwCoq4', 'Yuki Dog'], + ['7EKROB9iGFc', 'Yuki Dog'], + ['cX-KEISwDIw', 'Cole Keister'], + ['6v-lTFEoO6E', 'Camilo Fierro'], + ['1-sM8xqPFTM', 'Dimitri Bong'], + ['uTRtIcwkbGM', 'Lenin Estrada'], + ['dpn6K9e1vzY', 'Evan Clark'], + ['M8AKGLnbi90', 'Tatiana Rodriguez'], + ['sxGJv1SUlew', 'Jon Tyson'], + ['pT9TTuuzivQ', 'William Daigneault'], + ['KKNxCHLesGc', 'Julio Arcadio Santamaría Reyes'], + ['e_Du6fAT5dI', 'Thomas Fryatt'], + ['1IqHMmYaQJE', 'Mitchell Orr'], + ['BHh-jKrTIoU', 'NICOLAS TESSARI'], + ['l6GlfPH-8y4', 'Tatiana Rodriguez'], + ['d7TLLuiwm_c', 'Brett Jordan'], + ['I_PBuIa3Bm8', 'Karl Bewick'], + ['d2hWXEV8J-8', 'Harrison Kugler'], + ['AhKXmqa_7yU', 'Yuki Dog'], + ['aH79Nc7Npas', 'Irene Garcia'], + ['v3-zcCWMjgM', 'James Barker'], + ], + architecture: [ + ['jBanV-D3T-Q', 'Dimitry Anikin'], + ['VoQ35NRfZro', 'Dabbas'], + ['_u_wI4LaT7o', 'Alex Hudson'], + ['oSmn4cbhl8w', 'Jorge Gardner'], + ['c8GdokJMjWU', 'Mert Kahveci'], + ['nfPguKj20Ac', 'Ilya Panasenko'], + ['GGxUyCgfORg', 'photo_comments'], + ['oLUPaceKme0', 'Jack Schwartz'], + ['bo6oz4m4OXY', 'Vinicius Henrique'], + ['7HYbCXD2GSA', 'Theme Inn'], + ['6K4hh4VX3T0', 'SaiKrishna Saketh'], + ['lTxOZBNZ9yM', '[2Ni]'], + ['H3mL3kocOQ4', 'Artur Matosyan'], + ['ijxxeMO3c8E', 'Larry Teo'], + ['WMrd7-CjyF0', 'Anna Claire Schellenberg'], + ['FJIFiUCOTfc', 'Kirsten Drew'], + ['9daKXiWx5Eg', 'Anastasia Dulgier'], + ['47QjuZBn5dQ', 'Murugavel Oli'], + ['yjR2ne1gtAA', 'Marius'], + ['y9vO3FWDZb0', 'bckfwd'], + ['Ro6CB6x-VUg', 'Andreas NextVoyagePL'], + ['2td44mctvmI', 'Cameron Venti'], + ['M1uoNRrNrkE', 'Willian Justen de Vasconcellos'], + ['L-2jRW74fPY', 'Daryan Shamkhali'], + ['Qr5pi1_GlvY', 'Benno Klandt'], + ['twruXW0M2Mw', 'sk'], + ['B8vwUO2NM9Y', 'Stuart Frisby'], + ['p9jBrqMSU6Q', 'Han Leentvaar'], + ['4rGlazYAV3I', 'Dmitry Bayer'], + ], + flowers: [ + ['4QLKuXKAy7k', 'BEAUFIGEAU CELINE'], + ['DlYzHwAl32g', 'HISANARI KUNIMOTO'], + ['3TXuFNun-5Q', 'Erik Andres Reynoso'], + ['Rrhfeq9yeQ0', 'NeONBRAND'], + ['YaQdJyulJdU', 'Natasha V'], + ['wt4u1wNiT9I', 'Teo Zac'], + ['HG5RcKEawLA', 'Varshesh Joshi'], + ['VBtdWGCQ2yw', 'Vanessa Ochotorena'], + ['a-lIQzpvbHs', 'Thomas AE'], + ['D5RScffd8WU', 'NeONBRAND'], + ['mXQyEcINwa8', 'Thought Catalog'], + ['REczfcmwooE', 'Steve Harvey'], + ['7pGehyH7o64', 'Leonardo Wong'], + ['wvGQYtZ_c08', 'chuttersnap'], + ['jjqZ0gvHLYc', 'Suresh Purohit'], + ['9wQ-aGu0gBs', 'Joël de Vriend'], + ['8ANiHTtHbAQ', 'Nick Karvounis'], + ['Ehko8EbURbk', 'Chris Barbalis'], + ['Rlxfn__azLQ', 'Annie Spratt'], + ['q8I3Jeph4uU', 'Jake Dela Concepcion'], + ['wQjjYp8_a0Y', 'Henry Lorenzatto'], + ['GnwWHUXBfYI', 'Will Svec'], + ['pwFNVBlj5XU', 'Danijela Froki'], + ['RWz313DUECo', 'Laura Baker'], + ['lffwXgsqChg', 'Nils Schirmer'], + ['e1O6NCmhUt4', 'Brian McGowan'], + ], + food: [ + ['XPSXhLx143g', 'Wouter Meijering'], + ['VpxavZd4S-I', 'Mr Lemon'], + ['9vHOhKoNlNw', 'Edrece Stansberry'], + ['uScYRjZ2ol8', 'donald modeste'], + ['fCE-pTmFrPI', 'Valeriu Bondarenco'], + ['1ZTccDpF71k', 'Austin Paquette'], + ['Au-LzDMd_Cw', 'Stephanie McCabe'], + ['9cfHdC2Asak', 'cindy fernandez'], + ['4-rmvQRL2nY', 'Annie Spratt'], + ['8Yvwy4Kbd1g', 'Alex Kondratiev'], + ['4PtChDuxsDI', 'Dronile Hiraldo'], + ['Mi1SNlsyWAk', 'Louis Hansel @shotsoflouis'], + ['nn0whk6nzv4', 'Yulia Chinato'], + ['8JIIoSFtEbo', 'Massimo Virgilio'], + ['44eKcVXzFoc', 'Alonso Romero'], + ['JpbtAb-f3JA', 'Dollar Gill'], + ['Hvrm3efPYIA', 'Delaney Van'], + ['4E0dknSrQVU', 'Matteo Maretto'], + ['krNP2ESq-54', 'Joshua Bedford'], + ['DPNrBT1WCMs', 'Egor Lyfar'], + ['Yy-dHQP-Ax0', 'Markus Spiske'], + ['BKSntHf8oiU', 'Melissa Walker Horn'], + ['asaGSZEyltQ', 'Noora AlHammadi'], + ['U3hCd1S7FQ4', 'Louis Hansel @shotsoflouis'], + ['VOpJTnP6S9g', 'Liana Mikah'], + ['9TWavGempJc', 'Ashleigh Robertson'], + ['I2tgHl69Dco', 'Louis Hansel @shotsoflouis'], + ['fyQr1T3GE34', 'Petr Sevcovic'], + ['pGe5mc4Eip8', 'Daniel Park'], + ['gZsgKrNc8es', 'Dan Gold'], + ['XPSXhLx143g', 'Wouter Meijering'], + ['VpxavZd4S-I', 'Mr Lemon'], + ['9vHOhKoNlNw', 'Edrece Stansberry'], + ['uScYRjZ2ol8', 'donald modeste'], + ['fCE-pTmFrPI', 'Valeriu Bondarenco'], + ['1ZTccDpF71k', 'Austin Paquette'], + ['Au-LzDMd_Cw', 'Stephanie McCabe'], + ['9cfHdC2Asak', 'cindy fernandez'], + ['4-rmvQRL2nY', 'Annie Spratt'], + ['8Yvwy4Kbd1g', 'Alex Kondratiev'], + ['4PtChDuxsDI', 'Dronile Hiraldo'], + ['Mi1SNlsyWAk', 'Louis Hansel @shotsoflouis'], + ['nn0whk6nzv4', 'Yulia Chinato'], + ['8JIIoSFtEbo', 'Massimo Virgilio'], + ['44eKcVXzFoc', 'Alonso Romero'], + ['JpbtAb-f3JA', 'Dollar Gill'], + ['Hvrm3efPYIA', 'Delaney Van'], + ['4E0dknSrQVU', 'Matteo Maretto'], + ['krNP2ESq-54', 'Joshua Bedford'], + ['DPNrBT1WCMs', 'Egor Lyfar'], + ['Yy-dHQP-Ax0', 'Markus Spiske'], + ['BKSntHf8oiU', 'Melissa Walker Horn'], + ['asaGSZEyltQ', 'Noora AlHammadi'], + ['U3hCd1S7FQ4', 'Louis Hansel @shotsoflouis'], + ['VOpJTnP6S9g', 'Liana Mikah'], + ['9TWavGempJc', 'Ashleigh Robertson'], + ['I2tgHl69Dco', 'Louis Hansel @shotsoflouis'], + ['fyQr1T3GE34', 'Petr Sevcovic'], + ['pGe5mc4Eip8', 'Daniel Park'], + ['gZsgKrNc8es', 'Dan Gold'], + ], + candy: [ + ['51AhxwkYyHo', 'Viktor Forgacs'], + ['rS1GogPLVHk', 'Eaters Collective'], + ['9m6NQHyxk-s', 'Brooke Lark'], + ['DOmaKmeCp_8', 'Sarah Takforyan'], + ['aCPOKOb4qis', 'Jamie Street'], + ['On8Ov1TItnU', 'QooQee'], + ['KN7tpVQCmWA', 'Nick Fewings'], + ['9lmFbtJ2QzM', 'Tim Gouw'], + ['54hUU5pNSvo', 'Erol Ahmed'], + ['zYH4SubXCWY', 'Наталья Горох'], + ['wJHJY7PcDcg', 'Andrew Itaga'], + ['BnusUP5jydc', 'Marc Markstein'], + ['NI_fJ15rIfI', 'Szabo Viktor'], + ['LymVMRIUwPQ', 'Happy Films'], + ['mrNVnLEphdo', 'Greg Nunes'], + ['FKvoEKSV2LY', 'zhou yu'], + ['CKLF34baCTQ', 'Willian Justen de Vasconcellos'], + ['7uGCN9qshsY', 'Siora Photography'], + ['xBTnaTgleQE', 'Glen Carrie'], + ['sC_HExGwbhI', 'Erik Mclean'], + ['1gViVVlEaPc', 'Sérgio André'], + ['k9yY0XZTSnI', 'Fernando Hernandez'], + ['h1_R9-o9an0', 'emrecan arık'], + ['XB0ha-DSGoU', 'Laura Briedis'], + ['ONn4OfAnxZY', 'Monique Carrati'], + ['24p9dPeXdFA', 'Frederic Köberl'], + ['GL6J_sFYHLw', 'Yuiizaa September'], + ['LU_fCezP9-o', 'Amit Lahav'], + ['CSkAj_XqOVc', 'Mockaroon'], + ['xLvIcAYuuMQ', 'Luis Aguila'], + ], + fruit: [ + ['ZLc9yTIFzNk', 'Kelly Sikkema'], + ['DoxGtpAsdYY', 'Sanni Sahil'], + ['lyEkpuuIrg0', 'kaouther djouada'], + ['fDx4zHpnlOM', 'Nanxi wei'], + ['0AXNt5SdeXU', 'Sara Cervera'], + ['s05XKB6jK2c', 'Markus Spiske'], + ['bPMyJzKhCyA', 'Louis Hansel @shotsoflouis'], + ['4jeWN4puDrw', 'Carolyn Leber'], + ['8EScigZC6AU', 'MF Evelyn'], + ['1QbosWMxOx0', 'elCarito'], + ['NWA3s8r-1bc', 'Fli Hi'], + ['TfNUmbaIjj8', 'Sophie Dale'], + ['duskNBVv420', 'Louis Hansel @shotsoflouis'], + ['QYupKZjYDbw', 'Moritz Kindler'], + ['ozcHSq3XfVs', 'Callum Blacoe'], + ['ucY_-U1dM8U', 'Louis Hansel @shotsoflouis'], + ['4WLc_dWE-kc', 'Dmitry Mishin'], + ['BTqDasyX62E', 'Priscilla Du Preez'], + ['SD5dOSkhQdU', 'Alexander Mils'], + ['4pJekgmSmPM', 'Rinck Content Studio'], + ['0XGWys_GaFo', 'Toa Heftiba'], + ['uTZvsJsylYc', 'Louis Hansel @shotsoflouis'], + ['vAHaYh5s_Sc', 'RAPHAEL MAKSIAN'], + ['0N4A1c5tJSs', 'Miguel Vaz'], + ['VAhUq30sW0c', 'Cody Berg'], + ['qbO7Mlhq8PQ', 'JOSHUA COLEMAN'], + ['dLB32q_bRs0', 'Jason Leung'], + ['66itnKdGlC4', 'Fateme Azimi'], + ['_DYRZHbCIq0', 'Will Mcmahon'], + ], + landscapes: [ + ['FuaNmJPLAHg', 'MINSUN KIM'], + ['8flZ753v87Q', 'Ken Schlabach'], + ['6qdO_nFhYoI', 'Clay Banks'], + ['VMmPxFmfwfQ', 'Xiaopeng Ma'], + ['DAufF3R8B5Q', 'Sangga Rima Roman Selia'], + ['5U9n9gip7VY', 'Annie Spratt'], + ['Tot7FLHWotI', 'elCarito'], + ['vph4L_OjhuI', 'Marek Piwnicki'], + ['Nd8h38tRIlc', 'Kevin Horvat'], + ['nACRxCEAut8', 'ALEKSEY KUPRIKOV'], + ['PjyfZ-Ujut8', 'Denys Nevozhai'], + ['X6utHocVX8w', 'Ian Williams'], + ['cq6g1WssaJI', 'Tyler Casey'], + ['U1ad7OjdHx8', 'Dave Herring'], + ['si5673B4PIE', 'Annie Spratt'], + ['shv8H0Dv5iE', 'Aditya Chinchure'], + ['hjVrWFVyOqE', 'chuttersnap'], + ['_skrrHIcsEk', 'Edward Ma'], + ['GMNtcgo-KdA', 'Yoav Hornung'], + ['313dueuxHls', 'Jamie Street'], + ['SncOlKOY7NY', 'Ward Mercer'], + ['7R11NCmOY6k', 'Ivan Bandura'], + ['YFwOiSisSLM', 'Krzysztof Kotkowicz'], + ['gbO7zCOUJIg', 'Brian Kyed'], + ['5iwG4xlUgJ4', 'Jason Leung'], + ['G_PSim-dsvU', 'Alexandra Jitariuc'], + ['gVl6Hsi_pxo', 'Jean-Philippe Delberghe'], + ['p8h0_0pyW9k', 'Bence Balla-Schottner'], + ], + waterfalls: [ + ['Cwd0zYOIClY', 'Thomas Ensley'], + ['dGCHAo7mb2Q', 'Jamie Melville'], + ['bukitR21NO8', 'Jen Theodore'], + ['IKIY5bpd9eM', 'Bruno Kelzer'], + ['rrVm3b-uxkk', 'Jamie Melville'], + ['UegnUIW76gQ', 'Jamie Melville'], + ['d0cvao4fC6Q', 'Forest Simon'], + ['N8r2hH1siEY', 'Lester Hine'], + ['oRmeWoJx_nY', 'Michael Olsen'], + ['jVjwlfyJAkA', 'Tom Wheatley'], + ['ar6PYxBFgis', 'Gerald Berliner'], + ['9fpK7fPw6Is', 'Jamie Melville'], + ['jeXkw2HR1SU', 'Julia Caesar'], + ['Evs5MnlmUXY', 'Damian Kamp'], + ['phstcH4QKJc', 'Jakob Owens'], + ['7dmDlBfB9Vk', 'Spencer Watson'], + ['SHhaNnNR1xo', 'SaiKrishna Saketh'], + ['_6ghImrDiVU', 'Daniel H. Tong'], + ['gfaXzDmMY7M', 'RADIN AHMAD MUJAHID'], + ['rFbK1PP9LEA', 'Daniel Hernandez'], + ['s6Tv7b4SAoo', 'Miltiadis Fragkidis'], + ['ir9RUDjVpPo', 'Clay Banks'], + ['7emiteIwfuk', 'yunchuan luo'], + ['9tqrKo4B98I', 'Austin Farrington'], + ['dfazzUCjMro', 'oakie'], + ['4HEuHmA7WfM', 'Ketan Morris'], + ['eG1_rPekhTk', 'Jack B'], + ['_up7EDGdTqU', 'Hari Nandakumar'], + ['MhIPEJmVwaA', 'Karthik Chandran'], + ], + travel: [ + ['gC2Q_Tfub6c', 'James Lee'], + ['1Tcu61Qz7c0', 'Payas'], + ['eLMJ2x7s9ak', 'Alexandre Chambon'], + ['aNrRsB2wLDk', 'Ahmet Yalçınkaya'], + ['xu2WYJek5AI', 'Anastasia Petrova'], + ['xRoexKKv8fs', 'Robson Hatsukami Morgan'], + ['qdIer4A0afE', 'Camille Brodard'], + ['0lPZBa6-1J8', 'Torbjorn Sandbakk'], + ['CLm3pWXrS9Q', 'Tim Trad'], + ['D1fs2x11_jk', 'Raul Taciu'], + ['BZ3aE3ouAfc', 'Petr Sevcovic'], + ['_OUvt8kLf0s', 'GAWN AUSTRALIA'], + ['T_6yJJQ_-wA', 'Square Lab'], + ['1Z_mX3zzEBc', 'Andy Pearce'], + ['lWHJwoSZf7M', 'Joseph Costa'], + ['O6Euhw7NMbQ', 'Cosmic Timetraveler'], + ['bs1eqd6zSiU', 'Harley-Davidson'], + ['_qkuc1V9Gbg', 'Ken Cheung'], + ], + cars: [ + ['w5SZe8hoqlk', 'Rachel Lynette French'], + ['Nwk0ye_Y_As', 'Lex Valishvili'], + ['P9yqMy_9ZaY', 'Anastasia Dulgier'], + ['aGwBtbncMWo', 'Sandra Kaas'], + ['jV_QaRjbhWE', 'Clem Onojeghuo'], + ['SwmaJDvasuU', 'Gunnar Ridderström'], + ['BsJovWrQ7vE', 'Caroline'], + ['3t0Q-GYXE0U', 'Austin Park'], + ['tlCzxI2RQAc', 'Vinícius Henrique'], + ['fecFQcxsUok', 'Zachary Spears'], + ['upttrzCo8-U', 'takahiro taguchi'], + ['zoHustBfNxA', 'Lukas Werner'], + ['UZH69YA50qY', 'Austin Park'], + ['tJwY80NMkhk', 'Hanny Naibaho'], + ['esfNTaHsR1c', 'Willian Justen de Vasconcellos'], + ['Tas1kF-6aNA', 'Tyler Casey'], + ['7nrsVjvALnA', 'Denys Nevozhai'], + ['CRf3KYexpCA', 'Will Truettner'], + ['B8JpogxOnyw', 'Mike Von'], + ['mU5vnpJW_CQ', 'Luke Tanis'], + ['GaRPxo9Z86M', 'Court Cook'], + ['Orp-VAQ_gNA', 'Angello Lopez'], + ['q4UZ53rYYy0', 'Ali Moharami'], + ['XA2OEcvrrP8', 'Alexander Pidgeon'], + ['hXMv04v_py0', 'Patrick Schöpflin'], + ['G7sWGEF8pRc', 'Julian Hochgesang'], + ['PUnARRf-rE8', 'jean wimmerlin'], + ['UIk-rF4Df60', 'Igor Bumba'], + ['0dW-eQVL0WQ', 'Michael Heuser'], + ], + aviation: [ + ['bw1hXT_okL4', 'sayhitobel'], + ['ecOXN5jGtaU', 'Alexander Lobanov'], + ['rFujmwrNv1w', 'Abe Drzycimski'], + ['EKYsla2fER8', 'Paola Aguilar'], + ['1uiXp6fXd3w', 'Kevin Hackert'], + ['tVzGTraJ4T0', 'Samantha Gades'], + ['5h1-cHxJz1I', 'Nikli Nodin'], + ['u-1cYIua_aI', 'John Torcasio'], + ['l2OusPPMLxs', 'yvette Wynne'], + ['SUIvWFHBZas', 'Timothy Newman'], + ['Tvrnezn1N6g', 'Daniel Eledut'], + ['ZN0469D3v98', 'Jason Hafso'], + ['LNmKCmZ2pcI', 'Ricardo Resende'], + ['P_ExwnnPGyM', 'Tudose Alexandru'], + ['1vyWYdyUDGU', 'Ian Cumming'], + ['k102QVrpitQ', 'Jake Blucker'], + ['eB8-XtMtqZI', 'Miguel Ángel Sanz'], + ['1XDZavWyows', 'Samuel Sianipar'], + ['7WHD-pdECGU', 'Miguel Ángel Sanz'], + ['CN8HsCUCjUQ', 'Cameron Kitson'], + ['q_rNy9pRe78', 'Walter Walraven'], + ['a9SA6Zs1L9g', 'Tim Dennert'], + ['JW50PRr5UbI', 'Roland O'], + ['um_6nrOnPZ4', 'Pascal Meier'], + ['EpxT58kpBhc', 'Terence Burke'], + ['3fbjHj2k0vE', 'Dušan Smetana'], + ['9o4p3eCcRBE', 'Jeremy Bishop'], + ['sbfLwfoVX7E', 'Nour Betar'], + ['4VCJBAtlnNI', 'sippakorn yamkasikorn'], + ['LcbsrFbqwGk', 'Angel Barnes'], + ], + nature: [ + ['io0umElGQwU', 'Fabrizio Forte'], + ['lh2sQ_L3GaA', 'Daniel Lord'], + ['Gyr7o3BMsSc', 'Dave Francis'], + ['lRipDukRpd4', 'Kevin'], + ['GxymWkdnl4Y', 'Fernando Jorge'], + ['rWnw7JL0LPE', 'Rahul Gupta'], + ['YKuYS3ChMyw', 'Yuliya Kosolapova'], + ['ZsUjLYqbNz8', 'Pau Morfín'], + ['x2o3HX6RKG8', 'Dave'], + ['Few073pm6aQ', 'Dennis Bertuch'], + ['jP-wvLA7uyg', 'Gary Butterfield'], + ['euhs3wanL-I', 'Andrew Stapleton'], + ['UHt2TG_CHO0', 'Wolfgang Hasselmann'], + ['SAIb5NkbAaE', 'Alexis Gethin'], + ['tdNjdIW-_OY', 'Lachlan'], + ['0NtjFr0-DwM', 'Jane Carmona'], + ['fhg_nm1sufU', 'Pascal Debrunner'], + ['HJUDECvtxZE', 'jose alfonso sierra'], + ['8vS1CwHIEH8', 'Serge Sustavov'], + ['oTTnZcwQ2t4', 'Daniel Radford'], + ['TNhs9udas8Q', 'Migsar Navarro'], + ['zT9g3crTPcE', 'Devin L'], + ['hYuNG1zptM4', 'Roberto Gamito'], + ['DfOQRep1LKU', 'Wolfgang Rottmann'], + ['2sT_iJhIcEg', 'Anamika Pokharel'], + ['Zjcl2vaeGdo', 'Lucas Mitchell'], + ['b2T6zgFAW1E', 'Theme Inn'], + ['EnFQmcTtsjo', 'Yang Jing'], + ['PWI-CDVynhw', 'Thomas Galler'], + ['gTvLsDi-Uzo', 'Arun Raj'], + ], + trees: [ + ['aSCx7M1E4Vo', 'Markos Mant'], + ['3B9MYDjQ-EQ', 'Firasat Durrani'], + ['39oaRqfUacc', 'Nick'], + ['Y0NLa_KmEgU', 'Michael Ankes'], + ['i6nGVkbbUT4', 'Henry McIntosh'], + ['_WITb4g1ImY', 'Mark Basarab'], + ['prd5CXMsD68', 'JOHN TOWNER'], + ['avxSevmCKfw', 'Marta Pawlik'], + ['LZf0HMPg-3I', 'Max Brinton'], + ['Ovjx9FsXX9c', 'Andy Fitzsimon'], + ['YPtfMXRMUe0', 'Corey Agopian'], + ['h3y3XPXKoeY', 'Elke Karin Lugert'], + ['qeGC1zrsG8U', 'Tycho Atsma'], + ['dAkd3xaSDTA', 'Nick West'], + ['Prd-KB7CKo4', 'Austin Neill'], + ['iqu9ZTwTfVM', 'Clem Onojeghuo'], + ['klNpWLkgezo', 'Wil Stewart'], + ['Pm4U5IqI4dM', 'Lena Derevianko'], + ['5i664o1oY4c', 'Yannick Pulver'], + ['uoMFPm5_Xg8', 'Yoni Kozminsi'], + ['Qr2dZRNdf_M', 'Afifi Zulkifle'], + ['jxljuYqn8uU', 'Sebastian Pichler'], + ['H3mO3zNlvNU', 'Jonathan Knepper'], + ['pEb-Xf_qM0s', 'Martin Reisch'], + ['prSogOoFmkw', 'Braden Jarvis'], + ['y9csmronT3s', 'Alberto Restifo'], + ['R8R9H_xuvBY', 'Andrew Pons'], + ['fuT-m1yzUG4', 'Ales Krivec'], + ], + wildlife: [ + ['Tot7FLHWotI', 'elCarito'], + ['-I0EgcZdV0E', 'Maarten van den Heuvel'], + ['F21zt7_icpo', 'Tevin Trinh'], + ['gMxgp-MwCyQ', 'James Wainscoat'], + ['FFlb5Uj3vhc', 'Sigmund'], + ['hglSMjdh83M', 'Srinivasan Venkataraman'], + ['UgWcvfl4q5I', 'NOAA'], + ['PZ1nEPFNBJQ', 'Third Idea'], + ['f4yYs5P5GbA', 'rigel'], + ['e94T5ag-9x0', 'Jayden Brand'], + ['6Fcllk7ze_Q', 'Vivian Arcidiacono'], + ['O7G3II8E2Eo', 'Erik-Jan Leusink'], + ['-6UNL6Ghn_c', 'Silvio Kundt'], + ['EyZMGYn1Uj4', 'AGL Fotos'], + ['nxR7gvSokH8', 'Waldemar Brandt'], + ['sAGXVK6bNFc', 'Amar Yashlaha'], + ['8zLCXDWETEg', 'Clément ROY'], + ['4nPFQ2sUhUE', 'Justin Porter'], + ['mUNDTQrfnSk', 'Zahrin Lukman'], + ['VXcX0Joa09k', 'Max Rovensky'], + ['F_HycxA2lwc', 'Marthijn Brinks'], + ['ss01halnU4I', 'Dušan Smetana'], + ['lktWv61WoNI', 'Rory Lindholm'], + ['myeQ2RH1PX0', 'Alexander Ross'], + ['dhIAyAmfjz8', 'Third Idea'], + ['AGprQpF4STo', 'Jeff Lemond'], + ['P8pBJQVt4UA', 'Christer Gundersen'], + ['nt8Ek7sRgdA', 'Mikell Darling'], + ['YHv0BDThVOw', 'Nicholas Doherty'], + ], + interior: [ + ['9wGKENQ-qTI', 'Kunj Parekh'], + ['x3BCSWCAtrY', 'yann maignan'], + ['EF6z_6R94zQ', 'Orlova Maria'], + ['dC8NC2QBFyQ', 'Kari Shea'], + ['A_AuirVquQY', 'Abbie Bernet'], + ['IJf2v-StB4Y', 'yann maignan'], + ['frsN89Tf-1Q', 'Joshua Eckstein'], + ['s95oB2n9jng', 'Aaron Huber'], + ['n_vdmdtNh6M', 'Tiplada Mekvisan'], + ['GbiVL6t4T-o', 'gdtography'], + ['xx0oSB1YxRE', 'Joseph Albanese'], + ['McaNtoPEEcg', 'Brooke Cagle'], + ['s65VlkIYSDw', 'Robert Bye'], + ['pEaBEqXXk-M', 'Aw Creative'], + ['GaX7QTgs8pg', 'Waldemar Brandt'], + ['b75FBg97dU0', 'Bao Menglong'], + ['5i0GnoTTjSE', 'Philipp Berndt'], + ['buhmhprfo3g', 'Kara Eads'], + ['L7EwHkq1B2s', 'Kara Eads'], + ['gBdirnalxcQ', 'Nick Fewings'], + ['ILgaxpiQu0', 'Matteo Maretto'], + ['ydcMwcfY5E0', 'Clark Street Mercantile'], + ['zLT3VqWEgOQ', 'Sidekix Media'], + ['Yg0Rds6_TsY', 'Sergiu Cindea'], + ['gbS_fhrFo10', 'Bekah Russom'], + ['o3c-euNd_ZM', 'Alessandra Caretto'], + ['IEkMMvdZFc0', 'Nick Hillier'], + ['hnhE83RhAWg', 'Stefan Steinbauer'], + ], + statues: [ + ['ig8E7Mlrl7Y', 'Vidar Nordli-Mathisen'], + ['1ak3Z7ZmtQA', 'Ryan Lum'], + ['ntPF02wcTY', 'Gigi'], + ['5_i4OPeOAZU', 'Viktor Forgacs'], + ['iRON0g6iO0k', 'Alexandre Chambon'], + ['PhQ4CpXLEX4', 'Daniels Joffe'], + ['EfHqouvZU2Y', 'Bettina Otott Kovács'], + ['kaEhf0eZme8', 'Nils'], + ['4-4IDc21Gto', 'K. Mitch Hodge'], + ['Y8Xh7ZJFU5A', 'Vidar Nordli-Mathisen'], + ['fmawALmMLSA', 'Gabriel TRESCH'], + ['DUp4B6M0AMc', 'Sebastien'], + ['MFZUY4gqvA4', 'James Yarema'], + ['50vvwcNFFzU', 'Robert Anasch'], + ['TMRi8cD2umM', 'Frank Eiffert'], + ['wLx_WCkWvHg', 'Chris A. Tweten'], + ['Twoj21Av-so', 'Arthur Reeder'], + ['EiGDn8cwU4Y', 'Tessa Rampersad'], + ['fVY6UxZuECA', 'Christine Wehrmeier'], + ['uJdTBTJ9rbo', 'Christine Wehrmeier'], + ['4eEBFTBKx5E', 'Ralph Spandl'], + ['HtQRGemW_40', 'Ivan Bertona'], + ['uHBcinxOLhQ', 'K. Mitch Hodge'], + ['2TmsyZXMNTE', 'Emma Fabbri'], + ['9KkPloRgOUY', 'Matteo Maretto'], + ['KzPefInJW58', 'JOSHUA COLEMAN'], + ['szVTIkisN1M', 'David Siglin'], + ['iRzEPkYSETQ', 'Francois Olwage'], + ], + technology: [ + ['QpTCSHzhWuo', 'Joshua Hoehne'], + ['0lMpQaXfOCg', 'Barrett Ward'], + ['w33-zg-dNL4', 'Rami Al-zayat'], + ['MC5WbGo_bZM', 'Tom Pumford'], + ['iHJ7xouUyXs', 'Amith Nair'], + ['E3I9thV98kQ', 'Tatiana Lapina'], + ['JuUK7Er9nR4', 'Mohamed Boumaiza'], + ['Dei5oAC_wJc', 'Kenny Luo'], + ['ltwEbf_G9bs', 'Mario Caruso'], + ['gWdlDR4WpV4', 'Zarak Khan'], + ['0Um6Yr1cyx0', 'Antoine Beauvillain'], + ['_8S9nEmCZK0', 'Oliur'], + ['etFrnBJS1qc', 'NeONBRAND'], + ['ZMVtx_KJtOk', 'Thought Catalog'], + ['JNuKyKXLh8U', 'Noiseporn'], + ['5gzr-RM-rZM', 'Kenny Luo'], + ['eWaXmZsXKDs', 'Zane Lee'], + ['4qGbMEZb56c', 'Thomas William'], + ['hwqWxHoH2wk', 'Markus Spiske'], + ['vZJdYl5JVXY', 'Kaitlyn Baker'], + ['Lg8xTZjs6Lg', 'Marc Mueller'], + ['M5HQPjXrjlQ', 'Matt Hoffman'], + ['A-b37b-CrYE', 'Kenny Luo'], + ['Kj2SaNHG-hg', 'Christopher Burns'], + ['A1v0-iH3T5A', 'Patrick Hendry'], + ['iFBIdX54BOk', 'Keagan Henman'], + ], }) - .map(([category, photos]) => photos.map(([photo, copyright], index) => ({ - id: nanoid(), - path: `sfw/${category}/${photo}.jpeg`, - thumbnail: `sfw/${category}/thumbs/${photo}.jpeg`, - lazy: `sfw/${category}/lazy/${photo}.jpeg`, - mime: 'image/jpeg', - sfw_media_id: null, - group: category, - index, - copyright, - comment: `Courtesy of ${copyright}`, - }))) - .flat(); + .map(([category, photos]) => photos.map(([photo, copyright], index) => ({ + id: nanoid(), + path: `sfw/${category}/${photo}.jpeg`, + thumbnail: `sfw/${category}/thumbs/${photo}.jpeg`, + lazy: `sfw/${category}/lazy/${photo}.jpeg`, + mime: 'image/jpeg', + sfw_media_id: null, + group: category, + index, + copyright, + comment: `Courtesy of ${copyright}`, + }))) + .flat(); /* eslint-disable max-len */ exports.seed = knex => Promise.resolve() - .then(async () => { - const { inserted } = await upsert('media', sfw, 'path'); + .then(async () => { + const { inserted } = await upsert('media', sfw, 'path'); - const sfwMediaIds = inserted.map(mediaEntry => ({ - id: nanoid(), - media_id: mediaEntry.id, - })); + const sfwMediaIds = inserted.map(mediaEntry => ({ + id: nanoid(), + media_id: mediaEntry.id, + })); - await upsert('media_sfw', sfwMediaIds, 'media_id'); - }) - .then(async () => { - const tagMedia = tagPosters.concat(tagPhotos); + await upsert('media_sfw', sfwMediaIds, 'media_id'); + }) + .then(async () => { + const tagMedia = tagPosters.concat(tagPhotos); - const tags = await knex('tags').whereIn('slug', tagMedia.map(item => item.tagSlug)); + const tags = await knex('tags').whereIn('slug', tagMedia.map(item => item.tagSlug)); - const { inserted, updated } = await upsert('media', tagMedia.map(({ - id, path, thumbnail, lazy, mime, index, comment, - }) => ({ - id, path, thumbnail, lazy, mime, index, comment, - })), 'path', knex); + const { inserted, updated } = await upsert('media', tagMedia.map(({ + id, path, thumbnail, lazy, mime, index, comment, + }) => ({ + id, path, thumbnail, lazy, mime, index, comment, + })), 'path', knex); - const tagIdsBySlug = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.id }), {}); - const mediaIdsByPath = inserted.concat(updated).reduce((acc, item) => ({ ...acc, [item.path]: item.id }), {}); + const tagIdsBySlug = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.id }), {}); + const mediaIdsByPath = inserted.concat(updated).reduce((acc, item) => ({ ...acc, [item.path]: item.id }), {}); - const tagPosterEntries = tagPosters.map(poster => ({ - tag_id: tagIdsBySlug[poster.tagSlug], - media_id: mediaIdsByPath[poster.path], - })); + const tagPosterEntries = tagPosters.map(poster => ({ + tag_id: tagIdsBySlug[poster.tagSlug], + media_id: mediaIdsByPath[poster.path], + })); - const tagPhotoEntries = tagPhotos.map(photo => ({ - tag_id: tagIdsBySlug[photo.tagSlug], - media_id: mediaIdsByPath[photo.path], - })); + const tagPhotoEntries = tagPhotos.map(photo => ({ + tag_id: tagIdsBySlug[photo.tagSlug], + media_id: mediaIdsByPath[photo.path], + })); - return Promise.all([ - upsert('tags_posters', tagPosterEntries, 'tag_id', knex), - upsert('tags_photos', tagPhotoEntries, ['tag_id', 'media_id'], knex), - ]); - }); + return Promise.all([ + upsert('tags_posters', tagPosterEntries, 'tag_id', knex), + upsert('tags_photos', tagPhotoEntries, ['tag_id', 'media_id'], knex), + ]); + }); diff --git a/src/.eslintrc b/src/.eslintrc index 686fcae75..cb1bf23de 100644 --- a/src/.eslintrc +++ b/src/.eslintrc @@ -6,10 +6,11 @@ }, "rules": { "strict": 0, + "indent": ["error", "tab"], + "no-tabs": "off", "no-unused-vars": ["error", {"argsIgnorePattern": "^_"}], "no-console": 0, "no-underscore-dangle": 0, - "indent": "off", "prefer-destructuring": "off", "template-curly-spacing": "off", "object-curly-newline": "off", diff --git a/src/actors-legacy.js b/src/actors-legacy.js index e93193c57..246e9f8b1 100644 --- a/src/actors-legacy.js +++ b/src/actors-legacy.js @@ -18,522 +18,522 @@ const { curateSites } = require('./sites'); const { storeMedia, associateMedia } = require('./media'); async function curateActor(actor) { - const [aliases, avatar, photos, social] = await Promise.all([ - knex('actors').where({ alias_for: actor.id }), - knex('actors_avatars') - .where('actor_id', actor.id) - .join('media', 'media.id', 'actors_avatars.media_id') - .first(), - knex('actors_photos') - .where('actor_id', actor.id) - .join('media', 'media.id', 'actors_photos.media_id') - .orderBy('index'), - knex('actors_social') - .where('actor_id', actor.id) - .orderBy('platform', 'desc'), - ]); + const [aliases, avatar, photos, social] = await Promise.all([ + knex('actors').where({ alias_for: actor.id }), + knex('actors_avatars') + .where('actor_id', actor.id) + .join('media', 'media.id', 'actors_avatars.media_id') + .first(), + knex('actors_photos') + .where('actor_id', actor.id) + .join('media', 'media.id', 'actors_photos.media_id') + .orderBy('index'), + knex('actors_social') + .where('actor_id', actor.id) + .orderBy('platform', 'desc'), + ]); - const curatedActor = { - id: actor.id, - gender: actor.gender, - name: actor.name, - description: actor.description, - birthdate: actor.birthdate && new Date(actor.birthdate), - country: actor.country_alpha2, - origin: (actor.birth_city || actor.birth_state || actor.birth_country_alpha2) ? {} : null, - residence: (actor.residence_city || actor.residence_state || actor.residence_country_alpha2) ? {} : null, - ethnicity: actor.ethnicity, - height: actor.height, - weight: actor.weight, - bust: actor.bust, - waist: actor.waist, - hip: actor.hip, - naturalBoobs: actor.natural_boobs, - aliases: aliases.map(({ name }) => name), - slug: actor.slug, - avatar, - photos, - hasTattoos: actor.has_tattoos, - hasPiercings: actor.has_piercings, - tattoos: actor.tattoos, - piercings: actor.piercings, - social, - scrapedAt: actor.scraped_at, - }; + const curatedActor = { + id: actor.id, + gender: actor.gender, + name: actor.name, + description: actor.description, + birthdate: actor.birthdate && new Date(actor.birthdate), + country: actor.country_alpha2, + origin: (actor.birth_city || actor.birth_state || actor.birth_country_alpha2) ? {} : null, + residence: (actor.residence_city || actor.residence_state || actor.residence_country_alpha2) ? {} : null, + ethnicity: actor.ethnicity, + height: actor.height, + weight: actor.weight, + bust: actor.bust, + waist: actor.waist, + hip: actor.hip, + naturalBoobs: actor.natural_boobs, + aliases: aliases.map(({ name }) => name), + slug: actor.slug, + avatar, + photos, + hasTattoos: actor.has_tattoos, + hasPiercings: actor.has_piercings, + tattoos: actor.tattoos, + piercings: actor.piercings, + social, + scrapedAt: actor.scraped_at, + }; - if (curatedActor.birthdate) { - curatedActor.age = moment().diff(curatedActor.birthdate, 'years'); - } + if (curatedActor.birthdate) { + curatedActor.age = moment().diff(curatedActor.birthdate, 'years'); + } - if (actor.birth_city) curatedActor.origin.city = actor.birth_city; - if (actor.birth_state) curatedActor.origin.state = actor.birth_state; + if (actor.birth_city) curatedActor.origin.city = actor.birth_city; + if (actor.birth_state) curatedActor.origin.state = actor.birth_state; - if (actor.birth_country_alpha2) { - curatedActor.origin.country = { - alpha2: actor.birth_country_alpha2, - name: actor.birth_country_name, - alias: actor.birth_country_alias, - }; - } + if (actor.birth_country_alpha2) { + curatedActor.origin.country = { + alpha2: actor.birth_country_alpha2, + name: actor.birth_country_name, + alias: actor.birth_country_alias, + }; + } - if (actor.residence_city) curatedActor.residence.city = actor.residence_city; - if (actor.residence_state) curatedActor.residence.state = actor.residence_state; + if (actor.residence_city) curatedActor.residence.city = actor.residence_city; + if (actor.residence_state) curatedActor.residence.state = actor.residence_state; - if (actor.residence_country_alpha2) { - curatedActor.residence.country = { - alpha2: actor.residence_country_alpha2, - name: actor.residence_country_name, - alias: actor.residence_country_alias, - }; - } + if (actor.residence_country_alpha2) { + curatedActor.residence.country = { + alpha2: actor.residence_country_alpha2, + name: actor.residence_country_name, + alias: actor.residence_country_alias, + }; + } - return curatedActor; + return curatedActor; } function curateActors(releases) { - return Promise.all(releases.map(async release => curateActor(release))); + return Promise.all(releases.map(async release => curateActor(release))); } function curateActorEntry(actor, scraped, scrapeSuccess) { - const curatedActor = { - name: capitalize(actor.name), - slug: slugify(actor.name), - birthdate: actor.birthdate, - description: actor.description, - gender: actor.gender, - ethnicity: actor.ethnicity, - bust: actor.bust, - waist: actor.waist, - hip: actor.hip, - natural_boobs: actor.naturalBoobs, - height: actor.height, - weight: actor.weight, - hair: actor.hair, - eyes: actor.eyes, - has_tattoos: actor.hasTattoos, - has_piercings: actor.hasPiercings, - tattoos: actor.tattoos, - piercings: actor.piercings, - }; + const curatedActor = { + name: capitalize(actor.name), + slug: slugify(actor.name), + birthdate: actor.birthdate, + description: actor.description, + gender: actor.gender, + ethnicity: actor.ethnicity, + bust: actor.bust, + waist: actor.waist, + hip: actor.hip, + natural_boobs: actor.naturalBoobs, + height: actor.height, + weight: actor.weight, + hair: actor.hair, + eyes: actor.eyes, + has_tattoos: actor.hasTattoos, + has_piercings: actor.hasPiercings, + tattoos: actor.tattoos, + piercings: actor.piercings, + }; - if (actor.id) { - curatedActor.id = actor.id; - } + if (actor.id) { + curatedActor.id = actor.id; + } - if (actor.birthPlace) { - curatedActor.birth_city = actor.birthPlace.city; - curatedActor.birth_state = actor.birthPlace.state; - curatedActor.birth_country_alpha2 = actor.birthPlace.country; - } + if (actor.birthPlace) { + curatedActor.birth_city = actor.birthPlace.city; + curatedActor.birth_state = actor.birthPlace.state; + curatedActor.birth_country_alpha2 = actor.birthPlace.country; + } - if (actor.residencePlace) { - curatedActor.residence_city = actor.residencePlace.city; - curatedActor.residence_state = actor.residencePlace.state; - curatedActor.residence_country_alpha2 = actor.residencePlace.country; - } + if (actor.residencePlace) { + curatedActor.residence_city = actor.residencePlace.city; + curatedActor.residence_state = actor.residencePlace.state; + curatedActor.residence_country_alpha2 = actor.residencePlace.country; + } - if (scraped) { - curatedActor.scraped_at = new Date(); - curatedActor.scrape_success = scrapeSuccess; - } + if (scraped) { + curatedActor.scraped_at = new Date(); + curatedActor.scrape_success = scrapeSuccess; + } - return curatedActor; + return curatedActor; } function curateSocialEntry(url, actorId) { - const platforms = [ - // links supplied by PH often look like domain.com/domain.com/username - { - label: 'twitter', - pattern: 'http(s)\\://(*)twitter.com/:username(/)(?*)', - format: username => `https://www.twitter.com/${username}`, - }, - { - label: 'youtube', - pattern: 'http(s)\\://(*)youtube.com/channel/:username(?*)', - format: username => `https://www.youtube.com/channel/${username}`, - }, - { - label: 'instagram', - pattern: 'http(s)\\://(*)instagram.com/:username(/)(?*)', - format: username => `https://www.instagram.com/${username}`, - }, - { - label: 'snapchat', - pattern: 'http(s)\\://(*)snapchat.com/add/:username(/)(?*)', - format: username => `https://www.snapchat.com/add/${username}`, - }, - { - label: 'tumblr', - pattern: 'http(s)\\://:username.tumblr.com(*)', - format: username => `https://${username}.tumblr.com`, - }, - { - label: 'onlyfans', - pattern: 'http(s)\\://(*)onlyfans.com/:username(/)(?*)', - format: username => `https://www.onlyfans.com/${username}`, - }, - { - label: 'fancentro', - pattern: 'http(s)\\://(*)fancentro.com/:username(/)(?*)', - format: username => `https://www.fancentro.com/${username}`, - }, - { - label: 'modelhub', - pattern: 'http(s)\\://(*)modelhub.com/:username(/)(?*)', - format: username => `https://www.modelhub.com/${username}`, - }, - ]; + const platforms = [ + // links supplied by PH often look like domain.com/domain.com/username + { + label: 'twitter', + pattern: 'http(s)\\://(*)twitter.com/:username(/)(?*)', + format: username => `https://www.twitter.com/${username}`, + }, + { + label: 'youtube', + pattern: 'http(s)\\://(*)youtube.com/channel/:username(?*)', + format: username => `https://www.youtube.com/channel/${username}`, + }, + { + label: 'instagram', + pattern: 'http(s)\\://(*)instagram.com/:username(/)(?*)', + format: username => `https://www.instagram.com/${username}`, + }, + { + label: 'snapchat', + pattern: 'http(s)\\://(*)snapchat.com/add/:username(/)(?*)', + format: username => `https://www.snapchat.com/add/${username}`, + }, + { + label: 'tumblr', + pattern: 'http(s)\\://:username.tumblr.com(*)', + format: username => `https://${username}.tumblr.com`, + }, + { + label: 'onlyfans', + pattern: 'http(s)\\://(*)onlyfans.com/:username(/)(?*)', + format: username => `https://www.onlyfans.com/${username}`, + }, + { + label: 'fancentro', + pattern: 'http(s)\\://(*)fancentro.com/:username(/)(?*)', + format: username => `https://www.fancentro.com/${username}`, + }, + { + label: 'modelhub', + pattern: 'http(s)\\://(*)modelhub.com/:username(/)(?*)', + format: username => `https://www.modelhub.com/${username}`, + }, + ]; - const match = platforms.reduce((acc, platform) => { - if (acc) return acc; + const match = platforms.reduce((acc, platform) => { + if (acc) return acc; - const patternMatch = new UrlPattern(platform.pattern).match(url); + const patternMatch = new UrlPattern(platform.pattern).match(url); - if (patternMatch) { - return { - platform: platform.label, - original: url, - username: patternMatch.username, - url: platform.format ? platform.format(patternMatch.username) : url, - }; - } + if (patternMatch) { + return { + platform: platform.label, + original: url, + username: patternMatch.username, + url: platform.format ? platform.format(patternMatch.username) : url, + }; + } - return null; - }, null) || { url }; + return null; + }, null) || { url }; - return { - url: match.url, - platform: match.platform, - actor_id: actorId, - }; + return { + url: match.url, + platform: match.platform, + actor_id: actorId, + }; } async function curateSocialEntries(urls, actorId) { - if (!urls) { - return []; - } + if (!urls) { + return []; + } - const existingSocialLinks = await knex('actors_social').where('actor_id', actorId); + const existingSocialLinks = await knex('actors_social').where('actor_id', actorId); - return urls.reduce((acc, url) => { - const socialEntry = curateSocialEntry(url, actorId); + return urls.reduce((acc, url) => { + const socialEntry = curateSocialEntry(url, actorId); - if (acc.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase()) || existingSocialLinks.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase())) { - // prevent duplicates - return acc; - } + if (acc.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase()) || existingSocialLinks.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase())) { + // prevent duplicates + return acc; + } - return [...acc, socialEntry]; - }, []); + return [...acc, socialEntry]; + }, []); } async function fetchActors(queryObject, limit = 100) { - const releases = await knex('actors') - .select( - 'actors.*', - 'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias', - 'residence_countries.alpha2 as residence_country_alpha2', 'residence_countries.name as residence_country_name', 'residence_countries.alias as residence_country_alias', - ) - .leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2') - .leftJoin('countries as residence_countries', 'actors.residence_country_alpha2', 'residence_countries.alpha2') - .orderBy(['actors.name', 'actors.gender']) - .where(builder => whereOr(queryObject, 'actors', builder)) - .limit(limit); + const releases = await knex('actors') + .select( + 'actors.*', + 'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias', + 'residence_countries.alpha2 as residence_country_alpha2', 'residence_countries.name as residence_country_name', 'residence_countries.alias as residence_country_alias', + ) + .leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2') + .leftJoin('countries as residence_countries', 'actors.residence_country_alpha2', 'residence_countries.alpha2') + .orderBy(['actors.name', 'actors.gender']) + .where(builder => whereOr(queryObject, 'actors', builder)) + .limit(limit); - return curateActors(releases); + return curateActors(releases); } async function storeSocialLinks(urls, actorId) { - const curatedSocialEntries = await curateSocialEntries(urls, actorId); + const curatedSocialEntries = await curateSocialEntries(urls, actorId); - await knex('actors_social').insert(curatedSocialEntries); + await knex('actors_social').insert(curatedSocialEntries); } async function storeAvatars(avatars, actorId) { - if (!avatars || avatars.length === 0) { - return []; - } + if (!avatars || avatars.length === 0) { + return []; + } - const avatarsBySource = await storeMedia(avatars, 'actor', 'avatar'); - await associateMedia({ [actorId]: avatars }, avatarsBySource, 'actor', 'photo', 'avatar'); + const avatarsBySource = await storeMedia(avatars, 'actor', 'avatar'); + await associateMedia({ [actorId]: avatars }, avatarsBySource, 'actor', 'photo', 'avatar'); - return avatarsBySource; + return avatarsBySource; } async function storeActor(actor, scraped = false, scrapeSuccess = false) { - const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess); + const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess); - const [actorEntry] = await knex('actors') - .insert(curatedActor) - .returning('*'); + const [actorEntry] = await knex('actors') + .insert(curatedActor) + .returning('*'); - await storeSocialLinks(actor.social, actorEntry.id); + await storeSocialLinks(actor.social, actorEntry.id); - if (actor.avatars) { - await storeAvatars(actor.avatars, actorEntry.id); - } + if (actor.avatars) { + await storeAvatars(actor.avatars, actorEntry.id); + } - logger.info(`Added new entry for actor '${actor.name}'`); + logger.info(`Added new entry for actor '${actor.name}'`); - return actorEntry; + return actorEntry; } async function updateActor(actor, scraped = false, scrapeSuccess = false) { - const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess); + const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess); - const [actorEntry] = await knex('actors') - .where({ id: actor.id }) - .update(curatedActor) - .returning('*'); + const [actorEntry] = await knex('actors') + .where({ id: actor.id }) + .update(curatedActor) + .returning('*'); - await storeSocialLinks(actor.social, actor.id); + await storeSocialLinks(actor.social, actor.id); - logger.info(`Updated entry for actor '${actor.name}'`); + logger.info(`Updated entry for actor '${actor.name}'`); - return actorEntry; + return actorEntry; } async function mergeProfiles(profiles, actor) { - if (profiles.filter(Boolean).length === 0) { - return null; - } + if (profiles.filter(Boolean).length === 0) { + return null; + } - const mergedProfile = profiles.reduce((prevProfile, profile) => { - if (profile === null) { - return prevProfile; - } + const mergedProfile = profiles.reduce((prevProfile, profile) => { + if (profile === null) { + return prevProfile; + } - const accProfile = { - id: actor ? actor.id : null, - name: actor ? actor.name : (prevProfile.name || profile.name), - description: prevProfile.description || profile.description, - gender: prevProfile.gender || profile.gender, - birthdate: !prevProfile.birthdate || Number.isNaN(Number(prevProfile.birthdate)) ? profile.birthdate : prevProfile.birthdate, - birthPlace: prevProfile.birthPlace || profile.birthPlace, - residencePlace: prevProfile.residencePlace || profile.residencePlace, - nationality: prevProfile.nationality || profile.nationality, // used to derive country when not available - ethnicity: prevProfile.ethnicity || profile.ethnicity, - bust: prevProfile.bust || (/\d+\w+/.test(profile.bust) ? profile.bust : null), - waist: prevProfile.waist || profile.waist, - hip: prevProfile.hip || profile.hip, - naturalBoobs: prevProfile.naturalBoobs === undefined ? profile.naturalBoobs : prevProfile.naturalBoobs, - height: prevProfile.height || profile.height, - weight: prevProfile.weight || profile.weight, - hair: prevProfile.hair || profile.hair, - eyes: prevProfile.eyes || profile.eyes, - hasPiercings: prevProfile.hasPiercings === undefined ? profile.hasPiercings : prevProfile.hasPiercings, - hasTattoos: prevProfile.hasTattoos === undefined ? profile.hasTattoos : prevProfile.hasTattoos, - piercings: prevProfile.piercings || profile.piercings, - tattoos: prevProfile.tattoos || profile.tattoos, - social: prevProfile.social.concat(profile.social || []), - releases: prevProfile.releases.concat(profile.releases ? profile.releases : []), // don't flatten fallbacks - }; + const accProfile = { + id: actor ? actor.id : null, + name: actor ? actor.name : (prevProfile.name || profile.name), + description: prevProfile.description || profile.description, + gender: prevProfile.gender || profile.gender, + birthdate: !prevProfile.birthdate || Number.isNaN(Number(prevProfile.birthdate)) ? profile.birthdate : prevProfile.birthdate, + birthPlace: prevProfile.birthPlace || profile.birthPlace, + residencePlace: prevProfile.residencePlace || profile.residencePlace, + nationality: prevProfile.nationality || profile.nationality, // used to derive country when not available + ethnicity: prevProfile.ethnicity || profile.ethnicity, + bust: prevProfile.bust || (/\d+\w+/.test(profile.bust) ? profile.bust : null), + waist: prevProfile.waist || profile.waist, + hip: prevProfile.hip || profile.hip, + naturalBoobs: prevProfile.naturalBoobs === undefined ? profile.naturalBoobs : prevProfile.naturalBoobs, + height: prevProfile.height || profile.height, + weight: prevProfile.weight || profile.weight, + hair: prevProfile.hair || profile.hair, + eyes: prevProfile.eyes || profile.eyes, + hasPiercings: prevProfile.hasPiercings === undefined ? profile.hasPiercings : prevProfile.hasPiercings, + hasTattoos: prevProfile.hasTattoos === undefined ? profile.hasTattoos : prevProfile.hasTattoos, + piercings: prevProfile.piercings || profile.piercings, + tattoos: prevProfile.tattoos || profile.tattoos, + social: prevProfile.social.concat(profile.social || []), + releases: prevProfile.releases.concat(profile.releases ? profile.releases : []), // don't flatten fallbacks + }; - if (profile.avatar) { - const avatar = Array.isArray(profile.avatar) - ? profile.avatar.map(avatarX => ({ - src: avatarX.src || avatarX, - scraper: profile.scraper, - copyright: avatarX.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright, - })) - : { - src: profile.avatar.src || profile.avatar, - scraper: profile.scraper, - copyright: profile.avatar.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright, - }; + if (profile.avatar) { + const avatar = Array.isArray(profile.avatar) + ? profile.avatar.map(avatarX => ({ + src: avatarX.src || avatarX, + scraper: profile.scraper, + copyright: avatarX.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright, + })) + : { + src: profile.avatar.src || profile.avatar, + scraper: profile.scraper, + copyright: profile.avatar.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright, + }; - accProfile.avatars = prevProfile.avatars.concat([avatar]); // don't flatten fallbacks - } else { - accProfile.avatars = prevProfile.avatars; - } + accProfile.avatars = prevProfile.avatars.concat([avatar]); // don't flatten fallbacks + } else { + accProfile.avatars = prevProfile.avatars; + } - return accProfile; - }, { - social: [], - avatars: [], - releases: [], - }); + return accProfile; + }, { + social: [], + avatars: [], + releases: [], + }); - const [birthPlace, residencePlace] = await Promise.all([ - resolvePlace(mergedProfile.birthPlace), - resolvePlace(mergedProfile.residencePlace), - ]); + const [birthPlace, residencePlace] = await Promise.all([ + resolvePlace(mergedProfile.birthPlace), + resolvePlace(mergedProfile.residencePlace), + ]); - mergedProfile.birthPlace = birthPlace; - mergedProfile.residencePlace = residencePlace; + mergedProfile.birthPlace = birthPlace; + mergedProfile.residencePlace = residencePlace; - if (!mergedProfile.birthPlace && mergedProfile.nationality) { - const country = await knex('countries') - .where('nationality', 'ilike', `%${mergedProfile.nationality}%`) - .orderBy('priority', 'desc') - .first(); + if (!mergedProfile.birthPlace && mergedProfile.nationality) { + const country = await knex('countries') + .where('nationality', 'ilike', `%${mergedProfile.nationality}%`) + .orderBy('priority', 'desc') + .first(); - mergedProfile.birthPlace = { - country: country.alpha2, - }; - } + mergedProfile.birthPlace = { + country: country.alpha2, + }; + } - return mergedProfile; + return mergedProfile; } async function scrapeProfiles(sources, actorName, actorEntry, sitesBySlug) { - return Promise.map(sources, async (source) => { - // const [scraperSlug, scraper] = source; - const profileScrapers = [].concat(source).map(slug => ({ scraperSlug: slug, scraper: scrapers.actors[slug] })); + return Promise.map(sources, async (source) => { + // const [scraperSlug, scraper] = source; + const profileScrapers = [].concat(source).map(slug => ({ scraperSlug: slug, scraper: scrapers.actors[slug] })); - try { - return await profileScrapers.reduce(async (outcome, { scraper, scraperSlug }) => outcome.catch(async () => { - if (!scraper) { - logger.warn(`No profile profile scraper available for ${scraperSlug}`); - throw Object.assign(new Error(`No profile scraper available for ${scraperSlug}`)); - } + try { + return await profileScrapers.reduce(async (outcome, { scraper, scraperSlug }) => outcome.catch(async () => { + if (!scraper) { + logger.warn(`No profile profile scraper available for ${scraperSlug}`); + throw Object.assign(new Error(`No profile scraper available for ${scraperSlug}`)); + } - logger.verbose(`Searching '${actorName}' on ${scraperSlug}`); + logger.verbose(`Searching '${actorName}' on ${scraperSlug}`); - const site = sitesBySlug[scraperSlug] || null; - const profile = await scraper.fetchProfile(actorEntry ? actorEntry.name : actorName, scraperSlug, site, include); + const site = sitesBySlug[scraperSlug] || null; + const profile = await scraper.fetchProfile(actorEntry ? actorEntry.name : actorName, scraperSlug, site, include); - if (profile && typeof profile !== 'number') { - logger.verbose(`Found profile for '${actorName}' on ${scraperSlug}`); + if (profile && typeof profile !== 'number') { + logger.verbose(`Found profile for '${actorName}' on ${scraperSlug}`); - return { - ...profile, - name: actorName, - scraper: scraperSlug, - site, - releases: profile.releases?.map(release => (typeof release === 'string' - ? { url: release, site } - : { ...release, site: release.site || site } - )), - }; - } + return { + ...profile, + name: actorName, + scraper: scraperSlug, + site, + releases: profile.releases?.map(release => (typeof release === 'string' + ? { url: release, site } + : { ...release, site: release.site || site } + )), + }; + } - logger.verbose(`No profile for '${actorName}' available on ${scraperSlug}: ${profile}`); - throw Object.assign(new Error(`Profile for ${actorName} not available on ${scraperSlug}`), { warn: false }); - }), Promise.reject(new Error())); - } catch (error) { - if (error.warn !== false) { - logger.warn(`Error in scraper ${source}: ${error.message}`); - // logger.error(error.stack); - } - } + logger.verbose(`No profile for '${actorName}' available on ${scraperSlug}: ${profile}`); + throw Object.assign(new Error(`Profile for ${actorName} not available on ${scraperSlug}`), { warn: false }); + }), Promise.reject(new Error())); + } catch (error) { + if (error.warn !== false) { + logger.warn(`Error in scraper ${source}: ${error.message}`); + // logger.error(error.stack); + } + } - return null; - }); + return null; + }); } async function scrapeActors(actorNames) { - return Promise.map(actorNames || argv.actors, async (actorName) => { - try { - const actorSlug = slugify(actorName); - const actorEntry = await knex('actors').where({ slug: actorSlug }).first(); - const sources = argv.sources || config.profiles || Object.keys(scrapers.actors); + return Promise.map(actorNames || argv.actors, async (actorName) => { + try { + const actorSlug = slugify(actorName); + const actorEntry = await knex('actors').where({ slug: actorSlug }).first(); + const sources = argv.sources || config.profiles || Object.keys(scrapers.actors); - const finalSources = argv.withReleases ? sources.flat() : sources; // ignore race-to-success grouping when scenes are requested + const finalSources = argv.withReleases ? sources.flat() : sources; // ignore race-to-success grouping when scenes are requested - const [siteEntries, networkEntries] = await Promise.all([ - knex('sites') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .select( - 'sites.*', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', - ) - .whereIn('sites.slug', finalSources.flat()), - knex('networks').select('*').whereIn('slug', finalSources.flat()), - ]); + const [siteEntries, networkEntries] = await Promise.all([ + knex('sites') + .leftJoin('networks', 'sites.network_id', 'networks.id') + .select( + 'sites.*', + 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', + ) + .whereIn('sites.slug', finalSources.flat()), + knex('networks').select('*').whereIn('slug', finalSources.flat()), + ]); - const sites = await curateSites(siteEntries, true); - const networks = networkEntries.map(network => ({ ...network, isFallback: true })); - const sitesBySlug = [].concat(networks, sites).reduce((acc, site) => ({ ...acc, [site.slug]: site }), {}); + const sites = await curateSites(siteEntries, true); + const networks = networkEntries.map(network => ({ ...network, isFallback: true })); + const sitesBySlug = [].concat(networks, sites).reduce((acc, site) => ({ ...acc, [site.slug]: site }), {}); - const profiles = await scrapeProfiles(sources, actorName, actorEntry, sitesBySlug); - const profile = await mergeProfiles(profiles, actorEntry); + const profiles = await scrapeProfiles(sources, actorName, actorEntry, sitesBySlug); + const profile = await mergeProfiles(profiles, actorEntry); - if (profile === null) { - logger.warn(`Could not find profile for actor '${actorName}'`); + if (profile === null) { + logger.warn(`Could not find profile for actor '${actorName}'`); - if (argv.save && !actorEntry) { - await storeActor({ name: actorName }, false, false); - } + if (argv.save && !actorEntry) { + await storeActor({ name: actorName }, false, false); + } - return null; - } + return null; + } - if (argv.inspect) { - console.log(profile); - logger.info(`Found ${profile.releases.length} releases for ${actorName}`); - } + if (argv.inspect) { + console.log(profile); + logger.info(`Found ${profile.releases.length} releases for ${actorName}`); + } - if (argv.save) { - if (actorEntry && profile) { - await Promise.all([ - updateActor(profile, true, true), - storeAvatars(profile.avatars, actorEntry.id), - ]); + if (argv.save) { + if (actorEntry && profile) { + await Promise.all([ + updateActor(profile, true, true), + storeAvatars(profile.avatars, actorEntry.id), + ]); - return profile; - } + return profile; + } - await storeActor(profile, true, true); - } + await storeActor(profile, true, true); + } - return profile; - } catch (error) { - console.log(error); - logger.warn(`${actorName}: ${error}`); + return profile; + } catch (error) { + console.log(error); + logger.warn(`${actorName}: ${error}`); - return null; - } - }, { - concurrency: 3, - }); + return null; + } + }, { + concurrency: 3, + }); } async function scrapeBasicActors() { - const basicActors = await knex('actors').where('scraped_at', null); + const basicActors = await knex('actors').where('scraped_at', null); - return scrapeActors(basicActors.map(actor => actor.name)); + return scrapeActors(basicActors.map(actor => actor.name)); } async function associateActors(mappedActors, releases) { - const [existingActorEntries, existingAssociationEntries] = await Promise.all([ - knex('actors') - .whereIn('name', Object.values(mappedActors).map(actor => actor.name)) - .orWhereIn('slug', Object.keys(mappedActors)), - knex('releases_actors').whereIn('release_id', releases.map(release => release.id)), - ]); + const [existingActorEntries, existingAssociationEntries] = await Promise.all([ + knex('actors') + .whereIn('name', Object.values(mappedActors).map(actor => actor.name)) + .orWhereIn('slug', Object.keys(mappedActors)), + knex('releases_actors').whereIn('release_id', releases.map(release => release.id)), + ]); - const associations = await Promise.map(Object.entries(mappedActors), async ([actorSlug, actor]) => { - try { - const actorEntry = existingActorEntries.find(actorX => actorX.slug === actorSlug) + const associations = await Promise.map(Object.entries(mappedActors), async ([actorSlug, actor]) => { + try { + const actorEntry = existingActorEntries.find(actorX => actorX.slug === actorSlug) || await storeActor(actor); - // if a scene - return Array.from(actor.releaseIds) - .map(releaseId => ({ - release_id: releaseId, - actor_id: actorEntry.id, - })) - .filter(association => !existingAssociationEntries - // remove associations already in database - .some(associationEntry => associationEntry.actor_id === association.actor_id + // if a scene + return Array.from(actor.releaseIds) + .map(releaseId => ({ + release_id: releaseId, + actor_id: actorEntry.id, + })) + .filter(association => !existingAssociationEntries + // remove associations already in database + .some(associationEntry => associationEntry.actor_id === association.actor_id && associationEntry.release_id === association.release_id)); - } catch (error) { - logger.error(actor.name, error); - return null; - } - }); + } catch (error) { + logger.error(actor.name, error); + return null; + } + }); - await knex('releases_actors').insert(associations.filter(association => association).flat()); + await knex('releases_actors').insert(associations.filter(association => association).flat()); - // basic actor scraping is failure prone, don't run together with actor association - // await scrapebasicactors(), + // basic actor scraping is failure prone, don't run together with actor association + // await scrapebasicactors(), } module.exports = { - associateActors, - fetchActors, - scrapeActors, - scrapeBasicActors, + associateActors, + fetchActors, + scrapeActors, + scrapeBasicActors, }; diff --git a/src/actors.js b/src/actors.js index 7ddc481be..5dcb70121 100644 --- a/src/actors.js +++ b/src/actors.js @@ -1,125 +1,156 @@ 'use strict'; +const config = require('config'); +const Promise = require('bluebird'); + // const logger = require('./logger')(__filename); const knex = require('./knex'); +const scrapers = require('./scrapers/scrapers'); + +const argv = require('./argv'); const slugify = require('./utils/slugify'); const capitalize = require('./utils/capitalize'); function toBaseActors(actorsOrNames, release) { - return actorsOrNames.map((actorOrName) => { - const name = capitalize(actorOrName.name || actorOrName); - const slug = slugify(name); + return actorsOrNames.map((actorOrName) => { + const name = capitalize(actorOrName.name || actorOrName); + const slug = slugify(name); - const baseActor = { - name, - slug, - network: release.site.network, - }; + const baseActor = { + name, + slug, + network: release?.site.network, + }; - if (actorOrName.name) { - return { - ...actorOrName, - ...baseActor, - }; - } + if (actorOrName.name) { + return { + ...actorOrName, + ...baseActor, + }; + } - return baseActor; - }); + return baseActor; + }); } function curateActorEntry(baseActor, batchId) { - return { - name: baseActor.name, - slug: baseActor.slug, - network_id: null, - batch_id: batchId, - }; + return { + name: baseActor.name, + slug: baseActor.slug, + network_id: null, + batch_id: batchId, + }; } function curateActorEntries(baseActors, batchId) { - return baseActors.map(baseActor => curateActorEntry(baseActor, batchId)); + return baseActors.map(baseActor => curateActorEntry(baseActor, batchId)); } -async function scrapeProfiles() { +async function scrapeActors(actorNames) { + const baseActors = toBaseActors(actorNames); + const sources = argv.sources || config.profiles || Object.keys(scrapers.actors); + const siteSlugs = sources.flat(); + + const [networks, sites, existingActorEntries] = await Promise.all([ + knex('networks').whereIn('slug', siteSlugs), + knex('sites').whereIn('slug', siteSlugs), + knex('actors') + .select(['id', 'name', 'slug']) + .whereIn('slug', baseActors.map(baseActor => baseActor.slug)) + .whereNull('network_id'), + ]); + + const existingActorEntriesBySlug = existingActorEntries.reduce((acc, actorEntry) => ({ ...acc, [actorEntry.slug]: actorEntry }), {}); + const networksBySlug = networks.reduce((acc, network) => ({ ...acc, [network.slug]: { ...network, isNetwork: true } }), {}); + const sitesBySlug = sites.reduce((acc, site) => ({ ...acc, [site.slug]: site }), {}); + + const newBaseActors = baseActors.filter(baseActor => !existingActorEntriesBySlug[baseActor.slug]); + + const [batchId] = newBaseActors.length > 0 ? await knex('batches').insert({ comment: null }).returning('id') : [null]; + const curatedActorEntries = batchId && curateActorEntries(newBaseActors, batchId); + const newActorEntries = batchId && await knex('actors').insert(curatedActorEntries).returning(['id', 'name', 'slug']); + + const actorEntries = existingActorEntries.concat(Array.isArray(newActorEntries) ? newActorEntries : []); + + console.log(actorEntries, newActorEntries, actorEntries); } async function getOrCreateActors(baseActors, batchId) { - const existingActors = await knex('actors') - .select('id', 'alias_for', 'name', 'slug', 'network_id') - .whereIn('slug', baseActors.map(baseActor => baseActor.slug)) - .whereNull('network_id') - .orWhereIn(['slug', 'network_id'], baseActors.map(baseActor => [baseActor.slug, baseActor.network.id])); + const existingActors = await knex('actors') + .select('id', 'alias_for', 'name', 'slug', 'network_id') + .whereIn('slug', baseActors.map(baseActor => baseActor.slug)) + .whereNull('network_id') + .orWhereIn(['slug', 'network_id'], baseActors.map(baseActor => [baseActor.slug, baseActor.network.id])); - // const existingActorSlugs = new Set(existingActors.map(actor => actor.slug)); - const existingActorSlugs = existingActors.reduce((acc, actor) => ({ - ...acc, - [actor.network_id]: { - ...acc[actor.network_id], - [actor.slug]: true, - }, - }), {}); + // const existingActorSlugs = new Set(existingActors.map(actor => actor.slug)); + const existingActorSlugs = existingActors.reduce((acc, actor) => ({ + ...acc, + [actor.network_id]: { + ...acc[actor.network_id], + [actor.slug]: true, + }, + }), {}); - const uniqueBaseActors = baseActors.filter(baseActor => !existingActorSlugs[baseActor.network.id]?.[baseActor.slug] && !existingActorSlugs.null?.[baseActor.slug]); + const uniqueBaseActors = baseActors.filter(baseActor => !existingActorSlugs[baseActor.network.id]?.[baseActor.slug] && !existingActorSlugs.null?.[baseActor.slug]); - const curatedActorEntries = curateActorEntries(uniqueBaseActors, batchId); - const newActors = await knex('actors').insert(curatedActorEntries, ['id', 'alias_for', 'name', 'slug', 'network_id']); + const curatedActorEntries = curateActorEntries(uniqueBaseActors, batchId); + const newActors = await knex('actors').insert(curatedActorEntries, ['id', 'alias_for', 'name', 'slug', 'network_id']); - if (Array.isArray(newActors)) { - return newActors.concat(existingActors); - } + if (Array.isArray(newActors)) { + return newActors.concat(existingActors); + } - return existingActors; + return existingActors; } async function associateActors(releases, batchId) { - const baseActorsByReleaseId = releases.reduce((acc, release) => { - if (release.actors) { - acc[release.id] = toBaseActors(release.actors, release); - } + const baseActorsByReleaseId = releases.reduce((acc, release) => { + if (release.actors) { + acc[release.id] = toBaseActors(release.actors, release); + } - return acc; - }, {}); + return acc; + }, {}); - const baseActors = Object.values(baseActorsByReleaseId).flat(); + const baseActors = Object.values(baseActorsByReleaseId).flat(); - if (baseActors.length === 0) { - return; - } + if (baseActors.length === 0) { + return; + } - const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({ - ...acc, - [baseActor.slug]: { - ...acc[baseActor.slug], - [baseActor.network.id]: baseActor, - }, - }), {}); + const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({ + ...acc, + [baseActor.slug]: { + ...acc[baseActor.slug], + [baseActor.network.id]: baseActor, + }, + }), {}); - const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat(); + const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat(); - const actors = await getOrCreateActors(uniqueBaseActors, batchId); - console.log(actors); - const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({ - ...acc, - [actor.network_id]: { - ...acc[actor.network_id], - [actor.slug]: actor.alias_for || actor.id, - }, - }), {}); + const actors = await getOrCreateActors(uniqueBaseActors, batchId); - console.log(actorIdsBySlugAndNetworkId); + const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({ + ...acc, + [actor.network_id]: { + ...acc[actor.network_id], + [actor.slug]: actor.alias_for || actor.id, + }, + }), {}); - const releaseActorAssociations = Object.entries(baseActorsByReleaseId) - .map(([releaseId, releaseActors]) => releaseActors - .map(releaseActor => ({ - release_id: releaseId, - actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug], - }))) - .flat(); + const releaseActorAssociations = Object.entries(baseActorsByReleaseId) + .map(([releaseId, releaseActors]) => releaseActors + .map(releaseActor => ({ + release_id: releaseId, + actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug], + }))) + .flat(); - await knex.raw(`${knex('releases_actors').insert(releaseActorAssociations).toString()} ON CONFLICT DO NOTHING;`); + await knex.raw(`${knex('releases_actors').insert(releaseActorAssociations).toString()} ON CONFLICT DO NOTHING;`); } module.exports = { - associateActors, + associateActors, + scrapeActors, }; diff --git a/src/app.js b/src/app.js index 21f6893fd..11b55c64b 100644 --- a/src/app.js +++ b/src/app.js @@ -7,39 +7,39 @@ const knex = require('./knex'); const fetchUpdates = require('./updates'); const { fetchScenes, fetchMovies } = require('./deep'); const { storeReleases, updateReleasesSearch } = require('./store-releases'); -const { scrapeActors } = require('./actors-legacy'); +const { scrapeActors } = require('./actors'); async function init() { - if (argv.server) { - await initServer(); - return; - } + if (argv.server) { + await initServer(); + return; + } - if (argv.updateSearch) { - await updateReleasesSearch(); - } + if (argv.updateSearch) { + await updateReleasesSearch(); + } - if (argv.actors) { - await scrapeActors(argv.actors); - } + if (argv.actors) { + await scrapeActors(argv.actors); + } - const updateBaseScenes = (argv.scrape || argv.sites || argv.networks) && await fetchUpdates(); + const updateBaseScenes = (argv.scrape || argv.sites || argv.networks) && await fetchUpdates(); - const deepScenes = argv.deep - ? await fetchScenes([...(argv.scenes || []), ...(updateBaseScenes || [])]) - : updateBaseScenes; + const deepScenes = argv.deep + ? await fetchScenes([...(argv.scenes || []), ...(updateBaseScenes || [])]) + : updateBaseScenes; - const sceneMovies = deepScenes && argv.sceneMovies && deepScenes.map(scene => scene.movie).filter(Boolean); - const deepMovies = await fetchMovies([...(argv.movies || []), ...(sceneMovies || [])]); + const sceneMovies = deepScenes && argv.sceneMovies && deepScenes.map(scene => scene.movie).filter(Boolean); + const deepMovies = await fetchMovies([...(argv.movies || []), ...(sceneMovies || [])]); - if (argv.save) { - await storeReleases([ - ...(deepScenes || []), - ...(deepMovies || []), - ]); - } + if (argv.save) { + await storeReleases([ + ...(deepScenes || []), + ...(deepMovies || []), + ]); + } - knex.destroy(); + knex.destroy(); } module.exports = init; diff --git a/src/argv.js b/src/argv.js index 995f2ad19..e8b85f65b 100644 --- a/src/argv.js +++ b/src/argv.js @@ -4,188 +4,188 @@ const config = require('config'); const yargs = require('yargs'); const { argv } = yargs - .command('npm start') - .option('server', { - describe: 'Start web server', - type: 'boolean', - alias: 'web', - }) - .option('scrape', { - describe: 'Scrape sites and networks defined in configuration', - type: 'boolean', - }) - .option('networks', { - describe: 'Networks to scrape (overrides configuration)', - type: 'array', - alias: 'network', - }) - .option('sites', { - describe: 'Sites to scrape (overrides configuration)', - type: 'array', - alias: 'site', - }) - .option('actors', { - describe: 'Scrape actors by name or slug', - type: 'array', - alias: 'actor', - }) - .option('actor-scenes', { - describe: 'Fetch all scenes for an actor', - type: 'boolean', - alias: 'with-releases', - default: false, - }) - .option('movie-scenes', { - describe: 'Fetch all scenes for a movie', - type: 'boolean', - alias: 'with-releases', - default: false, - }) - .option('scene-movies', { - describe: 'Fetch movies for scenes', - type: 'boolean', - default: true, - }) - .option('profiles', { - describe: 'Scrape profiles for new actors after fetching scenes', - type: 'boolean', - alias: 'bios', - default: false, - }) - .option('scene', { - describe: 'Scrape scene info from URL', - type: 'array', - alias: 'scenes', - }) - .option('movie', { - describe: 'Scrape movie info from URL', - type: 'array', - alias: 'movies', - }) - .option('sources', { - describe: 'Use these scrapers for actor data', - type: 'array', - alias: 'source', - }) - .option('deep', { - describe: 'Fetch details for all releases', - type: 'boolean', - default: true, - }) - .option('latest', { - describe: 'Scrape latest releases if available', - type: 'boolean', - default: true, - }) - .option('upcoming', { - describe: 'Scrape upcoming releases if available', - type: 'boolean', - default: true, - }) - .option('redownload', { - describe: 'Don\'t ignore duplicates, update existing entries', - type: 'boolean', - alias: 'force', - }) - .option('after', { - describe: 'Don\'t fetch scenes older than', - type: 'string', - default: config.fetchAfter.join(' '), - }) - .option('last', { - describe: 'Get the latest x releases, no matter the date range', - type: 'number', - }) - .option('null-date-limit', { - describe: 'Limit amount of scenes when dates are missing.', - type: 'number', - default: config.nullDateLimit, - alias: 'limit', - }) - .option('page', { - describe: 'Page to start scraping at', - type: 'number', - default: 1, - }) - .option('save', { - describe: 'Save fetched releases to database', - type: 'boolean', - default: true, - }) - .option('media', { - describe: 'Include any release media', - type: 'boolean', - default: true, - }) - .option('media-limit', { - describe: 'Maximum amount of assets of each type per release', - type: 'number', - default: config.media.limit, - }) - .option('images', { - describe: 'Include any photos, posters or covers', - type: 'boolean', - default: true, - alias: 'pics', - }) - .option('videos', { - describe: 'Include any trailers or teasers', - type: 'boolean', - default: true, - }) - .option('posters', { - describe: 'Include release posters', - type: 'boolean', - default: true, - alias: 'poster', - }) - .option('covers', { - describe: 'Include release covers', - type: 'boolean', - default: true, - alias: 'cover', - }) - .option('photos', { - describe: 'Include release photos', - type: 'boolean', - default: true, - }) - .option('trailers', { - describe: 'Include release trailers', - type: 'boolean', - default: true, - alias: 'trailer', - }) - .option('teasers', { - describe: 'Include release teasers', - type: 'boolean', - default: true, - alias: 'teaser', - }) - .option('avatars', { - describe: 'Include actor avatars', - type: 'boolean', - default: true, - }) - .option('inspect', { - describe: 'Show data in console.', - type: 'boolean', - default: false, - }) - .option('level', { - describe: 'Log level', - type: 'string', - default: process.env.NODE_ENV === 'development' ? 'silly' : 'info', - }) - .option('debug', { - describe: 'Show error stack traces', - type: 'boolean', - default: process.env.NODE_ENV === 'development', - }) - .option('update-search', { - describe: 'Update search documents for all releases.', - type: 'boolean', - default: false, - }); + .command('npm start') + .option('server', { + describe: 'Start web server', + type: 'boolean', + alias: 'web', + }) + .option('scrape', { + describe: 'Scrape sites and networks defined in configuration', + type: 'boolean', + }) + .option('networks', { + describe: 'Networks to scrape (overrides configuration)', + type: 'array', + alias: 'network', + }) + .option('sites', { + describe: 'Sites to scrape (overrides configuration)', + type: 'array', + alias: 'site', + }) + .option('actors', { + describe: 'Scrape actors by name or slug', + type: 'array', + alias: 'actor', + }) + .option('actor-scenes', { + describe: 'Fetch all scenes for an actor', + type: 'boolean', + alias: 'with-releases', + default: false, + }) + .option('movie-scenes', { + describe: 'Fetch all scenes for a movie', + type: 'boolean', + alias: 'with-releases', + default: false, + }) + .option('scene-movies', { + describe: 'Fetch movies for scenes', + type: 'boolean', + default: true, + }) + .option('profiles', { + describe: 'Scrape profiles for new actors after fetching scenes', + type: 'boolean', + alias: 'bios', + default: false, + }) + .option('scene', { + describe: 'Scrape scene info from URL', + type: 'array', + alias: 'scenes', + }) + .option('movie', { + describe: 'Scrape movie info from URL', + type: 'array', + alias: 'movies', + }) + .option('sources', { + describe: 'Use these scrapers for actor data', + type: 'array', + alias: 'source', + }) + .option('deep', { + describe: 'Fetch details for all releases', + type: 'boolean', + default: true, + }) + .option('latest', { + describe: 'Scrape latest releases if available', + type: 'boolean', + default: true, + }) + .option('upcoming', { + describe: 'Scrape upcoming releases if available', + type: 'boolean', + default: true, + }) + .option('redownload', { + describe: 'Don\'t ignore duplicates, update existing entries', + type: 'boolean', + alias: 'force', + }) + .option('after', { + describe: 'Don\'t fetch scenes older than', + type: 'string', + default: config.fetchAfter.join(' '), + }) + .option('last', { + describe: 'Get the latest x releases, no matter the date range', + type: 'number', + }) + .option('null-date-limit', { + describe: 'Limit amount of scenes when dates are missing.', + type: 'number', + default: config.nullDateLimit, + alias: 'limit', + }) + .option('page', { + describe: 'Page to start scraping at', + type: 'number', + default: 1, + }) + .option('save', { + describe: 'Save fetched releases to database', + type: 'boolean', + default: true, + }) + .option('media', { + describe: 'Include any release media', + type: 'boolean', + default: true, + }) + .option('media-limit', { + describe: 'Maximum amount of assets of each type per release', + type: 'number', + default: config.media.limit, + }) + .option('images', { + describe: 'Include any photos, posters or covers', + type: 'boolean', + default: true, + alias: 'pics', + }) + .option('videos', { + describe: 'Include any trailers or teasers', + type: 'boolean', + default: true, + }) + .option('posters', { + describe: 'Include release posters', + type: 'boolean', + default: true, + alias: 'poster', + }) + .option('covers', { + describe: 'Include release covers', + type: 'boolean', + default: true, + alias: 'cover', + }) + .option('photos', { + describe: 'Include release photos', + type: 'boolean', + default: true, + }) + .option('trailers', { + describe: 'Include release trailers', + type: 'boolean', + default: true, + alias: 'trailer', + }) + .option('teasers', { + describe: 'Include release teasers', + type: 'boolean', + default: true, + alias: 'teaser', + }) + .option('avatars', { + describe: 'Include actor avatars', + type: 'boolean', + default: true, + }) + .option('inspect', { + describe: 'Show data in console.', + type: 'boolean', + default: false, + }) + .option('level', { + describe: 'Log level', + type: 'string', + default: process.env.NODE_ENV === 'development' ? 'silly' : 'info', + }) + .option('debug', { + describe: 'Show error stack traces', + type: 'boolean', + default: process.env.NODE_ENV === 'development', + }) + .option('update-search', { + describe: 'Update search documents for all releases.', + type: 'boolean', + default: false, + }); module.exports = argv; diff --git a/src/deep.js b/src/deep.js index fcfd84b3c..c648e6cf8 100644 --- a/src/deep.js +++ b/src/deep.js @@ -11,159 +11,160 @@ const { curateSites } = require('./sites'); const { curateNetworks } = require('./networks'); function urlToSiteSlug(url) { - try { - const slug = new URL(url) - .hostname - .match(/([\w-]+)\.\w+$/)?.[1]; + try { + const slug = new URL(url) + .hostname + .match(/([\w-]+)\.\w+$/)?.[1]; - return slug; - } catch (error) { - logger.warn(`Failed to derive site slug from '${url}': ${error.message}`); + return slug; + } catch (error) { + logger.warn(`Failed to derive site slug from '${url}': ${error.message}`); - return null; - } + return null; + } } async function findSites(baseReleases) { - const baseReleasesWithoutSite = baseReleases.filter(release => release.url && !release.site); + const baseReleasesWithoutSite = baseReleases.filter(release => release.url && !release.site); - const siteSlugs = Array.from(new Set( - baseReleasesWithoutSite - .map(baseRelease => urlToSiteSlug(baseRelease.url)) - .filter(Boolean), - )); + const siteSlugs = Array.from(new Set( + baseReleasesWithoutSite + .map(baseRelease => urlToSiteSlug(baseRelease.url)) + .filter(Boolean), + )); - const siteEntries = await knex('sites') - .leftJoin('networks', 'networks.id', 'sites.network_id') - .select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.parameters as network_parameters', 'networks.description as network_description') - .whereIn('sites.slug', siteSlugs); + const siteEntries = await knex('sites') + .leftJoin('networks', 'networks.id', 'sites.network_id') + .select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.parameters as network_parameters', 'networks.description as network_description') + .whereIn('sites.slug', siteSlugs); - const networkEntries = await knex('networks').whereIn('slug', siteSlugs); + const networkEntries = await knex('networks').whereIn('slug', siteSlugs); - const sites = await curateSites(siteEntries, true, false); - const networks = await curateNetworks(networkEntries, true, false, false); - const markedNetworks = networks.map(network => ({ ...network, isFallback: true })); + const sites = await curateSites(siteEntries, true, false); + const networks = await curateNetworks(networkEntries, true, false, false); + const markedNetworks = networks.map(network => ({ ...network, isNetwork: true })); - const sitesBySlug = [] - .concat(markedNetworks, sites) - .reduce((accSites, site) => ({ ...accSites, [site.slug]: site }), {}); + const sitesBySlug = [] + .concat(markedNetworks, sites) + .reduce((accSites, site) => ({ ...accSites, [site.slug]: site }), {}); - return sitesBySlug; + return sitesBySlug; } function toBaseReleases(baseReleasesOrUrls) { - return baseReleasesOrUrls - .map((baseReleaseOrUrl) => { - if (baseReleaseOrUrl.url) { - // base release with URL - return { - ...baseReleaseOrUrl, - deep: false, - }; - } + return baseReleasesOrUrls + .map((baseReleaseOrUrl) => { + if (baseReleaseOrUrl.url) { + // base release with URL + return { + ...baseReleaseOrUrl, + deep: false, + }; + } - if (/^http/.test(baseReleaseOrUrl)) { - // URL - return { - url: baseReleaseOrUrl, - deep: false, - }; - } + if (/^http/.test(baseReleaseOrUrl)) { + // URL + return { + url: baseReleaseOrUrl, + deep: false, + }; + } - if (typeof baseReleaseOrUrl === 'object' && !Array.isArray(baseReleaseOrUrl)) { - // base release without URL, prepare for passthrough - return { - ...baseReleaseOrUrl, - deep: false, - }; - } + if (typeof baseReleaseOrUrl === 'object' && !Array.isArray(baseReleaseOrUrl)) { + // base release without URL, prepare for passthrough + return { + ...baseReleaseOrUrl, + deep: false, + }; + } - logger.warn(`Malformed base release, discarding '${baseReleaseOrUrl}'`); - return null; - }) - .filter(Boolean); + logger.warn(`Malformed base release, discarding '${baseReleaseOrUrl}'`); + return null; + }) + .filter(Boolean); } async function scrapeRelease(baseRelease, sites, type = 'scene') { - const site = baseRelease.site || sites[urlToSiteSlug(baseRelease.url)]; + const site = baseRelease.site || sites[urlToSiteSlug(baseRelease.url)]; + const siteWithFallbackNetwork = site.isNetwork ? { ...site, network: site } : site; // make site.network available, even when site is network fallback - if (!site) { - logger.warn(`No site available for ${baseRelease.url}`); - return baseRelease; - } + if (!site) { + logger.warn(`No site available for ${baseRelease.url}`); + return baseRelease; + } - if ((!baseRelease.url && !baseRelease.path) || !argv.deep) { - return { - ...baseRelease, - site, - }; - } + if ((!baseRelease.url && !baseRelease.path) || !argv.deep) { + return { + ...baseRelease, + site, + }; + } - const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug]; + const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug]; - if (!scraper) { - logger.warn(`Could not find scraper for ${baseRelease.url}`); - return baseRelease; - } + if (!scraper) { + logger.warn(`Could not find scraper for ${baseRelease.url}`); + return baseRelease; + } - if ((type === 'scene' && !scraper.fetchScene) || (type === 'movie' && !scraper.fetchMovie)) { - logger.warn(`The '${site.name}'-scraper cannot fetch individual ${type}s`); - return baseRelease; - } + if ((type === 'scene' && !scraper.fetchScene) || (type === 'movie' && !scraper.fetchMovie)) { + logger.warn(`The '${site.name}'-scraper cannot fetch individual ${type}s`); + return baseRelease; + } - try { - logger.verbose(`Fetching ${type} ${baseRelease.url}`); + try { + logger.verbose(`Fetching ${type} ${baseRelease.url}`); - const scrapedRelease = type === 'scene' - ? await scraper.fetchScene(baseRelease.url, site, baseRelease, null, include) - : await scraper.fetchMovie(baseRelease.url, site, baseRelease, null, include); + const scrapedRelease = type === 'scene' + ? await scraper.fetchScene(baseRelease.url, siteWithFallbackNetwork, baseRelease, null, include) + : await scraper.fetchMovie(baseRelease.url, siteWithFallbackNetwork, baseRelease, null, include); - const mergedRelease = { - ...baseRelease, - ...scrapedRelease, - deep: !!scrapedRelease, - site, - }; + const mergedRelease = { + ...baseRelease, + ...scrapedRelease, + deep: !!scrapedRelease, + site, + }; - if (scrapedRelease && baseRelease?.tags) { - // accumulate all available tags - mergedRelease.tags = baseRelease.tags.concat(scrapedRelease.tags); - } + if (scrapedRelease && baseRelease?.tags) { + // accumulate all available tags + mergedRelease.tags = baseRelease.tags.concat(scrapedRelease.tags); + } - return mergedRelease; - } catch (error) { - logger.error(`Deep scrape failed for ${baseRelease.url}: ${error.message}`); - return baseRelease; - } + return mergedRelease; + } catch (error) { + logger.error(`Deep scrape failed for ${baseRelease.url}: ${error.message}`); + return baseRelease; + } } async function scrapeReleases(baseReleases, sites, type) { - return Promise.map( - baseReleases, - async baseRelease => scrapeRelease(baseRelease, sites, type), - { concurrency: 10 }, - ); + return Promise.map( + baseReleases, + async baseRelease => scrapeRelease(baseRelease, sites, type), + { concurrency: 10 }, + ); } async function fetchReleases(baseReleasesOrUrls, type = 'scene') { - const baseReleases = toBaseReleases(baseReleasesOrUrls); - const sites = await findSites(baseReleases); + const baseReleases = toBaseReleases(baseReleasesOrUrls); + const sites = await findSites(baseReleases); - const deepReleases = await scrapeReleases(baseReleases, sites, type); + const deepReleases = await scrapeReleases(baseReleases, sites, type); - return deepReleases; + return deepReleases; } async function fetchScenes(baseReleasesOrUrls) { - return fetchReleases(baseReleasesOrUrls, 'scene'); + return fetchReleases(baseReleasesOrUrls, 'scene'); } async function fetchMovies(baseReleasesOrUrls) { - return fetchReleases(baseReleasesOrUrls, 'movie'); + return fetchReleases(baseReleasesOrUrls, 'movie'); } module.exports = { - fetchReleases, - fetchScenes, - fetchMovies, + fetchReleases, + fetchScenes, + fetchMovies, }; diff --git a/src/knex.js b/src/knex.js index baebbfee9..022403461 100644 --- a/src/knex.js +++ b/src/knex.js @@ -4,8 +4,8 @@ const config = require('config'); const knex = require('knex'); module.exports = knex({ - client: 'pg', - connection: config.database, - // performance overhead, don't use asyncStackTraces in production - asyncStackTraces: process.env.NODE_ENV === 'development', + client: 'pg', + connection: config.database, + // performance overhead, don't use asyncStackTraces in production + asyncStackTraces: process.env.NODE_ENV === 'development', }); diff --git a/src/logger.js b/src/logger.js index 7e78981c5..6621e08bb 100644 --- a/src/logger.js +++ b/src/logger.js @@ -9,31 +9,31 @@ require('winston-daily-rotate-file'); const args = require('./argv'); function logger(filepath) { - const root = filepath.match(/src\/|dist\//); - const filename = filepath.slice(root.index + root[0].length) - .replace(path.extname(filepath), ''); + const root = filepath.match(/src\/|dist\//); + const filename = filepath.slice(root.index + root[0].length) + .replace(path.extname(filepath), ''); - return winston.createLogger({ - format: winston.format.combine( - winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), - winston.format(info => (info instanceof Error - ? { ...info, message: info.stack } - : { ...info, message: typeof info.message === 'string' ? info.message : util.inspect(info.message) }))(), - winston.format.colorize(), - winston.format.printf(({ level, timestamp, label, message }) => `${timestamp} ${level} [${label || filename}] ${message}`), - ), - transports: [ - new winston.transports.Console({ - level: args.level, - timestamp: true, - }), - new winston.transports.DailyRotateFile({ - datePattern: 'YYYY-MM-DD', - filename: 'log/%DATE%.log', - level: 'silly', - }), - ], - }); + return winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format(info => (info instanceof Error + ? { ...info, message: info.stack } + : { ...info, message: typeof info.message === 'string' ? info.message : util.inspect(info.message) }))(), + winston.format.colorize(), + winston.format.printf(({ level, timestamp, label, message }) => `${timestamp} ${level} [${label || filename}] ${message}`), + ), + transports: [ + new winston.transports.Console({ + level: args.level, + timestamp: true, + }), + new winston.transports.DailyRotateFile({ + datePattern: 'YYYY-MM-DD', + filename: 'log/%DATE%.log', + level: 'silly', + }), + ], + }); } module.exports = logger; diff --git a/src/media.js b/src/media.js index 87b615223..cf22e051b 100644 --- a/src/media.js +++ b/src/media.js @@ -19,591 +19,591 @@ const http = require('./utils/http'); const { get } = require('./utils/qu'); function sampleMedias(medias, limit = config.media.limit, preferLast = true) { - // limit media sets, use extras as fallbacks - if (medias.length <= limit) { - return medias; - } + // limit media sets, use extras as fallbacks + if (medias.length <= limit) { + return medias; + } - const chunkSize = Math.floor(medias.length / limit); - const rest = medias.length - (limit * chunkSize); + const chunkSize = Math.floor(medias.length / limit); + const rest = medias.length - (limit * chunkSize); - const chunks = Array.from( - { length: limit }, - (value, index) => { - const start = (chunkSize * index) + Math.min(index, rest); + const chunks = Array.from( + { length: limit }, + (value, index) => { + const start = (chunkSize * index) + Math.min(index, rest); - return medias.slice( - start, - start + chunkSize + (index < rest ? 1 : 0), - ); - }, - ); + return medias.slice( + start, + start + chunkSize + (index < rest ? 1 : 0), + ); + }, + ); - // flip last chunk so the very last image (often the best cumshot) is tried first - const lastPreferredChunks = preferLast - ? chunks.slice(0, -1).concat(chunks.slice(-1).reverse()) - : chunks; + // flip last chunk so the very last image (often the best cumshot) is tried first + const lastPreferredChunks = preferLast + ? chunks.slice(0, -1).concat(chunks.slice(-1).reverse()) + : chunks; - const groupedMedias = lastPreferredChunks.map((chunk) => { - // merge chunked medias into single media with grouped fallback priorities, - // so the first sources of each media is preferred over all second sources, etc. - const sources = chunk - .reduce((accSources, media) => { - media.sources.forEach((source, index) => { - if (!accSources[index]) { - accSources.push([source]); - return; - } + const groupedMedias = lastPreferredChunks.map((chunk) => { + // merge chunked medias into single media with grouped fallback priorities, + // so the first sources of each media is preferred over all second sources, etc. + const sources = chunk + .reduce((accSources, media) => { + media.sources.forEach((source, index) => { + if (!accSources[index]) { + accSources.push([source]); + return; + } - accSources[index].push(source); - }); + accSources[index].push(source); + }); - return accSources; - }, []) - .flat(); + return accSources; + }, []) + .flat(); - return { - id: chunk[0].id, - role: chunk[0].role, - sources, - }; - }); + return { + id: chunk[0].id, + role: chunk[0].role, + sources, + }; + }); - return groupedMedias; + return groupedMedias; } function itemsByKey(items, key) { - return items.reduce((acc, item) => ({ ...acc, [item[key]]: item }), {}); + return items.reduce((acc, item) => ({ ...acc, [item[key]]: item }), {}); } function toBaseSource(rawSource) { - if (rawSource.src || (rawSource.extract && rawSource.url)) { - const baseSource = {}; + if (rawSource.src || (rawSource.extract && rawSource.url)) { + const baseSource = {}; - if (rawSource.src) baseSource.src = rawSource.src; - if (rawSource.quality) baseSource.quality = rawSource.quality; - if (rawSource.type) baseSource.type = rawSource.type; + if (rawSource.src) baseSource.src = rawSource.src; + if (rawSource.quality) baseSource.quality = rawSource.quality; + if (rawSource.type) baseSource.type = rawSource.type; - if (rawSource.url) baseSource.url = rawSource.url; - if (rawSource.extract) baseSource.extract = rawSource.extract; + if (rawSource.url) baseSource.url = rawSource.url; + if (rawSource.extract) baseSource.extract = rawSource.extract; - if (rawSource.referer) baseSource.referer = rawSource.referer; - if (rawSource.host) baseSource.host = rawSource.host; + if (rawSource.referer) baseSource.referer = rawSource.referer; + if (rawSource.host) baseSource.host = rawSource.host; - if (rawSource.copyright) baseSource.copyright = rawSource.copyright; - if (rawSource.comment) baseSource.comment = rawSource.comment; - if (rawSource.group) baseSource.group = rawSource.group; + if (rawSource.copyright) baseSource.copyright = rawSource.copyright; + if (rawSource.comment) baseSource.comment = rawSource.comment; + if (rawSource.group) baseSource.group = rawSource.group; - return baseSource; - } + return baseSource; + } - if (typeof rawSource === 'string') { - return { - src: rawSource, - }; - } + if (typeof rawSource === 'string') { + return { + src: rawSource, + }; + } - return null; + return null; } function baseSourceToBaseMedia(baseSource, role) { - if (Array.isArray(baseSource)) { - if (baseSource.length > 0) { - return { - id: nanoid(), - role, - sources: baseSource, - }; - } + if (Array.isArray(baseSource)) { + if (baseSource.length > 0) { + return { + id: nanoid(), + role, + sources: baseSource, + }; + } - return null; - } + return null; + } - if (baseSource) { - return { - id: nanoid(), - role, - sources: [baseSource], - }; - } + if (baseSource) { + return { + id: nanoid(), + role, + sources: [baseSource], + }; + } - return null; + return null; } function fallbackMediaToBaseMedia(rawMedia, role) { - const baseSources = rawMedia - .map(source => toBaseSource(source)) - .filter(Boolean); + const baseSources = rawMedia + .map(source => toBaseSource(source)) + .filter(Boolean); - return baseSourceToBaseMedia(baseSources, role); + return baseSourceToBaseMedia(baseSources, role); } function toBaseMedias(rawMedias, role) { - if (!rawMedias || rawMedias.length === 0) { - return []; - } + if (!rawMedias || rawMedias.length === 0) { + return []; + } - const baseMedias = rawMedias.map((rawMedia) => { - if (!rawMedia) { - return null; - } + const baseMedias = rawMedias.map((rawMedia) => { + if (!rawMedia) { + return null; + } - if (Array.isArray(rawMedia)) { - // fallback sources provided - return fallbackMediaToBaseMedia(rawMedia, role); - } + if (Array.isArray(rawMedia)) { + // fallback sources provided + return fallbackMediaToBaseMedia(rawMedia, role); + } - const baseSource = toBaseSource(rawMedia); + const baseSource = toBaseSource(rawMedia); - return baseSourceToBaseMedia(baseSource, role); - }).filter(Boolean); + return baseSourceToBaseMedia(baseSource, role); + }).filter(Boolean); - const sampledBaseMedias = sampleMedias(baseMedias); + const sampledBaseMedias = sampleMedias(baseMedias); - return sampledBaseMedias; + return sampledBaseMedias; } async function findSourceDuplicates(baseMedias) { - const sourceUrls = baseMedias - .map(baseMedia => baseMedia.sources.map(source => source.src)) - .flat() - .filter(Boolean); + const sourceUrls = baseMedias + .map(baseMedia => baseMedia.sources.map(source => source.src)) + .flat() + .filter(Boolean); - const extractUrls = baseMedias - .map(baseMedia => baseMedia.sources.map(source => source.url)) - .flat() - .filter(Boolean); + const extractUrls = baseMedias + .map(baseMedia => baseMedia.sources.map(source => source.url)) + .flat() + .filter(Boolean); - const [existingSourceMedia, existingExtractMedia] = await Promise.all([ - knex('media').whereIn('source', sourceUrls), - knex('media').whereIn('source_page', extractUrls), - ]); + const [existingSourceMedia, existingExtractMedia] = await Promise.all([ + knex('media').whereIn('source', sourceUrls), + knex('media').whereIn('source_page', extractUrls), + ]); - const existingSourceMediaByUrl = itemsByKey(existingSourceMedia, 'source'); - const existingExtractMediaByUrl = itemsByKey(existingExtractMedia, 'source_page'); + const existingSourceMediaByUrl = itemsByKey(existingSourceMedia, 'source'); + const existingExtractMediaByUrl = itemsByKey(existingExtractMedia, 'source_page'); - return [existingSourceMediaByUrl, existingExtractMediaByUrl]; + return [existingSourceMediaByUrl, existingExtractMediaByUrl]; } async function findHashDuplicates(medias) { - const hashes = medias.map(media => media.meta?.hash || media.entry?.hash).filter(Boolean); + const hashes = medias.map(media => media.meta?.hash || media.entry?.hash).filter(Boolean); - const existingHashMediaEntries = await knex('media').whereIn('hash', hashes); - const existingHashMediaEntriesByHash = itemsByKey(existingHashMediaEntries, 'hash'); + const existingHashMediaEntries = await knex('media').whereIn('hash', hashes); + const existingHashMediaEntriesByHash = itemsByKey(existingHashMediaEntries, 'hash'); - const uniqueHashMedias = medias.filter(media => !media.entry && !existingHashMediaEntriesByHash[media.meta?.hash]); + const uniqueHashMedias = medias.filter(media => !media.entry && !existingHashMediaEntriesByHash[media.meta?.hash]); - const { selfDuplicateMedias, selfUniqueMediasByHash } = uniqueHashMedias.reduce((acc, media) => { - if (!media.meta?.hash) { - return acc; - } + const { selfDuplicateMedias, selfUniqueMediasByHash } = uniqueHashMedias.reduce((acc, media) => { + if (!media.meta?.hash) { + return acc; + } - if (acc.selfUniqueMediasByHash[media.meta.hash]) { - acc.selfDuplicateMedias.push({ - ...media, - use: acc.selfUniqueMediasByHash[media.meta.hash].id, - }); + if (acc.selfUniqueMediasByHash[media.meta.hash]) { + acc.selfDuplicateMedias.push({ + ...media, + use: acc.selfUniqueMediasByHash[media.meta.hash].id, + }); - return acc; - } + return acc; + } - acc.selfUniqueMediasByHash[media.meta.hash] = media; + acc.selfUniqueMediasByHash[media.meta.hash] = media; - return acc; - }, { - selfDuplicateMedias: [], - selfUniqueMediasByHash: {}, - }); + return acc; + }, { + selfDuplicateMedias: [], + selfUniqueMediasByHash: {}, + }); - const selfUniqueHashMedias = Object.values(selfUniqueMediasByHash); + const selfUniqueHashMedias = Object.values(selfUniqueMediasByHash); - const existingHashMedias = medias - .filter(media => existingHashMediaEntriesByHash[media.entry?.hash || media.meta?.hash]) - .map(media => ({ - ...media, - entry: existingHashMediaEntriesByHash[media.entry?.hash || media.meta?.hash], - })) - .concat(selfDuplicateMedias); + const existingHashMedias = medias + .filter(media => existingHashMediaEntriesByHash[media.entry?.hash || media.meta?.hash]) + .map(media => ({ + ...media, + entry: existingHashMediaEntriesByHash[media.entry?.hash || media.meta?.hash], + })) + .concat(selfDuplicateMedias); - return [selfUniqueHashMedias, existingHashMedias]; + return [selfUniqueHashMedias, existingHashMedias]; } async function extractSource(baseSource, { existingExtractMediaByUrl }) { - if (typeof baseSource.extract !== 'function' || !baseSource.url) { - return baseSource; - } + if (typeof baseSource.extract !== 'function' || !baseSource.url) { + return baseSource; + } - const existingExtractMedia = existingExtractMediaByUrl[baseSource.url]; + const existingExtractMedia = existingExtractMediaByUrl[baseSource.url]; - if (existingExtractMedia) { - // media entry found by extract URL - return { - ...baseSource, - entry: existingExtractMedia, - }; - } + if (existingExtractMedia) { + // media entry found by extract URL + return { + ...baseSource, + entry: existingExtractMedia, + }; + } - const res = await get(baseSource.url); + const res = await get(baseSource.url); - if (res.ok) { - const src = await baseSource.extract(res.item); + if (res.ok) { + const src = await baseSource.extract(res.item); - return { - ...baseSource, - src, - }; - } + return { + ...baseSource, + src, + }; + } - throw new Error(`Could not extract source from ${baseSource.url}: ${res.status}`); + throw new Error(`Could not extract source from ${baseSource.url}: ${res.status}`); } async function storeImageFile(media, hashDir, hashSubDir, filename, filedir, filepath) { - const thumbdir = path.join(media.role, 'thumbs', hashDir, hashSubDir); - const thumbpath = path.join(thumbdir, filename); + const thumbdir = path.join(media.role, 'thumbs', hashDir, hashSubDir); + const thumbpath = path.join(thumbdir, filename); - const lazydir = path.join(media.role, 'lazy', hashDir, hashSubDir); - const lazypath = path.join(lazydir, filename); + const lazydir = path.join(media.role, 'lazy', hashDir, hashSubDir); + const lazypath = path.join(lazydir, filename); - await Promise.all([ - fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }), - fsPromises.mkdir(path.join(config.media.path, thumbdir), { recursive: true }), - fsPromises.mkdir(path.join(config.media.path, lazydir), { recursive: true }), - ]); + await Promise.all([ + fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }), + fsPromises.mkdir(path.join(config.media.path, thumbdir), { recursive: true }), + fsPromises.mkdir(path.join(config.media.path, lazydir), { recursive: true }), + ]); - const image = sharp(media.file.path); - const info = await image.metadata(); + const image = sharp(media.file.path); + const info = await image.metadata(); - // generate thumbnail and lazy - await Promise.all([ - image - .resize({ - height: config.media.thumbnailSize, - withoutEnlargement: true, - }) - .jpeg({ quality: config.media.thumbnailQuality }) - .toFile(path.join(config.media.path, thumbpath)), - image - .resize({ - height: config.media.lazySize, - withoutEnlargement: true, - }) - .jpeg({ quality: config.media.lazyQuality }) - .toFile(path.join(config.media.path, lazypath)), - ]); + // generate thumbnail and lazy + await Promise.all([ + image + .resize({ + height: config.media.thumbnailSize, + withoutEnlargement: true, + }) + .jpeg({ quality: config.media.thumbnailQuality }) + .toFile(path.join(config.media.path, thumbpath)), + image + .resize({ + height: config.media.lazySize, + withoutEnlargement: true, + }) + .jpeg({ quality: config.media.lazyQuality }) + .toFile(path.join(config.media.path, lazypath)), + ]); - if (media.meta.subtype === 'jpeg') { - // move temp file to permanent location - await fsPromises.rename(media.file.path, path.join(config.media.path, filepath)); - } else { - // convert to JPEG and write to permanent location - await sharp(media.file.path) - .jpeg() - .toFile(path.join(config.media.path, filepath)); + if (media.meta.subtype === 'jpeg') { + // move temp file to permanent location + await fsPromises.rename(media.file.path, path.join(config.media.path, filepath)); + } else { + // convert to JPEG and write to permanent location + await sharp(media.file.path) + .jpeg() + .toFile(path.join(config.media.path, filepath)); - // remove temp file - await fsPromises.unlink(media.file.path); - } + // remove temp file + await fsPromises.unlink(media.file.path); + } - logger.silly(`Stored thumbnail, lazy and permanent media file for ${media.id} from ${media.src} at ${filepath}`); + logger.silly(`Stored thumbnail, lazy and permanent media file for ${media.id} from ${media.src} at ${filepath}`); - return { - ...media, - file: { - path: filepath, - thumbnail: thumbpath, - lazy: lazypath, - }, - meta: { - ...media.meta, - width: info.width, - height: info.height, - }, - }; + return { + ...media, + file: { + path: filepath, + thumbnail: thumbpath, + lazy: lazypath, + }, + meta: { + ...media.meta, + width: info.width, + height: info.height, + }, + }; } async function storeFile(media) { - try { - const hashDir = media.meta.hash.slice(0, 2); - const hashSubDir = media.meta.hash.slice(2, 4); - const hashFilename = media.meta.hash.slice(4); + try { + const hashDir = media.meta.hash.slice(0, 2); + const hashSubDir = media.meta.hash.slice(2, 4); + const hashFilename = media.meta.hash.slice(4); - const filename = media.quality - ? `${hashFilename}_${media.quality}.${media.meta.extension}` - : `${hashFilename}.${media.meta.extension}`; + const filename = media.quality + ? `${hashFilename}_${media.quality}.${media.meta.extension}` + : `${hashFilename}.${media.meta.extension}`; - const filedir = path.join(media.role, hashDir, hashSubDir); - const filepath = path.join(filedir, filename); + const filedir = path.join(media.role, hashDir, hashSubDir); + const filepath = path.join(filedir, filename); - if (media.meta.type === 'image') { - return storeImageFile(media, hashDir, hashSubDir, filename, filedir, filepath); - } + if (media.meta.type === 'image') { + return storeImageFile(media, hashDir, hashSubDir, filename, filedir, filepath); + } - const [stat] = await Promise.all([ - fsPromises.stat(media.file.path), - fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }), - ]); + const [stat] = await Promise.all([ + fsPromises.stat(media.file.path), + fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }), + ]); - await fsPromises.rename(media.file.path, path.join(config.media.path, filepath)); + await fsPromises.rename(media.file.path, path.join(config.media.path, filepath)); - logger.silly(`Stored permanent media file for ${media.id} from ${media.src} at ${filepath}`); + logger.silly(`Stored permanent media file for ${media.id} from ${media.src} at ${filepath}`); - return { - ...media, - file: { - path: filepath, - }, - meta: { - ...media.meta, - size: stat.size, - }, - }; - } catch (error) { - logger.warn(`Failed to store ${media.src}: ${error.message}`); + return { + ...media, + file: { + path: filepath, + }, + meta: { + ...media.meta, + size: stat.size, + }, + }; + } catch (error) { + logger.warn(`Failed to store ${media.src}: ${error.message}`); - return null; - } + return null; + } } async function fetchSource(source, baseMedia) { - logger.silly(`Fetching media from ${source.src}`); - // attempts + logger.silly(`Fetching media from ${source.src}`); + // attempts - async function attempt(attempts = 1) { - try { - const tempFilePath = path.join(config.media.path, 'temp', `${baseMedia.id}`); + async function attempt(attempts = 1) { + try { + const tempFilePath = path.join(config.media.path, 'temp', `${baseMedia.id}`); - const hasher = new blake2.Hash('blake2b'); - hasher.setEncoding('hex'); + const hasher = new blake2.Hash('blake2b'); + hasher.setEncoding('hex'); - const tempFileTarget = fs.createWriteStream(tempFilePath); - const hashStream = new PassThrough(); - let size = 0; + const tempFileTarget = fs.createWriteStream(tempFilePath); + const hashStream = new PassThrough(); + let size = 0; - hashStream.on('data', (chunk) => { - size += chunk.length; - hasher.write(chunk); - }); + hashStream.on('data', (chunk) => { + size += chunk.length; + hasher.write(chunk); + }); - const res = await http.get(source.src, { - ...(source.referer && { referer: source.referer }), - ...(source.host && { host: source.host }), - }, { - stream: true, // sources are fetched in parallel, don't gobble up memory - transforms: [hashStream], - destination: tempFileTarget, - }); + const res = await http.get(source.src, { + ...(source.referer && { referer: source.referer }), + ...(source.host && { host: source.host }), + }, { + stream: true, // sources are fetched in parallel, don't gobble up memory + transforms: [hashStream], + destination: tempFileTarget, + }); - hasher.end(); + hasher.end(); - const hash = hasher.read(); - const { pathname } = new URL(source.src); - const mimetype = res.headers['content-type'] || mime.getType(pathname); - const [type, subtype] = mimetype.split('/'); - const extension = mime.getExtension(mimetype); + const hash = hasher.read(); + const { pathname } = new URL(source.src); + const mimetype = res.headers['content-type'] || mime.getType(pathname); + const [type, subtype] = mimetype.split('/'); + const extension = mime.getExtension(mimetype); - if (!res.ok) { - throw new Error(`Response ${res.status} not OK`); - } + if (!res.ok) { + throw new Error(`Response ${res.status} not OK`); + } - return { - ...source, - file: { - path: tempFilePath, - }, - meta: { - hash, - mimetype, - extension, - type, - subtype, - size, - }, - }; - } catch (error) { - logger.warn(`Failed attempt ${attempts}/3 to fetch ${source.src}: ${error.message}`); + return { + ...source, + file: { + path: tempFilePath, + }, + meta: { + hash, + mimetype, + extension, + type, + subtype, + size, + }, + }; + } catch (error) { + logger.warn(`Failed attempt ${attempts}/3 to fetch ${source.src}: ${error.message}`); - if (attempts < 3) { - await Promise.delay(1000); + if (attempts < 3) { + await Promise.delay(1000); - return attempt(attempts + 1); - } + return attempt(attempts + 1); + } - throw new Error(`Failed to fetch ${source.src}: ${error.message}`); - } - } + throw new Error(`Failed to fetch ${source.src}: ${error.message}`); + } + } - return attempt(1); + return attempt(1); } async function trySource(baseSource, existingMedias, baseMedia) { - // catch error and try the next source - const extractedSource = await extractSource(baseSource, existingMedias); - const existingSourceMedia = existingMedias.existingSourceMediaByUrl[extractedSource.src]; + // catch error and try the next source + const extractedSource = await extractSource(baseSource, existingMedias); + const existingSourceMedia = existingMedias.existingSourceMediaByUrl[extractedSource.src]; - if (!argv.force && extractedSource.entry) { - logger.silly(`Media page URL already in database, not extracting ${baseSource.url}`); + if (!argv.force && extractedSource.entry) { + logger.silly(`Media page URL already in database, not extracting ${baseSource.url}`); - // media entry found during extraction, don't fetch - return extractedSource; - } + // media entry found during extraction, don't fetch + return extractedSource; + } - if (!argv.force && existingSourceMedia) { - logger.silly(`Media source URL already in database, skipping ${baseSource.src}`); + if (!argv.force && existingSourceMedia) { + logger.silly(`Media source URL already in database, skipping ${baseSource.src}`); - // media entry found by source URL, don't fetch - return { - ...baseSource, - entry: existingSourceMedia, - }; - } + // media entry found by source URL, don't fetch + return { + ...baseSource, + entry: existingSourceMedia, + }; + } - return fetchSource(extractedSource, baseMedia); + return fetchSource(extractedSource, baseMedia); } async function fetchMedia(baseMedia, existingMedias) { - try { - const source = await baseMedia.sources.reduce( - // try each source until success - (result, baseSource, baseSourceIndex) => result.catch(async (error) => { - if (error.message) { - logger.warn(error.message); - } + try { + const source = await baseMedia.sources.reduce( + // try each source until success + (result, baseSource, baseSourceIndex) => result.catch(async (error) => { + if (error.message) { + logger.warn(error.message); + } - return trySource(baseSource, existingMedias, baseMedia, baseSourceIndex); - }), - Promise.reject(new Error()), - ); + return trySource(baseSource, existingMedias, baseMedia, baseSourceIndex); + }), + Promise.reject(new Error()), + ); - return { - ...baseMedia, - ...source, - }; - } catch (error) { - logger.warn(error.message); + return { + ...baseMedia, + ...source, + }; + } catch (error) { + logger.warn(error.message); - return baseMedia; - } + return baseMedia; + } } function curateMediaEntry(media, index) { - if (media.entry) { - return media; - } + if (media.entry) { + return media; + } - const curatedMediaEntry = { - id: media.id, - path: media.file.path, - thumbnail: media.file.thumbnail, - lazy: media.file.lazy, - index, - mime: media.meta.mimetype, - hash: media.meta.hash, - size: media.meta.size, - width: media.meta.width, - height: media.meta.height, - entropy: media.meta.entropy, - source: media.src, - source_page: media.url, - scraper: media.scraper, - copyright: media.copyright, - comment: media.comment, - }; + const curatedMediaEntry = { + id: media.id, + path: media.file.path, + thumbnail: media.file.thumbnail, + lazy: media.file.lazy, + index, + mime: media.meta.mimetype, + hash: media.meta.hash, + size: media.meta.size, + width: media.meta.width, + height: media.meta.height, + entropy: media.meta.entropy, + source: media.src, + source_page: media.url, + scraper: media.scraper, + copyright: media.copyright, + comment: media.comment, + }; - return { - ...media, - newEntry: true, - entry: curatedMediaEntry, - }; + return { + ...media, + newEntry: true, + entry: curatedMediaEntry, + }; } async function storeMedias(baseMedias) { - await fsPromises.mkdir(path.join(config.media.path, 'temp'), { recursive: true }); + await fsPromises.mkdir(path.join(config.media.path, 'temp'), { recursive: true }); - const [existingSourceMediaByUrl, existingExtractMediaByUrl] = await findSourceDuplicates(baseMedias); + const [existingSourceMediaByUrl, existingExtractMediaByUrl] = await findSourceDuplicates(baseMedias); - const fetchedMedias = await Promise.map( - baseMedias, - async baseMedia => fetchMedia(baseMedia, { existingSourceMediaByUrl, existingExtractMediaByUrl }), - ); + const fetchedMedias = await Promise.map( + baseMedias, + async baseMedia => fetchMedia(baseMedia, { existingSourceMediaByUrl, existingExtractMediaByUrl }), + ); - const [uniqueHashMedias, existingHashMedias] = await findHashDuplicates(fetchedMedias); + const [uniqueHashMedias, existingHashMedias] = await findHashDuplicates(fetchedMedias); - const savedMedias = await Promise.map( - uniqueHashMedias, - async baseMedia => storeFile(baseMedia), - ); + const savedMedias = await Promise.map( + uniqueHashMedias, + async baseMedia => storeFile(baseMedia), + ); - const newMediaWithEntries = savedMedias.map((media, index) => curateMediaEntry(media, index)); - const newMediaEntries = newMediaWithEntries.filter(media => media.newEntry).map(media => media.entry); + const newMediaWithEntries = savedMedias.map((media, index) => curateMediaEntry(media, index)); + const newMediaEntries = newMediaWithEntries.filter(media => media.newEntry).map(media => media.entry); - await knex('media').insert(newMediaEntries); + await knex('media').insert(newMediaEntries); - return [...newMediaWithEntries, ...existingHashMedias]; + return [...newMediaWithEntries, ...existingHashMedias]; } async function associateReleaseMedia(releases) { - if (!argv.media) { - return; - } + if (!argv.media) { + return; + } - const baseMediasByReleaseId = releases.reduce((acc, release) => ({ - ...acc, - [release.id]: [ - ...(argv.images && argv.poster ? toBaseMedias([release.poster], 'posters') : []), - ...(argv.images && argv.poster ? toBaseMedias(release.covers, 'covers') : []), - ...(argv.images && argv.photos ? toBaseMedias(release.photos, 'photos') : []), - ...(argv.videos && argv.trailer ? toBaseMedias([release.trailer], 'trailers') : []), - ...(argv.videos && argv.teaser ? toBaseMedias([release.teaser], 'teasers') : []), - ], - }), {}); + const baseMediasByReleaseId = releases.reduce((acc, release) => ({ + ...acc, + [release.id]: [ + ...(argv.images && argv.poster ? toBaseMedias([release.poster], 'posters') : []), + ...(argv.images && argv.poster ? toBaseMedias(release.covers, 'covers') : []), + ...(argv.images && argv.photos ? toBaseMedias(release.photos, 'photos') : []), + ...(argv.videos && argv.trailer ? toBaseMedias([release.trailer], 'trailers') : []), + ...(argv.videos && argv.teaser ? toBaseMedias([release.teaser], 'teasers') : []), + ], + }), {}); - const baseMediasByRole = Object.values(baseMediasByReleaseId) - .flat() - .filter(Boolean) - .reduce((acc, baseMedia) => { - if (!acc[baseMedia.role]) acc[baseMedia.role] = []; - acc[baseMedia.role].push(baseMedia); + const baseMediasByRole = Object.values(baseMediasByReleaseId) + .flat() + .filter(Boolean) + .reduce((acc, baseMedia) => { + if (!acc[baseMedia.role]) acc[baseMedia.role] = []; + acc[baseMedia.role].push(baseMedia); - return acc; - }, {}); + return acc; + }, {}); - await Promise.reduce(['posters', 'covers', 'photos', 'teasers', 'trailers'], async (chain, role) => { - // stage by role so posters are prioritized over photos and videos - await chain; + await Promise.reduce(['posters', 'covers', 'photos', 'teasers', 'trailers'], async (chain, role) => { + // stage by role so posters are prioritized over photos and videos + await chain; - const baseMedias = baseMediasByRole[role]; + const baseMedias = baseMediasByRole[role]; - if (!baseMedias) { - return; - } + if (!baseMedias) { + return; + } - const storedMedias = await storeMedias(baseMedias); - const storedMediasById = itemsByKey(storedMedias, 'id'); + const storedMedias = await storeMedias(baseMedias); + const storedMediasById = itemsByKey(storedMedias, 'id'); - const associations = Object - .entries(baseMediasByReleaseId) - .reduce((acc, [releaseId, releaseBaseMedias]) => { - releaseBaseMedias.forEach((baseMedia) => { - const media = storedMediasById[baseMedia.id]; + const associations = Object + .entries(baseMediasByReleaseId) + .reduce((acc, [releaseId, releaseBaseMedias]) => { + releaseBaseMedias.forEach((baseMedia) => { + const media = storedMediasById[baseMedia.id]; - if (media) { - acc.push({ - release_id: releaseId, - media_id: media.use || media.entry.id, - }); - } - }); + if (media) { + acc.push({ + release_id: releaseId, + media_id: media.use || media.entry.id, + }); + } + }); - return acc; - }, []) - .filter(Boolean); + return acc; + }, []) + .filter(Boolean); - if (associations.length > 0) { - await knex.raw(`${knex(`releases_${role}`).insert(associations)} ON CONFLICT DO NOTHING`); - } - }, Promise.resolve()); + if (associations.length > 0) { + await knex.raw(`${knex(`releases_${role}`).insert(associations)} ON CONFLICT DO NOTHING`); + } + }, Promise.resolve()); } module.exports = { - associateReleaseMedia, + associateReleaseMedia, }; diff --git a/src/networks.js b/src/networks.js index 983bfac22..549bd1a3f 100644 --- a/src/networks.js +++ b/src/networks.js @@ -5,77 +5,77 @@ const whereOr = require('./utils/where-or'); const { fetchSites } = require('./sites'); async function curateNetwork(network, includeParameters = false, includeSites = true, includeStudios = false) { - const curatedNetwork = { - id: network.id, - name: network.name, - url: network.url, - description: network.description, - slug: network.slug, - parameters: includeParameters ? network.parameters : null, - }; + const curatedNetwork = { + id: network.id, + name: network.name, + url: network.url, + description: network.description, + slug: network.slug, + parameters: includeParameters ? network.parameters : null, + }; - if (includeSites) { - curatedNetwork.sites = await fetchSites({ network_id: network.id }); - } + if (includeSites) { + curatedNetwork.sites = await fetchSites({ network_id: network.id }); + } - if (includeStudios) { - const studios = await knex('studios').where({ network_id: network.id }); + if (includeStudios) { + const studios = await knex('studios').where({ network_id: network.id }); - curatedNetwork.studios = studios.map(studio => ({ - id: studio.id, - name: studio.name, - url: studio.url, - description: studio.description, - slug: studio.slug, - })); - } + curatedNetwork.studios = studios.map(studio => ({ + id: studio.id, + name: studio.name, + url: studio.url, + description: studio.description, + slug: studio.slug, + })); + } - return curatedNetwork; + return curatedNetwork; } function curateNetworks(releases) { - return Promise.all(releases.map(async release => curateNetwork(release))); + return Promise.all(releases.map(async release => curateNetwork(release))); } async function findNetworkByUrl(url) { - const { hostname } = new URL(url); - const domain = hostname.replace(/^www./, ''); + const { hostname } = new URL(url); + const domain = hostname.replace(/^www./, ''); - const network = await knex('networks') - .where('networks.url', 'like', `%${domain}`) - .orWhere('networks.url', url) - .first(); + const network = await knex('networks') + .where('networks.url', 'like', `%${domain}`) + .orWhere('networks.url', url) + .first(); - if (network) { - return curateNetwork(network, true); - } + if (network) { + return curateNetwork(network, true); + } - return null; + return null; } async function fetchNetworks(queryObject) { - const releases = await knex('networks') - .where(builder => whereOr(queryObject, 'networks', builder)) - .limit(100); + const releases = await knex('networks') + .where(builder => whereOr(queryObject, 'networks', builder)) + .limit(100); - return curateNetworks(releases); + return curateNetworks(releases); } async function fetchNetworksFromReleases() { - const releases = await knex('releases') - .select('site_id', '') - .leftJoin('sites', 'sites.id', 'releases.site_id') - .leftJoin('networks', 'networks.id', 'sites.network_id') - .groupBy('networks.id') - .limit(100); + const releases = await knex('releases') + .select('site_id', '') + .leftJoin('sites', 'sites.id', 'releases.site_id') + .leftJoin('networks', 'networks.id', 'sites.network_id') + .groupBy('networks.id') + .limit(100); - return curateNetworks(releases); + return curateNetworks(releases); } module.exports = { - curateNetwork, - curateNetworks, - fetchNetworks, - fetchNetworksFromReleases, - findNetworkByUrl, + curateNetwork, + curateNetworks, + fetchNetworks, + fetchNetworksFromReleases, + findNetworkByUrl, }; diff --git a/src/releases-legacy.js b/src/releases-legacy.js index 0b13c77c9..f587f1239 100644 --- a/src/releases-legacy.js +++ b/src/releases-legacy.js @@ -11,356 +11,356 @@ const whereOr = require('./utils/where-or'); const { associateTags } = require('./tags'); const { associateActors, scrapeBasicActors } = require('./actors'); const { - pluckItems, - storeMedia, - associateMedia, + pluckItems, + storeMedia, + associateMedia, } = require('./media'); const { fetchSites } = require('./sites'); const slugify = require('./utils/slugify'); const capitalize = require('./utils/capitalize'); function commonQuery(queryBuilder, { - filter = [], - after = new Date(0), // January 1970 - before = new Date(2 ** 44), // May 2109 - limit = 100, + filter = [], + after = new Date(0), // January 1970 + before = new Date(2 ** 44), // May 2109 + limit = 100, }) { - const finalFilter = [].concat(filter); // ensure filter is array + const finalFilter = [].concat(filter); // ensure filter is array - queryBuilder - .leftJoin('sites', 'releases.site_id', 'sites.id') - .leftJoin('studios', 'releases.studio_id', 'studios.id') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .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', 'networks.description as network_description', - ) - .whereNotExists((builder) => { - // apply tag filters - builder - .select('*') - .from('tags_associated') - .leftJoin('tags', 'tags_associated.tag_id', 'tags.id') - .whereIn('tags.slug', finalFilter) - .where('tags_associated.domain', 'releases') - .whereRaw('tags_associated.target_id = releases.id'); - }) - .andWhere('releases.date', '>', after) - .andWhere('releases.date', '<=', before) - .orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }]) - .limit(limit); + queryBuilder + .leftJoin('sites', 'releases.site_id', 'sites.id') + .leftJoin('studios', 'releases.studio_id', 'studios.id') + .leftJoin('networks', 'sites.network_id', 'networks.id') + .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', 'networks.description as network_description', + ) + .whereNotExists((builder) => { + // apply tag filters + builder + .select('*') + .from('tags_associated') + .leftJoin('tags', 'tags_associated.tag_id', 'tags.id') + .whereIn('tags.slug', finalFilter) + .where('tags_associated.domain', 'releases') + .whereRaw('tags_associated.target_id = releases.id'); + }) + .andWhere('releases.date', '>', after) + .andWhere('releases.date', '<=', before) + .orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }]) + .limit(limit); } async function curateRelease(release) { - const [actors, tags, media] = await Promise.all([ - knex('actors_associated') - .select( - 'actors.id', 'actors.name', 'actors.gender', 'actors.slug', 'actors.birthdate', - 'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias', - 'media.thumbnail as avatar', - ) - .where({ release_id: release.id }) - .leftJoin('actors', 'actors.id', 'actors_associated.actor_id') - .leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2') - .leftJoin('media', (builder) => { - builder - .on('media.target_id', 'actors.id') - .andOnVal('media.domain', 'actors') - .andOnVal('media.index', '0'); - }) - .orderBy('actors.gender'), - knex('tags_associated') - .select('tags.name', 'tags.slug') - .where({ - domain: 'releases', - target_id: release.id, - }) - .leftJoin('tags', 'tags.id', 'tags_associated.tag_id') - .orderBy('tags.priority', 'desc'), - knex('media') - .where({ - target_id: release.id, - domain: 'releases', - }) - .orderBy(['role', 'index']), - ]); + const [actors, tags, media] = await Promise.all([ + knex('actors_associated') + .select( + 'actors.id', 'actors.name', 'actors.gender', 'actors.slug', 'actors.birthdate', + 'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias', + 'media.thumbnail as avatar', + ) + .where({ release_id: release.id }) + .leftJoin('actors', 'actors.id', 'actors_associated.actor_id') + .leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2') + .leftJoin('media', (builder) => { + builder + .on('media.target_id', 'actors.id') + .andOnVal('media.domain', 'actors') + .andOnVal('media.index', '0'); + }) + .orderBy('actors.gender'), + knex('tags_associated') + .select('tags.name', 'tags.slug') + .where({ + domain: 'releases', + target_id: release.id, + }) + .leftJoin('tags', 'tags.id', 'tags_associated.tag_id') + .orderBy('tags.priority', 'desc'), + knex('media') + .where({ + target_id: release.id, + domain: 'releases', + }) + .orderBy(['role', 'index']), + ]); - const curatedRelease = { - id: release.id, - type: release.type, - title: release.title, - date: release.date, - dateAdded: release.created_at, - description: release.description, - url: release.url, - shootId: release.shoot_id, - entryId: release.entry_id, - actors: actors.map(actor => ({ - id: actor.id, - slug: actor.slug, - name: actor.name, - gender: actor.gender, - birthdate: actor.birthdate, - age: moment().diff(actor.birthdate, 'years'), - ageThen: moment(release.date).diff(actor.birthdate, 'years'), - avatar: actor.avatar, - origin: actor.birth_country_alpha2 - ? { - country: { - name: actor.birth_country_alias, - alpha2: actor.birth_country_alpha2, - }, - } - : null, - })), - director: release.director, - tags, - duration: release.duration, - photos: media.filter(item => item.role === 'photo'), - poster: media.filter(item => item.role === 'poster')[0], - covers: media.filter(item => item.role === 'cover'), - trailer: media.filter(item => item.role === 'trailer')[0], - site: { - id: release.site_id, - name: release.site_name, - independent: !!release.site_parameters?.independent, - slug: release.site_slug, - url: release.site_url, - }, - studio: release.studio_id - ? { - id: release.studio_id, - name: release.studio_name, - slug: release.studio_slug, - url: release.studio_url, - } - : null, - network: { - id: release.network_id, - name: release.network_name, - description: release.network_description, - slug: release.network_slug, - url: release.network_url, - }, - }; + const curatedRelease = { + id: release.id, + type: release.type, + title: release.title, + date: release.date, + dateAdded: release.created_at, + description: release.description, + url: release.url, + shootId: release.shoot_id, + entryId: release.entry_id, + actors: actors.map(actor => ({ + id: actor.id, + slug: actor.slug, + name: actor.name, + gender: actor.gender, + birthdate: actor.birthdate, + age: moment().diff(actor.birthdate, 'years'), + ageThen: moment(release.date).diff(actor.birthdate, 'years'), + avatar: actor.avatar, + origin: actor.birth_country_alpha2 + ? { + country: { + name: actor.birth_country_alias, + alpha2: actor.birth_country_alpha2, + }, + } + : null, + })), + director: release.director, + tags, + duration: release.duration, + photos: media.filter(item => item.role === 'photo'), + poster: media.filter(item => item.role === 'poster')[0], + covers: media.filter(item => item.role === 'cover'), + trailer: media.filter(item => item.role === 'trailer')[0], + site: { + id: release.site_id, + name: release.site_name, + independent: !!release.site_parameters?.independent, + slug: release.site_slug, + url: release.site_url, + }, + studio: release.studio_id + ? { + id: release.studio_id, + name: release.studio_name, + slug: release.studio_slug, + url: release.studio_url, + } + : null, + network: { + id: release.network_id, + name: release.network_name, + description: release.network_description, + slug: release.network_slug, + url: release.network_url, + }, + }; - return curatedRelease; + return curatedRelease; } function curateReleases(releases) { - return Promise.all(releases.map(async release => curateRelease(release))); + return Promise.all(releases.map(async release => curateRelease(release))); } async function attachChannelSite(release) { - if (!release.site?.isFallback && !release.channel?.force) { - return release; - } + if (!release.site?.isFallback && !release.channel?.force) { + return release; + } - if (!release.channel) { - throw new Error(`Unable to derive channel site from generic URL: ${release.url}`); - } + if (!release.channel) { + throw new Error(`Unable to derive channel site from generic URL: ${release.url}`); + } - const [site] = await fetchSites({ - name: release.channel.name || release.channel, - slug: release.channel.slug || release.channel, - }); + const [site] = await fetchSites({ + name: release.channel.name || release.channel, + slug: release.channel.slug || release.channel, + }); - if (site) { - return { - ...release, - site, - }; - } + if (site) { + return { + ...release, + site, + }; + } - throw new Error(`Unable to match channel '${release.channel.slug || release.channel}' from generic URL: ${release.url}`); + throw new Error(`Unable to match channel '${release.channel.slug || release.channel}' from generic URL: ${release.url}`); } async function attachStudio(release) { - if (!release.studio) { - return release; - } + if (!release.studio) { + return release; + } - const studio = await knex('studios') - .where('name', release.studio) - .orWhere('slug', release.studio) - .orWhere('url', release.studio) - .first(); + const studio = await knex('studios') + .where('name', release.studio) + .orWhere('slug', release.studio) + .orWhere('url', release.studio) + .first(); - return { - ...release, - studio, - }; + return { + ...release, + studio, + }; } async function curateReleaseEntry(release, batchId, existingRelease) { - const slug = slugify(release.title, { - encode: true, - limit: config.titleSlugLength, - }); + const slug = slugify(release.title, { + encode: true, + limit: config.titleSlugLength, + }); - const curatedRelease = { - site_id: release.site.id, - studio_id: release.studio ? release.studio.id : null, - shoot_id: release.shootId || null, - entry_id: release.entryId || null, - type: release.type, - url: release.url, - title: release.title, - slug, - date: release.date, - description: release.description, - // director: release.director, - duration: release.duration, - // likes: release.rating && release.rating.likes, - // dislikes: release.rating && release.rating.dislikes, - // rating: release.rating && release.rating.stars && Math.floor(release.rating.stars), - deep: typeof release.deep === 'boolean' ? release.deep : false, - deep_url: release.deepUrl, - updated_batch_id: batchId, - ...(!existingRelease && { created_batch_id: batchId }), - }; + const curatedRelease = { + site_id: release.site.id, + studio_id: release.studio ? release.studio.id : null, + shoot_id: release.shootId || null, + entry_id: release.entryId || null, + type: release.type, + url: release.url, + title: release.title, + slug, + date: release.date, + description: release.description, + // director: release.director, + duration: release.duration, + // likes: release.rating && release.rating.likes, + // dislikes: release.rating && release.rating.dislikes, + // rating: release.rating && release.rating.stars && Math.floor(release.rating.stars), + deep: typeof release.deep === 'boolean' ? release.deep : false, + deep_url: release.deepUrl, + updated_batch_id: batchId, + ...(!existingRelease && { created_batch_id: batchId }), + }; - return curatedRelease; + return curatedRelease; } async function fetchReleases(queryObject = {}, options = {}) { - const releases = await knex('releases') - .modify(commonQuery, options) - .andWhere(builder => whereOr(queryObject, 'releases', builder)); + const releases = await knex('releases') + .modify(commonQuery, options) + .andWhere(builder => whereOr(queryObject, 'releases', builder)); - return curateReleases(releases); + return curateReleases(releases); } async function fetchSiteReleases(queryObject, options = {}) { - const releases = await knex('releases') - .modify(commonQuery, options) - .where(builder => whereOr(queryObject, 'sites', builder)); + const releases = await knex('releases') + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'sites', builder)); - return curateReleases(releases); + return curateReleases(releases); } async function fetchNetworkReleases(queryObject, options = {}) { - const releases = await knex('releases') - .modify(commonQuery, options) - .where(builder => whereOr(queryObject, 'networks', builder)); + const releases = await knex('releases') + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'networks', builder)); - return curateReleases(releases); + return curateReleases(releases); } async function fetchActorReleases(queryObject, options = {}) { - const releases = await knex('actors_associated') - .leftJoin('releases', 'actors_associated.release_id', 'releases.id') - .leftJoin('actors', 'actors_associated.actor_id', 'actors.id') - .select( - 'actors.name as actor_name', - ) - .modify(commonQuery, options) - .where(builder => whereOr(queryObject, 'actors', builder)); + const releases = await knex('actors_associated') + .leftJoin('releases', 'actors_associated.release_id', 'releases.id') + .leftJoin('actors', 'actors_associated.actor_id', 'actors.id') + .select( + 'actors.name as actor_name', + ) + .modify(commonQuery, options) + .where(builder => whereOr(queryObject, 'actors', builder)); - return curateReleases(releases); + return curateReleases(releases); } async function fetchTagReleases(queryObject, options = {}) { - const releases = await knex('tags_associated') - .leftJoin('releases', 'tags_associated.target_id', 'releases.id') - .leftJoin('tags', 'tags_associated.tag_id', 'tags.id') - .select( - 'tags.name as tag_name', - ) - .modify(commonQuery, options) - .where('tags_associated.domain', 'releases') - .where(builder => whereOr(queryObject, 'tags', builder)); + const releases = await knex('tags_associated') + .leftJoin('releases', 'tags_associated.target_id', 'releases.id') + .leftJoin('tags', 'tags_associated.tag_id', 'tags.id') + .select( + 'tags.name as tag_name', + ) + .modify(commonQuery, options) + .where('tags_associated.domain', 'releases') + .where(builder => whereOr(queryObject, 'tags', builder)); - return curateReleases(releases); + return curateReleases(releases); } function accumulateActors(releases) { - return releases.reduce((acc, release) => { - if (!Array.isArray(release.actors)) return acc; + return releases.reduce((acc, release) => { + if (!Array.isArray(release.actors)) return acc; - release.actors.forEach((actor) => { - const actorName = actor.name ? actor.name.trim() : actor.trim(); - const actorSlug = slugify(actorName); + release.actors.forEach((actor) => { + const actorName = actor.name ? actor.name.trim() : actor.trim(); + const actorSlug = slugify(actorName); - if (!actorSlug) return; + if (!actorSlug) return; - if (!acc[actorSlug]) { - acc[actorSlug] = { - name: actorName, - slug: actorSlug, - releaseIds: new Set(), - avatars: [], - }; - } + if (!acc[actorSlug]) { + acc[actorSlug] = { + name: actorName, + slug: actorSlug, + releaseIds: new Set(), + avatars: [], + }; + } - acc[actorSlug].releaseIds.add(release.id); + acc[actorSlug].releaseIds.add(release.id); - if (actor.name) acc[actorSlug] = { ...acc[actorSlug], ...actor }; // actor input contains profile info - if (actor.avatar) { - const avatar = Array.isArray(actor.avatar) - ? actor.avatar.map(avatarX => ({ - src: avatarX.src || avatarX, - copyright: avatarX.copyright === undefined ? capitalize(release.site?.network?.name) : avatarX.copyright, - })) - : { - src: actor.avatar.src || actor.avatar, - copyright: actor.avatar.copyright === undefined ? capitalize(release.site?.network?.name) : actor.avatar.copyright, - }; + if (actor.name) acc[actorSlug] = { ...acc[actorSlug], ...actor }; // actor input contains profile info + if (actor.avatar) { + const avatar = Array.isArray(actor.avatar) + ? actor.avatar.map(avatarX => ({ + src: avatarX.src || avatarX, + copyright: avatarX.copyright === undefined ? capitalize(release.site?.network?.name) : avatarX.copyright, + })) + : { + src: actor.avatar.src || actor.avatar, + copyright: actor.avatar.copyright === undefined ? capitalize(release.site?.network?.name) : actor.avatar.copyright, + }; - acc[actorSlug].avatars = acc[actorSlug].avatars.concat([avatar]); // don't flatten fallbacks - } - }); + acc[actorSlug].avatars = acc[actorSlug].avatars.concat([avatar]); // don't flatten fallbacks + } + }); - return acc; - }, {}); + return acc; + }, {}); } async function storeReleaseAssets(releases) { - if (!argv.media) { - return; - } + if (!argv.media) { + return; + } - const releasePostersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.poster] }), {}); - const releaseCoversById = releases.reduce((acc, release) => ({ ...acc, [release.id]: release.covers }), {}); - const releaseTrailersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.trailer] }), {}); - const releaseTeasersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.teaser] }), {}); - const releasePhotosById = releases.reduce((acc, release) => ({ - ...acc, - [release.id]: pluckItems(release.photos), - }), {}); + const releasePostersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.poster] }), {}); + const releaseCoversById = releases.reduce((acc, release) => ({ ...acc, [release.id]: release.covers }), {}); + const releaseTrailersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.trailer] }), {}); + const releaseTeasersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.teaser] }), {}); + const releasePhotosById = releases.reduce((acc, release) => ({ + ...acc, + [release.id]: pluckItems(release.photos), + }), {}); - if (argv.images && argv.posters) { - const posters = await storeMedia(Object.values(releasePostersById).flat(), 'release', 'poster'); - if (posters) await associateMedia(releasePostersById, posters, 'release', 'poster'); - } + if (argv.images && argv.posters) { + const posters = await storeMedia(Object.values(releasePostersById).flat(), 'release', 'poster'); + if (posters) await associateMedia(releasePostersById, posters, 'release', 'poster'); + } - if (argv.images && argv.covers) { - const covers = await storeMedia(Object.values(releaseCoversById).flat(), 'release', 'cover'); - if (covers) await associateMedia(releaseCoversById, covers, 'release', 'cover'); - } + if (argv.images && argv.covers) { + const covers = await storeMedia(Object.values(releaseCoversById).flat(), 'release', 'cover'); + if (covers) await associateMedia(releaseCoversById, covers, 'release', 'cover'); + } - if (argv.images && argv.photos) { - const photos = await storeMedia(Object.values(releasePhotosById).flat(), 'release', 'photo'); - if (photos) await associateMedia(releasePhotosById, photos, 'release', 'photo'); - } + if (argv.images && argv.photos) { + const photos = await storeMedia(Object.values(releasePhotosById).flat(), 'release', 'photo'); + if (photos) await associateMedia(releasePhotosById, photos, 'release', 'photo'); + } - if (argv.videos && argv.trailers) { - const trailers = await storeMedia(Object.values(releaseTrailersById).flat(), 'release', 'trailer'); - if (trailers) await associateMedia(releaseTrailersById, trailers, 'release', 'trailer'); - } + if (argv.videos && argv.trailers) { + const trailers = await storeMedia(Object.values(releaseTrailersById).flat(), 'release', 'trailer'); + if (trailers) await associateMedia(releaseTrailersById, trailers, 'release', 'trailer'); + } - if (argv.videos && argv.teasers) { - const teasers = await storeMedia(Object.values(releaseTeasersById).flat(), 'release', 'teaser'); - if (teasers) await associateMedia(releaseTeasersById, teasers, 'release', 'teaser'); - } + if (argv.videos && argv.teasers) { + const teasers = await storeMedia(Object.values(releaseTeasersById).flat(), 'release', 'teaser'); + if (teasers) await associateMedia(releaseTeasersById, teasers, 'release', 'teaser'); + } } async function updateReleasesSearch(releaseIds) { - logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`); + logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`); - const documents = await knex.raw(` + const documents = await knex.raw(` SELECT releases.id AS release_id, TO_TSVECTOR( @@ -391,117 +391,117 @@ async function updateReleasesSearch(releaseIds) { GROUP BY releases.id, sites.name, sites.slug, sites.alias, sites.url, networks.name, networks.slug, networks.url; `, releaseIds && [releaseIds]); - if (documents.rows?.length > 0) { - const query = knex('releases_search').insert(documents.rows).toString(); - await knex.raw(`${query} ON CONFLICT (release_id) DO UPDATE SET document = EXCLUDED.document`); - } + if (documents.rows?.length > 0) { + const query = knex('releases_search').insert(documents.rows).toString(); + await knex.raw(`${query} ON CONFLICT (release_id) DO UPDATE SET document = EXCLUDED.document`); + } } async function storeRelease(release, batchId) { - if (!release.site) { - throw new Error(`Missing site, unable to store "${release.title}" (${release.url})`); - } + if (!release.site) { + throw new Error(`Missing site, unable to store "${release.title}" (${release.url})`); + } - if (!release.entryId) { - logger.warn(`Missing entry ID, unable to store "${release.title}" (${release.url})`); - return null; - } + if (!release.entryId) { + logger.warn(`Missing entry ID, unable to store "${release.title}" (${release.url})`); + return null; + } - const existingRelease = await knex('releases') - .where({ - entry_id: release.entryId, - site_id: release.site.id, - }) - .first(); + const existingRelease = await knex('releases') + .where({ + entry_id: release.entryId, + site_id: release.site.id, + }) + .first(); - const curatedRelease = await curateReleaseEntry(release, batchId, existingRelease); + const curatedRelease = await curateReleaseEntry(release, batchId, existingRelease); - if (existingRelease && !argv.redownload) { - return existingRelease; - } + if (existingRelease && !argv.redownload) { + return existingRelease; + } - if (existingRelease && argv.redownload) { - const [updatedRelease] = await knex('releases') - .where('id', existingRelease.id) - .update({ - ...existingRelease, - ...curatedRelease, - }) - .returning('*'); + if (existingRelease && argv.redownload) { + const [updatedRelease] = await knex('releases') + .where('id', existingRelease.id) + .update({ + ...existingRelease, + ...curatedRelease, + }) + .returning('*'); - if (updatedRelease) { - await associateTags(release, updatedRelease.id); - logger.info(`Updated release "${release.title}" (${existingRelease.id}, ${release.site.name})`); - } + if (updatedRelease) { + await associateTags(release, updatedRelease.id); + logger.info(`Updated release "${release.title}" (${existingRelease.id}, ${release.site.name})`); + } - await associateTags(release, existingRelease.id); + await associateTags(release, existingRelease.id); - return existingRelease; - } + return existingRelease; + } - const [releaseEntry] = await knex('releases') - .insert(curatedRelease) - .returning('*'); + const [releaseEntry] = await knex('releases') + .insert(curatedRelease) + .returning('*'); - await associateTags(release, releaseEntry.id); + await associateTags(release, releaseEntry.id); - logger.info(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`); + logger.info(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`); - return releaseEntry; + return releaseEntry; } async function storeReleases(releases) { - const [batchId] = await knex('batches').insert({ comment: null }).returning('id'); + const [batchId] = await knex('batches').insert({ comment: null }).returning('id'); - const storedReleases = await Promise.map(releases, async (release) => { - try { - const releaseWithChannelSite = await attachChannelSite(release); - const releaseWithStudio = await attachStudio(releaseWithChannelSite); - const storedRelease = await storeRelease(releaseWithStudio, batchId); + const storedReleases = await Promise.map(releases, async (release) => { + try { + const releaseWithChannelSite = await attachChannelSite(release); + const releaseWithStudio = await attachStudio(releaseWithChannelSite); + const storedRelease = await storeRelease(releaseWithStudio, batchId); - return storedRelease && { - id: storedRelease.id, - slug: storedRelease.slug, - ...releaseWithChannelSite, - }; - } catch (error) { - logger.error(error); + return storedRelease && { + id: storedRelease.id, + slug: storedRelease.slug, + ...releaseWithChannelSite, + }; + } catch (error) { + logger.error(error); - return null; - } - }, { - concurrency: 10, - }).filter(Boolean); + return null; + } + }, { + concurrency: 10, + }).filter(Boolean); - logger.info(`Stored ${storedReleases.length} new releases`); + logger.info(`Stored ${storedReleases.length} new releases`); - const actors = accumulateActors(storedReleases); + const actors = accumulateActors(storedReleases); - await associateActors(actors, storedReleases); + await associateActors(actors, storedReleases); - await Promise.all([ - // actors need to be stored before generating search - updateReleasesSearch(storedReleases.map(release => release.id)), - storeReleaseAssets(storedReleases), - ]); + await Promise.all([ + // actors need to be stored before generating search + updateReleasesSearch(storedReleases.map(release => release.id)), + storeReleaseAssets(storedReleases), + ]); - if (argv.withProfiles && Object.keys(actors).length > 0) { - await scrapeBasicActors(); - } + if (argv.withProfiles && Object.keys(actors).length > 0) { + await scrapeBasicActors(); + } - return { - releases: storedReleases, - actors, - }; + return { + releases: storedReleases, + actors, + }; } module.exports = { - fetchReleases, - fetchActorReleases, - fetchSiteReleases, - fetchNetworkReleases, - fetchTagReleases, - storeRelease, - storeReleases, - updateReleasesSearch, + fetchReleases, + fetchActorReleases, + fetchSiteReleases, + fetchNetworkReleases, + fetchTagReleases, + storeRelease, + storeReleases, + updateReleasesSearch, }; diff --git a/src/releases.js b/src/releases.js index ecf59981f..d5fe50897 100644 --- a/src/releases.js +++ b/src/releases.js @@ -3,18 +3,18 @@ const knex = require('./knex'); async function fetchReleases(limit = 100) { - const releases = await knex('releases').limit(limit); + const releases = await knex('releases').limit(limit); - return releases; + return releases; } async function searchReleases(query, limit = 100) { - const releases = await knex.raw('SELECT * FROM search_releases(?) LIMIT ?;', [query, limit]); + const releases = await knex.raw('SELECT * FROM search_releases(?) LIMIT ?;', [query, limit]); - return releases.rows; + return releases.rows; } module.exports = { - fetchReleases, - searchReleases, + fetchReleases, + searchReleases, }; diff --git a/src/scrape-releases.js b/src/scrape-releases.js deleted file mode 100644 index 82de59bbf..000000000 --- a/src/scrape-releases.js +++ /dev/null @@ -1,199 +0,0 @@ -'use strict'; - -const config = require('config'); -const Promise = require('bluebird'); - -const logger = require('./logger')(__filename); -const argv = require('./argv'); -const include = require('./utils/argv-include')(argv); -const knex = require('./knex'); -const scrapers = require('./scrapers/scrapers'); -const { findSiteByUrl } = require('./sites'); -const { findNetworkByUrl } = require('./networks'); -const { storeReleases } = require('./releases'); - -async function findSite(url, release) { - if (release?.site) return release.site; - if (!url) return null; - - const site = await findSiteByUrl(url); - - if (site) { - return site; - } - - const network = await findNetworkByUrl(url); - - if (network) { - return { - ...network, - network, - isFallback: true, - }; - } - - return null; -} - -async function scrapeRelease(source, basicRelease = null, type = 'scene', beforeFetchLatest) { - // profile scraper may return either URLs or pre-scraped scenes - const sourceIsUrlOrEmpty = typeof source === 'string' || source === undefined; - const url = sourceIsUrlOrEmpty ? source : source?.url; - const release = sourceIsUrlOrEmpty ? basicRelease : source; - - const site = basicRelease?.site || await findSite(url, release); - - if (!site) { - throw new Error(`Could not find site for ${url} in database`); - } - - if (!argv.deep && release) { - return { - ...release, - site, - }; - } - - const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug]; - - if (!scraper) { - throw new Error(`Could not find scraper for ${url}`); - } - - if ((type === 'scene' && !scraper.fetchScene) || (type === 'movie' && !scraper.fetchMovie)) { - if (release) { - logger.warn(`The '${site.name}'-scraper cannot fetch individual ${type}s`); - return null; - } - - throw new Error(`The '${site.name}'-scraper cannot fetch individual ${type}s`); - } - - if (!release) { - logger.info(`Scraping release from ${url}`); - } - - const scrapedRelease = type === 'scene' - ? await scraper.fetchScene(url, site, release, beforeFetchLatest, include) - : await scraper.fetchMovie(url, site, release, beforeFetchLatest, include); - - return { - ...release, - ...scrapedRelease, - ...(scrapedRelease && release?.tags && { - tags: release.tags.concat(scrapedRelease.tags), - }), - site, - }; -} - -async function accumulateMovies(releases) { - if (!argv.withMovies) return []; - - const moviesByUrl = releases.reduce((acc, release) => { - if (!release.movie) return acc; - const movie = release.movie.url ? release.movie : { url: release.movie }; - - if (!acc[movie.url]) { - acc[movie.url] = { - ...movie, - type: 'movie', - sceneIds: [], - }; - } - - acc[movie.url].sceneIds = acc[movie.url].sceneIds.concat(release.id); - - return acc; - }, {}); - - const movies = await Promise.map(Object.values(moviesByUrl), async movie => scrapeRelease(movie, null, 'movie')); - const { releases: storedMovies } = await storeReleases(movies); - - const movieAssociations = storedMovies.reduce((acc, movie) => acc.concat(movie.sceneIds.map(sceneId => ({ - movie_id: movie.id, - scene_id: sceneId, - }))), []); - - await knex('releases_movies').insert(movieAssociations); - - // console.log(moviesByUrl); - return movies; -} - -async function scrapeReleases(sources, type = 'scene') { - const scrapedReleases = await Promise.map(sources, async source => scrapeRelease(source, null, type), { - concurrency: 5, - }).filter(Boolean); - - const curatedReleases = scrapedReleases.map(scrapedRelease => ({ ...scrapedRelease, type })); - - if ((argv.scene || argv.movie) && argv.inspect) { - // only show when fetching from URL - } - - if (argv.save) { - const { releases: storedReleases } = await storeReleases(curatedReleases); - - await accumulateMovies(storedReleases); - - if (storedReleases) { - logger.info(storedReleases.map(storedRelease => `\nhttp://${config.web.host}:${config.web.port}/scene/${storedRelease.id}/${storedRelease.slug}`).join('')); - } - - return storedReleases; - } - - return curatedReleases; -} - -async function scrapeScenes(sources) { - return scrapeReleases(sources, 'scene'); -} - -async function scrapeMovies(sources) { - return scrapeReleases(sources, 'movie'); -} - -async function deepFetchReleases(baseReleases, beforeFetchLatest) { - const deepReleases = await Promise.map(baseReleases, async (release) => { - if (release.url || (release.path && release.site)) { - try { - const fullRelease = await scrapeRelease(release.url, release, 'scene', beforeFetchLatest); - - if (fullRelease) { - return { - ...release, - ...fullRelease, - deep: true, - }; - } - - logger.warn(`Release scraper returned empty result for ${release.url}`); - - return release; - } catch (error) { - logger.error(`Failed to scrape ${release.url}: ${error}`); - - return { - ...release, - deep: false, - }; - } - } - - return release; - }, { - concurrency: 2, - }); - - return deepReleases; -} - -module.exports = { - deepFetchReleases, - scrapeMovies, - scrapeRelease, - scrapeReleases, - scrapeScenes, -}; diff --git a/src/scrape-sites.js b/src/scrape-sites.js deleted file mode 100644 index e980219ef..000000000 --- a/src/scrape-sites.js +++ /dev/null @@ -1,184 +0,0 @@ -'use strict'; - -const Promise = require('bluebird'); -const moment = require('moment'); - -const argv = require('./argv'); -const include = require('./utils/argv-include')(argv); -const logger = require('./logger')(__filename); -const knex = require('./knex'); -const { fetchIncludedSites } = require('./sites'); -const scrapers = require('./scrapers/scrapers'); -const { deepFetchReleases } = require('./scrape-releases'); -const { storeReleases } = require('./releases'); - -function getAfterDate() { - if (/\d{2,4}-\d{2}-\d{2,4}/.test(argv.after)) { - // using date - return moment - .utc(argv.after, ['YYYY-MM-DD', 'DD-MM-YYYY']) - .toDate(); - } - - // using time distance (e.g. "1 month") - return moment - .utc() - .subtract(...argv.after.split(' ')) - .toDate(); -} - -async function findDuplicateReleaseIds(latestReleases, accReleases) { - const duplicateReleases = await knex('releases') - .whereIn('entry_id', latestReleases.map(({ entryId }) => entryId)); - - // include accumulated releases as duplicates to prevent an infinite - // loop when the next page contains the same releases as the previous - return new Set(duplicateReleases - .map(release => String(release.entry_id)) - .concat(accReleases.map(release => String(release.entryId)))); -} - -async function scrapeUniqueReleases(scraper, site, beforeFetchLatest, accSiteReleases, afterDate = getAfterDate(), accReleases = [], page = argv.page) { - if (!argv.latest || !scraper.fetchLatest) { - return []; - } - - const latestReleases = await scraper.fetchLatest(site, page, beforeFetchLatest, accSiteReleases, include); - - if (!Array.isArray(latestReleases)) { - logger.warn(`Scraper returned ${latestReleases || 'null'} when fetching latest from '${site.name}' on '${site.network.name}'`); - return accReleases; - } - - if (latestReleases.length === 0) { - return accReleases; - } - - const latestReleasesWithSite = latestReleases.map(release => ({ ...release, site })); - - const oldestReleaseOnPage = latestReleases.slice(-1)[0].date; - const duplicateReleaseIds = argv.redownload ? new Set() : await findDuplicateReleaseIds(latestReleases, accReleases); - - const uniqueReleases = latestReleasesWithSite - .filter(release => !duplicateReleaseIds.has(String(release.entryId)) // release is already in database - && (argv.last || !release.date || moment(release.date).isAfter(afterDate))); // release is older than specified date limit - - logger.verbose(`${site.name}: Scraped page ${page}, ${uniqueReleases.length} unique recent releases`); - - if ( - uniqueReleases.length > 0 - // && (oldestReleaseOnPage || page < argv.pages) - && ((oldestReleaseOnPage - ? moment(oldestReleaseOnPage).isAfter(afterDate) - : accReleases.length + uniqueReleases.length <= argv.nullDateLimit) - || (argv.last && accReleases.length + uniqueReleases.length < argv.last)) - ) { - // oldest release on page is newer that specified date range, or latest count has not yet been met, fetch next page - return scrapeUniqueReleases(scraper, site, beforeFetchLatest, accSiteReleases, afterDate, accReleases.concat(uniqueReleases), page + 1); - } - - if (argv.last && uniqueReleases.length >= argv.last) { - return accReleases.concat(uniqueReleases).slice(0, argv.last); - } - - if (oldestReleaseOnPage) { - return accReleases.concat(uniqueReleases); - } - - return accReleases.concat(uniqueReleases).slice(0, argv.nullDateLimit); -} - -async function scrapeUpcomingReleases(scraper, site, beforeFetchLatest) { - if (argv.upcoming && scraper.fetchUpcoming) { - const upcomingReleases = await scraper.fetchUpcoming(site, 1, beforeFetchLatest, include); - - return upcomingReleases - ? upcomingReleases.map(release => ({ ...release, site, upcoming: true })) - : []; - } - - return []; -} - -async function scrapeSiteReleases(scraper, site, accSiteReleases) { - const beforeFetchLatest = await scraper.beforeFetchLatest?.(site, accSiteReleases); - - const [newReleases, upcomingReleases] = await Promise.all([ - scrapeUniqueReleases(scraper, site, beforeFetchLatest, accSiteReleases), // fetch basic release info from scene overview - scrapeUpcomingReleases(scraper, site, beforeFetchLatest, accSiteReleases), // fetch basic release info from upcoming overview - ]); - - if (argv.upcoming) { - logger.info(`${site.name}: ${argv.latest ? `Found ${newReleases.length}` : 'Ignoring'} latest releases,${argv.upcoming ? ' ' : ' ignoring '}${upcomingReleases.length || '0'} upcoming releases`); - } - - const baseReleases = [...newReleases, ...upcomingReleases]; - - if (argv.deep) { - // follow URL for every release - return deepFetchReleases(baseReleases, beforeFetchLatest); - } - - return baseReleases; -} - -async function scrapeSite(site, network, accSiteReleases = []) { - if (site.parameters?.ignore) { - logger.warn(`Ignoring ${network.name}: ${site.name}`); - return []; - } - - const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug]; - - if (!scraper) { - logger.warn(`No scraper found for '${site.name}' (${site.slug})`); - return []; - } - - try { - const siteReleases = await scrapeSiteReleases(scraper, site, accSiteReleases); - - return siteReleases.map(release => ({ ...release, site })); - } catch (error) { - logger.error(`${site.name}: Failed to scrape releases: ${error.message}`); - - return []; - } -} - -async function scrapeSites() { - const networks = await fetchIncludedSites(); - - const scrapedNetworks = await Promise.map(networks, async (network) => { - if (network.parameters?.sequential) { - logger.info(`Scraping '${network.name}' sequentially`); - - return Promise.reduce(network.sites, async (acc, site) => { - const accSiteReleases = await acc; - const siteReleases = await scrapeSite(site, network, accSiteReleases); - - return accSiteReleases.concat(siteReleases); - }, Promise.resolve([])); - } - - return Promise.map(network.sites, async site => scrapeSite(site, network), { - concurrency: network.parameters?.concurrency || 2, - }); - }, - { - // 5 networks at a time - concurrency: 5, - }); - - const releases = scrapedNetworks.flat(2); - - if (argv.inspect) { - console.log(releases); - } - - if (argv.save) { - await storeReleases(releases); - } -} - -module.exports = scrapeSites; diff --git a/src/scrapers/21naturals.js b/src/scrapers/21naturals.js index 8d972832f..780164a75 100644 --- a/src/scrapers/21naturals.js +++ b/src/scrapers/21naturals.js @@ -3,8 +3,8 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchUpcoming: fetchApiUpcoming, - fetchScene, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchUpcoming: fetchApiUpcoming, + fetchScene, }; diff --git a/src/scrapers/21sextreme.js b/src/scrapers/21sextreme.js index 8d972832f..780164a75 100644 --- a/src/scrapers/21sextreme.js +++ b/src/scrapers/21sextreme.js @@ -3,8 +3,8 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchUpcoming: fetchApiUpcoming, - fetchScene, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchUpcoming: fetchApiUpcoming, + fetchScene, }; diff --git a/src/scrapers/21sextury.js b/src/scrapers/21sextury.js index 8d972832f..780164a75 100644 --- a/src/scrapers/21sextury.js +++ b/src/scrapers/21sextury.js @@ -3,8 +3,8 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchUpcoming: fetchApiUpcoming, - fetchScene, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchUpcoming: fetchApiUpcoming, + fetchScene, }; diff --git a/src/scrapers/adulttime.js b/src/scrapers/adulttime.js index 3870e7ed6..5c1aa7f1d 100644 --- a/src/scrapers/adulttime.js +++ b/src/scrapers/adulttime.js @@ -3,37 +3,37 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); function curateRelease(release, site) { - if (['bubblegumdungeon', 'ladygonzo'].includes(site.slug)) { - return { - ...release, - title: release.title.split(/:|\|/)[1].trim(), - }; - } + if (['bubblegumdungeon', 'ladygonzo'].includes(site.slug)) { + return { + ...release, + title: release.title.split(/:|\|/)[1].trim(), + }; + } - return release; + return release; } async function networkFetchScene(url, site, release) { - const scene = await fetchScene(url, site, release); + const scene = await fetchScene(url, site, release); - return curateRelease(scene, site); + return curateRelease(scene, site); } async function fetchLatest(site, page = 1) { - const releases = await fetchApiLatest(site, page, false); + const releases = await fetchApiLatest(site, page, false); - return releases.map(release => curateRelease(release, site)); + return releases.map(release => curateRelease(release, site)); } async function fetchUpcoming(site, page = 1) { - const releases = await fetchApiUpcoming(site, page, false); + const releases = await fetchApiUpcoming(site, page, false); - return releases.map(release => curateRelease(release, site)); + return releases.map(release => curateRelease(release, site)); } module.exports = { - fetchLatest, - fetchProfile: fetchApiProfile, - fetchScene: networkFetchScene, - fetchUpcoming, + fetchLatest, + fetchProfile: fetchApiProfile, + fetchScene: networkFetchScene, + fetchUpcoming, }; diff --git a/src/scrapers/amateurallure.js b/src/scrapers/amateurallure.js index e86830c59..2609e0306 100644 --- a/src/scrapers/amateurallure.js +++ b/src/scrapers/amateurallure.js @@ -3,47 +3,47 @@ const { fetchLatest, fetchScene } = require('./julesjordan'); function extractActors(scene) { - const release = scene; + const release = scene; - if (!scene.actors || scene.actors.length === 0) { - const introActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (\w+ \w+)/i); - const introTwoActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (?:(\w+)|(\w+ \w+)) and (\w+ \w+)/i); - const returnActorMatches = scene.title.match(/(?:(^\w+)|(\w+ \w+))(?:,| (?:return|visit|pov|give|suck|lick|milk|love|enjoy|service|is))/i); - const returnTwoActorMatches = scene.title.match(/(\w+ \w+) and (?:(\w+)|(\w+ \w+)) (?:return|visit|give|suck|lick|milk|love|enjoy|service|are)/i); + if (!scene.actors || scene.actors.length === 0) { + const introActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (\w+ \w+)/i); + const introTwoActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (?:(\w+)|(\w+ \w+)) and (\w+ \w+)/i); + const returnActorMatches = scene.title.match(/(?:(^\w+)|(\w+ \w+))(?:,| (?:return|visit|pov|give|suck|lick|milk|love|enjoy|service|is))/i); + const returnTwoActorMatches = scene.title.match(/(\w+ \w+) and (?:(\w+)|(\w+ \w+)) (?:return|visit|give|suck|lick|milk|love|enjoy|service|are)/i); - const rawActors = (introTwoActorMatches || introActorMatches || returnTwoActorMatches || returnActorMatches)?.slice(1); - const actors = rawActors?.filter((actor) => { - if (!actor) return false; - if (/swallow|\bcum|fuck|suck|give|giving|take|takes|taking|head|teen|babe|cute|beaut|naughty|teacher|nanny|adorable|brunette|blonde|bust|audition|from|\band\b|\bto\b/i.test(actor)) return false; + const rawActors = (introTwoActorMatches || introActorMatches || returnTwoActorMatches || returnActorMatches)?.slice(1); + const actors = rawActors?.filter((actor) => { + if (!actor) return false; + if (/swallow|\bcum|fuck|suck|give|giving|take|takes|taking|head|teen|babe|cute|beaut|naughty|teacher|nanny|adorable|brunette|blonde|bust|audition|from|\band\b|\bto\b/i.test(actor)) return false; - return true; - }); + return true; + }); - if (actors) { - release.actors = actors; - } - } + if (actors) { + release.actors = actors; + } + } - if (release.actors?.length > 1 || /threesome|threeway/.test(scene.title)) { - release.tags = scene.tags ? [...scene.tags, 'mff'] : ['mff']; - } + if (release.actors?.length > 1 || /threesome|threeway/.test(scene.title)) { + release.tags = scene.tags ? [...scene.tags, 'mff'] : ['mff']; + } - return release; + return release; } async function fetchLatestWrap(site, page = 1) { - const latest = await fetchLatest(site, page); + const latest = await fetchLatest(site, page); - return latest.map(scene => extractActors(scene)); + return latest.map(scene => extractActors(scene)); } async function fetchSceneWrap(url, site) { - const scene = await fetchScene(url, site); + const scene = await fetchScene(url, site); - return extractActors(scene); + return extractActors(scene); } module.exports = { - fetchLatest: fetchLatestWrap, - fetchScene: fetchSceneWrap, + fetchLatest: fetchLatestWrap, + fetchScene: fetchSceneWrap, }; diff --git a/src/scrapers/assylum.js b/src/scrapers/assylum.js index 178879e2a..34e0f5723 100644 --- a/src/scrapers/assylum.js +++ b/src/scrapers/assylum.js @@ -3,7 +3,7 @@ const { get, geta, ctxa } = require('../utils/q'); function extractActors(actorString) { - return actorString + return actorString ?.replace(/.*:|\(.*\)|\d+(-|\s)year(-|\s)old|nurses?|tangled/ig, '') // remove Patient:, (date) and other nonsense .split(/\band\b|\bvs\b|\/|,|&/ig) .map(actor => actor.trim()) @@ -12,120 +12,120 @@ function extractActors(actorString) { } function matchActors(actorString, models) { - return models - .filter(model => new RegExp(model.name, 'i') - .test(actorString)); + return models + .filter(model => new RegExp(model.name, 'i') + .test(actorString)); } function scrapeLatest(scenes, site, models) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - const pathname = qu.url('a.itemimg').slice(1); - [release.entryId] = pathname.split('/').slice(-1); - release.url = `${site.url}${pathname}`; + const pathname = qu.url('a.itemimg').slice(1); + [release.entryId] = pathname.split('/').slice(-1); + release.url = `${site.url}${pathname}`; - release.title = qu.q('.itemimg img', 'alt') || qu.q('h4 a', true); - release.description = qu.q('.mas_longdescription', true); - release.date = qu.date('.movie_info2', 'MM/DD/YY', /\d{2}\/\d{2}\/\d{2}/); + release.title = qu.q('.itemimg img', 'alt') || qu.q('h4 a', true); + release.description = qu.q('.mas_longdescription', true); + release.date = qu.date('.movie_info2', 'MM/DD/YY', /\d{2}\/\d{2}\/\d{2}/); - const actorString = qu.q('.mas_description', true); - const actors = matchActors(actorString, models); - if (actors.length > 0) release.actors = actors; - else release.actors = extractActors(actorString); + const actorString = qu.q('.mas_description', true); + const actors = matchActors(actorString, models); + if (actors.length > 0) release.actors = actors; + else release.actors = extractActors(actorString); - const posterPath = qu.img('.itemimg img'); - release.poster = `${site.url}/${posterPath}`; + const posterPath = qu.img('.itemimg img'); + release.poster = `${site.url}/${posterPath}`; - return release; - }); + return release; + }); } function scrapeScene({ html, qu }, url, site, models) { - const release = { url }; + const release = { url }; - [release.entryId] = url.split('/').slice(-1); - release.title = qu.q('.mas_title', true); - release.description = qu.q('.mas_longdescription', true); - release.date = qu.date('.mas_description', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/); + [release.entryId] = url.split('/').slice(-1); + release.title = qu.q('.mas_title', true); + release.description = qu.q('.mas_longdescription', true); + release.date = qu.date('.mas_description', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/); - const actorString = qu.q('.mas_description', true).replace(/\w+ \d{1,2}, \d{4}/, ''); - const actors = matchActors(actorString, models); - if (actors.length > 0) release.actors = actors; - else release.actors = extractActors(actorString); + const actorString = qu.q('.mas_description', true).replace(/\w+ \d{1,2}, \d{4}/, ''); + const actors = matchActors(actorString, models); + if (actors.length > 0) release.actors = actors; + else release.actors = extractActors(actorString); - release.tags = qu.all('.tags a', true); + release.tags = qu.all('.tags a', true); - release.photos = qu.imgs('.stills img').map(photoPath => `${site.url}/${photoPath}`); + release.photos = qu.imgs('.stills img').map(photoPath => `${site.url}/${photoPath}`); - const posterIndex = 'splash:'; - const poster = html.slice(html.indexOf('faceimages/', posterIndex), html.indexOf('.jpg', posterIndex) + 4); - if (poster) release.poster = `${site.url}/${poster}`; + const posterIndex = 'splash:'; + const poster = html.slice(html.indexOf('faceimages/', posterIndex), html.indexOf('.jpg', posterIndex) + 4); + if (poster) release.poster = `${site.url}/${poster}`; - const trailerIndex = html.indexOf('video/mp4'); - const trailer = html.slice(html.indexOf('/content', trailerIndex), html.indexOf('.mp4', trailerIndex) + 4); - if (trailer) release.trailer = { src: `${site.url}${trailer}` }; + const trailerIndex = html.indexOf('video/mp4'); + const trailer = html.slice(html.indexOf('/content', trailerIndex), html.indexOf('.mp4', trailerIndex) + 4); + if (trailer) release.trailer = { src: `${site.url}${trailer}` }; - return release; + return release; } function extractModels({ el }, site) { - const models = ctxa(el, '.item'); + const models = ctxa(el, '.item'); - return models.map(({ qu }) => { - const actor = { gender: 'female' }; + return models.map(({ qu }) => { + const actor = { gender: 'female' }; - const avatar = qu.q('.itemimg img'); - actor.avatar = `${site.url}/${avatar.src}`; - actor.name = avatar.alt - .split(':').slice(-1)[0] - .replace(/xtreme girl|nurse/ig, '') - .trim(); + const avatar = qu.q('.itemimg img'); + actor.avatar = `${site.url}/${avatar.src}`; + actor.name = avatar.alt + .split(':').slice(-1)[0] + .replace(/xtreme girl|nurse/ig, '') + .trim(); - const actorPath = qu.url('.itemimg'); - actor.url = `${site.url}${actorPath.slice(1)}`; + const actorPath = qu.url('.itemimg'); + actor.url = `${site.url}${actorPath.slice(1)}`; - return actor; - }); + return actor; + }); } async function fetchModels(site, page = 1, accModels = []) { - const url = `${site.url}/?models/${page}`; - const res = await get(url); + const url = `${site.url}/?models/${page}`; + const res = await get(url); - if (res.ok) { - const models = extractModels(res.item, site); - const nextPage = res.item.qa('.pagenumbers', true) - .map(pageX => Number(pageX)) - .filter(Boolean) // remove << and >> - .includes(page + 1); + if (res.ok) { + const models = extractModels(res.item, site); + const nextPage = res.item.qa('.pagenumbers', true) + .map(pageX => Number(pageX)) + .filter(Boolean) // remove << and >> + .includes(page + 1); - if (nextPage) { - return fetchModels(site, page + 1, accModels.concat(models)); - } + if (nextPage) { + return fetchModels(site, page + 1, accModels.concat(models)); + } - return accModels.concat(models, { name: 'Dr. Gray' }); - } + return accModels.concat(models, { name: 'Dr. Gray' }); + } - return []; + return []; } async function fetchLatest(site, page = 1, models) { - const url = `${site.url}/show.php?a=${site.parameters.a}_${page}`; - const res = await geta(url, '.item'); + const url = `${site.url}/show.php?a=${site.parameters.a}_${page}`; + const res = await geta(url, '.item'); - return res.ok ? scrapeLatest(res.items, site, models) : res.status; + return res.ok ? scrapeLatest(res.items, site, models) : res.status; } async function fetchScene(url, site, release, beforeFetchLatest) { - const models = beforeFetchLatest || await fetchModels(site); - const res = await get(url); + const models = beforeFetchLatest || await fetchModels(site); + const res = await get(url); - return res.ok ? scrapeScene(res.item, url, site, models) : res.status; + return res.ok ? scrapeScene(res.item, url, site, models) : res.status; } module.exports = { - fetchLatest, - fetchScene, - beforeFetchLatest: fetchModels, + fetchLatest, + fetchScene, + beforeFetchLatest: fetchModels, }; diff --git a/src/scrapers/aziani.js b/src/scrapers/aziani.js index efa46f057..0773ad232 100644 --- a/src/scrapers/aziani.js +++ b/src/scrapers/aziani.js @@ -5,141 +5,141 @@ const { get, getAll, initAll, extractDate } = require('../utils/qu'); const { feetInchesToCm } = require('../utils/convert'); function getFallbacks(source) { - return [ - source.replace('-1x.jpg', '-4x.jpg'), - source.replace('-1x.jpg', '-3x.jpg'), - source.replace('-1x.jpg', '-2x.jpg'), - source, - ]; + return [ + source.replace('-1x.jpg', '-4x.jpg'), + source.replace('-1x.jpg', '-3x.jpg'), + source.replace('-1x.jpg', '-2x.jpg'), + source, + ]; } function scrapeAll(scenes, site) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1]; - release.url = qu.url('a'); + release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1]; + release.url = qu.url('a'); - release.title = qu.q('h5 a', true); - release.date = qu.date('.icon-calendar + strong', 'MM/DD/YYYY'); + release.title = qu.q('h5 a', true); + release.date = qu.date('.icon-calendar + strong', 'MM/DD/YYYY'); - release.actors = qu.q('h3', true).replace(/featuring:\s?/i, '').split(', '); + release.actors = qu.q('h3', true).replace(/featuring:\s?/i, '').split(', '); - const photoCount = qu.q('.stdimage', 'cnt'); - [release.poster, ...release.photos] = Array.from({ length: Number(photoCount) }, (value, index) => { - const source = qu.img('.stdimage', `src${index}_1x`, site.url); + const photoCount = qu.q('.stdimage', 'cnt'); + [release.poster, ...release.photos] = Array.from({ length: Number(photoCount) }, (value, index) => { + const source = qu.img('.stdimage', `src${index}_1x`, site.url); - return getFallbacks(source); - }); + return getFallbacks(source); + }); - return release; - }); + return release; + }); } function scrapeScene({ html, qu }, url) { - const release = { url }; + const release = { url }; - release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1]; + release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1]; - release.title = qu.q('h2', true); - release.description = qu.q('p', true); + release.title = qu.q('h2', true); + release.description = qu.q('p', true); - release.date = extractDate(html, 'MM/DD/YYYY', /\b\d{2}\/\d{2}\/\d{4}\b/); + release.date = extractDate(html, 'MM/DD/YYYY', /\b\d{2}\/\d{2}\/\d{4}\b/); - release.actors = qu.all('h5:not(.video_categories) a').map(actor => ({ - name: qu.q(actor, null, true), - url: qu.url(actor, null), - })); + release.actors = qu.all('h5:not(.video_categories) a').map(actor => ({ + name: qu.q(actor, null, true), + url: qu.url(actor, null), + })); - release.tags = qu.all('.video_categories a', true); + release.tags = qu.all('.video_categories a', true); - release.duration = qu.dur('.video_categories + p'); + release.duration = qu.dur('.video_categories + p'); - const poster = qu.img('a img'); + const poster = qu.img('a img'); - release.poster = getFallbacks(poster); - release.photos = qu.imgs('.featured-video img', 'src0_1x').map(source => getFallbacks(source)); + release.poster = getFallbacks(poster); + release.photos = qu.imgs('.featured-video img', 'src0_1x').map(source => getFallbacks(source)); - return release; + return release; } function scrapeProfile({ el, qu }) { - const profile = {}; + const profile = {}; - const bio = Array.from(qu.q('.widget-content').childNodes).reduce((acc, node, index, nodes) => { - const nextNode = nodes[index + 1]; + const bio = Array.from(qu.q('.widget-content').childNodes).reduce((acc, node, index, nodes) => { + const nextNode = nodes[index + 1]; - if (node.tagName === 'STRONG' && nextNode?.nodeType === 3) { - acc[slugify(node.textContent, '_')] = nextNode.textContent.trim(); - } + if (node.tagName === 'STRONG' && nextNode?.nodeType === 3) { + acc[slugify(node.textContent, '_')] = nextNode.textContent.trim(); + } - return acc; - }, {}); + return acc; + }, {}); - if (bio.ethnicity) profile.ethnicity = bio.ethnicity; - if (bio.age) profile.age = Number(bio.age); + if (bio.ethnicity) profile.ethnicity = bio.ethnicity; + if (bio.age) profile.age = Number(bio.age); - if (bio.height && /\d{3}/.test(bio.height)) profile.height = Number(bio.height.match(/\d+/)[0]); - if (bio.height && /\d[;']\d/.test(bio.height)) profile.height = feetInchesToCm(bio.height); + if (bio.height && /\d{3}/.test(bio.height)) profile.height = Number(bio.height.match(/\d+/)[0]); + if (bio.height && /\d[;']\d/.test(bio.height)) profile.height = feetInchesToCm(bio.height); - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); - if (bust && /\d+[a-zA-Z]+/.test(bust)) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust && /\d+[a-zA-Z]+/.test(bust)) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - if (bio.bust_size && !profile.bust) profile.bust = bio.bust_size.toUpperCase(); + if (bio.bust_size && !profile.bust) profile.bust = bio.bust_size.toUpperCase(); - if (bio.birth_location) profile.birthPlace = bio.birth_location; - if (bio.status_married_or_single) profile.relationship = bio.status_married_or_single; + if (bio.birth_location) profile.birthPlace = bio.birth_location; + if (bio.status_married_or_single) profile.relationship = bio.status_married_or_single; - if (bio.eye_color) profile.eyes = bio.eye_color; + if (bio.eye_color) profile.eyes = bio.eye_color; - const avatar = qu.img('.tac img'); - profile.avatar = getFallbacks(avatar); + const avatar = qu.img('.tac img'); + profile.avatar = getFallbacks(avatar); - profile.releases = scrapeAll(initAll(el, '.featured-video')); + profile.releases = scrapeAll(initAll(el, '.featured-video')); - return profile; + return profile; } async function fetchLatest(site, page) { - const url = `${site.url}/tour/categories/movies_${page}_d.html`; - const res = await getAll(url, '.featured-video'); + const url = `${site.url}/tour/categories/movies_${page}_d.html`; + const res = await getAll(url, '.featured-video'); - if (res.ok) { - return scrapeAll(res.items, site); - } + if (res.ok) { + return scrapeAll(res.items, site); + } - return res.status; + return res.status; } async function fetchScene(url, site) { - const res = await get(url, '.page-content .row'); + const res = await get(url, '.page-content .row'); - if (res.ok) { - return scrapeScene(res.item, url, site); - } + if (res.ok) { + return scrapeScene(res.item, url, site); + } - return res.status; + return res.status; } async function fetchProfile(actorName, scraperSlug, site) { - const actorSlug = slugify(actorName, ''); - const url = `${site.url}/tour/models/${actorSlug}.html`; - const res = await get(url, '.page-content .row'); + const actorSlug = slugify(actorName, ''); + const url = `${site.url}/tour/models/${actorSlug}.html`; + const res = await get(url, '.page-content .row'); - if (res.ok) { - return scrapeProfile(res.item); - } + if (res.ok) { + return scrapeProfile(res.item); + } - return res.status; + return res.status; } module.exports = { - fetchLatest, - fetchProfile, - fetchScene, + fetchLatest, + fetchProfile, + fetchScene, }; diff --git a/src/scrapers/babes.js b/src/scrapers/babes.js index 01c4d3d90..68418c93a 100644 --- a/src/scrapers/babes.js +++ b/src/scrapers/babes.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'babes'); + return fetchProfile(actorName, 'babes'); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/bamvisions.js b/src/scrapers/bamvisions.js index 116073f0d..658432dbd 100644 --- a/src/scrapers/bamvisions.js +++ b/src/scrapers/bamvisions.js @@ -6,144 +6,144 @@ const slugify = require('../utils/slugify'); const { feetInchesToCm } = require('../utils/convert'); function scrapeAll(scenes, site) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - release.title = qu.q('h3 a', true); - release.url = qu.url('h3 a'); + release.title = qu.q('h3 a', true); + release.url = qu.url('h3 a'); - release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/); - release.duration = qu.dur('.item-meta li:nth-child(2)'); - release.description = qu.q('.description', true); + release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/); + release.duration = qu.dur('.item-meta li:nth-child(2)'); + release.description = qu.q('.description', true); - release.actors = qu.all('a[href*="/models"]', true); - if (/bts/i.test(release.title)) release.tags = ['behind the scenes']; + release.actors = qu.all('a[href*="/models"]', true); + if (/bts/i.test(release.title)) release.tags = ['behind the scenes']; - [release.poster, ...release.photos] = qu.all('.item-thumbs img') - .map(source => [ - source.getAttribute('src0_3x'), - source.getAttribute('src0_2x'), - source.getAttribute('src0_1x'), - ] - .filter(Boolean) - .map(fallback => (/^http/.test(fallback) ? fallback : `${site.url}${fallback}`))); + [release.poster, ...release.photos] = qu.all('.item-thumbs img') + .map(source => [ + source.getAttribute('src0_3x'), + source.getAttribute('src0_2x'), + source.getAttribute('src0_1x'), + ] + .filter(Boolean) + .map(fallback => (/^http/.test(fallback) ? fallback : `${site.url}${fallback}`))); - release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`; + release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`; - return release; - }); + return release; + }); } function scrapeScene({ html, qu }, url, site) { - const release = { url }; + const release = { url }; - release.title = qu.q('.item-episode h4 a', true); - release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/); - release.duration = qu.dur('.item-meta li:nth-child(2)'); - release.description = qu.q('.description', true); + release.title = qu.q('.item-episode h4 a', true); + release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/); + release.duration = qu.dur('.item-meta li:nth-child(2)'); + release.description = qu.q('.description', true); - release.actors = qu.all('.item-episode a[href*="/models"]', true); - if (/bts/i.test(release.title)) release.tags = ['behind the scenes']; + release.actors = qu.all('.item-episode a[href*="/models"]', true); + if (/bts/i.test(release.title)) release.tags = ['behind the scenes']; - const posterPath = html.match(/poster="(.*.jpg)"/)?.[1]; - const trailerPath = html.match(/video src="(.*.mp4)"/)?.[1]; + const posterPath = html.match(/poster="(.*.jpg)"/)?.[1]; + const trailerPath = html.match(/video src="(.*.mp4)"/)?.[1]; - if (posterPath) { - const poster = /^http/.test(posterPath) ? posterPath : `${site.url}${posterPath}`; - release.poster = [ - poster.replace('-1x', '-3x'), - poster.replace('-1x', '-2x'), - poster, - ]; - } + if (posterPath) { + const poster = /^http/.test(posterPath) ? posterPath : `${site.url}${posterPath}`; + release.poster = [ + poster.replace('-1x', '-3x'), + poster.replace('-1x', '-2x'), + poster, + ]; + } - if (trailerPath) { - const trailer = /^http/.test(trailerPath) ? trailerPath : `${site.url}${trailerPath}`; - release.trailer = { src: trailer }; - } + if (trailerPath) { + const trailer = /^http/.test(trailerPath) ? trailerPath : `${site.url}${trailerPath}`; + release.trailer = { src: trailer }; + } - release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`; + release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`; - return release; + return release; } async function fetchActorReleases(actorId, site, page = 1, accScenes = []) { - const url = `${site.url}/sets.php?id=${actorId}&page=${page}`; - const res = await get(url); + const url = `${site.url}/sets.php?id=${actorId}&page=${page}`; + const res = await get(url); - if (!res.ok) return []; + if (!res.ok) return []; - const quReleases = initAll(res.item.el, '.item-episode'); - const releases = scrapeAll(quReleases, site); + const quReleases = initAll(res.item.el, '.item-episode'); + const releases = scrapeAll(quReleases, site); - const nextPage = res.item.qu.q(`a[href*="page=${page + 1}"]`); + const nextPage = res.item.qu.q(`a[href*="page=${page + 1}"]`); - if (nextPage) { - return fetchActorReleases(actorId, site, page + 1, accScenes.concat(releases)); - } + if (nextPage) { + return fetchActorReleases(actorId, site, page + 1, accScenes.concat(releases)); + } - return accScenes.concat(releases); + return accScenes.concat(releases); } async function scrapeProfile({ qu }, site, withScenes) { - const profile = {}; + const profile = {}; - const bio = qu.all('.stats li', true).reduce((acc, row) => { - const [key, value] = row.split(':'); - return { ...acc, [slugify(key, '_')]: value.trim() }; - }, {}); + const bio = qu.all('.stats li', true).reduce((acc, row) => { + const [key, value] = row.split(':'); + return { ...acc, [slugify(key, '_')]: value.trim() }; + }, {}); - if (bio.height) profile.height = feetInchesToCm(bio.height); - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); + if (bio.height) profile.height = feetInchesToCm(bio.height); + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); - if (bust) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - profile.avatar = [ - qu.q('.profile-pic img', 'src0_3x'), - qu.q('.profile-pic img', 'src0_2x'), - qu.q('.profile-pic img', 'src0_1x'), - ].filter(Boolean).map(source => (/^http/.test(source) ? source : `${site.url}${source}`)); + profile.avatar = [ + qu.q('.profile-pic img', 'src0_3x'), + qu.q('.profile-pic img', 'src0_2x'), + qu.q('.profile-pic img', 'src0_1x'), + ].filter(Boolean).map(source => (/^http/.test(source) ? source : `${site.url}${source}`)); - if (withScenes) { - const actorId = qu.q('.profile-pic img', 'id')?.match(/set-target-(\d+)/)?.[1]; + if (withScenes) { + const actorId = qu.q('.profile-pic img', 'id')?.match(/set-target-(\d+)/)?.[1]; - if (actorId) { - profile.releases = await fetchActorReleases(actorId, site); - } - } + if (actorId) { + profile.releases = await fetchActorReleases(actorId, site); + } + } - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = `${site.url}/categories/movies/${page}/latest/`; - const res = await geta(url, '.item-episode'); + const url = `${site.url}/categories/movies/${page}/latest/`; + const res = await geta(url, '.item-episode'); - return res.ok ? scrapeAll(res.items, site) : res.status; + return res.ok ? scrapeAll(res.items, site) : res.status; } async function fetchScene(url, site) { - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeScene(res.item, url, site) : res.status; + return res.ok ? scrapeScene(res.item, url, site) : res.status; } async function fetchProfile(actorName, scraperSlug, site, include) { - const actorSlugA = slugify(actorName, ''); - const actorSlugB = slugify(actorName); + const actorSlugA = slugify(actorName, ''); + const actorSlugB = slugify(actorName); - const resA = await get(`${site.url}/models/${actorSlugA}.html`); - const res = resA.ok ? resA : await get(`${site.url}/models/${actorSlugB}.html`); + const resA = await get(`${site.url}/models/${actorSlugA}.html`); + const res = resA.ok ? resA : await get(`${site.url}/models/${actorSlugB}.html`); - return res.ok ? scrapeProfile(res.item, site, include.scenes) : res.status; + return res.ok ? scrapeProfile(res.item, site, include.scenes) : res.status; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/bang.js b/src/scrapers/bang.js index c1c43fb60..b1a80029a 100644 --- a/src/scrapers/bang.js +++ b/src/scrapers/bang.js @@ -8,99 +8,99 @@ const clusterId = '617fb597b659459bafe6472470d9073a'; const authKey = 'YmFuZy1yZWFkOktqVDN0RzJacmQ1TFNRazI='; const genderMap = { - M: 'male', - F: 'female', + M: 'male', + F: 'female', }; function getScreenUrl(item, scene) { - return `https://i.bang.com/screenshots/${scene.dvd.id}/movie/${scene.order}/${item.screenId}.jpg`; + return `https://i.bang.com/screenshots/${scene.dvd.id}/movie/${scene.order}/${item.screenId}.jpg`; } function encodeId(id) { - return Buffer - .from(id, 'hex') - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ','); + return Buffer + .from(id, 'hex') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ','); } function decodeId(id) { - const restoredId = id - .replace(/-/g, '+') - .replace(/_/g, '/') - .replace(/,/g, '='); + const restoredId = id + .replace(/-/g, '+') + .replace(/_/g, '/') + .replace(/,/g, '='); - return Buffer - .from(restoredId, 'base64') - .toString('hex'); + return Buffer + .from(restoredId, 'base64') + .toString('hex'); } function scrapeScene(scene, site) { - const release = { - site, - entryId: scene.id, - title: scene.name, - description: scene.description, - tags: scene.genres.concat(scene.actions).map(genre => genre.name), - duration: scene.duration, - }; + const release = { + site, + entryId: scene.id, + title: scene.name, + description: scene.description, + tags: scene.genres.concat(scene.actions).map(genre => genre.name), + duration: scene.duration, + }; - const slug = slugify(release.title); - release.url = `https://www.bang.com/video/${encodeId(release.entryId)}/${slug}`; + const slug = slugify(release.title); + release.url = `https://www.bang.com/video/${encodeId(release.entryId)}/${slug}`; - const date = new Date(scene.releaseDate); - release.date = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + const date = new Date(scene.releaseDate); + release.date = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); - release.actors = scene.actors.map(actor => ({ name: actor.name, gender: genderMap[actor.gender] })); + release.actors = scene.actors.map(actor => ({ name: actor.name, gender: genderMap[actor.gender] })); - if (scene.is4k) release.tags.push('4k'); - if (scene.gay) release.tags.push('gay'); + if (scene.is4k) release.tags.push('4k'); + if (scene.gay) release.tags.push('gay'); - const defaultPoster = scene.screenshots.find(photo => photo.default === true); - const photoset = scene.screenshots.filter(photo => photo.default === false); + const defaultPoster = scene.screenshots.find(photo => photo.default === true); + const photoset = scene.screenshots.filter(photo => photo.default === false); - const photos = defaultPoster ? photoset : photoset.slice(1); - const poster = defaultPoster || photoset[0]; + const photos = defaultPoster ? photoset : photoset.slice(1); + const poster = defaultPoster || photoset[0]; - release.poster = getScreenUrl(poster, scene); - release.photos = photos.map(photo => getScreenUrl(photo, scene)); + release.poster = getScreenUrl(poster, scene); + release.photos = photos.map(photo => getScreenUrl(photo, scene)); - release.trailer = { - src: `https://i.bang.com/v/${scene.dvd.id}/${scene.identifier}/preview.mp4`, - }; + release.trailer = { + src: `https://i.bang.com/v/${scene.dvd.id}/${scene.identifier}/preview.mp4`, + }; - release.channel = scene.series.name - .replace(/[! .]/g, '') - .replace('&', 'and'); + release.channel = scene.series.name + .replace(/[! .]/g, '') + .replace('&', 'and'); - return release; + return release; } function scrapeLatest(scenes, site) { - return scenes.map(({ _source: scene }) => scrapeScene(scene, site)); + return scenes.map(({ _source: scene }) => scrapeScene(scene, site)); } async function fetchLatest(site, page = 1) { - const res = await bhttp.post(`https://${clusterId}.us-east-1.aws.found.io/videos/video/_search`, { - size: 50, - from: (page - 1) * 50, - query: { - bool: { - must: [ - { - match: { - status: 'ok', - }, - }, - { - range: { - releaseDate: { - lte: 'now', - }, - }, - }, - /* + const res = await bhttp.post(`https://${clusterId}.us-east-1.aws.found.io/videos/video/_search`, { + size: 50, + from: (page - 1) * 50, + query: { + bool: { + must: [ + { + match: { + status: 'ok', + }, + }, + { + range: { + releaseDate: { + lte: 'now', + }, + }, + }, + /* * global fetch { nested: { @@ -122,66 +122,66 @@ async function fetchLatest(site, page = 1) { }, }, */ - { - nested: { - path: 'series', - query: { - bool: { - must: [ - { - match: { - 'series.id': { - operator: 'AND', - query: site.parameters.siteId, - }, - }, - }, - ], - }, - }, - }, - }, - ], - must_not: [ - { - match: { - type: 'trailer', - }, - }, - ], - }, - }, - sort: [ - { - releaseDate: { - order: 'desc', - }, - }, - ], - }, { - encodeJSON: true, - headers: { - Authorization: `Basic ${authKey}`, - }, - }); + { + nested: { + path: 'series', + query: { + bool: { + must: [ + { + match: { + 'series.id': { + operator: 'AND', + query: site.parameters.siteId, + }, + }, + }, + ], + }, + }, + }, + }, + ], + must_not: [ + { + match: { + type: 'trailer', + }, + }, + ], + }, + }, + sort: [ + { + releaseDate: { + order: 'desc', + }, + }, + ], + }, { + encodeJSON: true, + headers: { + Authorization: `Basic ${authKey}`, + }, + }); - return scrapeLatest(res.body.hits.hits, site); + return scrapeLatest(res.body.hits.hits, site); } async function fetchScene(url, site) { - const encodedId = new URL(url).pathname.split('/')[2]; - const entryId = decodeId(encodedId); + const encodedId = new URL(url).pathname.split('/')[2]; + const entryId = decodeId(encodedId); - const res = await bhttp.get(`https://${clusterId}.us-east-1.aws.found.io/videos/video/${entryId}`, { - headers: { - Authorization: `Basic ${authKey}`, - }, - }); + const res = await bhttp.get(`https://${clusterId}.us-east-1.aws.found.io/videos/video/${entryId}`, { + headers: { + Authorization: `Basic ${authKey}`, + }, + }); - return scrapeScene(res.body._source, site); // eslint-disable-line no-underscore-dangle + return scrapeScene(res.body._source, site); // eslint-disable-line no-underscore-dangle } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/bangbros.js b/src/scrapers/bangbros.js index e4d15cb5a..183219d2d 100644 --- a/src/scrapers/bangbros.js +++ b/src/scrapers/bangbros.js @@ -10,44 +10,44 @@ const slugify = require('../utils/slugify'); const { ex } = require('../utils/q'); function scrape(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const sceneElements = $('.echThumb').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const sceneElements = $('.echThumb').toArray(); - return sceneElements.map((element) => { - const sceneLinkElement = $(element).find('.thmb_lnk'); - const title = sceneLinkElement.attr('title'); - const url = `https://bangbros.com${sceneLinkElement.attr('href')}`; - const shootId = sceneLinkElement.attr('id') && sceneLinkElement.attr('id').split('-')[1]; - const entryId = url.split('/')[3].slice(5); + return sceneElements.map((element) => { + const sceneLinkElement = $(element).find('.thmb_lnk'); + const title = sceneLinkElement.attr('title'); + const url = `https://bangbros.com${sceneLinkElement.attr('href')}`; + const shootId = sceneLinkElement.attr('id') && sceneLinkElement.attr('id').split('-')[1]; + const entryId = url.split('/')[3].slice(5); - const date = moment.utc($(element).find('.thmb_mr_2 span.faTxt').text(), 'MMM D, YYYY').toDate(); - const actors = $(element).find('.cast-wrapper a.cast').map((actorIndex, actorElement) => $(actorElement).text().trim()).toArray(); + const date = moment.utc($(element).find('.thmb_mr_2 span.faTxt').text(), 'MMM D, YYYY').toDate(); + const actors = $(element).find('.cast-wrapper a.cast').map((actorIndex, actorElement) => $(actorElement).text().trim()).toArray(); - const photoElement = $(element).find('.rollover-image'); - const poster = `https:${photoElement.attr('data-original')}`; + const photoElement = $(element).find('.rollover-image'); + const poster = `https:${photoElement.attr('data-original')}`; - const photosUrl = photoElement.attr('data-rollover-url'); - const photosMaxIndex = photoElement.attr('data-rollover-max-index'); - const photos = Array.from({ length: photosMaxIndex }, (val, index) => `https:${photosUrl}big${index + 1}.jpg`); + const photosUrl = photoElement.attr('data-rollover-url'); + const photosMaxIndex = photoElement.attr('data-rollover-max-index'); + const photos = Array.from({ length: photosMaxIndex }, (val, index) => `https:${photosUrl}big${index + 1}.jpg`); - const duration = moment.duration(`0:${$(element).find('.thmb_pic b.tTm').text()}`).asSeconds(); - const channel = $(element).find('a[href*="/websites"]').attr('href').split('/').slice(-1)[0]; + const duration = moment.duration(`0:${$(element).find('.thmb_pic b.tTm').text()}`).asSeconds(); + const channel = $(element).find('a[href*="/websites"]').attr('href').split('/').slice(-1)[0]; - return { - url, - entryId, - shootId, - title, - actors, - date, - duration, - poster, - photos, - rating: null, - site, - channel, - }; - }); + return { + url, + entryId, + shootId, + title, + actors, + date, + duration, + poster, + photos, + rating: null, + site, + channel, + }; + }); } /* no dates available, breaks database @@ -80,63 +80,63 @@ function scrapeUpcoming(html, site) { */ function scrapeScene(html, url, _site) { - const { qu } = ex(html, '.playerSection'); - const release = {}; + const { qu } = ex(html, '.playerSection'); + const release = {}; - [release.shootId] = qu.q('.vdoTags + .vdoCast', true).match(/\w+$/); - [release.entryId] = url.split('/')[3].match(/\d+$/); - release.title = qu.q('.ps-vdoHdd h1', true); - release.description = qu.q('.vdoDesc', true); + [release.shootId] = qu.q('.vdoTags + .vdoCast', true).match(/\w+$/); + [release.entryId] = url.split('/')[3].match(/\d+$/); + release.title = qu.q('.ps-vdoHdd h1', true); + release.description = qu.q('.vdoDesc', true); - release.actors = qu.all('a[href*="/model"]', true); - release.tags = qu.all('.vdoTags a', true); + release.actors = qu.all('a[href*="/model"]', true); + release.tags = qu.all('.vdoTags a', true); - release.stars = Number(qu.q('div[class*="like"]', true).match(/^\d+/)[0]) / 20; + release.stars = Number(qu.q('div[class*="like"]', true).match(/^\d+/)[0]) / 20; - const poster = qu.img('img#player-overlay-image'); - release.poster = [ - poster, - poster.replace('/big_trailer', '/members/450x340'), // load error fallback - ]; + const poster = qu.img('img#player-overlay-image'); + release.poster = [ + poster, + poster.replace('/big_trailer', '/members/450x340'), // load error fallback + ]; - release.trailer = { src: qu.trailer() }; + release.trailer = { src: qu.trailer() }; - // all scenes seem to have 12 album photos available, not always included on the page - const firstPhotoUrl = ex(html).qu.img('img[data-slider-index="1"]'); - release.photos = Array.from({ length: 12 }, (val, index) => firstPhotoUrl.replace(/big\d+/, `big${index + 1}`)); + // all scenes seem to have 12 album photos available, not always included on the page + const firstPhotoUrl = ex(html).qu.img('img[data-slider-index="1"]'); + release.photos = Array.from({ length: 12 }, (val, index) => firstPhotoUrl.replace(/big\d+/, `big${index + 1}`)); - const [channel] = qu.url('a[href*="/websites"]').match(/\w+$/); + const [channel] = qu.url('a[href*="/websites"]').match(/\w+$/); - if (channel === 'bangcasting') release.channel = 'bangbroscasting'; - if (channel === 'remaster') release.channel = 'bangbrosremastered'; - else release.channel = channel; + if (channel === 'bangcasting') release.channel = 'bangbroscasting'; + if (channel === 'remaster') release.channel = 'bangbrosremastered'; + else release.channel = channel; - return release; + return release; } function scrapeProfile(html) { - const { q } = ex(html); - const profile = {}; + const { q } = ex(html); + const profile = {}; - const avatar = q('.profilePic img', 'src'); - if (avatar) profile.avatar = `https:${avatar}`; + const avatar = q('.profilePic img', 'src'); + if (avatar) profile.avatar = `https:${avatar}`; - profile.releases = scrape(html); + profile.releases = scrape(html); - return profile; + return profile; } function scrapeProfileSearch(html, actorName) { - const { qu } = ex(html); - const actorLink = qu.url(`a[title="${actorName}" i][href*="model"]`); + const { qu } = ex(html); + const actorLink = qu.url(`a[title="${actorName}" i][href*="model"]`); - return actorLink ? `https://bangbros.com${actorLink}` : null; + return actorLink ? `https://bangbros.com${actorLink}` : null; } async function fetchLatest(site, page = 1) { - const res = await bhttp.get(`${site.url}/${page}`); + const res = await bhttp.get(`${site.url}/${page}`); - return scrape(res.body.toString(), site); + return scrape(res.body.toString(), site); } /* @@ -148,43 +148,43 @@ async function fetchUpcoming(site) { */ async function fetchScene(url, site, release) { - if (!release?.date) { - logger.warn(`Scraping Bang Bros scene from URL without release date: ${url}`); - } + if (!release?.date) { + logger.warn(`Scraping Bang Bros scene from URL without release date: ${url}`); + } - const { origin } = new URL(url); - const res = await bhttp.get(url); + const { origin } = new URL(url); + const res = await bhttp.get(url); - if (!/https?:\/\/(www.)?bangbros.com\/?$/.test(origin)) { - throw new Error('Cannot fetch from this URL. Please find the scene on https://bangbros.com and try again.'); - } + if (!/https?:\/\/(www.)?bangbros.com\/?$/.test(origin)) { + throw new Error('Cannot fetch from this URL. Please find the scene on https://bangbros.com and try again.'); + } - return scrapeScene(res.body.toString(), url, site); + return scrapeScene(res.body.toString(), url, site); } async function fetchProfile(actorName) { - const actorSlug = slugify(actorName); - const url = `https://bangbros.com/search/${actorSlug}`; - const res = await bhttp.get(url); + const actorSlug = slugify(actorName); + const url = `https://bangbros.com/search/${actorSlug}`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - const actorUrl = scrapeProfileSearch(res.body.toString(), actorName); + if (res.statusCode === 200) { + const actorUrl = scrapeProfileSearch(res.body.toString(), actorName); - if (actorUrl) { - const actorRes = await bhttp.get(actorUrl); + if (actorUrl) { + const actorRes = await bhttp.get(actorUrl); - if (actorRes.statusCode === 200) { - return scrapeProfile(actorRes.body.toString()); - } - } - } + if (actorRes.statusCode === 200) { + return scrapeProfile(actorRes.body.toString()); + } + } + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, - // fetchUpcoming, no dates available + fetchLatest, + fetchScene, + fetchProfile, + // fetchUpcoming, no dates available }; diff --git a/src/scrapers/blowpass.js b/src/scrapers/blowpass.js index 72d3d3b5d..683baa116 100644 --- a/src/scrapers/blowpass.js +++ b/src/scrapers/blowpass.js @@ -5,33 +5,33 @@ const { fetchScene, fetchLatest, fetchUpcoming, fetchProfile } = require('./gamma'); async function fetchSceneWrapper(url, site, baseRelease) { - const release = await fetchScene(url, site, baseRelease); + const release = await fetchScene(url, site, baseRelease); - if (site.isFallback && release.channel) { - const channelUrl = url.replace('blowpass.com', `${release.channel}.com`); + if (site.isNetwork && release.channel) { + const channelUrl = url.replace('blowpass.com', `${release.channel}.com`); - if (['onlyteenblowjobs', 'mommyblowsbest'].includes(release.channel)) { - release.url = channelUrl.replace(/video\/\w+\//, 'scene/'); - return release; - } + if (['onlyteenblowjobs', 'mommyblowsbest'].includes(release.channel)) { + release.url = channelUrl.replace(/video\/\w+\//, 'scene/'); + return release; + } - release.url = channelUrl.replace(/video\/\w+\//, 'video/'); - } + release.url = channelUrl.replace(/video\/\w+\//, 'video/'); + } - return release; + return release; } function getActorReleasesUrl(actorPath, page = 1) { - return `https://www.blowpass.com/en/videos/blowpass/latest/All-Categories/0${actorPath}/${page}`; + return `https://www.blowpass.com/en/videos/blowpass/latest/All-Categories/0${actorPath}/${page}`; } async function networkFetchProfile(actorName, scraperSlug, site, include) { - return fetchProfile(actorName, scraperSlug, null, getActorReleasesUrl, include); + return fetchProfile(actorName, scraperSlug, null, getActorReleasesUrl, include); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchUpcoming, - fetchScene: fetchSceneWrapper, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchUpcoming, + fetchScene: fetchSceneWrapper, }; diff --git a/src/scrapers/boobpedia.js b/src/scrapers/boobpedia.js index a77e6866f..9d34cbf98 100644 --- a/src/scrapers/boobpedia.js +++ b/src/scrapers/boobpedia.js @@ -5,90 +5,90 @@ const bhttp = require('bhttp'); const { ex } = require('../utils/q'); function scrapeProfile(html) { - const { qu } = ex(html); /* eslint-disable-line object-curly-newline */ - const profile = {}; + const { qu } = ex(html); /* eslint-disable-line object-curly-newline */ + const profile = {}; - const bio = qu.all('.infobox tr[valign="top"]') - .map(detail => qu.all(detail, 'td', true)) - .reduce((acc, [key, value]) => ({ ...acc, [key.slice(0, -1).replace(/[\s+|/]/g, '_')]: value }), {}); + const bio = qu.all('.infobox tr[valign="top"]') + .map(detail => qu.all(detail, 'td', true)) + .reduce((acc, [key, value]) => ({ ...acc, [key.slice(0, -1).replace(/[\s+|/]/g, '_')]: value }), {}); - /* unreliable, see: Syren De Mer + /* unreliable, see: Syren De Mer const catlinks = qa('#mw-normal-catlinks a', true); const isTrans = catlinks.some(link => link.match(/shemale|transgender/i)); profile.gender = isTrans ? 'transsexual' : 'female'; */ - profile.birthdate = qu.date('.bday', 'YYYY-MM-DD'); + profile.birthdate = qu.date('.bday', 'YYYY-MM-DD'); - profile.description = qu.q('#mw-content-text > p', true); + profile.description = qu.q('#mw-content-text > p', true); - if (bio.Born) profile.birthPlace = bio.Born.slice(bio.Born.lastIndexOf(')') + 1); - if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity; + if (bio.Born) profile.birthPlace = bio.Born.slice(bio.Born.lastIndexOf(')') + 1); + if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity; - if (bio.Measurements) { - const measurements = bio.Measurements - .match(/\d+(\w+)?-\d+-\d+/g) + if (bio.Measurements) { + const measurements = bio.Measurements + .match(/\d+(\w+)?-\d+-\d+/g) ?.slice(-1)[0] // allow for both '34C-25-36' and '86-64-94 cm / 34-25-37 in' .split('-'); - // account for measuemrents being just e.g. '32EE' - if (measurements) { - const [bust, waist, hip] = measurements; + // account for measuemrents being just e.g. '32EE' + if (measurements) { + const [bust, waist, hip] = measurements; - if (/[a-zA-Z]/.test(bust)) profile.bust = bust; // only use bust if cup size is included + if (/[a-zA-Z]/.test(bust)) profile.bust = bust; // only use bust if cup size is included - profile.waist = Number(waist); - profile.hip = Number(hip); - } + profile.waist = Number(waist); + profile.hip = Number(hip); + } - if (/^\d+\w+$/.test(bio.Measurements)) profile.bust = bio.Measurements; - } + if (/^\d+\w+$/.test(bio.Measurements)) profile.bust = bio.Measurements; + } - if (bio.Bra_cup_size) { - const bust = bio.Bra_cup_size.match(/^\d+\w+/); - if (bust) [profile.bust] = bust; - } + if (bio.Bra_cup_size) { + const bust = bio.Bra_cup_size.match(/^\d+\w+/); + if (bust) [profile.bust] = bust; + } - if (bio.Boobs === 'Enhanced') profile.naturalBoobs = false; - if (bio.Boobs === 'Natural') profile.naturalBoobs = true; + if (bio.Boobs === 'Enhanced') profile.naturalBoobs = false; + if (bio.Boobs === 'Natural') profile.naturalBoobs = true; - if (bio.Height) profile.height = Number(bio.Height.match(/\d+\.\d+/g).slice(-1)[0]) * 100; - if (bio.Weight) profile.weight = Number(bio.Weight.match(/\d+/g)[1]); + if (bio.Height) profile.height = Number(bio.Height.match(/\d+\.\d+/g).slice(-1)[0]) * 100; + if (bio.Weight) profile.weight = Number(bio.Weight.match(/\d+/g)[1]); - if (bio.Eye_color) profile.eyes = bio.Eye_color; - if (bio.Hair) [profile.hair] = bio.Hair.split(','); + if (bio.Eye_color) profile.eyes = bio.Eye_color; + if (bio.Hair) [profile.hair] = bio.Hair.split(','); - if (bio.Blood_group) profile.blood = bio.Blood_group; - if (bio.Also_known_as) profile.aliases = bio.Also_known_as.split(', '); + if (bio.Blood_group) profile.blood = bio.Blood_group; + if (bio.Also_known_as) profile.aliases = bio.Also_known_as.split(', '); - const avatarThumbPath = qu.img('.image img'); + const avatarThumbPath = qu.img('.image img'); - if (avatarThumbPath && !/NoImageAvailable/.test(avatarThumbPath)) { - const avatarPath = avatarThumbPath.slice(0, avatarThumbPath.lastIndexOf('/')).replace('thumb/', ''); + if (avatarThumbPath && !/NoImageAvailable/.test(avatarThumbPath)) { + const avatarPath = avatarThumbPath.slice(0, avatarThumbPath.lastIndexOf('/')).replace('thumb/', ''); - profile.avatar = { - src: `http://www.boobpedia.com${avatarPath}`, - copyright: null, - }; - } + profile.avatar = { + src: `http://www.boobpedia.com${avatarPath}`, + copyright: null, + }; + } - profile.social = qu.urls('.infobox a.external'); + profile.social = qu.urls('.infobox a.external'); - return profile; + return profile; } async function fetchProfile(actorName) { - const actorSlug = actorName.replace(/\s+/, '_'); - const res = await bhttp.get(`http://www.boobpedia.com/boobs/${actorSlug}`); + const actorSlug = actorName.replace(/\s+/, '_'); + const res = await bhttp.get(`http://www.boobpedia.com/boobs/${actorSlug}`); - if (res.statusCode === 200) { - return scrapeProfile(res.body.toString()); - } + if (res.statusCode === 200) { + return scrapeProfile(res.body.toString()); + } - return null; + return null; } module.exports = { - fetchProfile, + fetchProfile, }; diff --git a/src/scrapers/brazzers.js b/src/scrapers/brazzers.js index 36a24fbf8..9aebfa24a 100644 --- a/src/scrapers/brazzers.js +++ b/src/scrapers/brazzers.js @@ -11,216 +11,216 @@ const slugify = require('../utils/slugify'); const { heightToCm, lbsToKg } = require('../utils/convert'); const hairMap = { - Blonde: 'blonde', - Brunette: 'brown', - 'Black Hair': 'black', - Redhead: 'red', + Blonde: 'blonde', + Brunette: 'brown', + 'Black Hair': 'black', + Redhead: 'red', }; function scrapeAll(html, site, upcoming) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const sceneElements = $('.release-card.scene').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const sceneElements = $('.release-card.scene').toArray(); - return sceneElements.reduce((acc, element) => { - const isUpcoming = $(element).find('.icon-upcoming.active').length === 1; + return sceneElements.reduce((acc, element) => { + const isUpcoming = $(element).find('.icon-upcoming.active').length === 1; - if ((upcoming && !isUpcoming) || (!upcoming && isUpcoming)) { - return acc; - } + if ((upcoming && !isUpcoming) || (!upcoming && isUpcoming)) { + return acc; + } - const sceneLinkElement = $(element).find('a'); + const sceneLinkElement = $(element).find('a'); - const url = `https://www.brazzers.com${sceneLinkElement.attr('href')}`; - const title = sceneLinkElement.attr('title'); - const entryId = url.split('/').slice(-3, -2)[0]; + const url = `https://www.brazzers.com${sceneLinkElement.attr('href')}`; + const title = sceneLinkElement.attr('title'); + const entryId = url.split('/').slice(-3, -2)[0]; - const date = moment.utc($(element).find('time').text(), 'MMMM DD, YYYY').toDate(); - const actors = $(element).find('.model-names a').map((actorIndex, actorElement) => $(actorElement).attr('title')).toArray(); + const date = moment.utc($(element).find('time').text(), 'MMMM DD, YYYY').toDate(); + const actors = $(element).find('.model-names a').map((actorIndex, actorElement) => $(actorElement).attr('title')).toArray(); - const likes = Number($(element).find('.label-rating .like-amount').text()); - const dislikes = Number($(element).find('.label-rating .dislike-amount').text()); + const likes = Number($(element).find('.label-rating .like-amount').text()); + const dislikes = Number($(element).find('.label-rating .dislike-amount').text()); - const poster = `https:${$(element).find('.card-main-img').attr('data-src')}`; - const photos = $(element).find('.card-overlay .image-under').map((photoIndex, photoElement) => `https:${$(photoElement).attr('data-src')}`).toArray(); + const poster = `https:${$(element).find('.card-main-img').attr('data-src')}`; + const photos = $(element).find('.card-overlay .image-under').map((photoIndex, photoElement) => `https:${$(photoElement).attr('data-src')}`).toArray(); - const channel = slugify($(element).find('.collection').attr('title'), ''); + const channel = slugify($(element).find('.collection').attr('title'), ''); - return acc.concat({ - url, - entryId, - title, - actors, - date, - poster, - photos, - rating: { - likes, - dislikes, - }, - channel, - site, - }); - }, []); + return acc.concat({ + url, + entryId, + title, + actors, + date, + poster, + photos, + rating: { + likes, + dislikes, + }, + channel, + site, + }); + }, []); } async function scrapeScene(html, url, _site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const release = {}; + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const release = {}; - const videoJson = $('script:contains("window.videoUiOptions")').html(); - const videoString = videoJson.slice(videoJson.indexOf('{"stream_info":'), videoJson.lastIndexOf('},') + 1); - const videoData = JSON.parse(videoString); + const videoJson = $('script:contains("window.videoUiOptions")').html(); + const videoString = videoJson.slice(videoJson.indexOf('{"stream_info":'), videoJson.lastIndexOf('},') + 1); + const videoData = JSON.parse(videoString); - [release.entryId] = url.split('/').slice(-3, -2); - release.title = $('.scene-title[itemprop="name"]').text(); + [release.entryId] = url.split('/').slice(-3, -2); + release.title = $('.scene-title[itemprop="name"]').text(); - release.description = $('#scene-description p[itemprop="description"]') - .contents() - .first() - .text() - .trim(); + release.description = $('#scene-description p[itemprop="description"]') + .contents() + .first() + .text() + .trim(); - release.date = moment.utc($('.more-scene-info .scene-date').text(), 'MMMM DD, YYYY').toDate(); - release.duration = Number($('.scene-length[itemprop="duration"]').attr('content').slice(1, -1)) * 60; + release.date = moment.utc($('.more-scene-info .scene-date').text(), 'MMMM DD, YYYY').toDate(); + release.duration = Number($('.scene-length[itemprop="duration"]').attr('content').slice(1, -1)) * 60; - const actorsFromCards = $('.featured-model .card-image a').map((actorIndex, actorElement) => { - const avatar = `https:${$(actorElement).find('img').attr('data-src')}`; + const actorsFromCards = $('.featured-model .card-image a').map((actorIndex, actorElement) => { + const avatar = `https:${$(actorElement).find('img').attr('data-src')}`; - return { - name: $(actorElement).attr('title'), - avatar: [avatar.replace('medium.jpg', 'large.jpg'), avatar], - }; - }).toArray(); + return { + name: $(actorElement).attr('title'), + avatar: [avatar.replace('medium.jpg', 'large.jpg'), avatar], + }; + }).toArray(); - release.actors = actorsFromCards || $('.related-model a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + release.actors = actorsFromCards || $('.related-model a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - release.likes = Number($('.label-rating .like').text()); - release.dislikes = Number($('.label-rating .dislike').text()); + release.likes = Number($('.label-rating .like').text()); + release.dislikes = Number($('.label-rating .dislike').text()); - const siteElement = $('.niche-site-logo'); - // const siteUrl = `https://www.brazzers.com${siteElement.attr('href').slice(0, -1)}`; - const siteName = siteElement.attr('title'); - release.channel = siteName.replace(/\s+/g, '').toLowerCase(); + const siteElement = $('.niche-site-logo'); + // const siteUrl = `https://www.brazzers.com${siteElement.attr('href').slice(0, -1)}`; + const siteName = siteElement.attr('title'); + release.channel = siteName.replace(/\s+/g, '').toLowerCase(); - release.tags = $('.tag-card-container a').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); - release.photos = $('.carousel-thumb a').map((photoIndex, photoElement) => `https:${$(photoElement).attr('href')}`).toArray(); + release.tags = $('.tag-card-container a').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); + release.photos = $('.carousel-thumb a').map((photoIndex, photoElement) => `https:${$(photoElement).attr('href')}`).toArray(); - const posterPath = videoData?.poster || $('meta[itemprop="thumbnailUrl"]').attr('content') || $('#trailer-player-container').attr('data-player-img'); - if (posterPath) release.poster = `https:${posterPath}`; + const posterPath = videoData?.poster || $('meta[itemprop="thumbnailUrl"]').attr('content') || $('#trailer-player-container').attr('data-player-img'); + if (posterPath) release.poster = `https:${posterPath}`; - if (videoData) { - release.trailer = Object.entries(videoData.stream_info.http.paths).map(([quality, path]) => ({ - src: `https:${path}`, - quality: Number(quality.match(/\d{3,}/)[0]), - })); - } + if (videoData) { + release.trailer = Object.entries(videoData.stream_info.http.paths).map(([quality, path]) => ({ + src: `https:${path}`, + quality: Number(quality.match(/\d{3,}/)[0]), + })); + } - return release; + return release; } function scrapeActorSearch(html, url, actorName) { - const { document } = new JSDOM(html).window; - const actorLink = document.querySelector(`a[title="${actorName}" i]`); + const { document } = new JSDOM(html).window; + const actorLink = document.querySelector(`a[title="${actorName}" i]`); - return actorLink ? actorLink.href : null; + return actorLink ? actorLink.href : null; } async function fetchActorReleases({ qu, html }, accReleases = []) { - const releases = scrapeAll(html); - const next = qu.url('.pagination .next a'); + const releases = scrapeAll(html); + const next = qu.url('.pagination .next a'); - if (next) { - const url = `https://www.brazzers.com${next}`; - const res = await get(url); + if (next) { + const url = `https://www.brazzers.com${next}`; + const res = await get(url); - if (res.ok) { - return fetchActorReleases(res.item, accReleases.concat(releases)); - } - } + if (res.ok) { + return fetchActorReleases(res.item, accReleases.concat(releases)); + } + } - return accReleases.concat(releases); + return accReleases.concat(releases); } async function scrapeProfile(html, url, actorName) { - const qProfile = ex(html); - const { q, qa } = qProfile; + const qProfile = ex(html); + const { q, qa } = qProfile; - const bioKeys = qa('.profile-spec-list label', true).map(key => key.replace(/\n+|\s{2,}/g, '').trim()); - const bioValues = qa('.profile-spec-list var', true).map(value => value.replace(/\n+|\s{2,}/g, '').trim()); + const bioKeys = qa('.profile-spec-list label', true).map(key => key.replace(/\n+|\s{2,}/g, '').trim()); + const bioValues = qa('.profile-spec-list var', true).map(value => value.replace(/\n+|\s{2,}/g, '').trim()); - const bio = bioKeys.reduce((acc, key, index) => ({ ...acc, [key]: bioValues[index] }), {}); + const bio = bioKeys.reduce((acc, key, index) => ({ ...acc, [key]: bioValues[index] }), {}); - const profile = { - name: actorName, - }; + const profile = { + name: actorName, + }; - profile.description = q('.model-profile-specs p', true); + profile.description = q('.model-profile-specs p', true); - if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity; - if (bio.Measurements && bio.Measurements.match(/\d+[A-Z]+-\d+-\d+/)) [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-'); - if (bio['Date of Birth'] && bio['Date of Birth'] !== 'Unknown') profile.birthdate = moment.utc(bio['Date of Birth'], 'MMMM DD, YYYY').toDate(); - if (bio['Birth Location']) profile.birthPlace = bio['Birth Location']; - if (bio['Pussy Type']) profile.pussy = bio['Pussy Type'].split(',').slice(-1)[0].toLowerCase(); + if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity; + if (bio.Measurements && bio.Measurements.match(/\d+[A-Z]+-\d+-\d+/)) [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-'); + if (bio['Date of Birth'] && bio['Date of Birth'] !== 'Unknown') profile.birthdate = moment.utc(bio['Date of Birth'], 'MMMM DD, YYYY').toDate(); + if (bio['Birth Location']) profile.birthPlace = bio['Birth Location']; + if (bio['Pussy Type']) profile.pussy = bio['Pussy Type'].split(',').slice(-1)[0].toLowerCase(); - if (bio.Height) profile.height = heightToCm(bio.Height); - if (bio.Weight) profile.weight = lbsToKg(bio.Weight.match(/\d+/)[0]); - if (bio['Hair Color']) profile.hair = hairMap[bio['Hair Color']] || bio['Hair Color'].toLowerCase(); + if (bio.Height) profile.height = heightToCm(bio.Height); + if (bio.Weight) profile.weight = lbsToKg(bio.Weight.match(/\d+/)[0]); + if (bio['Hair Color']) profile.hair = hairMap[bio['Hair Color']] || bio['Hair Color'].toLowerCase(); - if (bio['Tits Type'] && bio['Tits Type'].match('Natural')) profile.naturalBoobs = true; - if (bio['Tits Type'] && bio['Tits Type'].match('Enhanced')) profile.naturalBoobs = false; + if (bio['Tits Type'] && bio['Tits Type'].match('Natural')) profile.naturalBoobs = true; + if (bio['Tits Type'] && bio['Tits Type'].match('Enhanced')) profile.naturalBoobs = false; - if (bio['Body Art'] && bio['Body Art'].match('Tattoo')) profile.hasTattoos = true; - if (bio['Body Art'] && bio['Body Art'].match('Piercing')) profile.hasPiercings = true; + if (bio['Body Art'] && bio['Body Art'].match('Tattoo')) profile.hasTattoos = true; + if (bio['Body Art'] && bio['Body Art'].match('Piercing')) profile.hasPiercings = true; - const avatarEl = q('.big-pic-model-container img'); - if (avatarEl) profile.avatar = `https:${avatarEl.src}`; + const avatarEl = q('.big-pic-model-container img'); + if (avatarEl) profile.avatar = `https:${avatarEl.src}`; - profile.releases = await fetchActorReleases(qProfile); + profile.releases = await fetchActorReleases(qProfile); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const res = await bhttp.get(`${site.url}/page/${page}/`); + const res = await bhttp.get(`${site.url}/page/${page}/`); - return scrapeAll(res.body.toString(), site, false); + return scrapeAll(res.body.toString(), site, false); } async function fetchUpcoming(site) { - const res = await bhttp.get(`${site.url}/`); + const res = await bhttp.get(`${site.url}/`); - return scrapeAll(res.body.toString(), site, true); + return scrapeAll(res.body.toString(), site, true); } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - return scrapeScene(res.body.toString(), url, site); + return scrapeScene(res.body.toString(), url, site); } async function fetchProfile(actorName) { - const searchUrl = 'https://brazzers.com/pornstars-search/'; - const searchRes = await bhttp.get(searchUrl, { - headers: { - Cookie: `textSearch=${encodeURIComponent(actorName)};`, - }, - }); + const searchUrl = 'https://brazzers.com/pornstars-search/'; + const searchRes = await bhttp.get(searchUrl, { + headers: { + Cookie: `textSearch=${encodeURIComponent(actorName)};`, + }, + }); - const actorLink = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName); + const actorLink = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName); - if (actorLink) { - const url = `https://brazzers.com${actorLink}`; - const res = await bhttp.get(url); + if (actorLink) { + const url = `https://brazzers.com${actorLink}`; + const res = await bhttp.get(url); - return scrapeProfile(res.body.toString(), url, actorName); - } + return scrapeProfile(res.body.toString(), url, actorName); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchProfile, - fetchScene, - fetchUpcoming, + fetchLatest, + fetchProfile, + fetchScene, + fetchUpcoming, }; diff --git a/src/scrapers/burningangel.js b/src/scrapers/burningangel.js index c31fa6e80..562acaa6e 100644 --- a/src/scrapers/burningangel.js +++ b/src/scrapers/burningangel.js @@ -3,8 +3,8 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchScene, - fetchUpcoming: fetchApiUpcoming, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchScene, + fetchUpcoming: fetchApiUpcoming, }; diff --git a/src/scrapers/cherrypimps.js b/src/scrapers/cherrypimps.js index 7f5739574..b65be5521 100644 --- a/src/scrapers/cherrypimps.js +++ b/src/scrapers/cherrypimps.js @@ -4,139 +4,139 @@ const { get, geta, ctxa, ed } = require('../utils/q'); const slugify = require('../utils/slugify'); function scrapeAll(scenes, site) { - return scenes.map(({ qu }) => { - const url = qu.url('.text-thumb a'); - const { pathname } = new URL(url); - const channelUrl = qu.url('.badge'); + return scenes.map(({ qu }) => { + const url = qu.url('.text-thumb a'); + const { pathname } = new URL(url); + const channelUrl = qu.url('.badge'); - if (site?.parameters?.extract && qu.q('.badge', true) !== site.name) { - return null; - } + if (site?.parameters?.extract && qu.q('.badge', true) !== site.name) { + return null; + } - const release = {}; + const release = {}; - release.url = channelUrl ? `${channelUrl}${pathname}` : url; - release.entryId = pathname.match(/\/\d+/)[0].slice(1); - release.title = qu.q('.text-thumb a', true); + release.url = channelUrl ? `${channelUrl}${pathname}` : url; + release.entryId = pathname.match(/\/\d+/)[0].slice(1); + release.title = qu.q('.text-thumb a', true); - release.date = qu.date('.date', 'YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/); - release.duration = qu.dur('.date', /(\d{2}:)?\d{2}:\d{2}/); + release.date = qu.date('.date', 'YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/); + release.duration = qu.dur('.date', /(\d{2}:)?\d{2}:\d{2}/); - release.actors = qu.all('.category a', true); + release.actors = qu.all('.category a', true); - release.poster = qu.img('img.video_placeholder, .video-images img'); - release.teaser = { src: qu.trailer() }; + release.poster = qu.img('img.video_placeholder, .video-images img'); + release.teaser = { src: qu.trailer() }; - return release; - }).filter(Boolean); + return release; + }).filter(Boolean); } function scrapeScene({ q, qd, qa }, url, _site, baseRelease) { - const release = { url }; + const release = { url }; - const { pathname } = new URL(url); - release.entryId = pathname.match(/\/\d+/)[0].slice(1); + const { pathname } = new URL(url); + release.entryId = pathname.match(/\/\d+/)[0].slice(1); - release.title = q('.trailer-block_title', true); - release.description = q('.info-block:nth-child(3) .text', true); - release.date = qd('.info-block_data .text', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/); + release.title = q('.trailer-block_title', true); + release.description = q('.info-block:nth-child(3) .text', true); + release.date = qd('.info-block_data .text', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/); - const duration = baseRelease?.duration || Number(q('.info-block_data .text', true).match(/(\d+)\s+min/)?.[1]) * 60; - if (duration) release.duration = duration; + const duration = baseRelease?.duration || Number(q('.info-block_data .text', true).match(/(\d+)\s+min/)?.[1]) * 60; + if (duration) release.duration = duration; - release.actors = qa('.info-block_data a[href*="/models"]', true); - release.tags = qa('.info-block a[href*="/categories"]', true); + release.actors = qa('.info-block_data a[href*="/models"]', true); + release.tags = qa('.info-block a[href*="/categories"]', true); - const posterEl = q('.update_thumb'); - const poster = posterEl.getAttribute('src0_3x') || posterEl.getAttribute('src0_2x') || posterEl.dataset.src; + const posterEl = q('.update_thumb'); + const poster = posterEl.getAttribute('src0_3x') || posterEl.getAttribute('src0_2x') || posterEl.dataset.src; - if (poster && baseRelease?.poster) release.photos = [poster]; - else if (poster) release.poster = poster; + if (poster && baseRelease?.poster) release.photos = [poster]; + else if (poster) release.poster = poster; - return release; + return release; } function scrapeProfile({ q, qa, qtx }) { - const profile = {}; + const profile = {}; - const keys = qa('.model-descr_line:not(.model-descr_rait) p.text span', true); - const values = qa('.model-descr_line:not(.model-descr_rait) p.text').map(el => qtx(el)); - const bio = keys.reduce((acc, key, index) => ({ ...acc, [slugify(key, '_')]: values[index] }), {}); + const keys = qa('.model-descr_line:not(.model-descr_rait) p.text span', true); + const values = qa('.model-descr_line:not(.model-descr_rait) p.text').map(el => qtx(el)); + const bio = keys.reduce((acc, key, index) => ({ ...acc, [slugify(key, '_')]: values[index] }), {}); - if (bio.height) profile.height = Number(bio.height.match(/\((\d+)cm\)/)[1]); - if (bio.weight) profile.weight = Number(bio.weight.match(/\((\d+)kg\)/)[1]); - if (bio.race) profile.ethnicity = bio.race; + if (bio.height) profile.height = Number(bio.height.match(/\((\d+)cm\)/)[1]); + if (bio.weight) profile.weight = Number(bio.weight.match(/\((\d+)kg\)/)[1]); + if (bio.race) profile.ethnicity = bio.race; - if (bio.date_of_birth) profile.birthdate = ed(bio.date_of_birth, 'MMMM D, YYYY'); - if (bio.birthplace) profile.birthPlace = bio.birthplace; + if (bio.date_of_birth) profile.birthdate = ed(bio.date_of_birth, 'MMMM D, YYYY'); + if (bio.birthplace) profile.birthPlace = bio.birthplace; - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); - if (!/\?/.test(bust)) profile.bust = bust; - if (!/\?/.test(waist)) profile.waist = waist; - if (!/\?/.test(hip)) profile.hip = hip; - } + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); + if (!/\?/.test(bust)) profile.bust = bust; + if (!/\?/.test(waist)) profile.waist = waist; + if (!/\?/.test(hip)) profile.hip = hip; + } - if (bio.hair) profile.hair = bio.hair; - if (bio.eyes) profile.eyes = bio.eyes; + if (bio.hair) profile.hair = bio.hair; + if (bio.eyes) profile.eyes = bio.eyes; - if (/various/i.test(bio.tattoos)) profile.hasTattoos = true; - else if (/none/i.test(bio.tattoos)) profile.hasTattoos = false; - else if (bio.tattoos) { - profile.hasTattoos = true; - profile.tattoos = bio.tattoos; - } + if (/various/i.test(bio.tattoos)) profile.hasTattoos = true; + else if (/none/i.test(bio.tattoos)) profile.hasTattoos = false; + else if (bio.tattoos) { + profile.hasTattoos = true; + profile.tattoos = bio.tattoos; + } - if (/various/i.test(bio.piercings)) profile.hasPiercings = true; - else if (/none/i.test(bio.piercings)) profile.hasPiercings = false; - else if (bio.piercings) { - profile.hasPiercings = true; - profile.piercings = bio.piercings; - } + if (/various/i.test(bio.piercings)) profile.hasPiercings = true; + else if (/none/i.test(bio.piercings)) profile.hasPiercings = false; + else if (bio.piercings) { + profile.hasPiercings = true; + profile.piercings = bio.piercings; + } - if (bio.aliases) profile.aliases = bio.aliases.split(',').map(alias => alias.trim()); + if (bio.aliases) profile.aliases = bio.aliases.split(',').map(alias => alias.trim()); - const avatar = q('.model-img img'); - profile.avatar = avatar.getAttribute('src0_3x') || avatar.getAttribute('src0_2x') || avatar.dataset.src; + const avatar = q('.model-img img'); + profile.avatar = avatar.getAttribute('src0_3x') || avatar.getAttribute('src0_2x') || avatar.dataset.src; - const releases = qa('.video-thumb'); - profile.releases = scrapeAll(ctxa(releases)); + const releases = qa('.video-thumb'); + profile.releases = scrapeAll(ctxa(releases)); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = site.parameters?.extract - ? `https://cherrypimps.com/categories/movies_${page}.html` - : `${site.url}/categories/movies_${page}.html`; - const res = await geta(url, 'div.video-thumb'); + const url = site.parameters?.extract + ? `https://cherrypimps.com/categories/movies_${page}.html` + : `${site.url}/categories/movies_${page}.html`; + const res = await geta(url, 'div.video-thumb'); - return res.ok ? scrapeAll(res.items, site) : res.status; + return res.ok ? scrapeAll(res.items, site) : res.status; } async function fetchScene(url, site, release) { - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeScene(res.item, url, site, release) : res.status; + return res.ok ? scrapeScene(res.item, url, site, release) : res.status; } async function fetchProfile(actorName, scraperSlug) { - const actorSlug = slugify(actorName); - const actorSlug2 = slugify(actorName, ''); + const actorSlug = slugify(actorName); + const actorSlug2 = slugify(actorName, ''); - const [url, url2] = ['cherrypimps', 'wildoncam'].includes(scraperSlug) - ? [`https://${scraperSlug}.com/models/${actorSlug}.html`, `https://${scraperSlug}.com/models/${actorSlug2}.html`] - : [`https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug}.html`, `https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug2}.html`]; + const [url, url2] = ['cherrypimps', 'wildoncam'].includes(scraperSlug) + ? [`https://${scraperSlug}.com/models/${actorSlug}.html`, `https://${scraperSlug}.com/models/${actorSlug2}.html`] + : [`https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug}.html`, `https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug2}.html`]; - const res = await get(url); - if (res.ok) return scrapeProfile(res.item); + const res = await get(url); + if (res.ok) return scrapeProfile(res.item); - const res2 = await get(url2); - return res2.ok ? scrapeProfile(res2.item) : res2.status; + const res2 = await get(url2); + return res2.ok ? scrapeProfile(res2.item) : res2.status; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/ddfnetwork.js b/src/scrapers/ddfnetwork.js index ab70ccb9a..00d64face 100644 --- a/src/scrapers/ddfnetwork.js +++ b/src/scrapers/ddfnetwork.js @@ -7,182 +7,182 @@ const slugify = require('../utils/slugify'); /* eslint-disable newline-per-chained-call */ function scrapeAll(html, site, origin) { - return exa(html, '.card.m-1:not(.pornstar-card)').map(({ q, qa, qd }) => { - const release = {}; + return exa(html, '.card.m-1:not(.pornstar-card)').map(({ q, qa, qd }) => { + const release = {}; - release.title = q('a', 'title'); - release.url = `${site?.url || origin || 'https://ddfnetwork.com'}${q('a', 'href')}`; - [release.entryId] = release.url.split('/').slice(-1); + release.title = q('a', 'title'); + release.url = `${site?.url || origin || 'https://ddfnetwork.com'}${q('a', 'href')}`; + [release.entryId] = release.url.split('/').slice(-1); - release.date = qd('small[datetime]', 'YYYY-MM-DD HH:mm:ss', null, 'datetime'); - release.actors = qa('.card-subtitle a', true).filter(Boolean); + release.date = qd('small[datetime]', 'YYYY-MM-DD HH:mm:ss', null, 'datetime'); + release.actors = qa('.card-subtitle a', true).filter(Boolean); - const duration = parseInt(q('.card-info div:nth-child(2) .card-text', true), 10) * 60; - if (duration) release.duration = duration; + const duration = parseInt(q('.card-info div:nth-child(2) .card-text', true), 10) * 60; + if (duration) release.duration = duration; - release.poster = q('img').dataset.src; + release.poster = q('img').dataset.src; - return release; - }); + return release; + }); } async function scrapeScene(html, url, _site) { - const { qu } = ex(html); - const release = {}; + const { qu } = ex(html); + const release = {}; - [release.entryId] = url.split('/').slice(-1); + [release.entryId] = url.split('/').slice(-1); - release.title = qu.meta('itemprop=name'); - release.description = qu.q('.descr-box p', true); - release.date = qu.date('meta[itemprop=uploadDate]', 'YYYY-MM-DD', null, 'content') + release.title = qu.meta('itemprop=name'); + release.description = qu.q('.descr-box p', true); + release.date = qu.date('meta[itemprop=uploadDate]', 'YYYY-MM-DD', null, 'content') || qu.date('.title-border:nth-child(2) p', 'MM.DD.YYYY'); - release.actors = qu.all('.pornstar-card > a', 'title'); - release.tags = qu.all('.tags-tab .tags a', true); + release.actors = qu.all('.pornstar-card > a', 'title'); + release.tags = qu.all('.tags-tab .tags a', true); - release.duration = parseInt(qu.q('.icon-video-red + span', true), 10) * 60; - release.likes = Number(qu.q('.icon-like-red + span', true)); + release.duration = parseInt(qu.q('.icon-video-red + span', true), 10) * 60; + release.likes = Number(qu.q('.icon-like-red + span', true)); - release.poster = qu.poster(); - release.photos = qu.urls('.photo-slider-guest .card a'); + release.poster = qu.poster(); + release.photos = qu.urls('.photo-slider-guest .card a'); - release.trailer = qu.all('source[type="video/mp4"]').map(trailer => ({ - src: trailer.src, - quality: Number(trailer.attributes.res.value), - })); + release.trailer = qu.all('source[type="video/mp4"]').map(trailer => ({ + src: trailer.src, + quality: Number(trailer.attributes.res.value), + })); - return release; + return release; } async function fetchActorReleases(urls) { - // DDF Network and DDF Network Stream list all scenes, exclude - const sources = urls.filter(url => !/ddfnetwork/.test(url)); + // DDF Network and DDF Network Stream list all scenes, exclude + const sources = urls.filter(url => !/ddfnetwork/.test(url)); - const releases = await Promise.all(sources.map(async (url) => { - const { html } = await get(url); + const releases = await Promise.all(sources.map(async (url) => { + const { html } = await get(url); - return scrapeAll(html, null, new URL(url).origin); - })); + return scrapeAll(html, null, new URL(url).origin); + })); - // DDF cross-releases scenes between sites, filter duplicates by entryId - return Object.values(releases - .flat() - .sort((releaseA, releaseB) => releaseB.date - releaseA.date) // sort by date so earliest scene remains - .reduce((acc, release) => ({ ...acc, [release.entryId]: release }), {})); + // DDF cross-releases scenes between sites, filter duplicates by entryId + return Object.values(releases + .flat() + .sort((releaseA, releaseB) => releaseB.date - releaseA.date) // sort by date so earliest scene remains + .reduce((acc, release) => ({ ...acc, [release.entryId]: release }), {})); } async function scrapeProfile(html, _url, actorName) { - const { qu } = ex(html); + const { qu } = ex(html); - const keys = qu.all('.about-title', true).map(key => slugify(key, '_')); - const values = qu.all('.about-info').map((el) => { - if (el.children.length > 0) { - return Array.from(el.children, child => child.textContent.trim()).join(', '); - } + const keys = qu.all('.about-title', true).map(key => slugify(key, '_')); + const values = qu.all('.about-info').map((el) => { + if (el.children.length > 0) { + return Array.from(el.children, child => child.textContent.trim()).join(', '); + } - return el.textContent.trim(); - }); + return el.textContent.trim(); + }); - const bio = keys.reduce((acc, key, index) => { - if (values[index] === '-') return acc; + const bio = keys.reduce((acc, key, index) => { + if (values[index] === '-') return acc; - return { - ...acc, - [key]: values[index], - }; - }, {}); + return { + ...acc, + [key]: values[index], + }; + }, {}); - const profile = { - name: actorName, - }; + const profile = { + name: actorName, + }; - profile.description = qu.q('.description-box', true); - profile.birthdate = ed(bio.birthday, 'MMMM DD, YYYY'); + profile.description = qu.q('.description-box', true); + profile.birthdate = ed(bio.birthday, 'MMMM DD, YYYY'); - if (bio.nationality) profile.nationality = bio.nationality; + if (bio.nationality) profile.nationality = bio.nationality; - if (bio.bra_size) [profile.bust] = bio.bra_size.match(/\d+\w+/); - if (bio.waist) profile.waist = Number(bio.waist.match(/\d+/)[0]); - if (bio.hips) profile.hip = Number(bio.hips.match(/\d+/)[0]); + if (bio.bra_size) [profile.bust] = bio.bra_size.match(/\d+\w+/); + if (bio.waist) profile.waist = Number(bio.waist.match(/\d+/)[0]); + if (bio.hips) profile.hip = Number(bio.hips.match(/\d+/)[0]); - if (bio.height) profile.height = Number(bio.height.match(/\d{2,}/)[0]); + if (bio.height) profile.height = Number(bio.height.match(/\d{2,}/)[0]); - if (bio.tit_style && /Enhanced/.test(bio.tit_style)) profile.naturalBoobs = false; - if (bio.tit_style && /Natural/.test(bio.tit_style)) profile.naturalBoobs = true; + if (bio.tit_style && /Enhanced/.test(bio.tit_style)) profile.naturalBoobs = false; + if (bio.tit_style && /Natural/.test(bio.tit_style)) profile.naturalBoobs = true; - if (bio.body_art && /Tattoo/.test(bio.body_art)) profile.hasTattoos = true; - if (bio.body_art && /Piercing/.test(bio.body_art)) profile.hasPiercings = true; + if (bio.body_art && /Tattoo/.test(bio.body_art)) profile.hasTattoos = true; + if (bio.body_art && /Piercing/.test(bio.body_art)) profile.hasPiercings = true; - if (bio.hair_style) profile.hair = bio.hair_style.split(',')[0].trim().toLowerCase(); - if (bio.eye_color) profile.eyes = bio.eye_color.match(/\w+/)[0].toLowerCase(); + if (bio.hair_style) profile.hair = bio.hair_style.split(',')[0].trim().toLowerCase(); + if (bio.eye_color) profile.eyes = bio.eye_color.match(/\w+/)[0].toLowerCase(); - if (bio.shoe_size) profile.shoes = Number(bio.shoe_size.split('|')[1]); + if (bio.shoe_size) profile.shoes = Number(bio.shoe_size.split('|')[1]); - const avatarEl = qu.q('.pornstar-details .card-img-top'); - if (avatarEl && avatarEl.dataset.src.match('^//')) profile.avatar = `https:${avatarEl.dataset.src}`; + const avatarEl = qu.q('.pornstar-details .card-img-top'); + if (avatarEl && avatarEl.dataset.src.match('^//')) profile.avatar = `https:${avatarEl.dataset.src}`; - profile.releases = await fetchActorReleases(qu.urls('.find-me-tab li a')); + profile.releases = await fetchActorReleases(qu.urls('.find-me-tab li a')); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = site.parameters?.native - ? `${site.url}/videos/search/latest/ever/allsite/-/${page}` - : `https://ddfnetwork.com/videos/search/latest/ever/${new URL(site.url).hostname}/-/${page}`; + const url = site.parameters?.native + ? `${site.url}/videos/search/latest/ever/allsite/-/${page}` + : `https://ddfnetwork.com/videos/search/latest/ever/${new URL(site.url).hostname}/-/${page}`; - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeAll(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeAll(res.body.toString(), site); + } - return res.statusCode; + return res.statusCode; } async function fetchScene(url, site) { - // DDF's main site moved to Porn World - // const res = await bhttp.get(`https://ddfnetwork.com${new URL(url).pathname}`); - const res = await bhttp.get(url); + // DDF's main site moved to Porn World + // const res = await bhttp.get(`https://ddfnetwork.com${new URL(url).pathname}`); + const res = await bhttp.get(url); - return scrapeScene(res.body.toString(), url, site); + return scrapeScene(res.body.toString(), url, site); } async function fetchProfile(actorName) { - const resSearch = await bhttp.post('https://ddfnetwork.com/search/ajax', - { - type: 'hints', - word: actorName, - }, - { - decodeJSON: true, - headers: { - 'x-requested-with': 'XMLHttpRequest', - }, - }); + const resSearch = await bhttp.post('https://ddfnetwork.com/search/ajax', + { + type: 'hints', + word: actorName, + }, + { + decodeJSON: true, + headers: { + 'x-requested-with': 'XMLHttpRequest', + }, + }); - if (resSearch.statusCode !== 200 || Array.isArray(resSearch.body.list)) { - return null; - } + if (resSearch.statusCode !== 200 || Array.isArray(resSearch.body.list)) { + return null; + } - if (!resSearch.body.list.pornstarsName || resSearch.body.list.pornstarsName.length === 0) { - return null; - } + if (!resSearch.body.list.pornstarsName || resSearch.body.list.pornstarsName.length === 0) { + return null; + } - const [actor] = resSearch.body.list.pornstarsName; - const url = `https://ddfnetwork.com${actor.href}`; + const [actor] = resSearch.body.list.pornstarsName; + const url = `https://ddfnetwork.com${actor.href}`; - const resActor = await bhttp.get(url); + const resActor = await bhttp.get(url); - if (resActor.statusCode !== 200) { - return null; - } + if (resActor.statusCode !== 200) { + return null; + } - return scrapeProfile(resActor.body.toString(), url, actorName); + return scrapeProfile(resActor.body.toString(), url, actorName); } module.exports = { - fetchLatest, - fetchProfile, - fetchScene, + fetchLatest, + fetchProfile, + fetchScene, }; diff --git a/src/scrapers/digitalplayground.js b/src/scrapers/digitalplayground.js index 8077d9055..96d6a5d76 100644 --- a/src/scrapers/digitalplayground.js +++ b/src/scrapers/digitalplayground.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'digitalplayground', 'modelprofile'); + return fetchProfile(actorName, 'digitalplayground', 'modelprofile'); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/dogfart.js b/src/scrapers/dogfart.js index 1d9252e4d..603ec36b7 100644 --- a/src/scrapers/dogfart.js +++ b/src/scrapers/dogfart.js @@ -7,136 +7,136 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); async function getPhotos(albumUrl) { - const res = await bhttp.get(albumUrl); - const html = res.body.toString(); - const { document } = new JSDOM(html).window; + const res = await bhttp.get(albumUrl); + const html = res.body.toString(); + const { document } = new JSDOM(html).window; - const lastPhotoPage = Array.from(document.querySelectorAll('.preview-image-container a')).slice(-1)[0].href; - const lastPhotoIndex = parseInt(lastPhotoPage.match(/\d+.jpg/)[0], 10); + const lastPhotoPage = Array.from(document.querySelectorAll('.preview-image-container a')).slice(-1)[0].href; + const lastPhotoIndex = parseInt(lastPhotoPage.match(/\d+.jpg/)[0], 10); - const photoUrls = Array.from({ length: lastPhotoIndex }, (value, index) => { - const pageUrl = `https://blacksonblondes.com${lastPhotoPage.replace(/\d+.jpg/, `${(index + 1).toString().padStart(3, '0')}.jpg`)}`; + const photoUrls = Array.from({ length: lastPhotoIndex }, (value, index) => { + const pageUrl = `https://blacksonblondes.com${lastPhotoPage.replace(/\d+.jpg/, `${(index + 1).toString().padStart(3, '0')}.jpg`)}`; - return { - url: pageUrl, - extract: ({ qu }) => qu.q('.scenes-module img', 'src'), - }; - }); + return { + url: pageUrl, + extract: ({ qu }) => qu.q('.scenes-module img', 'src'), + }; + }); - return photoUrls; + return photoUrls; } function scrapeLatest(html, site) { - const { document } = new JSDOM(html).window; - const sceneElements = Array.from(document.querySelectorAll('.recent-updates')); + const { document } = new JSDOM(html).window; + const sceneElements = Array.from(document.querySelectorAll('.recent-updates')); - return sceneElements.reduce((acc, element) => { - const siteUrl = element.querySelector('.help-block').textContent; + return sceneElements.reduce((acc, element) => { + const siteUrl = element.querySelector('.help-block').textContent; - if (`www.${siteUrl.toLowerCase()}` !== new URL(site.url).host) { - // different dogfart site - return acc; - } + if (`www.${siteUrl.toLowerCase()}` !== new URL(site.url).host) { + // different dogfart site + return acc; + } - const sceneLinkElement = element.querySelector('.thumbnail'); - const url = `https://dogfartnetwork.com${sceneLinkElement.href}`; - const { pathname } = new URL(url); - const entryId = `${site.slug}_${pathname.split('/')[4]}`; + const sceneLinkElement = element.querySelector('.thumbnail'); + const url = `https://dogfartnetwork.com${sceneLinkElement.href}`; + const { pathname } = new URL(url); + const entryId = `${site.slug}_${pathname.split('/')[4]}`; - const title = element.querySelector('.scene-title').textContent; - const actors = title.split(/[,&]|\band\b/).map(actor => actor.trim()); + const title = element.querySelector('.scene-title').textContent; + const actors = title.split(/[,&]|\band\b/).map(actor => actor.trim()); - const poster = `https:${element.querySelector('img').src}`; - const teaser = sceneLinkElement.dataset.preview_clip_url; + const poster = `https:${element.querySelector('img').src}`; + const teaser = sceneLinkElement.dataset.preview_clip_url; - return [ - ...acc, - { - url, - entryId, - title, - actors, - poster, - teaser: { - src: teaser, - }, - site, - }, - ]; - }, []); + return [ + ...acc, + { + url, + entryId, + title, + actors, + poster, + teaser: { + src: teaser, + }, + site, + }, + ]; + }, []); } async function scrapeScene(html, url, site) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const title = document.querySelector('.description-title').textContent; - const actors = Array.from(document.querySelectorAll('.more-scenes a')).map(({ textContent }) => textContent); - const metaDescription = document.querySelector('meta[itemprop="description"]').content; - const description = metaDescription - ? metaDescription.content - : document.querySelector('.description') - .textContent - .replace(/[ \t\n]{2,}/g, ' ') - .replace('...read more', '') - .trim(); + const title = document.querySelector('.description-title').textContent; + const actors = Array.from(document.querySelectorAll('.more-scenes a')).map(({ textContent }) => textContent); + const metaDescription = document.querySelector('meta[itemprop="description"]').content; + const description = metaDescription + ? metaDescription.content + : document.querySelector('.description') + .textContent + .replace(/[ \t\n]{2,}/g, ' ') + .replace('...read more', '') + .trim(); - const channel = document.querySelector('.site-name').textContent.split('.')[0].toLowerCase(); - const { origin, pathname } = new URL(url); - const entryId = `${channel}_${pathname.split('/').slice(-2)[0]}`; + const channel = document.querySelector('.site-name').textContent.split('.')[0].toLowerCase(); + const { origin, pathname } = new URL(url); + const entryId = `${channel}_${pathname.split('/').slice(-2)[0]}`; - const date = new Date(document.querySelector('meta[itemprop="uploadDate"]').content); - const duration = moment - .duration(`00:${document - .querySelectorAll('.extra-info p')[1] - .textContent - .match(/\d+:\d+$/)[0]}`) - .asSeconds(); + const date = new Date(document.querySelector('meta[itemprop="uploadDate"]').content); + const duration = moment + .duration(`00:${document + .querySelectorAll('.extra-info p')[1] + .textContent + .match(/\d+:\d+$/)[0]}`) + .asSeconds(); - const trailerElement = document.querySelector('.html5-video'); - const poster = `https:${trailerElement.dataset.poster}`; - const { trailer } = trailerElement.dataset; + const trailerElement = document.querySelector('.html5-video'); + const poster = `https:${trailerElement.dataset.poster}`; + const { trailer } = trailerElement.dataset; - const lastPhotosUrl = Array.from(document.querySelectorAll('.pagination a')).slice(-1)[0].href; - const photos = await getPhotos(`${origin}${pathname}${lastPhotosUrl}`, site, url); + const lastPhotosUrl = Array.from(document.querySelectorAll('.pagination a')).slice(-1)[0].href; + const photos = await getPhotos(`${origin}${pathname}${lastPhotosUrl}`, site, url); - const stars = Math.floor(Number(document.querySelector('span[itemprop="average"]')?.textContent || document.querySelector('span[itemprop="ratingValue"]')?.textContent) / 2); - const tags = Array.from(document.querySelectorAll('.scene-details .categories a')).map(({ textContent }) => textContent); + const stars = Math.floor(Number(document.querySelector('span[itemprop="average"]')?.textContent || document.querySelector('span[itemprop="ratingValue"]')?.textContent) / 2); + const tags = Array.from(document.querySelectorAll('.scene-details .categories a')).map(({ textContent }) => textContent); - return { - entryId, - url: `${origin}${pathname}`, - title, - description, - actors, - date, - duration, - poster, - photos, - trailer: { - src: trailer, - }, - tags, - rating: { - stars, - }, - site, - channel, - }; + return { + entryId, + url: `${origin}${pathname}`, + title, + description, + actors, + date, + duration, + poster, + photos, + trailer: { + src: trailer, + }, + tags, + rating: { + stars, + }, + site, + channel, + }; } async function fetchLatest(site, page = 1) { - const res = await bhttp.get(`https://dogfartnetwork.com/tour/scenes/?p=${page}`); + const res = await bhttp.get(`https://dogfartnetwork.com/tour/scenes/?p=${page}`); - return scrapeLatest(res.body.toString(), site); + return scrapeLatest(res.body.toString(), site); } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - return scrapeScene(res.body.toString(), url, site); + return scrapeScene(res.body.toString(), url, site); } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/evilangel.js b/src/scrapers/evilangel.js index c31fa6e80..562acaa6e 100644 --- a/src/scrapers/evilangel.js +++ b/src/scrapers/evilangel.js @@ -3,8 +3,8 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchScene, - fetchUpcoming: fetchApiUpcoming, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchScene, + fetchUpcoming: fetchApiUpcoming, }; diff --git a/src/scrapers/fakehub.js b/src/scrapers/fakehub.js index 5fa25f17e..7f6d24a9e 100644 --- a/src/scrapers/fakehub.js +++ b/src/scrapers/fakehub.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'fakehub', 'modelprofile'); + return fetchProfile(actorName, 'fakehub', 'modelprofile'); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/famedigital.js b/src/scrapers/famedigital.js index 9698814d9..ac6e5a743 100644 --- a/src/scrapers/famedigital.js +++ b/src/scrapers/famedigital.js @@ -1,115 +1,115 @@ 'use strict'; const { - fetchLatest, - fetchApiLatest, - fetchUpcoming, - fetchApiUpcoming, - fetchScene, - fetchProfile, - fetchApiProfile, - scrapeAll, + fetchLatest, + fetchApiLatest, + fetchUpcoming, + fetchApiUpcoming, + fetchScene, + fetchProfile, + fetchApiProfile, + scrapeAll, } = require('./gamma'); const { get } = require('../utils/q'); const slugify = require('../utils/slugify'); function extractLowArtActors(release) { - const actors = release.title - .replace(/solo/i, '') - .split(/,|\band\b/ig) - .map(actor => actor.trim()); + const actors = release.title + .replace(/solo/i, '') + .split(/,|\band\b/ig) + .map(actor => actor.trim()); - return { - ...release, - actors, - }; + return { + ...release, + actors, + }; } async function networkFetchLatest(site, page = 1) { - if (site.parameters?.api) return fetchApiLatest(site, page, false); + if (site.parameters?.api) return fetchApiLatest(site, page, false); - const releases = await fetchLatest(site, page); + const releases = await fetchLatest(site, page); - if (site.slug === 'lowartfilms') { - return releases.map(release => extractLowArtActors(release)); - } + if (site.slug === 'lowartfilms') { + return releases.map(release => extractLowArtActors(release)); + } - return releases; + return releases; } async function networkFetchScene(url, site) { - const release = await fetchScene(url, site); + const release = await fetchScene(url, site); - if (site.slug === 'lowartfilms') { - return extractLowArtActors(release); - } + if (site.slug === 'lowartfilms') { + return extractLowArtActors(release); + } - return release; + return release; } async function networkFetchUpcoming(site, page = 1) { - if (site.parameters?.api) return fetchApiUpcoming(site, page, true); + if (site.parameters?.api) return fetchApiUpcoming(site, page, true); - return fetchUpcoming(site, page); + return fetchUpcoming(site, page); } function getActorReleasesUrl(actorPath, page = 1) { - return `https://www.peternorth.com/en/videos/All-Categories/0${actorPath}/All-Dvds/0/latest/${page}`; + return `https://www.peternorth.com/en/videos/All-Categories/0${actorPath}/All-Dvds/0/latest/${page}`; } async function fetchClassicProfile(actorName, siteSlug) { - const actorSlug = slugify(actorName); + const actorSlug = slugify(actorName); - const url = `https://${siteSlug}.com/en/pornstars`; - const pornstarsRes = await get(url); + const url = `https://${siteSlug}.com/en/pornstars`; + const pornstarsRes = await get(url); - if (!pornstarsRes.ok) return null; + if (!pornstarsRes.ok) return null; - const actorPath = pornstarsRes.item.qa('option[value*="/pornstar"]') - .find(el => slugify(el.textContent) === actorSlug) + const actorPath = pornstarsRes.item.qa('option[value*="/pornstar"]') + .find(el => slugify(el.textContent) === actorSlug) ?.value; - if (actorPath) { - const actorUrl = `https://${siteSlug}.com${actorPath}`; - const res = await get(actorUrl); + if (actorPath) { + const actorUrl = `https://${siteSlug}.com${actorPath}`; + const res = await get(actorUrl); - if (res.ok) { - const releases = scrapeAll(res.item, null, `https://www.${siteSlug}.com`, false); + if (res.ok) { + const releases = scrapeAll(res.item, null, `https://www.${siteSlug}.com`, false); - return { releases }; - } - } + return { releases }; + } + } - return null; + return null; } async function networkFetchProfile(actorName, scraperSlug, site, include) { - // not all Fame Digital sites offer Gamma actors - const [devils, rocco, peter, silvia] = await Promise.all([ - fetchApiProfile(actorName, 'devilsfilm', true), - fetchApiProfile(actorName, 'roccosiffredi'), - include.scenes ? fetchProfile(actorName, 'peternorth', true, getActorReleasesUrl, include) : [], - include.scenes ? fetchClassicProfile(actorName, 'silviasaint') : [], - include.scenes ? fetchClassicProfile(actorName, 'silverstonedvd') : [], - ]); + // not all Fame Digital sites offer Gamma actors + const [devils, rocco, peter, silvia] = await Promise.all([ + fetchApiProfile(actorName, 'devilsfilm', true), + fetchApiProfile(actorName, 'roccosiffredi'), + include.scenes ? fetchProfile(actorName, 'peternorth', true, getActorReleasesUrl, include) : [], + include.scenes ? fetchClassicProfile(actorName, 'silviasaint') : [], + include.scenes ? fetchClassicProfile(actorName, 'silverstonedvd') : [], + ]); - if (devils || rocco || peter) { - const releases = [].concat(devils?.releases || [], rocco?.releases || [], peter?.releases || [], silvia?.releases || []); + if (devils || rocco || peter) { + const releases = [].concat(devils?.releases || [], rocco?.releases || [], peter?.releases || [], silvia?.releases || []); - return { - ...peter, - ...rocco, - ...devils, - releases, - }; - } + return { + ...peter, + ...rocco, + ...devils, + releases, + }; + } - return null; + return null; } module.exports = { - fetchLatest: networkFetchLatest, - fetchProfile: networkFetchProfile, - fetchScene: networkFetchScene, - fetchUpcoming: networkFetchUpcoming, + fetchLatest: networkFetchLatest, + fetchProfile: networkFetchProfile, + fetchScene: networkFetchScene, + fetchUpcoming: networkFetchUpcoming, }; diff --git a/src/scrapers/fantasymassage.js b/src/scrapers/fantasymassage.js index 448e1646f..d0840db6b 100644 --- a/src/scrapers/fantasymassage.js +++ b/src/scrapers/fantasymassage.js @@ -4,7 +4,7 @@ const { fetchLatest, fetchUpcoming, fetchScene } = require('./gamma'); module.exports = { - fetchLatest, - fetchScene, - fetchUpcoming, + fetchLatest, + fetchScene, + fetchUpcoming, }; diff --git a/src/scrapers/freeones.js b/src/scrapers/freeones.js index ecf8b7ef2..ac0cfce28 100644 --- a/src/scrapers/freeones.js +++ b/src/scrapers/freeones.js @@ -5,89 +5,89 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); function scrapeProfile(html, actorName) { - const { document } = new JSDOM(html).window; - const profile = { name: actorName }; + const { document } = new JSDOM(html).window; + const profile = { name: actorName }; - const bio = Array.from(document.querySelectorAll('a[href^="/babes"]'), el => decodeURI(el.href)).reduce((acc, item) => { - const keyMatch = item.match(/\[\w+\]/); + const bio = Array.from(document.querySelectorAll('a[href^="/babes"]'), el => decodeURI(el.href)).reduce((acc, item) => { + const keyMatch = item.match(/\[\w+\]/); - if (keyMatch) { - const key = keyMatch[0].slice(1, -1); - const [, value] = item.split('='); + if (keyMatch) { + const key = keyMatch[0].slice(1, -1); + const [, value] = item.split('='); - // both hip and waist link to 'waist', assume biggest value is hip - if (key === 'waist' && acc.waist) { - if (acc.waist > value) { - acc.hip = acc.waist; - acc.waist = value; + // both hip and waist link to 'waist', assume biggest value is hip + if (key === 'waist' && acc.waist) { + if (acc.waist > value) { + acc.hip = acc.waist; + acc.waist = value; - return acc; - } + return acc; + } - acc.hip = value; + acc.hip = value; - return acc; - } + return acc; + } - acc[key] = value; - } + acc[key] = value; + } - return acc; - }, {}); + return acc; + }, {}); - if (bio.dateOfBirth) profile.birthdate = moment.utc(bio.dateOfBirth, 'YYYY-MM-DD').toDate(); + if (bio.dateOfBirth) profile.birthdate = moment.utc(bio.dateOfBirth, 'YYYY-MM-DD').toDate(); - if (profile.placeOfBirth || bio.country) profile.birthPlace = `${bio.placeOfBirth}, ${bio.country}`; - profile.eyes = bio.eyeColor; - profile.hair = bio.hairColor; - profile.ethnicity = bio.ethnicity; + if (profile.placeOfBirth || bio.country) profile.birthPlace = `${bio.placeOfBirth}, ${bio.country}`; + profile.eyes = bio.eyeColor; + profile.hair = bio.hairColor; + profile.ethnicity = bio.ethnicity; - profile.bust = bio.bra; - if (bio.waist) profile.waist = Number(bio.waist.split(',')[0]); - if (bio.hip) profile.hip = Number(bio.hip.split(',')[0]); + profile.bust = bio.bra; + if (bio.waist) profile.waist = Number(bio.waist.split(',')[0]); + if (bio.hip) profile.hip = Number(bio.hip.split(',')[0]); - if (bio.height) profile.height = Number(bio.height.split(',')[0]); - if (bio.weight) profile.weight = Number(bio.weight.split(',')[0]); + if (bio.height) profile.height = Number(bio.height.split(',')[0]); + if (bio.weight) profile.weight = Number(bio.weight.split(',')[0]); - profile.social = Array.from(document.querySelectorAll('.profile-meta-item a.social-icons'), el => el.href); + profile.social = Array.from(document.querySelectorAll('.profile-meta-item a.social-icons'), el => el.href); - const avatar = document.querySelector('.profile-image-large img').src; - if (!avatar.match('placeholder')) profile.avatar = { src: avatar, copyright: null }; + const avatar = document.querySelector('.profile-image-large img').src; + if (!avatar.match('placeholder')) profile.avatar = { src: avatar, copyright: null }; - return profile; + return profile; } function scrapeSearch(html) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - return document.querySelector('a.image-link')?.href || null; + return document.querySelector('a.image-link')?.href || null; } async function fetchProfile(actorName) { - const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-'); + const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-'); - const res = await bhttp.get(`https://freeones.nl/${actorSlug}/profile`); + const res = await bhttp.get(`https://freeones.nl/${actorSlug}/profile`); - if (res.statusCode === 200) { - return scrapeProfile(res.body.toString(), actorName); - } + if (res.statusCode === 200) { + return scrapeProfile(res.body.toString(), actorName); + } - const searchRes = await bhttp.get(`https://freeones.nl/babes?q=${actorName}`); - const actorPath = scrapeSearch(searchRes.body.toString()); + const searchRes = await bhttp.get(`https://freeones.nl/babes?q=${actorName}`); + const actorPath = scrapeSearch(searchRes.body.toString()); - if (actorPath) { - const actorRes = await bhttp.get(`https://freeones.nl${actorPath}/profile`); + if (actorPath) { + const actorRes = await bhttp.get(`https://freeones.nl${actorPath}/profile`); - if (actorRes.statusCode === 200) { - return scrapeProfile(actorRes.body.toString(), actorName); - } + if (actorRes.statusCode === 200) { + return scrapeProfile(actorRes.body.toString(), actorName); + } - return null; - } + return null; + } - return null; + return null; } module.exports = { - fetchProfile, + fetchProfile, }; diff --git a/src/scrapers/freeones_legacy.js b/src/scrapers/freeones_legacy.js index 7711a1c37..df1c92517 100644 --- a/src/scrapers/freeones_legacy.js +++ b/src/scrapers/freeones_legacy.js @@ -6,135 +6,135 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); async function scrapeProfileFrontpage(html, url, name) { - const { document } = new JSDOM(html).window; - const bioEl = document.querySelector('.dashboard-bio-list'); + const { document } = new JSDOM(html).window; + const bioEl = document.querySelector('.dashboard-bio-list'); - const bioUrl = `https:${document.querySelector('.seemore a').href}`; + const bioUrl = `https:${document.querySelector('.seemore a').href}`; - const keys = Array.from(bioEl.querySelectorAll('dt'), el => el.textContent.trim()); - const values = Array.from(bioEl.querySelectorAll('dd'), el => el.textContent.trim()); + const keys = Array.from(bioEl.querySelectorAll('dt'), el => el.textContent.trim()); + const values = Array.from(bioEl.querySelectorAll('dd'), el => el.textContent.trim()); - const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {}); + const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {}); - const profile = { - name, - gender: 'female', - }; + const profile = { + name, + gender: 'female', + }; - const birthdateString = bio['Date of Birth:']; - const measurementsString = bio['Measurements:']; + const birthdateString = bio['Date of Birth:']; + const measurementsString = bio['Measurements:']; - const birthCityString = bio['Place of Birth:']; - const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString; + const birthCityString = bio['Place of Birth:']; + const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString; - const birthCountryString = bio['Country of Origin:']; - const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString; + const birthCountryString = bio['Country of Origin:']; + const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString; - const piercingsString = bio['Piercings:']; - const tattoosString = bio['Tattoos:']; + const piercingsString = bio['Piercings:']; + const tattoosString = bio['Tattoos:']; - if (birthdateString && birthdateString !== 'Unknown (add)') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate(); - if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement)); + if (birthdateString && birthdateString !== 'Unknown (add)') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate(); + if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement)); - if (bio['Fake Boobs:']) profile.naturalBoobs = bio['Fake Boobs:'] === 'No'; - profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`; + if (bio['Fake Boobs:']) profile.naturalBoobs = bio['Fake Boobs:'] === 'No'; + profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`; - profile.hair = bio['Hair Color:'].toLowerCase(); - profile.eyes = bio['Eye Color:'].toLowerCase(); + profile.hair = bio['Hair Color:'].toLowerCase(); + profile.eyes = bio['Eye Color:'].toLowerCase(); - if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None'); - if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None'); + if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None'); + if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None'); - if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString; - if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString; + if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString; + if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString; - profile.social = Array.from(bioEl.querySelectorAll('.dashboard-socialmedia a'), el => el.href); + profile.social = Array.from(bioEl.querySelectorAll('.dashboard-socialmedia a'), el => el.href); - return { - profile, - url: bioUrl, - }; + return { + profile, + url: bioUrl, + }; } async function scrapeProfileBio(html, frontpageProfile, url, name) { - const { document } = new JSDOM(html).window; - const bioEl = document.querySelector('#biographyTable'); + const { document } = new JSDOM(html).window; + const bioEl = document.querySelector('#biographyTable'); - const keys = Array.from(bioEl.querySelectorAll('td:nth-child(1)'), el => el.textContent.trim()); - const values = Array.from(bioEl.querySelectorAll('td:nth-child(2)'), el => el.textContent.trim()); + const keys = Array.from(bioEl.querySelectorAll('td:nth-child(1)'), el => el.textContent.trim()); + const values = Array.from(bioEl.querySelectorAll('td:nth-child(2)'), el => el.textContent.trim()); - const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {}); + const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {}); - const profile = { - ...frontpageProfile, - name, - gender: 'female', - }; + const profile = { + ...frontpageProfile, + name, + gender: 'female', + }; - const birthdateString = bio['Date of Birth:']; - const measurementsString = bio['Measurements:']; + const birthdateString = bio['Date of Birth:']; + const measurementsString = bio['Measurements:']; - const birthCityString = bio['Place of Birth:']; - const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString; + const birthCityString = bio['Place of Birth:']; + const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString; - const birthCountryString = bio['Country of Origin:']; - const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString; + const birthCountryString = bio['Country of Origin:']; + const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString; - const piercingsString = bio['Piercings:']; - const tattoosString = bio['Tattoos:']; + const piercingsString = bio['Piercings:']; + const tattoosString = bio['Tattoos:']; - if (birthdateString && birthdateString !== 'Unknown') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate(); - if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement)); + if (birthdateString && birthdateString !== 'Unknown') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate(); + if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement)); - if (bio['Fake boobs']) profile.naturalBoobs = bio['Fake boobs:'] === 'No'; - profile.ethnicity = bio['Ethnicity:']; + if (bio['Fake boobs']) profile.naturalBoobs = bio['Fake boobs:'] === 'No'; + profile.ethnicity = bio['Ethnicity:']; - profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`; + profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`; - profile.hair = bio['Hair Color:'].toLowerCase(); - profile.eyes = bio['Eye Color:'].toLowerCase(); - profile.height = Number(bio['Height:'].match(/\d+/)[0]); - profile.weight = Number(bio['Weight:'].match(/\d+/)[0]); + profile.hair = bio['Hair Color:'].toLowerCase(); + profile.eyes = bio['Eye Color:'].toLowerCase(); + profile.height = Number(bio['Height:'].match(/\d+/)[0]); + profile.weight = Number(bio['Weight:'].match(/\d+/)[0]); - if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None'); - if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None'); + if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None'); + if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None'); - if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString; - if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString; + if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString; + if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString; - profile.social = Array.from(bioEl.querySelectorAll('#socialmedia a'), el => el.href); + profile.social = Array.from(bioEl.querySelectorAll('#socialmedia a'), el => el.href); - return profile; + return profile; } async function fetchProfile(actorName) { - const slug = actorName.replace(' ', '_'); - const frontpageUrl = `https://www.freeones.com/html/v_links/${slug}`; + const slug = actorName.replace(' ', '_'); + const frontpageUrl = `https://www.freeones.com/html/v_links/${slug}`; - const resFrontpage = await bhttp.get(frontpageUrl); + const resFrontpage = await bhttp.get(frontpageUrl); - if (resFrontpage.statusCode === 200) { - const { url, bio } = await scrapeProfileFrontpage(resFrontpage.body.toString(), frontpageUrl, actorName); - const resBio = await bhttp.get(url); + if (resFrontpage.statusCode === 200) { + const { url, bio } = await scrapeProfileFrontpage(resFrontpage.body.toString(), frontpageUrl, actorName); + const resBio = await bhttp.get(url); - return scrapeProfileBio(resBio.body.toString(), bio, url, actorName); - } + return scrapeProfileBio(resBio.body.toString(), bio, url, actorName); + } - // apparently some actors are appended 'Babe' as their surname... - const fallbackSlug = `${slug}_Babe`; - const fallbackUrl = `https://www.freeones.com/html/s_links/${fallbackSlug}`; - const resFallback = await bhttp.get(fallbackUrl); + // apparently some actors are appended 'Babe' as their surname... + const fallbackSlug = `${slug}_Babe`; + const fallbackUrl = `https://www.freeones.com/html/s_links/${fallbackSlug}`; + const resFallback = await bhttp.get(fallbackUrl); - if (resFallback.statusCode === 200) { - const { url, profile } = await scrapeProfileFrontpage(resFallback.body.toString(), fallbackUrl, actorName); - const resBio = await bhttp.get(url); + if (resFallback.statusCode === 200) { + const { url, profile } = await scrapeProfileFrontpage(resFallback.body.toString(), fallbackUrl, actorName); + const resBio = await bhttp.get(url); - return scrapeProfileBio(resBio.body.toString(), profile, url, actorName); - } + return scrapeProfileBio(resBio.body.toString(), profile, url, actorName); + } - return null; + return null; } module.exports = { - fetchProfile, + fetchProfile, }; diff --git a/src/scrapers/fullpornnetwork.js b/src/scrapers/fullpornnetwork.js index 2b8521b82..1347fe741 100644 --- a/src/scrapers/fullpornnetwork.js +++ b/src/scrapers/fullpornnetwork.js @@ -4,93 +4,93 @@ const { get, geta, ctxa } = require('../utils/q'); const slugify = require('../utils/slugify'); function scrapeAll(scenes) { - return scenes.map(({ el, qu }) => { - const release = {}; + return scenes.map(({ el, qu }) => { + const release = {}; - release.entryId = el.dataset.setid || qu.q('.update_thumb', 'id').match(/\w+-\w+-(\d+)-\d+/)[1]; - release.url = qu.url('.title'); + release.entryId = el.dataset.setid || qu.q('.update_thumb', 'id').match(/\w+-\w+-(\d+)-\d+/)[1]; + release.url = qu.url('.title'); - release.title = qu.q('.title', true); - release.description = qu.q('.title', 'title'); + release.title = qu.q('.title', true); + release.description = qu.q('.title', 'title'); - release.date = qu.date('.video-data > span:last-child', 'YYYY-MM-DD'); - release.duration = qu.dur('.video-data > span'); + release.date = qu.date('.video-data > span:last-child', 'YYYY-MM-DD'); + release.duration = qu.dur('.video-data > span'); - release.actors = qu.all('.update_models a', true); + release.actors = qu.all('.update_models a', true); - const poster = qu.q('.update_thumb', 'src0_1x'); - release.poster = [ - poster.replace('-1x', '-2x'), - poster, - ]; + const poster = qu.q('.update_thumb', 'src0_1x'); + release.poster = [ + poster.replace('-1x', '-2x'), + poster, + ]; - return release; - }); + return release; + }); } function scrapeScene({ q, qa, qd, qtx }, url, _site) { - const release = { url }; + const release = { url }; - release.entryId = q('#image_parent img', 'id').match(/\w+-\w+-(\d+)-\d+/)[1]; + release.entryId = q('#image_parent img', 'id').match(/\w+-\w+-(\d+)-\d+/)[1]; - release.title = q('.trailer_title', true); - release.description = qtx('.text p'); - release.date = qd('span[data-dateadded]', 'YYYY-MM-DD', null, 'data-dateadded'); + release.title = q('.trailer_title', true); + release.description = qtx('.text p'); + release.date = qd('span[data-dateadded]', 'YYYY-MM-DD', null, 'data-dateadded'); - release.actors = qa('.update_models a', true); - release.tags = qa('.video-info a[href*="/categories"]', true); + release.actors = qa('.update_models a', true); + release.tags = qa('.video-info a[href*="/categories"]', true); - const poster = q('#image_parent img', 'src0_1x'); - release.poster = [ - poster.replace('-1x', '-2x'), - poster, - ]; + const poster = q('#image_parent img', 'src0_1x'); + release.poster = [ + poster.replace('-1x', '-2x'), + poster, + ]; - return release; + return release; } function scrapeProfile({ el, q, qtx }) { - const profile = {}; + const profile = {}; - const description = qtx('.model-bio'); - if (description) profile.description = description; + const description = qtx('.model-bio'); + if (description) profile.description = description; - profile.avatar = [ - q('.model-image img', 'src0_2x'), - q('.model-image img', 'src0_1x'), - ]; + profile.avatar = [ + q('.model-image img', 'src0_2x'), + q('.model-image img', 'src0_1x'), + ]; - profile.releases = scrapeAll(ctxa(el, '.update')); + profile.releases = scrapeAll(ctxa(el, '.update')); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = `${site.url}/categories/movies_${page}_d.html`; - const res = await geta(url, '.latest-updates .update'); + const url = `${site.url}/categories/movies_${page}_d.html`; + const res = await geta(url, '.latest-updates .update'); - return res.ok ? scrapeAll(res.items, site) : res.status; + return res.ok ? scrapeAll(res.items, site) : res.status; } async function fetchScene(url, site) { - const res = await get(url, '.content-wrapper'); + const res = await get(url, '.content-wrapper'); - return res.ok ? scrapeScene(res.item, url, site) : res.status; + return res.ok ? scrapeScene(res.item, url, site) : res.status; } async function fetchProfile(actorName, scraperSlug) { - const actorSlug = slugify(actorName, ''); - const url = scraperSlug === 'povperverts' - ? `https://povperverts.net/models/${actorSlug}.html` - : `https://${scraperSlug}.com/models/${actorSlug}.html`; + const actorSlug = slugify(actorName, ''); + const url = scraperSlug === 'povperverts' + ? `https://povperverts.net/models/${actorSlug}.html` + : `https://${scraperSlug}.com/models/${actorSlug}.html`; - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeProfile(res.item, actorName) : res.status; + return res.ok ? scrapeProfile(res.item, actorName) : res.status; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/gamma.js b/src/scrapers/gamma.js index 26f25c4eb..d6c1a6c0a 100644 --- a/src/scrapers/gamma.js +++ b/src/scrapers/gamma.js @@ -12,618 +12,618 @@ const { ex, get } = require('../utils/q'); const slugify = require('../utils/slugify'); function getAlbumUrl(albumPath, site) { - if (site.parameters?.photos) { - return /^http/.test(site.parameters.photos) - ? `${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}` - : `${site.url}${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}`; - } + if (site.parameters?.photos) { + return /^http/.test(site.parameters.photos) + ? `${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}` + : `${site.url}${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}`; + } - if (site.url && site.parameters?.photos !== false) { - return `${site.url}${albumPath}`; - } + if (site.url && site.parameters?.photos !== false) { + return `${site.url}${albumPath}`; + } - return null; + return null; } async function fetchPhotos(url) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - return res.body.toString(); + return res.body.toString(); } function scrapePhotos(html, includeThumbnails = true) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); + const $ = cheerio.load(html, { normalizeWhitespace: true }); - return $('.preview .imgLink, .pgFooterThumb a').toArray().map((linkEl) => { - const url = $(linkEl).attr('href'); + return $('.preview .imgLink, .pgFooterThumb a').toArray().map((linkEl) => { + const url = $(linkEl).attr('href'); - if (/\/join|\/createaccount/.test(url)) { - // URL links to join page instead of full photo, extract thumbnail - // /createaccount is used by e.g. Tricky Spa native site - const src = $(linkEl).find('img').attr('src'); + if (/\/join|\/createaccount/.test(url)) { + // URL links to join page instead of full photo, extract thumbnail + // /createaccount is used by e.g. Tricky Spa native site + const src = $(linkEl).find('img').attr('src'); - if (/previews\//.test(src)) { - // resource often serves full photo at a modifier URL anyway, add as primary source - const highRes = src - .replace('previews/', '') - .replace('_tb.jpg', '.jpg'); + if (/previews\//.test(src)) { + // resource often serves full photo at a modifier URL anyway, add as primary source + const highRes = src + .replace('previews/', '') + .replace('_tb.jpg', '.jpg'); - // keep original thumbnail as fallback in case full photo is not available - return [highRes, src]; - } + // keep original thumbnail as fallback in case full photo is not available + return [highRes, src]; + } - if (!includeThumbnails) return null; + if (!includeThumbnails) return null; - return src; - } + return src; + } - // URL links to full photo - return url; - }).filter(Boolean); + // URL links to full photo + return url; + }).filter(Boolean); } async function getPhotos(albumPath, site, includeThumbnails = true) { - const albumUrl = getAlbumUrl(albumPath, site); + const albumUrl = getAlbumUrl(albumPath, site); - if (!albumUrl) { - return []; - } + if (!albumUrl) { + return []; + } - try { - const html = await fetchPhotos(albumUrl); - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const photos = scrapePhotos(html, includeThumbnails); + try { + const html = await fetchPhotos(albumUrl); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const photos = scrapePhotos(html, includeThumbnails); - const lastPage = $('.Gamma_Paginator a.last').attr('href')?.match(/\d+$/)[0]; + const lastPage = $('.Gamma_Paginator a.last').attr('href')?.match(/\d+$/)[0]; - if (lastPage) { - const otherPages = Array.from({ length: Number(lastPage) }, (_value, index) => index + 1).slice(1); + if (lastPage) { + const otherPages = Array.from({ length: Number(lastPage) }, (_value, index) => index + 1).slice(1); - const otherPhotos = await Promise.map(otherPages, async (page) => { - const pageUrl = `${albumUrl}/${page}`; - const pageHtml = await fetchPhotos(pageUrl); + const otherPhotos = await Promise.map(otherPages, async (page) => { + const pageUrl = `${albumUrl}/${page}`; + const pageHtml = await fetchPhotos(pageUrl); - return scrapePhotos(pageHtml, includeThumbnails); - }, { - concurrency: 2, - }); + return scrapePhotos(pageHtml, includeThumbnails); + }, { + concurrency: 2, + }); - return photos.concat(otherPhotos.flat()); - } + return photos.concat(otherPhotos.flat()); + } - return photos; - } catch (error) { - logger.warn(`Failed to fetch ${site.name} photos from ${albumUrl}: ${error.message}`); + return photos; + } catch (error) { + logger.warn(`Failed to fetch ${site.name} photos from ${albumUrl}: ${error.message}`); - return []; - } + return []; + } } async function scrapeApiReleases(json, site) { - return json.map((scene) => { - if (site.parameters?.extract && scene.sitename !== site.parameters.extract) { - return null; - } + return json.map((scene) => { + if (site.parameters?.extract && scene.sitename !== site.parameters.extract) { + return null; + } - const release = { - entryId: scene.clip_id, - title: scene.title, - description: scene.description, - duration: scene.length, - likes: scene.ratings_up, - dislikes: scene.ratings_down, - }; + const release = { + entryId: scene.clip_id, + title: scene.title, + description: scene.description, + duration: scene.length, + likes: scene.ratings_up, + dislikes: scene.ratings_down, + }; - release.path = `/${scene.url_title}/${release.entryId}`; + release.path = `/${scene.url_title}/${release.entryId}`; - if (site.parameters?.scene) release.url = `${site.parameters.scene}${release.path}`; - else if (site.url && site.parameters?.scene !== false) release.url = `${site.url}/en/video${release.path}`; + if (site.parameters?.scene) release.url = `${site.parameters.scene}${release.path}`; + else if (site.url && site.parameters?.scene !== false) release.url = `${site.url}/en/video${release.path}`; - release.date = moment.utc(scene.release_date, 'YYYY-MM-DD').toDate(); - release.actors = scene.actors.map(actor => ({ name: actor.name, gender: actor.gender })); - release.director = scene.directors[0]?.name || null; + release.date = moment.utc(scene.release_date, 'YYYY-MM-DD').toDate(); + release.actors = scene.actors.map(actor => ({ name: actor.name, gender: actor.gender })); + release.director = scene.directors[0]?.name || null; - release.tags = scene.master_categories - .concat(scene.categories?.map(category => category.name)) - .filter(Boolean); // some categories don't have a name + release.tags = scene.master_categories + .concat(scene.categories?.map(category => category.name)) + .filter(Boolean); // some categories don't have a name - const posterPath = scene.pictures.resized || (scene.pictures.nsfw?.top && Object.values(scene.pictures.nsfw.top)[0]); + const posterPath = scene.pictures.resized || (scene.pictures.nsfw?.top && Object.values(scene.pictures.nsfw.top)[0]); - if (posterPath) { - release.poster = [ - `https://images-evilangel.gammacdn.com/movies${posterPath}`, - `https://transform.gammacdn.com/movies${posterPath}`, - ]; - } + if (posterPath) { + release.poster = [ + `https://images-evilangel.gammacdn.com/movies${posterPath}`, + `https://transform.gammacdn.com/movies${posterPath}`, + ]; + } - // release.movie = `${site.url}/en/movie/${scene.url_movie_title}/${scene.movie_id}`; + // release.movie = `${site.url}/en/movie/${scene.url_movie_title}/${scene.movie_id}`; - return release; - }).filter(Boolean); + return release; + }).filter(Boolean); } function scrapeAll(html, site, networkUrl, hasTeaser = true) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const scenesElements = $('li[data-itemtype=scene], div[data-itemtype=scenes]').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const scenesElements = $('li[data-itemtype=scene], div[data-itemtype=scenes]').toArray(); - return scenesElements.map((element) => { - const release = {}; + return scenesElements.map((element) => { + const release = {}; - const sceneLinkElement = $(element).find('.sceneTitle a, .tlcTitle a'); + const sceneLinkElement = $(element).find('.sceneTitle a, .tlcTitle a'); - if (site) release.url = `${networkUrl ? site.network.url : site.url}${sceneLinkElement.attr('href')}`; - else release.url = `${networkUrl}${sceneLinkElement.attr('href')}`; + if (site) release.url = `${networkUrl ? site.network.url : site.url}${sceneLinkElement.attr('href')}`; + else release.url = `${networkUrl}${sceneLinkElement.attr('href')}`; - release.title = sceneLinkElement.attr('title'); - release.entryId = $(element).attr('data-itemid'); + release.title = sceneLinkElement.attr('title'); + release.entryId = $(element).attr('data-itemid'); - const dateEl = $(element).find('.sceneDate, .tlcSpecsDate .tlcDetailsValue').text() || null; - if (dateEl) { - release.date = moment - .utc(dateEl, ['MM-DD-YYYY', 'YYYY-MM-DD']) - .toDate(); - } + const dateEl = $(element).find('.sceneDate, .tlcSpecsDate .tlcDetailsValue').text() || null; + if (dateEl) { + release.date = moment + .utc(dateEl, ['MM-DD-YYYY', 'YYYY-MM-DD']) + .toDate(); + } - release.actors = $(element).find('.sceneActors a, .tlcActors a') - .map((actorIndex, actorElement) => $(actorElement).attr('title')) - .toArray(); + release.actors = $(element).find('.sceneActors a, .tlcActors a') + .map((actorIndex, actorElement) => $(actorElement).attr('title')) + .toArray(); - [release.likes, release.dislikes] = $(element).find('.value') - .toArray() - .map(value => Number($(value).text())); + [release.likes, release.dislikes] = $(element).find('.value') + .toArray() + .map(value => Number($(value).text())); - const posterEl = $(element).find('.imgLink img, .tlcImageItem'); - if (posterEl) release.poster = posterEl.attr('data-original') || posterEl.attr('src'); + const posterEl = $(element).find('.imgLink img, .tlcImageItem'); + if (posterEl) release.poster = posterEl.attr('data-original') || posterEl.attr('src'); - if (hasTeaser) { - release.teaser = [ - { src: `https://videothumb.gammacdn.com/600x339/${release.entryId}.mp4` }, - { src: `https://videothumb.gammacdn.com/307x224/${release.entryId}.mp4` }, - ]; - } + if (hasTeaser) { + release.teaser = [ + { src: `https://videothumb.gammacdn.com/600x339/${release.entryId}.mp4` }, + { src: `https://videothumb.gammacdn.com/307x224/${release.entryId}.mp4` }, + ]; + } - return release; - }); + return release; + }); } async function scrapeScene(html, url, site, baseRelease, mobileHtml) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const m$ = mobileHtml && cheerio.load(mobileHtml, { normalizeWhitespace: true }); - const release = { $, url }; + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const m$ = mobileHtml && cheerio.load(mobileHtml, { normalizeWhitespace: true }); + const release = { $, url }; - const json = $('script[type="application/ld+json"]').html(); - const videoJson = $('script:contains("window.ScenePlayerOptions")').html(); + const json = $('script[type="application/ld+json"]').html(); + const videoJson = $('script:contains("window.ScenePlayerOptions")').html(); - const [data, data2] = json ? JSON.parse(json) : []; - const videoData = videoJson && JSON.parse(videoJson.slice(videoJson.indexOf('{'), videoJson.indexOf('};') + 1)); + const [data, data2] = json ? JSON.parse(json) : []; + const videoData = videoJson && JSON.parse(videoJson.slice(videoJson.indexOf('{'), videoJson.indexOf('};') + 1)); - release.entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1]; - release.title = videoData?.playerOptions?.sceneInfos.sceneTitle || data?.name; + release.entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1]; + release.title = videoData?.playerOptions?.sceneInfos.sceneTitle || data?.name; - // date in data object is not the release date of the scene, but the date the entry was added; only use as fallback - const dateString = $('.updatedDate').first().text().trim(); - const dateMatch = dateString.match(/\d{2,4}[-/]\d{2}[-/]\d{2,4}/)?.[0]; + // date in data object is not the release date of the scene, but the date the entry was added; only use as fallback + const dateString = $('.updatedDate').first().text().trim(); + const dateMatch = dateString.match(/\d{2,4}[-/]\d{2}[-/]\d{2,4}/)?.[0]; - if (dateMatch) release.date = moment.utc(dateMatch, ['MM-DD-YYYY', 'YYYY-MM-DD']).toDate(); - else if (data?.dateCreated) release.date = moment.utc(data.dateCreated, 'YYYY-MM-DD').toDate(); - else release.date = videoData.playerOptions.sceneInfos.sceneReleaseDate; + if (dateMatch) release.date = moment.utc(dateMatch, ['MM-DD-YYYY', 'YYYY-MM-DD']).toDate(); + else if (data?.dateCreated) release.date = moment.utc(data.dateCreated, 'YYYY-MM-DD').toDate(); + else release.date = videoData.playerOptions.sceneInfos.sceneReleaseDate; - if (data) { - release.description = data.description; - if (data.director?.[0]?.name) release.director = data.director[0].name; - else if (data2?.director?.[0]?.name) release.director = data2.director[0].name; + if (data) { + release.description = data.description; + if (data.director?.[0]?.name) release.director = data.director[0].name; + else if (data2?.director?.[0]?.name) release.director = data2.director[0].name; - const stars = (data.aggregateRating.ratingValue / data.aggregateRating.bestRating) * 5; - if (stars) release.rating = { stars }; + const stars = (data.aggregateRating.ratingValue / data.aggregateRating.bestRating) * 5; + if (stars) release.rating = { stars }; - release.duration = moment.duration(data.duration.slice(2)).asSeconds(); - } + release.duration = moment.duration(data.duration.slice(2)).asSeconds(); + } - const actors = data?.actor || data2?.actor; + const actors = data?.actor || data2?.actor; - if (actors) { - release.actors = actors.map(actor => ({ - name: actor.name, - gender: actor.gender, - })); - } + if (actors) { + release.actors = actors.map(actor => ({ + name: actor.name, + gender: actor.gender, + })); + } - const hasTrans = release.actors?.some(actor => actor.gender === 'shemale'); - const rawTags = data?.keywords?.split(', ') || data2?.keywords?.split(', ') || []; - release.tags = hasTrans ? [...rawTags, 'transsexual'] : rawTags; + const hasTrans = release.actors?.some(actor => actor.gender === 'shemale'); + const rawTags = data?.keywords?.split(', ') || data2?.keywords?.split(', ') || []; + release.tags = hasTrans ? [...rawTags, 'transsexual'] : rawTags; - const channel = data?.productionCompany?.name || $('.studioLink a, .siteLink a').attr('title')?.trim() || $('.siteNameSpan').text()?.trim().toLowerCase().replace('.com', ''); - if (channel) release.channel = slugify(channel, ''); + const channel = data?.productionCompany?.name || $('.studioLink a, .siteLink a').attr('title')?.trim() || $('.siteNameSpan').text()?.trim().toLowerCase().replace('.com', ''); + if (channel) release.channel = slugify(channel, ''); - if (videoData.picPreview && new URL(videoData.picPreview).pathname.length > 1) release.poster = videoData.picPreview; // sometimes links to just https://images02-fame.gammacdn.com/ + if (videoData.picPreview && new URL(videoData.picPreview).pathname.length > 1) release.poster = videoData.picPreview; // sometimes links to just https://images02-fame.gammacdn.com/ - const photoLink = $('.picturesItem a').attr('href'); - const mobilePhotos = m$ ? m$('.preview-displayer a img').map((photoIndex, photoEl) => $(photoEl).attr('src')).toArray() : []; + const photoLink = $('.picturesItem a').attr('href'); + const mobilePhotos = m$ ? m$('.preview-displayer a img').map((photoIndex, photoEl) => $(photoEl).attr('src')).toArray() : []; - if (photoLink) { - const photos = await getPhotos(photoLink, site, mobilePhotos.length < 3); // only get thumbnails when less than 3 mobile photos are available + if (photoLink) { + const photos = await getPhotos(photoLink, site, mobilePhotos.length < 3); // only get thumbnails when less than 3 mobile photos are available - if (photos.length < 7) release.photos = [...photos, ...mobilePhotos]; // probably only teaser photos available, supplement with mobile album - else release.photos = photos; - } else { - release.photos = mobilePhotos; - } + if (photos.length < 7) release.photos = [...photos, ...mobilePhotos]; // probably only teaser photos available, supplement with mobile album + else release.photos = photos; + } else { + release.photos = mobilePhotos; + } - const trailer = `${videoData.playerOptions.host}${videoData.url}`; - release.trailer = [ - { - src: trailer.replace('hd', 'sm'), - quality: 240, - }, - { - src: trailer.replace('hd', 'med'), - quality: 360, - }, - { - src: trailer.replace('hd', 'big'), - quality: 480, - }, - { - // probably 540p - src: trailer, - quality: parseInt(videoData.sizeOnLoad, 10), - }, - { - src: trailer.replace('hd', '720p'), - quality: 720, - }, - { - src: trailer.replace('hd', '1080p'), - quality: 1080, - }, - { - src: trailer.replace('hd', '4k'), - quality: 2160, - }, - ]; + const trailer = `${videoData.playerOptions.host}${videoData.url}`; + release.trailer = [ + { + src: trailer.replace('hd', 'sm'), + quality: 240, + }, + { + src: trailer.replace('hd', 'med'), + quality: 360, + }, + { + src: trailer.replace('hd', 'big'), + quality: 480, + }, + { + // probably 540p + src: trailer, + quality: parseInt(videoData.sizeOnLoad, 10), + }, + { + src: trailer.replace('hd', '720p'), + quality: 720, + }, + { + src: trailer.replace('hd', '1080p'), + quality: 1080, + }, + { + src: trailer.replace('hd', '4k'), + quality: 2160, + }, + ]; - return release; + return release; } function scrapeActorSearch(html, url, actorName) { - const { document } = new JSDOM(html).window; - const actorLink = document.querySelector(`a[title="${actorName}" i]`); + const { document } = new JSDOM(html).window; + const actorLink = document.querySelector(`a[title="${actorName}" i]`); - return actorLink ? actorLink.href : null; + return actorLink ? actorLink.href : null; } async function fetchActorReleases(profileUrl, getActorReleasesUrl, page = 1, accReleases = []) { - const { origin, pathname } = new URL(profileUrl); - const profilePath = `/${pathname.split('/').slice(-2).join('/')}`; + const { origin, pathname } = new URL(profileUrl); + const profilePath = `/${pathname.split('/').slice(-2).join('/')}`; - const url = getActorReleasesUrl(profilePath, page); - const res = await get(url); + const url = getActorReleasesUrl(profilePath, page); + const res = await get(url); - if (!res.ok) return []; + if (!res.ok) return []; - const releases = scrapeAll(res.html, null, origin); - const nextPage = res.item.qu.url('.Gamma_Paginator a.next'); + const releases = scrapeAll(res.html, null, origin); + const nextPage = res.item.qu.url('.Gamma_Paginator a.next'); - if (nextPage) { - return fetchActorReleases(profileUrl, getActorReleasesUrl, page + 1, accReleases.concat(releases)); - } + if (nextPage) { + return fetchActorReleases(profileUrl, getActorReleasesUrl, page + 1, accReleases.concat(releases)); + } - return accReleases.concat(releases); + return accReleases.concat(releases); } async function scrapeProfile(html, url, actorName, _siteSlug, getActorReleasesUrl, withReleases) { - const { q } = ex(html); + const { q } = ex(html); - const avatar = q('img.actorPicture'); - const hair = q('.actorProfile .attribute_hair_color', true); - const height = q('.actorProfile .attribute_height', true); - const weight = q('.actorProfile .attribute_weight', true); - const alias = q('.actorProfile .attribute_alternate_names', true); - const nationality = q('.actorProfile .attribute_home', true); + const avatar = q('img.actorPicture'); + const hair = q('.actorProfile .attribute_hair_color', true); + const height = q('.actorProfile .attribute_height', true); + const weight = q('.actorProfile .attribute_weight', true); + const alias = q('.actorProfile .attribute_alternate_names', true); + const nationality = q('.actorProfile .attribute_home', true); - const profile = { - name: actorName, - }; + const profile = { + name: actorName, + }; - if (avatar) { - // larger sizes usually available, provide fallbacks - const avatars = [ - avatar.src.replace(/\d+x\d+/, '500x750'), - avatar.src.replace(/\d+x\d+/, '240x360'), - avatar.src.replace(/\d+x\d+/, '200x300'), - avatar.src, - ]; + if (avatar) { + // larger sizes usually available, provide fallbacks + const avatars = [ + avatar.src.replace(/\d+x\d+/, '500x750'), + avatar.src.replace(/\d+x\d+/, '240x360'), + avatar.src.replace(/\d+x\d+/, '200x300'), + avatar.src, + ]; - profile.avatar = avatars; - } + profile.avatar = avatars; + } - profile.description = q('.actorBio p:not(.bioTitle)', true); + profile.description = q('.actorBio p:not(.bioTitle)', true); - if (hair) profile.hair = hair.split(':')[1].trim(); - if (height) profile.height = Number(height.match(/\d+/)[0]); - if (weight) profile.weight = Number(weight.match(/\d+/)[0]); - if (alias) profile.aliases = alias.split(':')[1].trim().split(', '); - if (nationality) profile.nationality = nationality.split(':')[1].trim(); + if (hair) profile.hair = hair.split(':')[1].trim(); + if (height) profile.height = Number(height.match(/\d+/)[0]); + if (weight) profile.weight = Number(weight.match(/\d+/)[0]); + if (alias) profile.aliases = alias.split(':')[1].trim().split(', '); + if (nationality) profile.nationality = nationality.split(':')[1].trim(); - if (getActorReleasesUrl && withReleases) { - profile.releases = await fetchActorReleases(url, getActorReleasesUrl); - } + if (getActorReleasesUrl && withReleases) { + profile.releases = await fetchActorReleases(url, getActorReleasesUrl); + } - return profile; + return profile; } function scrapeApiProfile(data, releases, siteSlug) { - const profile = {}; + const profile = {}; - if (data.male === 1) profile.gender = 'male'; - if (data.female === 1) profile.gender = 'female'; - if (data.shemale === 1 || data.trans === 1) profile.gender = 'transsexual'; + if (data.male === 1) profile.gender = 'male'; + if (data.female === 1) profile.gender = 'female'; + if (data.shemale === 1 || data.trans === 1) profile.gender = 'transsexual'; - if (data.description) profile.description = data.description.trim(); + if (data.description) profile.description = data.description.trim(); - if (data.attributes.ethnicity) profile.ethnicity = data.attributes.ethnicity; - if (data.attributes.eye_color) profile.eyes = data.attributes.eye_color; - if (data.attributes.hair_color) profile.hair = data.attributes.hair_color; + if (data.attributes.ethnicity) profile.ethnicity = data.attributes.ethnicity; + if (data.attributes.eye_color) profile.eyes = data.attributes.eye_color; + if (data.attributes.hair_color) profile.hair = data.attributes.hair_color; - const avatarPath = Object.values(data.pictures).reverse()[0]; - if (avatarPath) profile.avatar = `https://images01-evilangel.gammacdn.com/actors${avatarPath}`; + const avatarPath = Object.values(data.pictures).reverse()[0]; + if (avatarPath) profile.avatar = `https://images01-evilangel.gammacdn.com/actors${avatarPath}`; - profile.releases = releases.map(release => `https://${siteSlug}.com/en/video/${release.url_title}/${release.clip_id}`); + profile.releases = releases.map(release => `https://${siteSlug}.com/en/video/${release.url_title}/${release.clip_id}`); - return profile; + return profile; } function getApiUrl(appId, apiKey) { - const userAgent = 'Algolia for vanilla JavaScript (lite) 3.27.0;instantsearch.js 2.7.4;JS Helper 2.26.0'; + const userAgent = 'Algolia for vanilla JavaScript (lite) 3.27.0;instantsearch.js 2.7.4;JS Helper 2.26.0'; - const apiUrl = `https://${appId.toLowerCase()}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=${userAgent}&x-algolia-application-id=${appId}&x-algolia-api-key=${apiKey}`; + const apiUrl = `https://${appId.toLowerCase()}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=${userAgent}&x-algolia-application-id=${appId}&x-algolia-api-key=${apiKey}`; - return { - appId, - apiKey, - userAgent, - apiUrl, - }; + return { + appId, + apiKey, + userAgent, + apiUrl, + }; } async function fetchApiCredentials(referer, site) { - if (site?.parameters?.appId && site?.parameters?.apiKey) { - return getApiUrl(site.parameters.appId, site.parameters.apiKey); - } + if (site?.parameters?.appId && site?.parameters?.apiKey) { + return getApiUrl(site.parameters.appId, site.parameters.apiKey); + } - const res = await bhttp.get(referer); - const body = res.body.toString(); + const res = await bhttp.get(referer); + const body = res.body.toString(); - const apiLine = body.split('\n').find(bodyLine => bodyLine.match('apiKey')); + const apiLine = body.split('\n').find(bodyLine => bodyLine.match('apiKey')); - if (!apiLine) { - throw new Error(`No Gamma API key found for ${referer}`); - } + if (!apiLine) { + throw new Error(`No Gamma API key found for ${referer}`); + } - const apiSerial = apiLine.slice(apiLine.indexOf('{'), apiLine.indexOf('};') + 1); - const apiData = JSON.parse(apiSerial); + const apiSerial = apiLine.slice(apiLine.indexOf('{'), apiLine.indexOf('};') + 1); + const apiData = JSON.parse(apiSerial); - const { applicationID: appId, apiKey } = apiData.api.algolia; + const { applicationID: appId, apiKey } = apiData.api.algolia; - return getApiUrl(appId, apiKey); + return getApiUrl(appId, apiKey); } async function fetchApiLatest(site, page = 1, preData, include, upcoming = false) { - const referer = site.parameters?.referer || `${site.parameters?.networkReferer ? site.network.url : site.url}/en/videos`; - const { apiUrl } = await fetchApiCredentials(referer, site); + const referer = site.parameters?.referer || `${site.parameters?.networkReferer ? site.network.url : site.url}/en/videos`; + const { apiUrl } = await fetchApiCredentials(referer, site); - const res = await bhttp.post(apiUrl, { - requests: [ - { - indexName: 'all_scenes', - params: `query=&hitsPerPage=36&maxValuesPerFacet=100&page=${page - 1}&facetFilters=[["lesbian:"],["bisex:"],["shemale:"],["upcoming:${upcoming ? 1 : 0}"]]&filters=sitename:${site.slug} OR channels.id:${site.slug}`, - }, - ], - }, { - headers: { - Referer: referer, - }, - encodeJSON: true, - }); + const res = await bhttp.post(apiUrl, { + requests: [ + { + indexName: 'all_scenes', + params: `query=&hitsPerPage=36&maxValuesPerFacet=100&page=${page - 1}&facetFilters=[["lesbian:"],["bisex:"],["shemale:"],["upcoming:${upcoming ? 1 : 0}"]]&filters=sitename:${site.slug} OR channels.id:${site.slug}`, + }, + ], + }, { + headers: { + Referer: referer, + }, + encodeJSON: true, + }); - if (res.statusCode === 200 && res.body.results?.[0]?.hits) { - return scrapeApiReleases(res.body.results[0].hits, site); - } + if (res.statusCode === 200 && res.body.results?.[0]?.hits) { + return scrapeApiReleases(res.body.results[0].hits, site); + } - return []; + return []; } async function fetchApiUpcoming(site, page = 1, preData, include) { - return fetchApiLatest(site, page, preData, include, true); + return fetchApiLatest(site, page, preData, include, true); } function getLatestUrl(site, page) { - if (site.parameters?.latest) { - if (/^http/.test(site.parameters.latest)) { - return /%d/.test(site.parameters.latest) - ? util.format(site.parameters.latest, page) - : `${site.parameters.latest}${page}`; - } + if (site.parameters?.latest) { + if (/^http/.test(site.parameters.latest)) { + return /%d/.test(site.parameters.latest) + ? util.format(site.parameters.latest, page) + : `${site.parameters.latest}${page}`; + } - return /%d/.test(site.parameters.latest) - ? util.format(`${site.url}${site.parameters.latest}`, page) - : `${site.url}${site.parameters.latest}${page}`; - } + return /%d/.test(site.parameters.latest) + ? util.format(`${site.url}${site.parameters.latest}`, page) + : `${site.url}${site.parameters.latest}${page}`; + } - return `${site.url}/en/videos/AllCategories/0/${page}`; + return `${site.url}/en/videos/AllCategories/0/${page}`; } function getUpcomingUrl(site) { - if (site.parameters?.upcoming) { - return /^http/.test(site.parameters.upcoming) - ? `${site.parameters.upcoming}` - : `${site.url}${site.parameters.upcoming}`; - } + if (site.parameters?.upcoming) { + return /^http/.test(site.parameters.upcoming) + ? `${site.parameters.upcoming}` + : `${site.url}${site.parameters.upcoming}`; + } - return `${site.url}/en/videos/AllCategories/0/1/upcoming`; + return `${site.url}/en/videos/AllCategories/0/1/upcoming`; } async function fetchLatest(site, page = 1) { - const url = getLatestUrl(site, page); - const res = await bhttp.get(url); + const url = getLatestUrl(site, page); + const res = await bhttp.get(url); - return scrapeAll(res.body.toString(), site); + return scrapeAll(res.body.toString(), site); } async function fetchUpcoming(site) { - const url = getUpcomingUrl(site); - const res = await bhttp.get(url); + const url = getUpcomingUrl(site); + const res = await bhttp.get(url); - return scrapeAll(res.body.toString(), site, null, false); + return scrapeAll(res.body.toString(), site, null, false); } function getDeepUrl(url, site, baseRelease, mobile) { - const filter = new Set(['en', 'video', 'scene', site.slug, site.network.slug]); - const pathname = baseRelease?.path || new URL(url).pathname - .split('/') - .filter(component => !filter.has(component)) - .join('/'); // reduce to scene ID and title slug + const filter = new Set(['en', 'video', 'scene', site.slug, site.network.slug]); + const pathname = baseRelease?.path || new URL(url).pathname + .split('/') + .filter(component => !filter.has(component)) + .join('/'); // reduce to scene ID and title slug - const sceneId = baseRelease?.entryId || pathname.match(/\/(\d+)\//)?.[1]; + const sceneId = baseRelease?.entryId || pathname.match(/\/(\d+)\//)?.[1]; - if (mobile && /%d/.test(mobile)) { - return util.format(mobile, sceneId); - } + if (mobile && /%d/.test(mobile)) { + return util.format(mobile, sceneId); + } - if (mobile && sceneId) { - return `${mobile}${pathname}`; - } + if (mobile && sceneId) { + return `${mobile}${pathname}`; + } - if (site.parameters?.deep) { - return `${site.parameters.deep}${pathname}`; - } + if (site.parameters?.deep) { + return `${site.parameters.deep}${pathname}`; + } - return url; + return url; } async function fetchScene(url, site, baseRelease) { - if (site.parameters?.deep === false) { - return baseRelease; - } + if (site.parameters?.deep === false) { + return baseRelease; + } - const deepUrl = getDeepUrl(url, site, baseRelease); - const mobileUrl = getDeepUrl(url, site, baseRelease, site.parameters?.mobile || site.network.parameters?.mobile); + const deepUrl = getDeepUrl(url, site, baseRelease); + const mobileUrl = getDeepUrl(url, site, baseRelease, site.parameters?.mobile || site.network.parameters?.mobile); - if (deepUrl) { - const [res, mobileRes] = await Promise.all([ - bhttp.get(deepUrl), - mobileUrl && bhttp.get(mobileUrl, { - headers: { - // don't redirect to main site - 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Mobile Safari/537.36', - }, - }), - ]); + if (deepUrl) { + const [res, mobileRes] = await Promise.all([ + bhttp.get(deepUrl), + mobileUrl && bhttp.get(mobileUrl, { + headers: { + // don't redirect to main site + 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Mobile Safari/537.36', + }, + }), + ]); - if (res.statusCode === 200) { - const mobileBody = mobileRes?.statusCode === 200 ? mobileRes.body.toString() : null; - const scene = await scrapeScene(res.body.toString(), url, site, baseRelease, mobileBody); - return { ...scene, deepUrl }; - } - } + if (res.statusCode === 200) { + const mobileBody = mobileRes?.statusCode === 200 ? mobileRes.body.toString() : null; + const scene = await scrapeScene(res.body.toString(), url, site, baseRelease, mobileBody); + return { ...scene, deepUrl }; + } + } - return null; + return null; } async function fetchActorScenes(actorName, apiUrl, siteSlug) { - const res = await bhttp.post(apiUrl, { - requests: [ - { - indexName: 'all_scenes', - params: `query=&filters=sitename:${siteSlug}&hitsPerPage=36&maxValuesPerFacet=100&page=0&facetFilters=[["lesbian:"],["bisex:"],["shemale:"],["actors.name:${actorName}"]]`, - }, - ], - }, { - headers: { - Referer: `https://www.${siteSlug}.com/en/videos`, - }, - encodeJSON: true, - }); + const res = await bhttp.post(apiUrl, { + requests: [ + { + indexName: 'all_scenes', + params: `query=&filters=sitename:${siteSlug}&hitsPerPage=36&maxValuesPerFacet=100&page=0&facetFilters=[["lesbian:"],["bisex:"],["shemale:"],["actors.name:${actorName}"]]`, + }, + ], + }, { + headers: { + Referer: `https://www.${siteSlug}.com/en/videos`, + }, + encodeJSON: true, + }); - if (res.statusCode === 200 && res.body.results[0].hits.length > 0) { - return res.body.results[0].hits; - } + if (res.statusCode === 200 && res.body.results[0].hits.length > 0) { + return res.body.results[0].hits; + } - return []; + return []; } async function fetchProfile(actorName, siteSlug, altSearchUrl, getActorReleasesUrl, include) { - const actorSlug = actorName.toLowerCase().replace(/\s+/, '+'); - const searchUrl = altSearchUrl - ? `https://www.${siteSlug}.com/en/search/${actorSlug}/1/actor` - : `https://www.${siteSlug}.com/en/search/${siteSlug}/actor/${actorSlug}`; - const searchRes = await bhttp.get(searchUrl); + const actorSlug = actorName.toLowerCase().replace(/\s+/, '+'); + const searchUrl = altSearchUrl + ? `https://www.${siteSlug}.com/en/search/${actorSlug}/1/actor` + : `https://www.${siteSlug}.com/en/search/${siteSlug}/actor/${actorSlug}`; + const searchRes = await bhttp.get(searchUrl); - if (searchRes.statusCode !== 200) { - return null; - } + if (searchRes.statusCode !== 200) { + return null; + } - const actorUrl = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName); + const actorUrl = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName); - if (actorUrl) { - const url = `https://${siteSlug}.com${actorUrl}`; - const actorRes = await bhttp.get(url); + if (actorUrl) { + const url = `https://${siteSlug}.com${actorUrl}`; + const actorRes = await bhttp.get(url); - if (actorRes.statusCode !== 200) { - return null; - } + if (actorRes.statusCode !== 200) { + return null; + } - return scrapeProfile(actorRes.body.toString(), url, actorName, siteSlug, getActorReleasesUrl, include.scenes); - } + return scrapeProfile(actorRes.body.toString(), url, actorName, siteSlug, getActorReleasesUrl, include.scenes); + } - return null; + return null; } async function fetchApiProfile(actorName, siteSlug) { - const actorSlug = encodeURI(actorName); - const referer = `https://www.${siteSlug}.com/en/search`; + const actorSlug = encodeURI(actorName); + const referer = `https://www.${siteSlug}.com/en/search`; - const { apiUrl } = await fetchApiCredentials(referer); + const { apiUrl } = await fetchApiCredentials(referer); - const res = await bhttp.post(apiUrl, { - requests: [ - { - indexName: 'all_actors', - params: `query=${actorSlug}`, - }, - ], - }, { - headers: { - Referer: referer, - }, - encodeJSON: true, - }); + const res = await bhttp.post(apiUrl, { + requests: [ + { + indexName: 'all_actors', + params: `query=${actorSlug}`, + }, + ], + }, { + headers: { + Referer: referer, + }, + encodeJSON: true, + }); - if (res.statusCode === 200 && res.body.results[0].hits.length > 0) { - const actorData = res.body.results[0].hits.find(actor => slugify(actor.name) === slugify(actorName)); + if (res.statusCode === 200 && res.body.results[0].hits.length > 0) { + const actorData = res.body.results[0].hits.find(actor => slugify(actor.name) === slugify(actorName)); - if (actorData) { - const actorScenes = await fetchActorScenes(actorData.name, apiUrl, siteSlug); + if (actorData) { + const actorScenes = await fetchActorScenes(actorData.name, apiUrl, siteSlug); - return scrapeApiProfile(actorData, actorScenes, siteSlug); - } - } + return scrapeApiProfile(actorData, actorScenes, siteSlug); + } + } - return null; + return null; } module.exports = { - fetchApiLatest, - fetchApiProfile, - fetchApiUpcoming, - fetchLatest, - fetchProfile, - fetchScene, - fetchUpcoming, - getPhotos, - scrapeApiProfile, - scrapeApiReleases, - scrapeProfile, - scrapeAll, - scrapeScene, + fetchApiLatest, + fetchApiProfile, + fetchApiUpcoming, + fetchLatest, + fetchProfile, + fetchScene, + fetchUpcoming, + getPhotos, + scrapeApiProfile, + scrapeApiReleases, + scrapeProfile, + scrapeAll, + scrapeScene, }; diff --git a/src/scrapers/girlsway.js b/src/scrapers/girlsway.js index 151bb8458..518aabe40 100644 --- a/src/scrapers/girlsway.js +++ b/src/scrapers/girlsway.js @@ -4,7 +4,7 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchScene, - fetchUpcoming: fetchApiUpcoming, + fetchLatest: fetchApiLatest, + fetchScene, + fetchUpcoming: fetchApiUpcoming, }; diff --git a/src/scrapers/hush.js b/src/scrapers/hush.js index 7cfa8c462..28983710b 100644 --- a/src/scrapers/hush.js +++ b/src/scrapers/hush.js @@ -8,404 +8,403 @@ const slugify = require('../utils/slugify'); const { feetInchesToCm } = require('../utils/convert'); async function getChannelRegExp(site) { - if (!['hushpass', 'interracialpass'].includes(site.network.slug)) return null; + if (!['hushpass', 'interracialpass'].includes(site.network.slug)) return null; - const sites = await knex('sites').where('network_id', site.network.id); + const sites = await knex('sites').where('network_id', site.network.id); - return new RegExp(sites.map(channel => channel.parameters?.match || channel.name).join('|'), 'i'); + return new RegExp(sites.map(channel => channel.parameters?.match || channel.name).join('|'), 'i'); } function deriveEntryId(release) { - if (release.date && release.title) { - return `${slugify(fd(release.date, 'YYYY-MM-DD'))}-${slugify(release.title)}`; - } + if (release.date && release.title) { + return `${slugify(fd(release.date, 'YYYY-MM-DD'))}-${slugify(release.title)}`; + } - return null; + return null; } function extractPoster(posterPath, site, baseRelease) { - if (posterPath && !/400.jpg/.test(posterPath)) { - const poster = `${site.parameters?.media || site.url}${posterPath}`; - const posterSources = [ - poster, - // upscaled - poster.replace('-1x', '-2x'), - poster.replace('-1x', '-3x'), - ]; + if (posterPath && !/400.jpg/.test(posterPath)) { + const poster = `${site.parameters?.media || site.url}${posterPath}`; + const posterSources = [ + poster, + // upscaled + poster.replace('-1x', '-2x'), + poster.replace('-1x', '-3x'), + ]; - if (baseRelease?.poster) { - return [posterSources, [baseRelease.poster]]; - } + if (baseRelease?.poster) { + return [posterSources, [baseRelease.poster]]; + } - return [posterSources, []]; - } + return [posterSources, []]; + } - return [baseRelease?.poster || null, []]; + return [baseRelease?.poster || null, []]; } function getImageWithFallbacks(q, selector, site, el) { - const sources = el - ? [ - q(el, selector, 'src0_3x'), - q(el, selector, 'src0_2x'), - q(el, selector, 'src0_1x'), - ] - : [ - q(selector, 'src0_3x'), - q(selector, 'src0_2x'), - q(selector, 'src0_1x'), - ]; + const sources = el + ? [ + q(el, selector, 'src0_3x'), + q(el, selector, 'src0_2x'), + q(el, selector, 'src0_1x'), + ] + : [ + q(selector, 'src0_3x'), + q(selector, 'src0_2x'), + q(selector, 'src0_1x'), + ]; - return sources.filter(Boolean).map(src => `${site.parameters?.media || site.url}${src}`); + return sources.filter(Boolean).map(src => `${site.parameters?.media || site.url}${src}`); } function scrapeAll(scenes, site) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - release.title = qu.q('h3 a', 'title') || qu.q('h3 a', true); - release.url = qu.url('h3 a'); + release.title = qu.q('h3 a', 'title') || qu.q('h3 a', true); + release.url = qu.url('h3 a'); - release.date = qu.date('.modeldata p', 'YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/); - release.duration = qu.dur('.modeldata p'); + release.date = qu.date('.modeldata p', 'YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/); + release.duration = qu.dur('.modeldata p'); - if (/bts|behind the scenes/i.test(release.title)) release.tags = ['behind the scenes']; + if (/bts|behind the scenes/i.test(release.title)) release.tags = ['behind the scenes']; - release.poster = getImageWithFallbacks(qu.q, '.modelimg img', site); + release.poster = getImageWithFallbacks(qu.q, '.modelimg img', site); - // release.entryId = q('.modelimg img', 'id').match(/set-target-(\d+)/)[1]; - release.entryId = deriveEntryId(release); + // release.entryId = q('.modelimg img', 'id').match(/set-target-(\d+)/)[1]; + release.entryId = deriveEntryId(release); - return release; - }); + return release; + }); } function scrapeAllT1(scenes, site, accSiteReleases) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - release.title = qu.q('h4 a', 'title') || qu.q('h4 a', true); - release.url = qu.url('h4 a'); + release.title = qu.q('h4 a', 'title') || qu.q('h4 a', true); + release.url = qu.url('h4 a'); - release.date = qu.date('.more-info-div', 'MMM D, YYYY'); - release.duration = qu.dur('.more-info-div'); + release.date = qu.date('.more-info-div', 'MMM D, YYYY'); + release.duration = qu.dur('.more-info-div'); - if (/bts|behind the scenes/i.test(release.title)) release.tags = ['behind the scenes']; + if (/bts|behind the scenes/i.test(release.title)) release.tags = ['behind the scenes']; - const posterPath = qu.q('.img-div img', 'src0_1x') || qu.img('img.video_placeholder'); + const posterPath = qu.q('.img-div img', 'src0_1x') || qu.img('img.video_placeholder'); - if (posterPath) { - const poster = /^http/.test(posterPath) ? posterPath : `${site.parameters?.media || site.url}${posterPath}`; + if (posterPath) { + const poster = /^http/.test(posterPath) ? posterPath : `${site.parameters?.media || site.url}${posterPath}`; - release.poster = [ - poster.replace('-1x', '-3x'), - poster.replace('-1x', '-2x'), - poster, - ]; - } + release.poster = [ + poster.replace('-1x', '-3x'), + poster.replace('-1x', '-2x'), + poster, + ]; + } - // release.entryId = q('.img-div img', 'id')?.match(/set-target-(\d+)/)[1]; - release.entryId = deriveEntryId(release); + // release.entryId = q('.img-div img', 'id')?.match(/set-target-(\d+)/)[1]; + release.entryId = deriveEntryId(release); - if (site.parameters?.accFilter && accSiteReleases?.map(accRelease => accRelease.entryId).includes(release.entryId)) { - // filter out releases that were already scraped from a categorized site - return null; - } + if (site.parameters?.accFilter && accSiteReleases?.map(accRelease => accRelease.entryId).includes(release.entryId)) { + // filter out releases that were already scraped from a categorized site + return null; + } - return release; - }).filter(Boolean); + return release; + }).filter(Boolean); } function scrapeAllTour(scenes) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - release.title = qu.q('h4 a', true); - release.url = qu.url('a'); - release.date = qu.date('.tour_update_models + span', 'YYYY-MM-DD'); + release.title = qu.q('h4 a', true); + release.url = qu.url('a'); + release.date = qu.date('.tour_update_models + span', 'YYYY-MM-DD'); - release.actors = qu.all('.tour_update_models a', true); + release.actors = qu.all('.tour_update_models a', true); - release.poster = qu.img('a img'); + release.poster = qu.img('a img'); - release.entryId = deriveEntryId(release); + release.entryId = deriveEntryId(release); - return release; - }); + return release; + }); } function scrapeScene({ html, qu }, site, url, baseRelease) { - const release = { url }; + const release = { url }; - release.title = qu.q('.centerwrap h2', true); - release.description = qu.q('.videocontent p', true); + release.title = qu.q('.centerwrap h2', true); + release.description = qu.q('.videocontent p', true); - release.date = qu.date('.videodetails .date', 'MM/DD/YYYY'); - release.duration = qu.dur('.videodetails .date'); + release.date = qu.date('.videodetails .date', 'MM/DD/YYYY'); + release.duration = qu.dur('.videodetails .date'); - release.actors = qu.all('.modelname a', true); + release.actors = qu.all('.modelname a', true); - const posterPath = html.match(/poster="([\w-/.]+)"/)?.[1]; - [release.poster, release.photos] = extractPoster(posterPath, site, baseRelease); + const posterPath = html.match(/poster="([\w-/.]+)"/)?.[1]; + [release.poster, release.photos] = extractPoster(posterPath, site, baseRelease); - const trailerPath = html.match(/\/trailers\/.*.mp4/); - if (trailerPath) release.trailer = { src: `${site.parameters?.media || site.url}${trailerPath}` }; + const trailerPath = html.match(/\/trailers\/.*.mp4/); + if (trailerPath) release.trailer = { src: `${site.parameters?.media || site.url}${trailerPath}` }; - const stars = qu.q('.modelrates + p', true).match(/\d.\d/)?.[0]; - if (stars) release.stars = Number(stars); + const stars = qu.q('.modelrates + p', true).match(/\d.\d/)?.[0]; + if (stars) release.stars = Number(stars); - // release.entryId = html.match(/set-target-(\d+)/)[1]; - release.entryId = deriveEntryId(release); + // release.entryId = html.match(/set-target-(\d+)/)[1]; + release.entryId = deriveEntryId(release); - return release; + return release; } function scrapeSceneT1({ html, qu }, site, url, baseRelease, channelRegExp) { - const release = { url }; + const release = { url }; - release.title = qu.q('.trailer-section-head .section-title', true); - release.description = qu.text('.row .update-info-block'); + release.title = qu.q('.trailer-section-head .section-title', true); + release.description = qu.text('.row .update-info-block'); - release.date = qu.date('.update-info-row', 'MMM D, YYYY', /\w+ \d{1,2}, \d{4}/); - release.duration = qu.dur('.update-info-row:nth-child(2)'); + release.date = qu.date('.update-info-row', 'MMM D, YYYY', /\w+ \d{1,2}, \d{4}/); + release.duration = qu.dur('.update-info-row:nth-child(2)'); - release.actors = qu.all('.models-list-thumbs a').map(el => ({ - name: qu.q(el, 'span', true), - avatar: getImageWithFallbacks(qu.q, 'img', site, el), - })); + release.actors = qu.all('.models-list-thumbs a').map(el => ({ + name: qu.q(el, 'span', true), + avatar: getImageWithFallbacks(qu.q, 'img', site, el), + })); - release.tags = qu.all('.tags a', true); + release.tags = qu.all('.tags a', true); - // const posterPath = html.match(/poster="(.*\.jpg)/)?.[1]; - const posterPath = qu.q('.player-thumb img', 'src0_1x'); - [release.poster, release.photos] = extractPoster(posterPath, site, baseRelease); + // const posterPath = html.match(/poster="(.*\.jpg)/)?.[1]; + const posterPath = qu.q('.player-thumb img', 'src0_1x'); + [release.poster, release.photos] = extractPoster(posterPath, site, baseRelease); - const trailer = html.match(/ channelRegExp.test(tag)); + if (channelRegExp) { + const channel = release.tags.find(tag => channelRegExp.test(tag)); - if (channel) { - release.channel = { - force: true, - slug: slugify(channel, ''), - }; - } - } + if (channel) { + release.channel = { + force: true, + slug: slugify(channel, ''), + }; + } + } - // release.entryId = q('.player-thumb img', 'id')?.match(/set-target-(\d+)/)[1]; - release.entryId = deriveEntryId(release); + // release.entryId = q('.player-thumb img', 'id')?.match(/set-target-(\d+)/)[1]; + release.entryId = deriveEntryId(release); - return release; + return release; } function scrapeSceneTour({ html, qu }, site, url) { - const release = {}; + const release = {}; - if (url) release.url = url; - release.title = qu.q('.update_title, .video-title', true); - release.description = qu.q('.latest_update_description, .video-summary', true); + if (url) release.url = url; + release.title = qu.q('.update_title, .video-title', true); + release.description = qu.q('.latest_update_description, .video-summary', true); - const date = qu.date('.availdate, .update_date', 'YYYY-MM-DD'); - if (date) release.date = date; + const date = qu.date('.availdate, .update_date', 'YYYY-MM-DD'); + if (date) release.date = date; - release.actors = qu.all('.update_block_info .tour_update_models a, .video-model .tour_update_models a', true); - release.tags = qu.all('.update_tags a, .tour_update_tags a', true); + release.actors = qu.all('.update_block_info .tour_update_models a, .video-model .tour_update_models a', true); + release.tags = qu.all('.update_tags a, .tour_update_tags a', true); - const [photo, poster, ...photos] = qu.imgs('.update_image img:not(.play_icon_overlay)'); - if (poster || photo) release.poster = poster || photo; - if ((photo && poster) || photos) release.photos = poster ? [photo, ...photos] : photos; // don't use first photo when already used as fallback poster + const [photo, poster, ...photos] = qu.imgs('.update_image img:not(.play_icon_overlay)'); + if (poster || photo) release.poster = poster || photo; + if ((photo && poster) || photos) release.photos = poster ? [photo, ...photos] : photos; // don't use first photo when already used as fallback poster - if (release.date) release.entryId = deriveEntryId(release); + if (release.date) release.entryId = deriveEntryId(release); - const trailerCode = qu.q('.update_image a', 'onclick'); - const trailerPath = trailerCode?.match(/tload\('(.*)'\)/)?.[1] || html.match(/\/trailer\/.*\.mp4/)?.[0]; - if (trailerPath && /^http/.test(trailerPath)) release.trailer = { src: trailerPath }; - else if (trailerPath) release.trailer = { src: `${site.parameters?.media || site.url}${trailerPath}` }; + const trailerCode = qu.q('.update_image a', 'onclick'); + const trailerPath = trailerCode?.match(/tload\('(.*)'\)/)?.[1] || html.match(/\/trailer\/.*\.mp4/)?.[0]; + if (trailerPath && /^http/.test(trailerPath)) release.trailer = { src: trailerPath }; + else if (trailerPath) release.trailer = { src: `${site.parameters?.media || site.url}${trailerPath}` }; - return release; + return release; } function scrapeProfile({ el, qu }, site) { - const profile = {}; + const profile = {}; - const bio = qu.texts('.stats p').reduce((acc, info) => { - const [key, value] = info.split(':'); + const bio = qu.texts('.stats p').reduce((acc, info) => { + const [key, value] = info.split(':'); - return { - ...acc, - [slugify(key, '_')]: value.trim(), - }; - }, {}); + return { + ...acc, + [slugify(key, '_')]: value.trim(), + }; + }, {}); - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); - if (bust) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - if (bio.age) profile.age = Number(bio.age); - if (bio.height) profile.height = feetInchesToCm(bio.height); + if (bio.age) profile.age = Number(bio.age); + if (bio.height) profile.height = feetInchesToCm(bio.height); - profile.avatar = getImageWithFallbacks(qu.q, '.profileimg img', site); + profile.avatar = getImageWithFallbacks(qu.q, '.profileimg img', site); - const qReleases = ctxa(el, '.modelFeatures .modelfeature'); - profile.releases = scrapeAll(qReleases, site); + const qReleases = ctxa(el, '.modelFeatures .modelfeature'); + profile.releases = scrapeAll(qReleases, site); - return profile; + return profile; } function scrapeProfileT1({ el, qu }, site) { - const profile = {}; + const profile = {}; - const bio = qu.all('.detail-div + .detail-div p, .detail-div p', true).reduce((acc, info) => { - const [key, value] = info.split(':'); + const bio = qu.all('.detail-div + .detail-div p, .detail-div p', true).reduce((acc, info) => { + const [key, value] = info.split(':'); - if (!value) return acc; + if (!value) return acc; - return { - ...acc, - [slugify(key, '_')]: value.trim(), - }; - }, {}); + return { + ...acc, + [slugify(key, '_')]: value.trim(), + }; + }, {}); - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); - if (bust) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - if (bio.fun_fact) profile.description = bio.fun_fact; - if (bio.age) profile.age = Number(bio.age); + if (bio.fun_fact) profile.description = bio.fun_fact; + if (bio.age) profile.age = Number(bio.age); - const heightMetric = bio.height?.match(/(\d{3})(\b|c)/); - const heightImperial = bio.height?.match(/\d{1}(\.\d)?/g); - if (heightMetric) profile.height = Number(heightMetric[1]); - if (heightImperial) profile.height = feetInchesToCm(Number(heightImperial[0]), Number(heightImperial[1])); + const heightMetric = bio.height?.match(/(\d{3})(\b|c)/); + const heightImperial = bio.height?.match(/\d{1}(\.\d)?/g); + if (heightMetric) profile.height = Number(heightMetric[1]); + if (heightImperial) profile.height = feetInchesToCm(Number(heightImperial[0]), Number(heightImperial[1])); - profile.avatar = getImageWithFallbacks(qu.q, '.img-div img', site); + profile.avatar = getImageWithFallbacks(qu.q, '.img-div img', site); - const qReleases = ctxa(el, '.item-video'); - profile.releases = scrapeAllT1(qReleases, site); + const qReleases = ctxa(el, '.item-video'); + profile.releases = scrapeAllT1(qReleases, site); - return profile; + return profile; } function scrapeProfileTour({ el, qu }, site) { - const profile = {}; + const profile = {}; - const bio = qu.texts('.model_bio').reduce((acc, info) => { - const [key, value] = info.split(':'); + const bio = qu.texts('.model_bio').reduce((acc, info) => { + const [key, value] = info.split(':'); - return { - ...acc, - [slugify(key, '_')]: value.trim(), - }; - }, {}); + return { + ...acc, + [slugify(key, '_')]: value.trim(), + }; + }, {}); - if (bio.date_of_birth) profile.birthdate = ed(bio.date_of_birth, 'MMMM D, YYYY'); - if (bio.birthplace) profile.birthPlace = bio.birthplace; - if (bio.fun_fact) profile.description = bio.fun_fact; + if (bio.date_of_birth) profile.birthdate = ed(bio.date_of_birth, 'MMMM D, YYYY'); + if (bio.birthplace) profile.birthPlace = bio.birthplace; + if (bio.fun_fact) profile.description = bio.fun_fact; - if (bio.ethnicity) profile.ethnicity = bio.ethnicity; + if (bio.ethnicity) profile.ethnicity = bio.ethnicity; - if (bio.height) profile.height = Number(bio.height.match(/^\d{2,3}/)?.[0]); - if (bio.weight) profile.weight = Number(bio.weight.match(/^\d{2,3}/)?.[0]); + if (bio.height) profile.height = Number(bio.height.match(/^\d{2,3}/)?.[0]); + if (bio.weight) profile.weight = Number(bio.weight.match(/^\d{2,3}/)?.[0]); - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); - if (bust) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - if (bio.natural_breasts && /yes/i.test(bio.natural_breasts)) profile.naturalBoobs = true; - if (bio.natural_breasts && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false; + if (bio.natural_breasts && /yes/i.test(bio.natural_breasts)) profile.naturalBoobs = true; + if (bio.natural_breasts && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false; - if (bio.tattoos && /yes/i.test(bio.tattoos)) profile.hasTattoos = true; - if (bio.tattoos && /no/i.test(bio.tattoos)) profile.hasTattoos = false; - if (bio.piercings && /yes/i.test(bio.piercings)) profile.hasPiercings = true; - if (bio.piercings && /no/i.test(bio.piercings)) profile.hasPiercings = false; + if (bio.tattoos && /yes/i.test(bio.tattoos)) profile.hasTattoos = true; + if (bio.tattoos && /no/i.test(bio.tattoos)) profile.hasTattoos = false; + if (bio.piercings && /yes/i.test(bio.piercings)) profile.hasPiercings = true; + if (bio.piercings && /no/i.test(bio.piercings)) profile.hasPiercings = false; - if (bio.aliases) profile.aliases = bio.aliases.split(',').map(alias => alias.trim()); + if (bio.aliases) profile.aliases = bio.aliases.split(',').map(alias => alias.trim()); - profile.avatar = getImageWithFallbacks(qu.q, '.model_picture img', site); + profile.avatar = getImageWithFallbacks(qu.q, '.model_picture img', site); - const qReleases = ctxa(el, '.update_block'); - profile.releases = qReleases.map((qRelease) => { - const url = qRelease.qu.url('.update_image a[href]'); - const release = scrapeSceneTour(qRelease, site); + const qReleases = ctxa(el, '.update_block'); + profile.releases = qReleases.map((qRelease) => { + const url = qRelease.qu.url('.update_image a[href]'); + const release = scrapeSceneTour(qRelease, site); - if (!/\/(signup|join)/i.test(url)) release.url = url; - release.entryId = deriveEntryId(release); - release.site = site; + if (!/\/(signup|join)/i.test(url)) release.url = url; + release.entryId = deriveEntryId(release); + release.site = site; - return release; - }); + return release; + }); - return profile; + return profile; } async function fetchLatest(site, page = 1, _beforeFetchLatest, accSiteReleases) { - const url = (site.parameters?.latest && util.format(site.parameters.latest, page)) + const url = (site.parameters?.latest && util.format(site.parameters.latest, page)) || (site.parameters?.t1 && `${site.url}/t1/categories/movies_${page}_d.html`) || `${site.url}/categories/movies_${page}_d.html`; - const res = await geta(url, '.modelfeature, .item-video, .updateItem'); + const res = await geta(url, '.modelfeature, .item-video, .updateItem'); - if (!res.ok) return res.status; - if (site.parameters?.t1) return scrapeAllT1(res.items, site, accSiteReleases); - if (site.parameters?.tour) return scrapeAllTour(res.items, site, accSiteReleases); + if (!res.ok) return res.status; + if (site.parameters?.t1) return scrapeAllT1(res.items, site, accSiteReleases); + if (site.parameters?.tour) return scrapeAllTour(res.items, site, accSiteReleases); - return scrapeAll(res.items, site, accSiteReleases); + return scrapeAll(res.items, site, accSiteReleases); } async function fetchScene(url, site, baseRelease, beforeFetchLatest) { - const channelRegExp = beforeFetchLatest || await getChannelRegExp(site); - const res = await get(url); + const channelRegExp = beforeFetchLatest || await getChannelRegExp(site); + const res = await get(url); - if (!res.ok) return res.status; - if (site.parameters?.t1) return scrapeSceneT1(res.item, site, url, baseRelease, channelRegExp); - if (site.parameters?.tour) return scrapeSceneTour(res.item, site, url, baseRelease); + if (!res.ok) return res.status; + if (site.parameters?.t1) return scrapeSceneT1(res.item, site, url, baseRelease, channelRegExp); + if (site.parameters?.tour) return scrapeSceneTour(res.item, site, url, baseRelease); - return scrapeScene(res.item, site, url, baseRelease); + return scrapeScene(res.item, site, url, baseRelease); } async function fetchProfile(actorName, scraperSlug, site) { - const actorSlugA = slugify(actorName, ''); - const actorSlugB = slugify(actorName); + const actorSlugA = slugify(actorName, ''); + const actorSlugB = slugify(actorName); - const t1 = site.parameters?.t1 ? 't1/' : ''; + const t1 = site.parameters?.t1 ? 't1/' : ''; - const res1 = site.parameters?.profile - ? await get(util.format(site.parameters.profile, actorSlugA)) - : await get(`${site.url}/${t1}models/${actorSlugA}.html`); + const res1 = site.parameters?.profile + ? await get(util.format(site.parameters.profile, actorSlugA)) + : await get(`${site.url}/${t1}models/${actorSlugA}.html`); - const res = (res1.ok && res1) - || (site.parameters?.profile - ? await get(util.format(site.parameters.profile, actorSlugB)) - : await get(`${site.url}/${t1}models/${actorSlugB}.html`)); + const res = (res1.ok && res1) + || (site.parameters?.profile && await get(util.format(site.parameters.profile, actorSlugB))) + || await get(`${site.url}/${t1}models/${actorSlugB}.html`); - if (!res.ok) return res.status; - if (site.parameters?.t1) return scrapeProfileT1(res.item, site); - if (site.parameters?.tour) return scrapeProfileTour(res.item, site); + if (!res.ok) return res.status; + if (site.parameters?.t1) return scrapeProfileT1(res.item, site); + if (site.parameters?.tour) return scrapeProfileTour(res.item, site); - return scrapeProfile(res.item, site); + return scrapeProfile(res.item, site); } module.exports = { - beforeFetchLatest: getChannelRegExp, - fetchLatest, - fetchScene, - fetchProfile, + beforeFetchLatest: getChannelRegExp, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/iconmale.js b/src/scrapers/iconmale.js index a41736a9f..dea8600ea 100644 --- a/src/scrapers/iconmale.js +++ b/src/scrapers/iconmale.js @@ -3,9 +3,9 @@ const { fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'iconmale'); + return fetchProfile(actorName, 'iconmale'); } module.exports = { - fetchProfile: networkFetchProfile, + fetchProfile: networkFetchProfile, }; diff --git a/src/scrapers/insex.js b/src/scrapers/insex.js index 713e206a6..4aec50bc6 100644 --- a/src/scrapers/insex.js +++ b/src/scrapers/insex.js @@ -4,104 +4,104 @@ const bhttp = require('bhttp'); const { get, exa, ed } = require('../utils/q'); function scrapeLatest(html, site) { - const scenes = site.slug === 'paintoy' - ? exa(html, '#articleTable table[cellspacing="2"]') - : exa(html, 'body > table'); + const scenes = site.slug === 'paintoy' + ? exa(html, '#articleTable table[cellspacing="2"]') + : exa(html, 'body > table'); - return scenes.map(({ qu }) => { - // if (q('.articleTitleText')) return scrapeFirstLatest(ctx(el), site); - const release = {}; + return scenes.map(({ qu }) => { + // if (q('.articleTitleText')) return scrapeFirstLatest(ctx(el), site); + const release = {}; - const titleEl = qu.q('.galleryTitleText, .articleTitleText'); - const [title, ...actors] = titleEl.textContent.split('|'); - const date = qu.date('.articlePostDateText td', 'MMM D, YYYY'); + const titleEl = qu.q('.galleryTitleText, .articleTitleText'); + const [title, ...actors] = titleEl.textContent.split('|'); + const date = qu.date('.articlePostDateText td', 'MMM D, YYYY'); - const url = qu.url(titleEl, 'a'); - [release.entryId] = url.split('/').slice(-2); - release.url = `${site.url}${url}`; + const url = qu.url(titleEl, 'a'); + [release.entryId] = url.split('/').slice(-2); + release.url = `${site.url}${url}`; - if (date) { - release.title = title.trim(); - release.date = date; - } else { - // title should contain date instead, not applicable in brief mode - release.title = title.slice(title.indexOf(':') + 1).trim(); - release.date = ed(title.slice(0, title.indexOf(':')), 'MMM D, YYYY'); - } + if (date) { + release.title = title.trim(); + release.date = date; + } else { + // title should contain date instead, not applicable in brief mode + release.title = title.slice(title.indexOf(':') + 1).trim(); + release.date = ed(title.slice(0, title.indexOf(':')), 'MMM D, YYYY'); + } - release.actors = actors.map(actor => actor.trim()); + release.actors = actors.map(actor => actor.trim()); - const description = qu.q('.articleCopyText', true); - if (description) release.description = description.slice(0, description.lastIndexOf('(')); + const description = qu.q('.articleCopyText', true); + if (description) release.description = description.slice(0, description.lastIndexOf('(')); - const duration = qu.dur('.articleCopyText a:nth-child(2)'); - if (duration) release.duration = duration; + const duration = qu.dur('.articleCopyText a:nth-child(2)'); + if (duration) release.duration = duration; - release.likes = parseInt(qu.q('.articlePostDateText td:nth-child(3)', true), 10); + release.likes = parseInt(qu.q('.articlePostDateText td:nth-child(3)', true), 10); - const cover = qu.img('a img'); - release.covers = [[ - cover.replace('_thumbnail', ''), - cover, - ]]; + const cover = qu.img('a img'); + release.covers = [[ + cover.replace('_thumbnail', ''), + cover, + ]]; - return release; - }); + return release; + }); } function scrapeScene({ qu }, site) { - const release = {}; + const release = {}; - const titleEl = qu.q('.articleTitleText'); - const [title, ...actors] = titleEl.textContent.split('|'); + const titleEl = qu.q('.articleTitleText'); + const [title, ...actors] = titleEl.textContent.split('|'); - const url = qu.url(titleEl, 'a'); - [release.entryId] = url.split('/').slice(-2); - release.url = `${site.url}${url}`; + const url = qu.url(titleEl, 'a'); + [release.entryId] = url.split('/').slice(-2); + release.url = `${site.url}${url}`; - release.title = title.trim(); - release.description = qu.q('.articleCopyText', true); + release.title = title.trim(); + release.description = qu.q('.articleCopyText', true); - release.actors = actors.map(actor => actor.trim()); - release.date = qu.date('.articlePostDateText', 'MMMM D, YYYY'); - release.duration = qu.dur('.articlePostDateText a:nth-child(2)'); + release.actors = actors.map(actor => actor.trim()); + release.date = qu.date('.articlePostDateText', 'MMMM D, YYYY'); + release.duration = qu.dur('.articlePostDateText a:nth-child(2)'); - const [cover, ...photos] = qu.imgs('img[src*="images"]'); - release.covers = [cover]; - release.photos = photos; + const [cover, ...photos] = qu.imgs('img[src*="images"]'); + release.covers = [cover]; + release.photos = photos; - release.poster = qu.poster(); + release.poster = qu.poster(); - const trailer = qu.trailer(); - if (trailer) release.trailer = { src: trailer }; + const trailer = qu.trailer(); + if (trailer) release.trailer = { src: trailer }; - return release; + return release; } async function fetchLatest(site, page = 1) { - const url = site.slug === 'paintoy' // paintoy's site is partially broken, use front page - ? `${site.url}/corporal/punishment/gallery.php?type=brief&page=${page}` - : `${site.url}/scripts/switch_tour.php?type=brief&page=${page}`; + const url = site.slug === 'paintoy' // paintoy's site is partially broken, use front page + ? `${site.url}/corporal/punishment/gallery.php?type=brief&page=${page}` + : `${site.url}/scripts/switch_tour.php?type=brief&page=${page}`; - const res = await bhttp.get(url, { - type: 'brief', - page, - }); + const res = await bhttp.get(url, { + type: 'brief', + page, + }); - if (res.statusCode === 200) { - return scrapeLatest(site.slug === 'paintoy' ? res.body.toString() : res.body.html, site); - } + if (res.statusCode === 200) { + return scrapeLatest(site.slug === 'paintoy' ? res.body.toString() : res.body.html, site); + } - return null; + return null; } async function fetchScene(url, site) { - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeScene(res.item, site) : res.status; + return res.ok ? scrapeScene(res.item, site) : res.status; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/jayrock.js b/src/scrapers/jayrock.js index d3dba53ba..d492927e7 100644 --- a/src/scrapers/jayrock.js +++ b/src/scrapers/jayrock.js @@ -9,116 +9,116 @@ const slugify = require('../utils/slugify'); const { fetchApiLatest, fetchScene } = require('./gamma'); async function fetchToken(site) { - const res = await bhttp.get(site.url); - const html = res.body.toString(); + const res = await bhttp.get(site.url); + const html = res.body.toString(); - const time = html.match(/"aet":\d+/)[0].split(':')[1]; - const ah = html.match(/"ah":"[\w-]+"/)[0].split(':')[1].slice(1, -1); - const token = ah.split('').reverse().join(''); + const time = html.match(/"aet":\d+/)[0].split(':')[1]; + const ah = html.match(/"ah":"[\w-]+"/)[0].split(':')[1].slice(1, -1); + const token = ah.split('').reverse().join(''); - return { time, token }; + return { time, token }; } async function fetchActors(entryId, site, { token, time }) { - const url = `${site.url}/sapi/${token}/${time}/model.getModelContent?_method=model.getModelContent&tz=1&fields[0]=modelId.stageName&fields[1]=_last&fields[2]=modelId.upsellLink&fields[3]=modelId.upsellText&limit=25&transitParameters[contentId]=${entryId}`; - const res = await bhttp.get(url); + const url = `${site.url}/sapi/${token}/${time}/model.getModelContent?_method=model.getModelContent&tz=1&fields[0]=modelId.stageName&fields[1]=_last&fields[2]=modelId.upsellLink&fields[3]=modelId.upsellText&limit=25&transitParameters[contentId]=${entryId}`; + const res = await bhttp.get(url); - if (res.statusCode === 200 && res.body.status === true) { - return Object.values(res.body.response.collection).map(actor => Object.values(actor.modelId.collection)[0].stageName); - } + if (res.statusCode === 200 && res.body.status === true) { + return Object.values(res.body.response.collection).map(actor => Object.values(actor.modelId.collection)[0].stageName); + } - return []; + return []; } async function fetchTrailerLocation(entryId, site) { - const url = `${site.url}/api/download/${entryId}/hd1080/stream`; + const url = `${site.url}/api/download/${entryId}/hd1080/stream`; - try { - const res = await bhttp.get(url, { - followRedirects: false, - }); + try { + const res = await bhttp.get(url, { + followRedirects: false, + }); - if (res.statusCode === 302) { - return res.headers.location; - } - } catch (error) { - logger.warn(`${site.name}: Unable to fetch trailer at '${url}': ${error.message}`); - } + if (res.statusCode === 302) { + return res.headers.location; + } + } catch (error) { + logger.warn(`${site.name}: Unable to fetch trailer at '${url}': ${error.message}`); + } - return null; + return null; } async function scrapeScene(scene, site, tokens) { - const release = { - entryId: scene.id, - title: scene.title, - duration: scene.length, - site, - meta: { - tokens, // attach tokens to reduce number of requests required for deep fetching - }, - }; + const release = { + entryId: scene.id, + title: scene.title, + duration: scene.length, + site, + meta: { + tokens, // attach tokens to reduce number of requests required for deep fetching + }, + }; - release.url = `${site.url}/scene/${release.entryId}/${slugify(release.title, { encode: true })}`; - release.date = new Date(scene.sites.collection[scene.id].publishDate); - release.poster = scene._resources.primary[0].url; + release.url = `${site.url}/scene/${release.entryId}/${slugify(release.title, { encode: true })}`; + release.date = new Date(scene.sites.collection[scene.id].publishDate); + release.poster = scene._resources.primary[0].url; - if (scene.tags) release.tags = Object.values(scene.tags.collection).map(tag => tag.alias); - if (scene._resources.base) release.photos = scene._resources.base.map(resource => resource.url); + if (scene.tags) release.tags = Object.values(scene.tags.collection).map(tag => tag.alias); + if (scene._resources.base) release.photos = scene._resources.base.map(resource => resource.url); - const [actors, trailer] = await Promise.all([ - fetchActors(release.entryId, site, tokens), - fetchTrailerLocation(release.entryId, site), - ]); + const [actors, trailer] = await Promise.all([ + fetchActors(release.entryId, site, tokens), + fetchTrailerLocation(release.entryId, site), + ]); - release.actors = actors; - if (trailer) release.trailer = { src: trailer, quality: 1080 }; + release.actors = actors; + if (trailer) release.trailer = { src: trailer, quality: 1080 }; - return release; + return release; } function scrapeLatest(scenes, site, tokens) { - return Promise.map(scenes, async scene => scrapeScene(scene, site, tokens), { concurrency: 10 }); + return Promise.map(scenes, async scene => scrapeScene(scene, site, tokens), { concurrency: 10 }); } async function fetchLatest(site, page = 1) { - if (site.parameters?.useGamma) { - return fetchApiLatest(site, page); - } + if (site.parameters?.useGamma) { + return fetchApiLatest(site, page); + } - const { time, token } = await fetchToken(site); + const { time, token } = await fetchToken(site); - // transParameters[v1] includes _resources, [v2] includes photos, [preset] is mandatory - const url = `${site.url}/sapi/${token}/${time}/content.load?limit=50&offset=${(page - 1) * 50}&transitParameters[v1]=OhUOlmasXD&transitParameters[v2]=OhUOlmasXD&transitParameters[preset]=videos`; - const res = await bhttp.get(url); + // transParameters[v1] includes _resources, [v2] includes photos, [preset] is mandatory + const url = `${site.url}/sapi/${token}/${time}/content.load?limit=50&offset=${(page - 1) * 50}&transitParameters[v1]=OhUOlmasXD&transitParameters[v2]=OhUOlmasXD&transitParameters[preset]=videos`; + const res = await bhttp.get(url); - if (res.statusCode === 200 && res.body.status) { - return scrapeLatest(res.body.response.collection, site, { time, token }); - } + if (res.statusCode === 200 && res.body.status) { + return scrapeLatest(res.body.response.collection, site, { time, token }); + } - return null; + return null; } async function fetchNetworkScene(url, site, release) { - if (site.parameters?.useGamma) { - return fetchScene(url, site, release); - } + if (site.parameters?.useGamma) { + return fetchScene(url, site, release); + } - const { time, token } = release?.meta.tokens || await fetchToken(site); // use attached tokens when deep fetching - const { pathname } = new URL(url); - const entryId = pathname.split('/')[2]; + const { time, token } = release?.meta.tokens || await fetchToken(site); // use attached tokens when deep fetching + const { pathname } = new URL(url); + const entryId = pathname.split('/')[2]; - const apiUrl = `${site.url}/sapi/${token}/${time}/content.load?filter[id][fields][0]=id&filter[id][values][0]=${entryId}&transitParameters[v1]=ykYa8ALmUD&transitParameters[preset]=scene`; - const res = await bhttp.get(apiUrl); + const apiUrl = `${site.url}/sapi/${token}/${time}/content.load?filter[id][fields][0]=id&filter[id][values][0]=${entryId}&transitParameters[v1]=ykYa8ALmUD&transitParameters[preset]=scene`; + const res = await bhttp.get(apiUrl); - if (res.statusCode === 200 && res.body.status) { - return scrapeScene(res.body.response.collection[0], site, { time, token }); - } + if (res.statusCode === 200 && res.body.status) { + return scrapeScene(res.body.response.collection[0], site, { time, token }); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene: fetchNetworkScene, + fetchLatest, + fetchScene: fetchNetworkScene, }; diff --git a/src/scrapers/jesseloadsmonsterfacials.js b/src/scrapers/jesseloadsmonsterfacials.js index 622f9df16..19a49155d 100644 --- a/src/scrapers/jesseloadsmonsterfacials.js +++ b/src/scrapers/jesseloadsmonsterfacials.js @@ -3,83 +3,83 @@ const { get, initAll } = require('../utils/qu'); function scrapeLatest(scenes, dates, site) { - return scenes.map(({ qu }, index) => { - const release = {}; + return scenes.map(({ qu }, index) => { + const release = {}; - const path = qu.url('a'); - release.url = `${site.url}/visitors/${path}`; - release.entryId = path.match(/videos\/([a-zA-Z0-9]+)(?:_hd)?_trailer/)?.[1]; + const path = qu.url('a'); + release.url = `${site.url}/visitors/${path}`; + release.entryId = path.match(/videos\/([a-zA-Z0-9]+)(?:_hd)?_trailer/)?.[1]; - if (dates && dates[index]) { - release.date = dates[index].qu.date(null, 'MM/DD/YYYY'); - } + if (dates && dates[index]) { + release.date = dates[index].qu.date(null, 'MM/DD/YYYY'); + } - release.description = qu.q('tbody tr:nth-child(3) font', true); + release.description = qu.q('tbody tr:nth-child(3) font', true); - const infoLine = qu.q('font[color="#663366"]', true); - if (infoLine) release.duration = Number(infoLine.match(/(\d+) min/)[1]) * 60; + const infoLine = qu.q('font[color="#663366"]', true); + if (infoLine) release.duration = Number(infoLine.match(/(\d+) min/)[1]) * 60; - const poster = qu.img('img[src*="photos/"][width="400"]'); - release.poster = `${site.url}/visitors/${poster}`; - release.photos = qu.imgs('img[src*="photos/"]:not([width="400"])').map(source => `${site.url}/visitors/${source}`); + const poster = qu.img('img[src*="photos/"][width="400"]'); + release.poster = `${site.url}/visitors/${poster}`; + release.photos = qu.imgs('img[src*="photos/"]:not([width="400"])').map(source => `${site.url}/visitors/${source}`); - return release; - }); + return release; + }); } function scrapeScene({ qu }, url, site) { - const release = { url }; + const release = { url }; - const { pathname } = new URL(url); - release.entryId = pathname.match(/videos\/(\w+)_hd_trailer/)[1]; + const { pathname } = new URL(url); + release.entryId = pathname.match(/videos\/(\w+)_hd_trailer/)[1]; - const actor = qu.q('font[color="#990033"] strong', true); - release.actors = [actor]; + const actor = qu.q('font[color="#990033"] strong', true); + release.actors = [actor]; - const hdTrailer = qu.url('a[href*="hd_trailer.mp4"]'); - const sdTrailer = qu.url('a[href*="hd_trailer_mobile.mp4"]'); + const hdTrailer = qu.url('a[href*="hd_trailer.mp4"]'); + const sdTrailer = qu.url('a[href*="hd_trailer_mobile.mp4"]'); - release.trailer = [ - { - src: `${site.url}/visitors/videos/${hdTrailer}`, - quality: 1080, - }, - { - src: `${site.url}/visitors/videos/${sdTrailer}`, - quality: 270, - }, - ]; + release.trailer = [ + { + src: `${site.url}/visitors/videos/${hdTrailer}`, + quality: 1080, + }, + { + src: `${site.url}/visitors/videos/${sdTrailer}`, + quality: 270, + }, + ]; - return release; + return release; } async function fetchLatest(site, page = 1) { - const url = `https://jesseloadsmonsterfacials.com/visitors/tour_${page.toString().padStart(2, '0')}.html`; - const res = await get(url); + const url = `https://jesseloadsmonsterfacials.com/visitors/tour_${page.toString().padStart(2, '0')}.html`; + const res = await get(url); - if (!res.ok) { - return res.status; - } + if (!res.ok) { + return res.status; + } - const { el } = res.item; + const { el } = res.item; - const scenes = initAll(el, 'table[width="880"]'); - const dates = initAll(el, 'font[color="#000000"] strong:not(:empty)'); + const scenes = initAll(el, 'table[width="880"]'); + const dates = initAll(el, 'font[color="#000000"] strong:not(:empty)'); - return scrapeLatest(scenes, dates, site); + return scrapeLatest(scenes, dates, site); } async function fetchScene(url, site) { - const res = await get(url); + const res = await get(url); - if (res.ok) { - return scrapeScene(res.item, url, site); - } + if (res.ok) { + return scrapeScene(res.item, url, site); + } - return res.status; + return res.status; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/julesjordan.js b/src/scrapers/julesjordan.js index da44f0513..3b83daa2a 100644 --- a/src/scrapers/julesjordan.js +++ b/src/scrapers/julesjordan.js @@ -13,406 +13,406 @@ const { heightToCm } = require('../utils/convert'); const slugify = require('../utils/slugify'); async function fetchPhotos(url) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - return res.body.toString(); + return res.body.toString(); } function scrapePhotos(html, type) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); + const $ = cheerio.load(html, { normalizeWhitespace: true }); - const photos = $('.photo_gallery_thumbnail_wrapper .thumbs') - .toArray() - .map((photoElement) => { - const src = $(photoElement).attr('src'); + const photos = $('.photo_gallery_thumbnail_wrapper .thumbs') + .toArray() + .map((photoElement) => { + const src = $(photoElement).attr('src'); - // high res often available in alternative directories, but not always, provide original as fallback - if (type === 'caps') { - return [ - src.replace('capthumbs/', 'caps/'), - src, - ]; - } + // high res often available in alternative directories, but not always, provide original as fallback + if (type === 'caps') { + return [ + src.replace('capthumbs/', 'caps/'), + src, + ]; + } - return [ - src.replace('thumbs/', 'photos/'), - src.replace('thumbs/', '1600watermarked/'), - src.replace('thumbs/', '1280watermarked/'), - src.replace('thumbs/', '1024watermarked/'), - src, - ]; - }); + return [ + src.replace('thumbs/', 'photos/'), + src.replace('thumbs/', '1600watermarked/'), + src.replace('thumbs/', '1280watermarked/'), + src.replace('thumbs/', '1024watermarked/'), + src, + ]; + }); - return photos; + return photos; } async function getPhotosLegacy(entryId, site, type = 'highres', page = 1) { - const albumUrl = `${site.url}/trial/gallery.php?id=${entryId}&type=${type}&page=${page}`; + const albumUrl = `${site.url}/trial/gallery.php?id=${entryId}&type=${type}&page=${page}`; - logger.warn(`Jules Jordan is using legacy photo scraper for ${albumUrl} (page ${page})`); + logger.warn(`Jules Jordan is using legacy photo scraper for ${albumUrl} (page ${page})`); - const html = await fetchPhotos(albumUrl); - const $ = cheerio.load(html, { normalizeWhitespace: true }); + const html = await fetchPhotos(albumUrl); + const $ = cheerio.load(html, { normalizeWhitespace: true }); - // don't add first URL to pages to prevent unnecessary duplicate request - const photos = scrapePhotos(html, type); - const pages = Array.from(new Set($('.page_numbers a').toArray().map(el => $(el).attr('href')))); + // don't add first URL to pages to prevent unnecessary duplicate request + const photos = scrapePhotos(html, type); + const pages = Array.from(new Set($('.page_numbers a').toArray().map(el => $(el).attr('href')))); - const otherPhotos = pages - ? await Promise.map(pages, async (pageX) => { - const pageUrl = `https://www.julesjordan.com/trial/${pageX}`; - const pageHtml = await fetchPhotos(pageUrl); + const otherPhotos = pages + ? await Promise.map(pages, async (pageX) => { + const pageUrl = `https://www.julesjordan.com/trial/${pageX}`; + const pageHtml = await fetchPhotos(pageUrl); - return scrapePhotos(pageHtml, type); - }, { - concurrency: 2, - }) - : []; + return scrapePhotos(pageHtml, type); + }, { + concurrency: 2, + }) + : []; - const allPhotos = photos.concat(otherPhotos.flat()); + const allPhotos = photos.concat(otherPhotos.flat()); - if (allPhotos.length === 0 && type === 'highres') { - // photos not available, try for screencaps instead - return getPhotosLegacy(entryId, site, 'caps', 1); - } + if (allPhotos.length === 0 && type === 'highres') { + // photos not available, try for screencaps instead + return getPhotosLegacy(entryId, site, 'caps', 1); + } - return allPhotos; + return allPhotos; } async function getPhotos(entryId, site, type = 'highres', page = 1) { - const albumUrl = `${site.parameters?.photos || `${site.url}/gallery.php`}?id=${entryId}&type=${type}&page=${page}`; + const albumUrl = `${site.parameters?.photos || `${site.url}/gallery.php`}?id=${entryId}&type=${type}&page=${page}`; - const res = await bhttp.get(albumUrl); - const html = res.body.toString(); + const res = await bhttp.get(albumUrl); + const html = res.body.toString(); - const sourceLines = html.split(/\n/).filter(line => line.match(/ptx\["\w+"\]/)); - const sources = sourceLines.reduce((acc, sourceLine) => { - const quality = sourceLine.match(/\["\w+"\]/)[0].slice(2, -2); - const sourceStart = sourceLine.match(/\/trial|\/tour|\/content/); + const sourceLines = html.split(/\n/).filter(line => line.match(/ptx\["\w+"\]/)); + const sources = sourceLines.reduce((acc, sourceLine) => { + const quality = sourceLine.match(/\["\w+"\]/)[0].slice(2, -2); + const sourceStart = sourceLine.match(/\/trial|\/tour|\/content/); - if (!sourceStart) return acc; - const source = sourceLine.slice(sourceStart.index, sourceLine.indexOf('.jpg') + 4); + if (!sourceStart) return acc; + const source = sourceLine.slice(sourceStart.index, sourceLine.indexOf('.jpg') + 4); - if (!source) return acc; - if (!acc[quality]) acc[quality] = []; + if (!source) return acc; + if (!acc[quality]) acc[quality] = []; - acc[quality].push(`${site.url}${source}`); + acc[quality].push(`${site.url}${source}`); - return acc; - }, {}); + return acc; + }, {}); - if (type === 'highres') { - if (sources['1600'] && sources['1600'].length > 0) return sources['1600']; - if (sources['1280'] && sources['1280'].length > 0) return sources['1280']; - if (sources['1024'] && sources['1024'].length > 0) return sources['1024']; - if (sources.Thumbs && sources.Thumbs.length > 0) return sources.Thumbs; + if (type === 'highres') { + if (sources['1600'] && sources['1600'].length > 0) return sources['1600']; + if (sources['1280'] && sources['1280'].length > 0) return sources['1280']; + if (sources['1024'] && sources['1024'].length > 0) return sources['1024']; + if (sources.Thumbs && sources.Thumbs.length > 0) return sources.Thumbs; - // no photos available, try for screencaps instead - return getPhotos(entryId, site, 'caps', 1); - } + // no photos available, try for screencaps instead + return getPhotos(entryId, site, 'caps', 1); + } - if (sources.jpg && sources.jpg.length > 0) return sources.jpg; - if (sources['Video Cap Thumbs'] && sources['Video Cap Thumbs'].length > 0) return sources['Video Cap Thumbs']; + if (sources.jpg && sources.jpg.length > 0) return sources.jpg; + if (sources['Video Cap Thumbs'] && sources['Video Cap Thumbs'].length > 0) return sources['Video Cap Thumbs']; - // no screencaps available either, try legacy scraper just in case - return getPhotosLegacy(entryId, site, 'highres', 1); + // no screencaps available either, try legacy scraper just in case + return getPhotosLegacy(entryId, site, 'highres', 1); } function getEntryId(html) { - const entryId = html.match(/showtagform\((\d+)\)/); + const entryId = html.match(/showtagform\((\d+)\)/); - if (entryId) { - return entryId[1]; - } + if (entryId) { + return entryId[1]; + } - const setIdIndex = html.indexOf('setid:"'); + const setIdIndex = html.indexOf('setid:"'); - if (setIdIndex) { - return html.slice(setIdIndex, html.indexOf(',', setIdIndex)).match(/\d+/)[0]; - } + if (setIdIndex) { + return html.slice(setIdIndex, html.indexOf(',', setIdIndex)).match(/\d+/)[0]; + } - return null; + return null; } function scrapeAll(scenes, site) { - return scenes.map(({ el, qu }) => { - const release = {}; + return scenes.map(({ el, qu }) => { + const release = {}; - release.entryId = el.dataset.setid || qu.q('.rating_box')?.dataset.id; + release.entryId = el.dataset.setid || qu.q('.rating_box')?.dataset.id; - release.url = qu.url('.update_title, .dvd_info > a, a ~ a'); - release.title = qu.q('.update_title, .dvd_info > a, a ~ a', true); - release.date = qu.date('.update_date', 'MM/DD/YYYY'); + release.url = qu.url('.update_title, .dvd_info > a, a ~ a'); + release.title = qu.q('.update_title, .dvd_info > a, a ~ a', true); + release.date = qu.date('.update_date', 'MM/DD/YYYY'); - release.actors = qu.all('.update_models a', true); + release.actors = qu.all('.update_models a', true); - const dvdPhotos = qu.imgs('.dvd_preview_thumb'); - const photoCount = Number(qu.q('a img.thumbs', 'cnt')) || 1; + const dvdPhotos = qu.imgs('.dvd_preview_thumb'); + const photoCount = Number(qu.q('a img.thumbs', 'cnt')) || 1; - [release.poster, ...release.photos] = dvdPhotos.length - ? dvdPhotos - : Array.from({ length: photoCount }).map((value, index) => { - const src = qu.img('a img.thumbs', `src${index}_1x`) || qu.img('a img.thumbs', `src${index}`) || qu.img('a img.thumbs'); + [release.poster, ...release.photos] = dvdPhotos.length + ? dvdPhotos + : Array.from({ length: photoCount }).map((value, index) => { + const src = qu.img('a img.thumbs', `src${index}_1x`) || qu.img('a img.thumbs', `src${index}`) || qu.img('a img.thumbs'); - return src ? { - src: /^http/.test(src) ? src : `${site.url}${src}`, - referer: site.url, - } : null; - }).filter(Boolean); + return src ? { + src: /^http/.test(src) ? src : `${site.url}${src}`, + referer: site.url, + } : null; + }).filter(Boolean); - const teaserScript = qu.html('script'); - if (teaserScript) { - const src = teaserScript.slice(teaserScript.indexOf('http'), teaserScript.indexOf('.mp4') + 4); - if (src) release.teaser = { src }; - } + const teaserScript = qu.html('script'); + if (teaserScript) { + const src = teaserScript.slice(teaserScript.indexOf('http'), teaserScript.indexOf('.mp4') + 4); + if (src) release.teaser = { src }; + } - return release; - }); + return release; + }); } function scrapeUpcoming(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const scenesElements = $('#coming_soon_carousel').find('.table').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const scenesElements = $('#coming_soon_carousel').find('.table').toArray(); - return scenesElements.map((element) => { - const entryId = $(element).find('.upcoming_updates_thumb').attr('id').match(/\d+/)[0]; + return scenesElements.map((element) => { + const entryId = $(element).find('.upcoming_updates_thumb').attr('id').match(/\d+/)[0]; - const details = $(element).find('.update_details_comingsoon') - .eq(1) - .children() - .remove(); + const details = $(element).find('.update_details_comingsoon') + .eq(1) + .children() + .remove(); - const title = details - .end() - .text() - .trim(); + const title = details + .end() + .text() + .trim(); - const actors = details - .text() - .trim() - .split(', '); + const actors = details + .text() + .trim() + .split(', '); - const date = moment - .utc($(element).find('.update_date_comingsoon').text().slice(7), 'MM/DD/YYYY') - .toDate(); + const date = moment + .utc($(element).find('.update_date_comingsoon').text().slice(7), 'MM/DD/YYYY') + .toDate(); - const photoElement = $(element).find('a img.thumbs'); - const posterPath = photoElement.attr('src'); - const poster = posterPath.match(/^http/) ? posterPath : `${site.url}${posterPath}`; + const photoElement = $(element).find('a img.thumbs'); + const posterPath = photoElement.attr('src'); + const poster = posterPath.match(/^http/) ? posterPath : `${site.url}${posterPath}`; - const videoClass = $(element).find('.update_thumbnail div').attr('class'); - const videoScript = $(element).find(`script:contains(${videoClass})`).html(); - const teaser = videoScript.slice(videoScript.indexOf('https://'), videoScript.indexOf('.mp4') + 4); + const videoClass = $(element).find('.update_thumbnail div').attr('class'); + const videoScript = $(element).find(`script:contains(${videoClass})`).html(); + const teaser = videoScript.slice(videoScript.indexOf('https://'), videoScript.indexOf('.mp4') + 4); - return { - url: null, - entryId, - title, - date, - actors, - poster, - teaser: { - src: teaser, - }, - rating: null, - site, - }; - }); + return { + url: null, + entryId, + title, + date, + actors, + poster, + teaser: { + src: teaser, + }, + rating: null, + site, + }; + }); } async function scrapeScene({ html, qu }, url, site, include) { - const release = { url, site }; + const release = { url, site }; - release.entryId = getEntryId(html); - release.title = qu.q('.title_bar_hilite', true); - release.description = qu.q('.update_description', true); + release.entryId = getEntryId(html); + release.title = qu.q('.title_bar_hilite', true); + release.description = qu.q('.update_description', true); - release.date = qu.date('.update_date', 'MM/DD/YYYY', null, 'innerHTML'); + release.date = qu.date('.update_date', 'MM/DD/YYYY', null, 'innerHTML'); - release.actors = qu.all('.backgroundcolor_info > .update_models a, .item .update_models a', true); - release.tags = qu.all('.update_tags a', true); + release.actors = qu.all('.backgroundcolor_info > .update_models a, .item .update_models a', true); + release.tags = qu.all('.update_tags a', true); - const posterPath = html.match(/useimage = "(.*)"/)?.[1]; + const posterPath = html.match(/useimage = "(.*)"/)?.[1]; - if (posterPath) { - const poster = /^http/.test(posterPath) ? posterPath : `${site.url}${posterPath}`; + if (posterPath) { + const poster = /^http/.test(posterPath) ? posterPath : `${site.url}${posterPath}`; - if (poster) { - release.poster = { - src: poster, - referer: site.url, - }; - } - } + if (poster) { + release.poster = { + src: poster, + referer: site.url, + }; + } + } - if (include.trailer && site.slug !== 'manuelferrara') { - const trailerLines = html.split('\n').filter(line => /movie\["trailer\w*"\]\[/i.test(line)); + if (include.trailer && site.slug !== 'manuelferrara') { + const trailerLines = html.split('\n').filter(line => /movie\["trailer\w*"\]\[/i.test(line)); - if (trailerLines.length) { - release.trailer = trailerLines.map((trailerLine) => { - const src = trailerLine.match(/path:"([\w:/.&=?%]+)"/)?.[1]; - const quality = trailerLine.match(/movie_height:'(\d+)/)?.[1]; + if (trailerLines.length) { + release.trailer = trailerLines.map((trailerLine) => { + const src = trailerLine.match(/path:"([\w:/.&=?%]+)"/)?.[1]; + const quality = trailerLine.match(/movie_height:'(\d+)/)?.[1]; - return src && { - src: /^http/.test(src) ? src : `${site.url}${src}`, - quality: quality && Number(quality.replace('558', '540')), - }; - }).filter(Boolean); - } - } + return src && { + src: /^http/.test(src) ? src : `${site.url}${src}`, + quality: quality && Number(quality.replace('558', '540')), + }; + }).filter(Boolean); + } + } - if (include.photos) release.photos = await getPhotos(release.entryId, site); + if (include.photos) release.photos = await getPhotos(release.entryId, site); - if (qu.exists('.update_dvds a')) { - release.movie = { - url: qu.url('.update_dvds a'), - title: qu.q('.update_dvds a', true), - }; - } + if (qu.exists('.update_dvds a')) { + release.movie = { + url: qu.url('.update_dvds a'), + title: qu.q('.update_dvds a', true), + }; + } - const stars = Number(qu.q('.avg_rating', true)?.replace(/[\s|Avg Rating:]/g, '')); - if (stars) release.stars = stars; + const stars = Number(qu.q('.avg_rating', true)?.replace(/[\s|Avg Rating:]/g, '')); + if (stars) release.stars = stars; - return release; + return release; } function scrapeMovie({ el, qu }, url, site) { - const movie = { url, site }; + const movie = { url, site }; - movie.entryId = qu.q('.dvd_details_overview .rating_box').dataset.id; - movie.title = qu.q('.title_bar span', true); - movie.covers = qu.urls('#dvd-cover-flip > a'); - movie.channel = slugify(qu.q('.update_date a', true), ''); + movie.entryId = qu.q('.dvd_details_overview .rating_box').dataset.id; + movie.title = qu.q('.title_bar span', true); + movie.covers = qu.urls('#dvd-cover-flip > a'); + movie.channel = slugify(qu.q('.update_date a', true), ''); - // movie.releases = Array.from(document.querySelectorAll('.cell.dvd_info > a'), el => el.href); - const sceneQus = ctxa(el, '.dvd_details'); - const scenes = scrapeAll(sceneQus, site); + // movie.releases = Array.from(document.querySelectorAll('.cell.dvd_info > a'), el => el.href); + const sceneQus = ctxa(el, '.dvd_details'); + const scenes = scrapeAll(sceneQus, site); - const curatedScenes = scenes + const curatedScenes = scenes ?.map(scene => ({ ...scene, movie })) .sort((sceneA, sceneB) => sceneA.date - sceneB.date); - movie.date = curatedScenes?.[0].date; + movie.date = curatedScenes?.[0].date; - return { - ...movie, - ...(curatedScenes && { scenes: curatedScenes }), - }; + return { + ...movie, + ...(curatedScenes && { scenes: curatedScenes }), + }; } function scrapeProfile(html, url, actorName) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const bio = document.querySelector('.model_bio').textContent; - const avatarEl = document.querySelector('.model_bio_pic img'); + const bio = document.querySelector('.model_bio').textContent; + const avatarEl = document.querySelector('.model_bio_pic img'); - const profile = { - name: actorName, - }; + const profile = { + name: actorName, + }; - const heightString = bio.match(/\d+ feet \d+ inches/); - const ageString = bio.match(/Age:\s*(\d{2})/); - const birthDateString = bio.match(/Age:\s*(\w+ \d{1,2}, \d{4})/); - const measurementsString = bio.match(/\w+-\d+-\d+/); + const heightString = bio.match(/\d+ feet \d+ inches/); + const ageString = bio.match(/Age:\s*(\d{2})/); + const birthDateString = bio.match(/Age:\s*(\w+ \d{1,2}, \d{4})/); + const measurementsString = bio.match(/\w+-\d+-\d+/); - if (birthDateString) profile.birthdate = parseDate(birthDateString[1], 'MMMM D, YYYY'); - if (ageString) profile.age = Number(ageString[1]); + if (birthDateString) profile.birthdate = parseDate(birthDateString[1], 'MMMM D, YYYY'); + if (ageString) profile.age = Number(ageString[1]); - if (heightString) profile.height = heightToCm(heightString[0]); + if (heightString) profile.height = heightToCm(heightString[0]); - if (measurementsString) { - const [bust, waist, hip] = measurementsString[0].split('-'); + if (measurementsString) { + const [bust, waist, hip] = measurementsString[0].split('-'); - if (bust) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - if (avatarEl) { - const avatarSources = [ - avatarEl.getAttribute('src0_3x'), - avatarEl.getAttribute('src0_2x'), - avatarEl.getAttribute('src0_1x'), - avatarEl.getAttribute('src0'), - avatarEl.getAttribute('src'), - ].filter(Boolean); + if (avatarEl) { + const avatarSources = [ + avatarEl.getAttribute('src0_3x'), + avatarEl.getAttribute('src0_2x'), + avatarEl.getAttribute('src0_1x'), + avatarEl.getAttribute('src0'), + avatarEl.getAttribute('src'), + ].filter(Boolean); - if (avatarSources.length) profile.avatar = avatarSources; - } + if (avatarSources.length) profile.avatar = avatarSources; + } - profile.releases = Array.from(document.querySelectorAll('.category_listing_block .update_details > a:first-child'), el => el.href); + profile.releases = Array.from(document.querySelectorAll('.category_listing_block .update_details > a:first-child'), el => el.href); - console.log(profile); + console.log(profile); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = site.parameters?.latest - ? util.format(site.parameters.latest, page) - : `${site.url}/trial/categories/movies_${page}_d.html`; + const url = site.parameters?.latest + ? util.format(site.parameters.latest, page) + : `${site.url}/trial/categories/movies_${page}_d.html`; - // const res = await bhttp.get(url); - const res = await geta(url, '.update_details'); + // const res = await bhttp.get(url); + const res = await geta(url, '.update_details'); - return res.ok ? scrapeAll(res.items, site) : res.status; + return res.ok ? scrapeAll(res.items, site) : res.status; } async function fetchUpcoming(site) { - if (site.parameters?.upcoming === false) return null; + if (site.parameters?.upcoming === false) return null; - const url = site.parameters?.upcoming ? util.format(site.parameters.upcoming) : `${site.url}/trial/index.php`; - const res = await bhttp.get(url); + const url = site.parameters?.upcoming ? util.format(site.parameters.upcoming) : `${site.url}/trial/index.php`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeUpcoming(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeUpcoming(res.body.toString(), site); + } - return res.statusCode; + return res.statusCode; } async function fetchScene(url, site, baseRelease, preflight, include) { - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeScene(res.item, url, site, include) : res.status; + return res.ok ? scrapeScene(res.item, url, site, include) : res.status; } async function fetchMovie(url, site) { - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeMovie(res.item, url, site) : res.status; + return res.ok ? scrapeMovie(res.item, url, site) : res.status; } async function fetchProfile(actorName) { - const actorSlugA = slugify(actorName, '-'); - const actorSlugB = slugify(actorName, ''); + const actorSlugA = slugify(actorName, '-'); + const actorSlugB = slugify(actorName, ''); - const urlA = `https://julesjordan.com/trial/models/${actorSlugA}.html`; - const urlB = `https://julesjordan.com/trial/models/${actorSlugB}.html`; + const urlA = `https://julesjordan.com/trial/models/${actorSlugA}.html`; + const urlB = `https://julesjordan.com/trial/models/${actorSlugB}.html`; - const resA = await bhttp.get(urlA); + const resA = await bhttp.get(urlA); - if (resA.statusCode === 200) { - const profile = scrapeProfile(resA.body.toString(), urlA, actorName); + if (resA.statusCode === 200) { + const profile = scrapeProfile(resA.body.toString(), urlA, actorName); - return profile; - } + return profile; + } - const resB = await bhttp.get(urlB); + const resB = await bhttp.get(urlB); - if (resB.statusCode === 200) { - const profile = scrapeProfile(resB.body.toString(), urlB, actorName); + if (resB.statusCode === 200) { + const profile = scrapeProfile(resB.body.toString(), urlB, actorName); - return profile; - } + return profile; + } - return null; + return null; } module.exports = { - fetchLatest, - fetchMovie, - fetchProfile, - fetchUpcoming, - fetchScene, + fetchLatest, + fetchMovie, + fetchProfile, + fetchUpcoming, + fetchScene, }; diff --git a/src/scrapers/kellymadison.js b/src/scrapers/kellymadison.js index 50c99acab..9b29be650 100644 --- a/src/scrapers/kellymadison.js +++ b/src/scrapers/kellymadison.js @@ -7,184 +7,184 @@ const moment = require('moment'); const { feetInchesToCm } = require('../utils/convert'); const siteMapByKey = { - PF: 'pornfidelity', - TF: 'teenfidelity', - KM: 'kellymadison', + PF: 'pornfidelity', + TF: 'teenfidelity', + KM: 'kellymadison', }; const siteMapBySlug = Object.entries(siteMapByKey).reduce((acc, [key, value]) => ({ ...acc, [value]: key }), {}); function extractTextNode(parentEl) { - return Array.from(parentEl).reduce((acc, el) => (el.nodeType === 3 ? `${acc}${el.textContent.trim()}` : acc), ''); + return Array.from(parentEl).reduce((acc, el) => (el.nodeType === 3 ? `${acc}${el.textContent.trim()}` : acc), ''); } function scrapeLatest(html, site) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - return Array.from(document.querySelectorAll('.episode'), (scene) => { - const release = { site }; + return Array.from(document.querySelectorAll('.episode'), (scene) => { + const release = { site }; - release.shootId = scene.querySelector('.card-meta .text-right').textContent.trim(); + release.shootId = scene.querySelector('.card-meta .text-right').textContent.trim(); - const siteId = release.shootId.match(/\w{2}/)[0]; - const siteSlug = siteMapByKey[siteId]; + const siteId = release.shootId.match(/\w{2}/)[0]; + const siteSlug = siteMapByKey[siteId]; - if (site.slug !== siteSlug) { - // using generic network overview, scene is not from the site we want - return null; - } + if (site.slug !== siteSlug) { + // using generic network overview, scene is not from the site we want + return null; + } - const durationEl = scene.querySelector('.content a'); + const durationEl = scene.querySelector('.content a'); - [release.entryId] = durationEl.href.match(/\d+$/); - release.url = `${site.url}/episodes/${release.entryId}`; + [release.entryId] = durationEl.href.match(/\d+$/); + release.url = `${site.url}/episodes/${release.entryId}`; - release.title = scene.querySelector('h5 a').textContent.trim(); + release.title = scene.querySelector('h5 a').textContent.trim(); - const dateEl = scene.querySelector('.card-meta .text-left').childNodes; - const dateString = extractTextNode(dateEl); + const dateEl = scene.querySelector('.card-meta .text-left').childNodes; + const dateString = extractTextNode(dateEl); - release.date = moment.utc(dateString, ['MMM D', 'MMM D, YYYY']).toDate(); - release.actors = Array.from(scene.querySelectorAll('.models a'), el => el.textContent); + release.date = moment.utc(dateString, ['MMM D', 'MMM D, YYYY']).toDate(); + release.actors = Array.from(scene.querySelectorAll('.models a'), el => el.textContent); - const durationString = durationEl.textContent.match(/\d+ min/); - if (durationString) release.duration = Number(durationString[0].match(/\d+/)[0]) * 60; + const durationString = durationEl.textContent.match(/\d+ min/); + if (durationString) release.duration = Number(durationString[0].match(/\d+/)[0]) * 60; - release.poster = scene.querySelector('.card-img-top').dataset.src; - release.teaser = { - src: scene.querySelector('video').src, - }; + release.poster = scene.querySelector('.card-img-top').dataset.src; + release.teaser = { + src: scene.querySelector('video').src, + }; - return release; - }).filter(scene => scene); + return release; + }).filter(scene => scene); } function scrapeScene(html, url, site, baseRelease) { - const { document } = new JSDOM(html).window; - const release = { url, site }; + const { document } = new JSDOM(html).window; + const release = { url, site }; - const titleEl = document.querySelector('.card-header.row h4').childNodes; - const titleString = extractTextNode(titleEl); + const titleEl = document.querySelector('.card-header.row h4').childNodes; + const titleString = extractTextNode(titleEl); - if (!baseRelease) [release.entryId] = url.match(/\d+/); + if (!baseRelease) [release.entryId] = url.match(/\d+/); - release.title = titleString - .replace('Trailer: ', '') - .replace(/- \w+ #\d+$/, '') - .trim(); + release.title = titleString + .replace('Trailer: ', '') + .replace(/- \w+ #\d+$/, '') + .trim(); - release.channel = titleString.match(/\w+ #\d+$/)[0].match(/\w+/)[0].toLowerCase(); + release.channel = titleString.match(/\w+ #\d+$/)[0].match(/\w+/)[0].toLowerCase(); - const episode = titleString.match(/#\d+$/)[0]; - const siteKey = siteMapBySlug[release.channel]; + const episode = titleString.match(/#\d+$/)[0]; + const siteKey = siteMapBySlug[release.channel]; - release.shootId = `${siteKey} ${episode}`; - release.description = document.querySelector('p.card-text').textContent.trim(); + release.shootId = `${siteKey} ${episode}`; + release.description = document.querySelector('p.card-text').textContent.trim(); - const dateEl = document.querySelector('.card-body h4.card-title:nth-child(3)').childNodes; - const dateString = extractTextNode(dateEl); + const dateEl = document.querySelector('.card-body h4.card-title:nth-child(3)').childNodes; + const dateString = extractTextNode(dateEl); - release.date = moment.utc(dateString, 'YYYY-MM-DD').toDate(); - release.actors = Array.from(document.querySelectorAll('.card-body h4.card-title:nth-child(4) a'), el => el.textContent); + release.date = moment.utc(dateString, 'YYYY-MM-DD').toDate(); + release.actors = Array.from(document.querySelectorAll('.card-body h4.card-title:nth-child(4) a'), el => el.textContent); - const durationRaw = document.querySelector('.card-body h4.card-title:nth-child(1)').textContent; - const durationString = durationRaw.match(/\d+:\d+/)[0]; + const durationRaw = document.querySelector('.card-body h4.card-title:nth-child(1)').textContent; + const durationString = durationRaw.match(/\d+:\d+/)[0]; - release.duration = moment.duration(`00:${durationString}`).asSeconds(); + release.duration = moment.duration(`00:${durationString}`).asSeconds(); - const trailerStart = document.body.innerHTML.indexOf('player.updateSrc'); - const trailerString = document.body.innerHTML.slice(trailerStart, document.body.innerHTML.indexOf(');', trailerStart)); + const trailerStart = document.body.innerHTML.indexOf('player.updateSrc'); + const trailerString = document.body.innerHTML.slice(trailerStart, document.body.innerHTML.indexOf(');', trailerStart)); - const trailers = trailerString.match(/https:\/\/.*.mp4/g); - const resolutions = trailerString.match(/res: '\d+'/g).map((res) => { - const resolution = Number(res.match(/\d+/)[0]); + const trailers = trailerString.match(/https:\/\/.*.mp4/g); + const resolutions = trailerString.match(/res: '\d+'/g).map((res) => { + const resolution = Number(res.match(/\d+/)[0]); - return resolution === 4000 ? 2160 : resolution; // 4k is not 4000 pixels high - }); + return resolution === 4000 ? 2160 : resolution; // 4k is not 4000 pixels high + }); - release.trailer = trailers.map((trailer, index) => ({ - src: trailer, - quality: resolutions[index], - })); + release.trailer = trailers.map((trailer, index) => ({ + src: trailer, + quality: resolutions[index], + })); - const posterPrefix = html.indexOf('poster:'); - const poster = html.slice(html.indexOf('http', posterPrefix), html.indexOf('.jpg', posterPrefix) + 4); + const posterPrefix = html.indexOf('poster:'); + const poster = html.slice(html.indexOf('http', posterPrefix), html.indexOf('.jpg', posterPrefix) + 4); - if (baseRelease?.poster) release.photos = [poster]; - else release.poster = poster; + if (baseRelease?.poster) release.photos = [poster]; + else release.poster = poster; - return release; + return release; } function scrapeProfile(html, actorName) { - const { document } = new JSDOM(html).window; - const profile = { name: actorName }; + const { document } = new JSDOM(html).window; + const profile = { name: actorName }; - const bioKeys = Array.from(document.querySelectorAll('table.table td:nth-child(1)'), el => el.textContent.slice(0, -1)); - const bioValues = Array.from(document.querySelectorAll('table.table td:nth-child(2)'), el => el.textContent); - const bio = bioKeys.reduce((acc, key, index) => ({ ...acc, [key]: bioValues[index] }), {}); + const bioKeys = Array.from(document.querySelectorAll('table.table td:nth-child(1)'), el => el.textContent.slice(0, -1)); + const bioValues = Array.from(document.querySelectorAll('table.table td:nth-child(2)'), el => el.textContent); + const bio = bioKeys.reduce((acc, key, index) => ({ ...acc, [key]: bioValues[index] }), {}); - if (bio.Measurements) [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-'); - if (bio.Birthplace) profile.birthPlace = bio.Birthplace; + if (bio.Measurements) [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-'); + if (bio.Birthplace) profile.birthPlace = bio.Birthplace; - if (bio.Height) { - const [feet, inches] = bio.Height.match(/\d+/g); - profile.height = feetInchesToCm(feet, inches); - } + if (bio.Height) { + const [feet, inches] = bio.Height.match(/\d+/g); + profile.height = feetInchesToCm(feet, inches); + } - if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity; + if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity; - const avatarEl = Array.from(document.querySelectorAll('img')).find(photo => photo.src.match('model')); + const avatarEl = Array.from(document.querySelectorAll('img')).find(photo => photo.src.match('model')); - if (avatarEl) profile.avatar = avatarEl.src; + if (avatarEl) profile.avatar = avatarEl.src; - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = `https://kellymadison.com/episodes/search?page=${page}`; // TLS issues with teenfidelity.com, same overview on all sites - const res = await bhttp.get(url, { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - }, - }); + const url = `https://kellymadison.com/episodes/search?page=${page}`; // TLS issues with teenfidelity.com, same overview on all sites + const res = await bhttp.get(url, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); - if (res.statusCode === 200 && res.body.status === 'success') { - return scrapeLatest(res.body.html, site); - } + if (res.statusCode === 200 && res.body.status === 'success') { + return scrapeLatest(res.body.html, site); + } - return null; + return null; } async function fetchScene(url, site, baseRelease) { - const { pathname } = new URL(url); + const { pathname } = new URL(url); - const res = await bhttp.get(`https://www.kellymadison.com${pathname}`, { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - }, - }); + const res = await bhttp.get(`https://www.kellymadison.com${pathname}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); - return scrapeScene(res.body.toString(), url, site, baseRelease); + return scrapeScene(res.body.toString(), url, site, baseRelease); } async function fetchProfile(actorName) { - const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-'); - const res = await bhttp.get(`https://www.kellymadison.com/models/${actorSlug}`, { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - }, - }); + const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-'); + const res = await bhttp.get(`https://www.kellymadison.com/models/${actorSlug}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); - if (res.statusCode === 200) { - return scrapeProfile(res.body.toString(), actorName); - } + if (res.statusCode === 200) { + return scrapeProfile(res.body.toString(), actorName); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchProfile, - fetchScene, + fetchLatest, + fetchProfile, + fetchScene, }; diff --git a/src/scrapers/kink.js b/src/scrapers/kink.js index 5603322ea..35ed5f5b1 100644 --- a/src/scrapers/kink.js +++ b/src/scrapers/kink.js @@ -5,116 +5,116 @@ const cheerio = require('cheerio'); const moment = require('moment'); function scrapeLatest(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const sceneElements = $('.shoot-list .shoot').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const sceneElements = $('.shoot-list .shoot').toArray(); - return sceneElements.map((element) => { - const sceneLinkElement = $(element).find('.shoot-thumb-title a'); - const href = sceneLinkElement.attr('href'); - const url = `https://kink.com${href}`; - const shootId = href.split('/')[2]; - const title = sceneLinkElement.text().trim(); + return sceneElements.map((element) => { + const sceneLinkElement = $(element).find('.shoot-thumb-title a'); + const href = sceneLinkElement.attr('href'); + const url = `https://kink.com${href}`; + const shootId = href.split('/')[2]; + const title = sceneLinkElement.text().trim(); - const poster = $(element).find('.adimage').attr('src'); - const photos = $(element).find('.rollover .roll-image').map((photoIndex, photoElement) => $(photoElement).attr('data-imagesrc')).toArray(); + const poster = $(element).find('.adimage').attr('src'); + const photos = $(element).find('.rollover .roll-image').map((photoIndex, photoElement) => $(photoElement).attr('data-imagesrc')).toArray(); - const date = moment.utc($(element).find('.date').text(), 'MMM DD, YYYY').toDate(); - const actors = $(element).find('.shoot-thumb-models a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - const stars = $(element).find('.average-rating').attr('data-rating') / 10; + const date = moment.utc($(element).find('.date').text(), 'MMM DD, YYYY').toDate(); + const actors = $(element).find('.shoot-thumb-models a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + const stars = $(element).find('.average-rating').attr('data-rating') / 10; - const timestamp = $(element).find('.video span').text(); - const timestampComponents = timestamp.split(':'); // fix mixed hh:mm:ss and mm:ss format - const duration = moment.duration(timestampComponents.length > 2 ? timestamp : `0:${timestamp}`).asSeconds(); + const timestamp = $(element).find('.video span').text(); + const timestampComponents = timestamp.split(':'); // fix mixed hh:mm:ss and mm:ss format + const duration = moment.duration(timestampComponents.length > 2 ? timestamp : `0:${timestamp}`).asSeconds(); - return { - url, - shootId, - entryId: shootId, - title, - actors, - date, - photos, - poster, - rating: { - stars, - }, - duration, - site, - }; - }); + return { + url, + shootId, + entryId: shootId, + title, + actors, + date, + photos, + poster, + rating: { + stars, + }, + duration, + site, + }; + }); } async function scrapeScene(html, url, shootId, ratingRes, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); + const $ = cheerio.load(html, { normalizeWhitespace: true }); - // const title = $('h1.shoot-title').text().replace(/\ue800/, ''); // fallback, special character is 'like'-heart - const title = $('h1.shoot-title span.favorite-button').attr('data-title'); - const actorsRaw = $('.shoot-info p.starring'); + // const title = $('h1.shoot-title').text().replace(/\ue800/, ''); // fallback, special character is 'like'-heart + const title = $('h1.shoot-title span.favorite-button').attr('data-title'); + const actorsRaw = $('.shoot-info p.starring'); - const photos = $('.gallery .thumb img').map((photoIndex, photoElement) => $(photoElement).attr('data-image-file')).toArray(); - const trailerVideo = $('.player span[data-type="trailer-src"]').attr('data-url'); - const trailerPoster = $('.player video#kink-player').attr('poster'); + const photos = $('.gallery .thumb img').map((photoIndex, photoElement) => $(photoElement).attr('data-image-file')).toArray(); + const trailerVideo = $('.player span[data-type="trailer-src"]').attr('data-url'); + const trailerPoster = $('.player video#kink-player').attr('poster'); - const date = moment.utc($(actorsRaw) - .prev() - .text() - .trim() - .replace('Date: ', ''), - 'MMMM DD, YYYY') - .toDate(); + const date = moment.utc($(actorsRaw) + .prev() + .text() + .trim() + .replace('Date: ', ''), + 'MMMM DD, YYYY') + .toDate(); - const actors = $(actorsRaw).find('span.names a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - const description = $('.shoot-info .description').text().trim(); + const actors = $(actorsRaw).find('span.names a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + const description = $('.shoot-info .description').text().trim(); - const { average: stars } = ratingRes.body; + const { average: stars } = ratingRes.body; - const siteName = $('.shoot-logo a').attr('href').split('/')[2]; - const siteSlug = siteName.replace(/\s+/g, '').toLowerCase(); + const siteName = $('.shoot-logo a').attr('href').split('/')[2]; + const siteSlug = siteName.replace(/\s+/g, '').toLowerCase(); - const tags = $('.tag-list > a[href*="/tag"]').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); - const channel = siteSlug; + const tags = $('.tag-list > a[href*="/tag"]').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); + const channel = siteSlug; - return { - url, - shootId, - entryId: shootId, - title, - date, - actors, - description, - photos, - poster: trailerPoster, - trailer: { - src: trailerVideo, - quality: 480, - }, - rating: { - stars, - }, - tags, - site, - channel, - }; + return { + url, + shootId, + entryId: shootId, + title, + date, + actors, + description, + photos, + poster: trailerPoster, + trailer: { + src: trailerVideo, + quality: 480, + }, + rating: { + stars, + }, + tags, + site, + channel, + }; } async function fetchLatest(site, page = 1) { - const res = await bhttp.get(`${site.url}/latest/page/${page}`); + const res = await bhttp.get(`${site.url}/latest/page/${page}`); - return scrapeLatest(res.body.toString(), site); + return scrapeLatest(res.body.toString(), site); } async function fetchScene(url, site) { - const shootId = new URL(url).pathname.split('/')[2]; + const shootId = new URL(url).pathname.split('/')[2]; - const [res, ratingRes] = await Promise.all([ - bhttp.get(url), - bhttp.get(`https://kink.com/api/ratings/${shootId}`), - ]); + const [res, ratingRes] = await Promise.all([ + bhttp.get(url), + bhttp.get(`https://kink.com/api/ratings/${shootId}`), + ]); - return scrapeScene(res.body.toString(), url, shootId, ratingRes, site); + return scrapeScene(res.body.toString(), url, shootId, ratingRes, site); } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/legalporno.js b/src/scrapers/legalporno.js index bd253a176..eafe24811 100644 --- a/src/scrapers/legalporno.js +++ b/src/scrapers/legalporno.js @@ -8,201 +8,201 @@ const moment = require('moment'); const slugify = require('../utils/slugify'); function extractTitle(originalTitle) { - const titleComponents = originalTitle.split(' '); - const sceneIdMatch = titleComponents.slice(-1)[0].match(/(AB|AF|GP|SZ|IV|GIO|RS|TW|MA|FM|SAL|NR|AA|GL|BZ|FS|KS|OT)\d+/); // detect studio prefixes - const shootId = sceneIdMatch ? sceneIdMatch[0] : null; - const title = sceneIdMatch ? titleComponents.slice(0, -1).join(' ') : originalTitle; + const titleComponents = originalTitle.split(' '); + const sceneIdMatch = titleComponents.slice(-1)[0].match(/(AB|AF|GP|SZ|IV|GIO|RS|TW|MA|FM|SAL|NR|AA|GL|BZ|FS|KS|OT)\d+/); // detect studio prefixes + const shootId = sceneIdMatch ? sceneIdMatch[0] : null; + const title = sceneIdMatch ? titleComponents.slice(0, -1).join(' ') : originalTitle; - return { shootId, title }; + return { shootId, title }; } function getPoster(posterElement, sceneId) { - const posterStyle = posterElement.attr('style'); + const posterStyle = posterElement.attr('style'); - if (posterStyle) { - return posterStyle.slice(posterStyle.indexOf('(') + 1, -1); - } + if (posterStyle) { + return posterStyle.slice(posterStyle.indexOf('(') + 1, -1); + } - const posterRange = posterElement.attr('data-casting'); - const posterRangeData = posterRange ? JSON.parse(posterRange) : null; - const posterTimeRange = posterRangeData[Math.floor(Math.random() * posterRangeData.length)]; + const posterRange = posterElement.attr('data-casting'); + const posterRangeData = posterRange ? JSON.parse(posterRange) : null; + const posterTimeRange = posterRangeData[Math.floor(Math.random() * posterRangeData.length)]; - if (!posterTimeRange) { - return null; - } + if (!posterTimeRange) { + return null; + } - if (typeof posterTimeRange === 'number') { - // poster time is already a single time value - return `https://legalporno.com/casting/${sceneId}/${posterTimeRange}`; - } + if (typeof posterTimeRange === 'number') { + // poster time is already a single time value + return `https://legalporno.com/casting/${sceneId}/${posterTimeRange}`; + } - const [max, min] = posterTimeRange.split('-'); - const posterTime = Math.floor(Math.random() * (Number(max) - Number(min) + 1) + Number(min)); + const [max, min] = posterTimeRange.split('-'); + const posterTime = Math.floor(Math.random() * (Number(max) - Number(min) + 1) + Number(min)); - return `https://legalporno.com/casting/${sceneId}/${posterTime}`; + return `https://legalporno.com/casting/${sceneId}/${posterTime}`; } function scrapeLatest(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const scenesElements = $('.thumbnails > div').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const scenesElements = $('.thumbnails > div').toArray(); - return scenesElements.map((element) => { - const sceneLinkElement = $(element).find('.thumbnail-title a'); - const url = sceneLinkElement.attr('href'); + return scenesElements.map((element) => { + const sceneLinkElement = $(element).find('.thumbnail-title a'); + const url = sceneLinkElement.attr('href'); - const originalTitle = sceneLinkElement.text().trim(); // title attribute breaks when they use \\ escaping - const { shootId, title } = extractTitle(originalTitle); - const entryId = new URL(url).pathname.split('/')[2]; + const originalTitle = sceneLinkElement.text().trim(); // title attribute breaks when they use \\ escaping + const { shootId, title } = extractTitle(originalTitle); + const entryId = new URL(url).pathname.split('/')[2]; - const date = moment.utc($(element).attr('release'), 'YYYY/MM/DD').toDate(); + const date = moment.utc($(element).attr('release'), 'YYYY/MM/DD').toDate(); - const sceneId = $(element).attr('data-content'); - const posterElement = $(element).find('.thumbnail-avatar'); + const sceneId = $(element).attr('data-content'); + const posterElement = $(element).find('.thumbnail-avatar'); - const poster = getPoster(posterElement, sceneId); + const poster = getPoster(posterElement, sceneId); - return { - url, - shootId, - entryId, - title, - date, - poster, - site, - }; - }); + return { + url, + shootId, + entryId, + title, + date, + poster, + site, + }; + }); } async function scrapeScene(html, url, site, useGallery) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const playerObject = $('script:contains("new WatchPage")').html(); - const playerData = playerObject && playerObject.slice(playerObject.indexOf('{"swf":'), playerObject.lastIndexOf('},') + 1); - const data = playerData && JSON.parse(playerData); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const playerObject = $('script:contains("new WatchPage")').html(); + const playerData = playerObject && playerObject.slice(playerObject.indexOf('{"swf":'), playerObject.lastIndexOf('},') + 1); + const data = playerData && JSON.parse(playerData); - const release = { url }; + const release = { url }; - const originalTitle = $('h1.watchpage-title').text().trim(); - const { shootId, title } = extractTitle(originalTitle); + const originalTitle = $('h1.watchpage-title').text().trim(); + const { shootId, title } = extractTitle(originalTitle); - release.shootId = shootId; - release.entryId = new URL(url).pathname.split('/')[2]; + release.shootId = shootId; + release.entryId = new URL(url).pathname.split('/')[2]; - release.title = title; - release.date = moment.utc($('span[title="Release date"] a').text(), 'YYYY-MM-DD').toDate(); + release.title = title; + release.date = moment.utc($('span[title="Release date"] a').text(), 'YYYY-MM-DD').toDate(); - const [actorsElement, tagsElement, descriptionElement] = $('.scene-description__row').toArray(); + const [actorsElement, tagsElement, descriptionElement] = $('.scene-description__row').toArray(); - release.description = $('meta[name="description"]')?.attr('content')?.trim() + release.description = $('meta[name="description"]')?.attr('content')?.trim() || (descriptionElement && $(descriptionElement).find('dd').text().trim()); - release.actors = $(actorsElement) - .find('a[href*="com/model"]') - .map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + release.actors = $(actorsElement) + .find('a[href*="com/model"]') + .map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - release.duration = moment.duration($('span[title="Runtime"]').text().trim()).asSeconds(); - release.tags = $(tagsElement).find('a').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); + release.duration = moment.duration($('span[title="Runtime"]').text().trim()).asSeconds(); + release.tags = $(tagsElement).find('a').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); - const photos = useGallery - ? $('.gallery a img').map((photoIndex, photoElement) => $(photoElement).attr('src')).toArray() - : $('.screenshots img').map((photoIndex, photoElement) => $(photoElement).attr('src')).toArray(); + const photos = useGallery + ? $('.gallery a img').map((photoIndex, photoElement) => $(photoElement).attr('src')).toArray() + : $('.screenshots img').map((photoIndex, photoElement) => $(photoElement).attr('src')).toArray(); - release.photos = photos.map((source) => { - // source without parameters sometimes serves larger preview photo - const { origin, pathname } = new URL(source); + release.photos = photos.map((source) => { + // source without parameters sometimes serves larger preview photo + const { origin, pathname } = new URL(source); - return `${origin}${pathname}`; + return `${origin}${pathname}`; - /* disable thumbnail as fallback, usually enough high res photos available + /* disable thumbnail as fallback, usually enough high res photos available return [ `${origin}${pathname}`, source, ]; */ - }); + }); - const posterStyle = $('#player').attr('style'); - const poster = posterStyle.slice(posterStyle.indexOf('(') + 1, -1); + const posterStyle = $('#player').attr('style'); + const poster = posterStyle.slice(posterStyle.indexOf('(') + 1, -1); - release.poster = poster || release.photos.slice(Math.floor(release.photos.length / 3) * -1); // poster unavailable, try last 1/3rd of high res photos as fallback + release.poster = poster || release.photos.slice(Math.floor(release.photos.length / 3) * -1); // poster unavailable, try last 1/3rd of high res photos as fallback - if (data) { - const qualityMap = { - web: 240, - vga: 480, - hd: 720, - '1080p': 1080, - }; + if (data) { + const qualityMap = { + web: 240, + vga: 480, + hd: 720, + '1080p': 1080, + }; - release.trailer = data.clip.qualities.map(trailer => ({ - src: trailer.src, - type: trailer.type, - quality: qualityMap[trailer.quality] || trailer.quality, - })); - } + release.trailer = data.clip.qualities.map(trailer => ({ + src: trailer.src, + type: trailer.type, + quality: qualityMap[trailer.quality] || trailer.quality, + })); + } - const studioName = $('.watchpage-studioname').first().text().trim(); - release.studio = slugify(studioName, ''); + const studioName = $('.watchpage-studioname').first().text().trim(); + release.studio = slugify(studioName, ''); - return release; + return release; } async function scrapeProfile(html, _url, actorName) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const profile = { - name: actorName, - }; + const profile = { + name: actorName, + }; - const avatarEl = document.querySelector('.model--avatar img[src^="http"]'); - const entries = Array.from(document.querySelectorAll('.model--description tr'), el => el.textContent.replace(/\n/g, '').split(':')); + const avatarEl = document.querySelector('.model--avatar img[src^="http"]'); + const entries = Array.from(document.querySelectorAll('.model--description tr'), el => el.textContent.replace(/\n/g, '').split(':')); - const bio = entries - .filter(entry => entry.length === 2) // ignore entries without ':' (About section, see Blanche Bradburry) - .reduce((acc, [key, value]) => ({ ...acc, [key.trim()]: value.trim() }), {}); + const bio = entries + .filter(entry => entry.length === 2) // ignore entries without ':' (About section, see Blanche Bradburry) + .reduce((acc, [key, value]) => ({ ...acc, [key.trim()]: value.trim() }), {}); - profile.birthPlace = bio.Nationality; + profile.birthPlace = bio.Nationality; - if (bio.Age) profile.age = bio.Age; - if (avatarEl) profile.avatar = avatarEl.src; + if (bio.Age) profile.age = bio.Age; + if (avatarEl) profile.avatar = avatarEl.src; - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const res = await bhttp.get(`${site.url}/new-videos/${page}`); + const res = await bhttp.get(`${site.url}/new-videos/${page}`); - return scrapeLatest(res.body.toString(), site); + return scrapeLatest(res.body.toString(), site); } async function fetchScene(url, site) { - const useGallery = true; + const useGallery = true; - // TODO: fall back on screenshots when gallery is not available - const res = useGallery - ? await bhttp.get(`${url}/gallery#gallery`) - : await bhttp.get(`${url}/screenshots#screenshots`); + // TODO: fall back on screenshots when gallery is not available + const res = useGallery + ? await bhttp.get(`${url}/gallery#gallery`) + : await bhttp.get(`${url}/screenshots#screenshots`); - return scrapeScene(res.body.toString(), url, site, useGallery); + return scrapeScene(res.body.toString(), url, site, useGallery); } async function fetchProfile(actorName) { - const res = await bhttp.get(`https://www.legalporno.com/api/autocomplete/search?q=${actorName.replace(' ', '+')}`); - const data = res.body; + const res = await bhttp.get(`https://www.legalporno.com/api/autocomplete/search?q=${actorName.replace(' ', '+')}`); + const data = res.body; - const result = data.terms.find(item => item.type === 'model'); + const result = data.terms.find(item => item.type === 'model'); - if (result) { - const bioRes = await bhttp.get(result.url); - const html = bioRes.body.toString(); + if (result) { + const bioRes = await bhttp.get(result.url); + const html = bioRes.body.toString(); - return scrapeProfile(html, result.url, actorName); - } + return scrapeProfile(html, result.url, actorName); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchProfile, - fetchScene, + fetchLatest, + fetchProfile, + fetchScene, }; diff --git a/src/scrapers/men.js b/src/scrapers/men.js index 4ae2fd3c4..304e59ad9 100644 --- a/src/scrapers/men.js +++ b/src/scrapers/men.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'men', 'modelprofile'); + return fetchProfile(actorName, 'men', 'modelprofile'); } module.exports = { - fetchLatest, - fetchScene, - fetchProfile: networkFetchProfile, + fetchLatest, + fetchScene, + fetchProfile: networkFetchProfile, }; diff --git a/src/scrapers/metrohd.js b/src/scrapers/metrohd.js index 4fa609284..78c2b30b6 100644 --- a/src/scrapers/metrohd.js +++ b/src/scrapers/metrohd.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'devianthardcore'); + return fetchProfile(actorName, 'devianthardcore'); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/mikeadriano.js b/src/scrapers/mikeadriano.js index af703cf13..341c048be 100644 --- a/src/scrapers/mikeadriano.js +++ b/src/scrapers/mikeadriano.js @@ -8,232 +8,232 @@ const moment = require('moment'); const { get } = require('../utils/http'); const descriptionTags = { - 'anal cream pie': 'anal creampie', - 'ass to mouth': 'ass to mouth', - 'cream pie in her ass': 'anal creampie', - 'eats ass': 'ass eating', - facial: 'facial', - gaped: 'gaping', - gapes: 'gaping', - gape: 'gaping', - 'rectal cream pie': 'anal creampie', - rimming: 'ass eating', + 'anal cream pie': 'anal creampie', + 'ass to mouth': 'ass to mouth', + 'cream pie in her ass': 'anal creampie', + 'eats ass': 'ass eating', + facial: 'facial', + gaped: 'gaping', + gapes: 'gaping', + gape: 'gaping', + 'rectal cream pie': 'anal creampie', + rimming: 'ass eating', }; function deriveTagsFromDescription(description) { - const matches = (description || '').toLowerCase().match(new RegExp(Object.keys(descriptionTags).join('|'), 'g')); + const matches = (description || '').toLowerCase().match(new RegExp(Object.keys(descriptionTags).join('|'), 'g')); - return matches - ? matches.map(match => descriptionTags[match]) - : []; + return matches + ? matches.map(match => descriptionTags[match]) + : []; } async function scrapeLatestA(html, site) { - const { document } = new JSDOM(html).window; - const sceneElements = document.querySelectorAll('.content-item-large, .content-item'); + const { document } = new JSDOM(html).window; + const sceneElements = document.querySelectorAll('.content-item-large, .content-item'); - return Promise.all(Array.from(sceneElements, async (element) => { - const $ = cheerio.load(element.innerHTML, { normalizeWhitespace: true }); + return Promise.all(Array.from(sceneElements, async (element) => { + const $ = cheerio.load(element.innerHTML, { normalizeWhitespace: true }); - const titleElement = element.querySelector('h3.title a'); - const title = titleElement.textContent; - const url = titleElement.href; - const entryId = url.split('/').slice(-2)[0]; + const titleElement = element.querySelector('h3.title a'); + const title = titleElement.textContent; + const url = titleElement.href; + const entryId = url.split('/').slice(-2)[0]; - const descriptionElement = element.querySelector('.desc'); - const description = descriptionElement && descriptionElement.textContent.trim(); - const date = moment(element.querySelector('.date, time').textContent, 'Do MMM YYYY').toDate(); + const descriptionElement = element.querySelector('.desc'); + const description = descriptionElement && descriptionElement.textContent.trim(); + const date = moment(element.querySelector('.date, time').textContent, 'Do MMM YYYY').toDate(); - const actors = Array.from(element.querySelectorAll('h4.models a'), actorElement => actorElement.textContent); + const actors = Array.from(element.querySelectorAll('h4.models a'), actorElement => actorElement.textContent); - const durationString = element.querySelector('.total-time').textContent.trim(); - // timestamp is sometimes 00:00, sometimes 0:00:00 - const duration = durationString.split(':').length === 3 - ? moment.duration(durationString).asSeconds() - : moment.duration(`00:${durationString}`).asSeconds(); + const durationString = element.querySelector('.total-time').textContent.trim(); + // timestamp is sometimes 00:00, sometimes 0:00:00 + const duration = durationString.split(':').length === 3 + ? moment.duration(durationString).asSeconds() + : moment.duration(`00:${durationString}`).asSeconds(); - const ratingElement = element.querySelector('.rating'); - const stars = ratingElement && ratingElement.dataset.rating; + const ratingElement = element.querySelector('.rating'); + const stars = ratingElement && ratingElement.dataset.rating; - const [poster, ...primaryPhotos] = Array.from(element.querySelectorAll('img'), imageElement => imageElement.src); - const secondaryPhotos = $('.thumb-top, .thumb-bottom') - .map((photoIndex, photoElement) => $(photoElement).css()['background-image']) - .toArray() - .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); + const [poster, ...primaryPhotos] = Array.from(element.querySelectorAll('img'), imageElement => imageElement.src); + const secondaryPhotos = $('.thumb-top, .thumb-bottom') + .map((photoIndex, photoElement) => $(photoElement).css()['background-image']) + .toArray() + .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); - const photos = [...primaryPhotos, ...secondaryPhotos]; - const tags = deriveTagsFromDescription(description); + const photos = [...primaryPhotos, ...secondaryPhotos]; + const tags = deriveTagsFromDescription(description); - const scene = { - url, - entryId, - title, - description, - actors, - director: 'Mike Adriano', - date, - duration, - tags, - poster, - photos, - rating: { - stars, - }, - site, - }; + const scene = { + url, + entryId, + title, + description, + actors, + director: 'Mike Adriano', + date, + duration, + tags, + poster, + photos, + rating: { + stars, + }, + site, + }; - return scene; - })); + return scene; + })); } async function scrapeLatestB(html) { - const { document } = new JSDOM(html).window; - const sceneElements = document.querySelectorAll('.content-border'); + const { document } = new JSDOM(html).window; + const sceneElements = document.querySelectorAll('.content-border'); - return Promise.all(Array.from(sceneElements, async (element) => { - const $ = cheerio.load(element.innerHTML, { normalizeWhitespace: true }); - const release = { - director: 'Mike Adriano', - }; + return Promise.all(Array.from(sceneElements, async (element) => { + const $ = cheerio.load(element.innerHTML, { normalizeWhitespace: true }); + const release = { + director: 'Mike Adriano', + }; - const titleElement = element.querySelector('.content-title-wrap a'); - release.title = titleElement.title || titleElement.textContent.trim(); - release.url = titleElement.href; - release.entryId = release.url.split('/').slice(-2)[0]; + const titleElement = element.querySelector('.content-title-wrap a'); + release.title = titleElement.title || titleElement.textContent.trim(); + release.url = titleElement.href; + release.entryId = release.url.split('/').slice(-2)[0]; - release.description = element.querySelector('.content-description').textContent.trim(); - release.date = (moment(element.querySelector('.mobile-date').textContent, 'MM/DD/YYYY') + release.description = element.querySelector('.content-description').textContent.trim(); + release.date = (moment(element.querySelector('.mobile-date').textContent, 'MM/DD/YYYY') || moment(element.querySelector('.date').textContent, 'Do MMM YYYY')).toDate(); - release.actors = Array.from(element.querySelectorAll('.content-models a'), actorElement => actorElement.textContent); + release.actors = Array.from(element.querySelectorAll('.content-models a'), actorElement => actorElement.textContent); - const durationString = element.querySelector('.total-time').textContent.trim(); - // timestamp is somethines 00:00, sometimes 0:00:00 - release.duration = durationString.split(':').length === 3 - ? moment.duration(durationString).asSeconds() - : moment.duration(`00:${durationString}`).asSeconds(); + const durationString = element.querySelector('.total-time').textContent.trim(); + // timestamp is somethines 00:00, sometimes 0:00:00 + release.duration = durationString.split(':').length === 3 + ? moment.duration(durationString).asSeconds() + : moment.duration(`00:${durationString}`).asSeconds(); - const [poster, ...primaryPhotos] = Array.from(element.querySelectorAll('a img'), imageElement => imageElement.src); - const secondaryPhotos = $('.thumb-mouseover') - .map((photoIndex, photoElement) => $(photoElement).css()['background-image']) - .toArray() - .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); + const [poster, ...primaryPhotos] = Array.from(element.querySelectorAll('a img'), imageElement => imageElement.src); + const secondaryPhotos = $('.thumb-mouseover') + .map((photoIndex, photoElement) => $(photoElement).css()['background-image']) + .toArray() + .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); - release.poster = poster; - release.photos = [...primaryPhotos, ...secondaryPhotos]; + release.poster = poster; + release.photos = [...primaryPhotos, ...secondaryPhotos]; - release.tags = deriveTagsFromDescription(release.description); - return release; - })); + release.tags = deriveTagsFromDescription(release.description); + return release; + })); } async function scrapeSceneA(html, url) { - const { document } = new JSDOM(html).window; - const element = document.querySelector('.content-page-info'); - const release = { - url, - director: 'Mike Adriano', - }; + const { document } = new JSDOM(html).window; + const element = document.querySelector('.content-page-info'); + const release = { + url, + director: 'Mike Adriano', + }; - release.entryId = url.split('/').slice(-2)[0]; - release.title = element.querySelector('.title').textContent.trim(); - release.description = element.querySelector('.desc').textContent.trim(); - release.date = moment(element.querySelector('.post-date').textContent.trim(), 'Do MMM YYYY').toDate(); + release.entryId = url.split('/').slice(-2)[0]; + release.title = element.querySelector('.title').textContent.trim(); + release.description = element.querySelector('.desc').textContent.trim(); + release.date = moment(element.querySelector('.post-date').textContent.trim(), 'Do MMM YYYY').toDate(); - release.actors = Array.from(element.querySelectorAll('.models a'), actorElement => actorElement.textContent); + release.actors = Array.from(element.querySelectorAll('.models a'), actorElement => actorElement.textContent); - const durationString = element.querySelector('.total-time').textContent.trim(); - // timestamp is sometimes 00:00, sometimes 0:00:00 - release.duration = durationString.split(':').length === 3 - ? moment.duration(durationString).asSeconds() - : moment.duration(`00:${durationString}`).asSeconds(); + const durationString = element.querySelector('.total-time').textContent.trim(); + // timestamp is sometimes 00:00, sometimes 0:00:00 + release.duration = durationString.split(':').length === 3 + ? moment.duration(durationString).asSeconds() + : moment.duration(`00:${durationString}`).asSeconds(); - const { poster } = document.querySelector('.content-page-header video'); - const { src, type } = document.querySelector('.content-page-header source'); + const { poster } = document.querySelector('.content-page-header video'); + const { src, type } = document.querySelector('.content-page-header source'); - release.poster = poster; - release.trailer = { src, type }; + release.poster = poster; + release.trailer = { src, type }; - release.tags = deriveTagsFromDescription(release.description); + release.tags = deriveTagsFromDescription(release.description); - return release; + return release; } async function scrapeSceneB(html, url, site) { - const { document } = new JSDOM(html).window; - const element = document.querySelector('.content-page-info'); + const { document } = new JSDOM(html).window; + const element = document.querySelector('.content-page-info'); - const entryId = url.split('/').slice(-2)[0]; - const title = element.querySelector('.title').textContent.trim(); - const description = element.querySelector('.desc').textContent.trim(); - const date = moment(element.querySelector('.date').textContent.trim(), 'Do MMM YYYY').toDate(); + const entryId = url.split('/').slice(-2)[0]; + const title = element.querySelector('.title').textContent.trim(); + const description = element.querySelector('.desc').textContent.trim(); + const date = moment(element.querySelector('.date').textContent.trim(), 'Do MMM YYYY').toDate(); - const actors = Array.from(element.querySelectorAll('.models a'), actorElement => actorElement.textContent); + const actors = Array.from(element.querySelectorAll('.models a'), actorElement => actorElement.textContent); - const durationString = element.querySelector('.total-time').textContent.trim(); - // timestamp is somethines 00:00, sometimes 0:00:00 - const duration = durationString.split(':').length === 3 - ? moment.duration(durationString).asSeconds() - : moment.duration(`00:${durationString}`).asSeconds(); + const durationString = element.querySelector('.total-time').textContent.trim(); + // timestamp is somethines 00:00, sometimes 0:00:00 + const duration = durationString.split(':').length === 3 + ? moment.duration(durationString).asSeconds() + : moment.duration(`00:${durationString}`).asSeconds(); - const { poster } = document.querySelector('.content-page-header-inner video'); - const { src, type } = document.querySelector('.content-page-header-inner source'); + const { poster } = document.querySelector('.content-page-header-inner video'); + const { src, type } = document.querySelector('.content-page-header-inner source'); - const tags = deriveTagsFromDescription(description); + const tags = deriveTagsFromDescription(description); - const scene = { - url, - entryId, - title, - description, - actors, - director: 'Mike Adriano', - date, - duration, - tags, - poster, - trailer: { - src, - type, - }, - site, - }; + const scene = { + url, + entryId, + title, + description, + actors, + director: 'Mike Adriano', + date, + duration, + tags, + poster, + trailer: { + src, + type, + }, + site, + }; - return scene; + return scene; } async function fetchLatest(site, page = 1) { - const { host } = new URL(site.url); - const url = `https://tour.${host}/videos?page=${page}`; + const { host } = new URL(site.url); + const url = `https://tour.${host}/videos?page=${page}`; - const res = await get(url); + const res = await get(url); - if (res.code === 200) { - if (host === 'trueanal.com' || host === 'swallowed.com') { - return scrapeLatestA(res.html, site); - } + if (res.code === 200) { + if (host === 'trueanal.com' || host === 'swallowed.com') { + return scrapeLatestA(res.html, site); + } - return scrapeLatestB(res.html, site); - } + return scrapeLatestB(res.html, site); + } - return res.code; + return res.code; } async function fetchScene(url, site) { - const { host } = new URL(site.url); - const res = await get(url); + const { host } = new URL(site.url); + const res = await get(url); - if (res.code === 200) { - if (host === 'trueanal.com' || host === 'swallowed.com') { - return scrapeSceneA(res.body.toString(), url, site); - } + if (res.code === 200) { + if (host === 'trueanal.com' || host === 'swallowed.com') { + return scrapeSceneA(res.body.toString(), url, site); + } - return scrapeSceneB(res.body.toString(), url, site); - } + return scrapeSceneB(res.body.toString(), url, site); + } - return res.code; + return res.code; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/milehighmedia.js b/src/scrapers/milehighmedia.js index 3f7b0a39f..a77460a0f 100644 --- a/src/scrapers/milehighmedia.js +++ b/src/scrapers/milehighmedia.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'milehighmedia'); + return fetchProfile(actorName, 'milehighmedia'); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/mindgeek.js b/src/scrapers/mindgeek.js index 1d74499eb..ea5c5b197 100644 --- a/src/scrapers/mindgeek.js +++ b/src/scrapers/mindgeek.js @@ -13,257 +13,257 @@ const { inchesToCm, lbsToKg } = require('../utils/convert'); const { cookieToData } = require('../utils/cookies'); function getThumbs(scene) { - if (scene.images.poster) { - return scene.images.poster.map(image => image.xl.url); - } + if (scene.images.poster) { + return scene.images.poster.map(image => image.xl.url); + } - if (scene.images.card_main_rect) { - return scene.images.card_main_rect - .concat(scene.images.card_secondary_rect || []) - .map(image => image.xl.url.replace('.thumb', '')); - } + if (scene.images.card_main_rect) { + return scene.images.card_main_rect + .concat(scene.images.card_secondary_rect || []) + .map(image => image.xl.url.replace('.thumb', '')); + } - return []; + return []; } function scrapeLatestX(data, site) { - if (site.parameters?.extract === true && data.collections.length > 0) { - // release should not belong to any channel - return null; - } + if (site.parameters?.extract === true && data.collections.length > 0) { + // release should not belong to any channel + return null; + } - if (typeof site.parameters?.extract === 'string' && !data.collections.some(collection => collection.shortName === site.parameters.extract)) { - // release should belong to specific channel - return null; - } + if (typeof site.parameters?.extract === 'string' && !data.collections.some(collection => collection.shortName === site.parameters.extract)) { + // release should belong to specific channel + return null; + } - const release = { - entryId: data.id, - title: data.title, - description: data.description, - }; + const release = { + entryId: data.id, + title: data.title, + description: data.description, + }; - const hostname = site.parameters?.native ? site.url : site.network.url; + const hostname = site.parameters?.native ? site.url : site.network.url; - release.url = `${hostname}/scene/${release.entryId}/`; - release.date = new Date(data.dateReleased); - release.actors = data.actors.map(actor => ({ name: actor.name, gender: actor.gender })); + release.url = `${hostname}/scene/${release.entryId}/`; + release.date = new Date(data.dateReleased); + release.actors = data.actors.map(actor => ({ name: actor.name, gender: actor.gender })); - release.tags = data.tags.map(tag => tag.name); + release.tags = data.tags.map(tag => tag.name); - release.duration = data.videos.mediabook?.length; - [release.poster, ...release.photos] = getThumbs(data); + release.duration = data.videos.mediabook?.length; + [release.poster, ...release.photos] = getThumbs(data); - const teaserSources = data.videos.mediabook?.files; + const teaserSources = data.videos.mediabook?.files; - if (teaserSources) { - release.teaser = Object.values(teaserSources).map(teaser => ({ - src: teaser.urls.view, - quality: parseInt(teaser.format, 10), - })); - } + if (teaserSources) { + release.teaser = Object.values(teaserSources).map(teaser => ({ + src: teaser.urls.view, + quality: parseInt(teaser.format, 10), + })); + } - return release; + return release; } async function scrapeLatest(items, site) { - const latestReleases = await Promise.all(items.map(async data => scrapeLatestX(data, site))); + const latestReleases = await Promise.all(items.map(async data => scrapeLatestX(data, site))); - return latestReleases.filter(Boolean); + return latestReleases.filter(Boolean); } function scrapeScene(data, url, _site, networkName) { - const release = {}; + const release = {}; - const { id: entryId, title, description } = data; + const { id: entryId, title, description } = data; - release.entryId = data.id; - release.title = title; - release.description = description; + release.entryId = data.id; + release.title = title; + release.description = description; - release.date = new Date(data.dateReleased); - release.actors = data.actors.map(actor => ({ name: actor.name, gender: actor.gender })); + release.date = new Date(data.dateReleased); + release.actors = data.actors.map(actor => ({ name: actor.name, gender: actor.gender })); - release.tags = data.tags.map(tag => tag.name); + release.tags = data.tags.map(tag => tag.name); - [release.poster, ...release.photos] = getThumbs(data); + [release.poster, ...release.photos] = getThumbs(data); - const teaserSources = data.videos.mediabook?.files; + const teaserSources = data.videos.mediabook?.files; - if (teaserSources) { - release.teaser = Object.values(teaserSources).map(teaser => ({ - src: teaser.urls.view, - quality: parseInt(teaser.format, 10), - })); - } + if (teaserSources) { + release.teaser = Object.values(teaserSources).map(teaser => ({ + src: teaser.urls.view, + quality: parseInt(teaser.format, 10), + })); + } - const siteName = data.collections[0]?.name || data.brand; - release.channel = slugify(siteName, ''); + const siteName = data.collections[0]?.name || data.brand; + release.channel = slugify(siteName, ''); - release.url = url || `https://www.${networkName || data.brand}.com/scene/${entryId}/`; + release.url = url || `https://www.${networkName || data.brand}.com/scene/${entryId}/`; - return release; + return release; } function getUrl(site) { - const { search } = new URL(site.url); + const { search } = new URL(site.url); - if (search.match(/\?site=\d+/)) { - return site.url; - } + if (search.match(/\?site=\d+/)) { + return site.url; + } - if (site.parameters?.native) { - return `${site.url}/scenes`; - } + if (site.parameters?.native) { + return `${site.url}/scenes`; + } - if (site.parameters?.extract) { - return `${site.url}/scenes`; - } + if (site.parameters?.extract) { + return `${site.url}/scenes`; + } - if (site.parameters?.siteId) { - return `${site.network.url}/scenes?site=${site.parameters.siteId}`; - } + if (site.parameters?.siteId) { + return `${site.network.url}/scenes?site=${site.parameters.siteId}`; + } - throw new Error(`Mind Geek site '${site.name}' (${site.url}) not supported`); + throw new Error(`Mind Geek site '${site.name}' (${site.url}) not supported`); } async function getSession(url) { - const cookieJar = new CookieJar(); - const session = bhttp.session({ cookieJar }); + const cookieJar = new CookieJar(); + const session = bhttp.session({ cookieJar }); - await session.get(url); + await session.get(url); - const cookieString = await cookieJar.getCookieStringAsync(url); - const { instance_token: instanceToken } = cookieToData(cookieString); + const cookieString = await cookieJar.getCookieStringAsync(url); + const { instance_token: instanceToken } = cookieToData(cookieString); - return { session, instanceToken }; + return { session, instanceToken }; } function scrapeProfile(data, html, releases = [], networkName) { - const { qa, qd } = ex(html); + const { qa, qd } = ex(html); - const profile = { - description: data.bio, - aliases: data.aliases, - }; + const profile = { + description: data.bio, + aliases: data.aliases, + }; - const [bust, waist, hip] = data.measurements.split('-'); + const [bust, waist, hip] = data.measurements.split('-'); - profile.gender = data.gender === 'other' ? 'transsexual' : data.gender; + profile.gender = data.gender === 'other' ? 'transsexual' : data.gender; - if (profile.gender === 'female') { - if (bust) profile.bust = bust.toUpperCase(); - if (waist) profile.waist = waist; - if (hip) profile.hip = hip; - } + if (profile.gender === 'female') { + if (bust) profile.bust = bust.toUpperCase(); + if (waist) profile.waist = waist; + if (hip) profile.hip = hip; + } - if (data.birthPlace) profile.birthPlace = data.birthPlace; - if (data.height) profile.height = inchesToCm(data.height); - if (data.weight) profile.weight = lbsToKg(data.weight); + if (data.birthPlace) profile.birthPlace = data.birthPlace; + if (data.height) profile.height = inchesToCm(data.height); + if (data.weight) profile.weight = lbsToKg(data.weight); - if (data.images.card_main_rect?.[0]) { - profile.avatar = data.images.card_main_rect[0].xl?.url + if (data.images.card_main_rect?.[0]) { + profile.avatar = data.images.card_main_rect[0].xl?.url || data.images.card_main_rect[0].lg?.url || data.images.card_main_rect[0].md?.url || data.images.card_main_rect[0].sm?.url || data.images.card_main_rect[0].xs?.url; - } + } - const birthdate = qa('li').find(el => /Date of Birth/.test(el.textContent)); - if (birthdate) profile.birthdate = qd(birthdate, 'span', 'MMMM Do, YYYY'); + const birthdate = qa('li').find(el => /Date of Birth/.test(el.textContent)); + if (birthdate) profile.birthdate = qd(birthdate, 'span', 'MMMM Do, YYYY'); - profile.releases = releases.map(release => scrapeScene(release, null, null, networkName)); + profile.releases = releases.map(release => scrapeScene(release, null, null, networkName)); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = getUrl(site); - const { search } = new URL(url); - const siteId = new URLSearchParams(search).get('site'); + const url = getUrl(site); + const { search } = new URL(url); + const siteId = new URLSearchParams(search).get('site'); - const { session, instanceToken } = await getSession(url); + const { session, instanceToken } = await getSession(url); - const beforeDate = moment().add('1', 'day').format('YYYY-MM-DD'); - const limit = 10; - const apiUrl = site.parameters?.native || site.parameters?.extract - ? `https://site-api.project1service.com/v2/releases?dateReleased=<${beforeDate}&limit=${limit}&offset=${limit * (page - 1)}&orderBy=-dateReleased&type=scene` - : `https://site-api.project1service.com/v2/releases?collectionId=${siteId}&dateReleased=<${beforeDate}&limit=${limit}&offset=${limit * (page - 1)}&orderBy=-dateReleased&type=scene`; + const beforeDate = moment().add('1', 'day').format('YYYY-MM-DD'); + const limit = 10; + const apiUrl = site.parameters?.native || site.parameters?.extract + ? `https://site-api.project1service.com/v2/releases?dateReleased=<${beforeDate}&limit=${limit}&offset=${limit * (page - 1)}&orderBy=-dateReleased&type=scene` + : `https://site-api.project1service.com/v2/releases?collectionId=${siteId}&dateReleased=<${beforeDate}&limit=${limit}&offset=${limit * (page - 1)}&orderBy=-dateReleased&type=scene`; - const res = await session.get(apiUrl, { - headers: { - Instance: instanceToken, - Origin: site.url, - Referer: url, - }, - }); + const res = await session.get(apiUrl, { + headers: { + Instance: instanceToken, + Origin: site.url, + Referer: url, + }, + }); - if (res.statusCode === 200 && res.body.result) { - return scrapeLatest(res.body.result, site); - } + if (res.statusCode === 200 && res.body.result) { + return scrapeLatest(res.body.result, site); + } - return null; + return null; } async function fetchScene(url, site) { - const entryId = url.match(/\d+/)[0]; - const { session, instanceToken } = await getSession(url); + const entryId = url.match(/\d+/)[0]; + const { session, instanceToken } = await getSession(url); - const res = await session.get(`https://site-api.project1service.com/v2/releases/${entryId}`, { - headers: { - Instance: instanceToken, - }, - }); + const res = await session.get(`https://site-api.project1service.com/v2/releases/${entryId}`, { + headers: { + Instance: instanceToken, + }, + }); - if (res.statusCode === 200 && res.body.result) { - return scrapeScene(res.body.result, url, site); - } + if (res.statusCode === 200 && res.body.result) { + return scrapeScene(res.body.result, url, site); + } - return null; + return null; } async function fetchProfile(actorName, networkName, actorPath = 'model') { - const url = `https://www.${networkName}.com`; - const { session, instanceToken } = await getSession(url); + const url = `https://www.${networkName}.com`; + const { session, instanceToken } = await getSession(url); - const res = await session.get(`https://site-api.project1service.com/v1/actors/?search=${encodeURI(actorName)}`, { - headers: { - Instance: instanceToken, - }, - }); + const res = await session.get(`https://site-api.project1service.com/v1/actors/?search=${encodeURI(actorName)}`, { + headers: { + Instance: instanceToken, + }, + }); - if (res.statusCode === 200) { - const actorData = res.body.result.find(actor => actor.name.toLowerCase() === actorName.toLowerCase()); + if (res.statusCode === 200) { + const actorData = res.body.result.find(actor => actor.name.toLowerCase() === actorName.toLowerCase()); - if (actorData) { - const actorUrl = `https://www.${networkName}.com/${actorPath}/${actorData.id}/`; - const actorReleasesUrl = `https://site-api.project1service.com/v2/releases?actorId=${actorData.id}&limit=100&offset=0&orderBy=-dateReleased&type=scene`; + if (actorData) { + const actorUrl = `https://www.${networkName}.com/${actorPath}/${actorData.id}/`; + const actorReleasesUrl = `https://site-api.project1service.com/v2/releases?actorId=${actorData.id}&limit=100&offset=0&orderBy=-dateReleased&type=scene`; - const [actorRes, actorReleasesRes] = await Promise.all([ - bhttp.get(actorUrl), - session.get(actorReleasesUrl, { - headers: { - Instance: instanceToken, - }, - }), - ]); + const [actorRes, actorReleasesRes] = await Promise.all([ + bhttp.get(actorUrl), + session.get(actorReleasesUrl, { + headers: { + Instance: instanceToken, + }, + }), + ]); - if (actorRes.statusCode === 200 && actorReleasesRes.statusCode === 200 && actorReleasesRes.body.result) { - return scrapeProfile(actorData, actorRes.body.toString(), actorReleasesRes.body.result, networkName); - } + if (actorRes.statusCode === 200 && actorReleasesRes.statusCode === 200 && actorReleasesRes.body.result) { + return scrapeProfile(actorData, actorRes.body.toString(), actorReleasesRes.body.result, networkName); + } - if (actorRes.statusCode === 200) { - return scrapeProfile(actorData, actorRes.body.toString(), null, networkName); - } - } - } + if (actorRes.statusCode === 200) { + return scrapeProfile(actorData, actorRes.body.toString(), null, networkName); + } + } + } - return null; + return null; } module.exports = { - scrapeLatestX, - fetchLatest, - fetchScene, - fetchProfile, + scrapeLatestX, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/mofos.js b/src/scrapers/mofos.js index 274c6cda6..83cfd1a7c 100644 --- a/src/scrapers/mofos.js +++ b/src/scrapers/mofos.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'mofos'); + return fetchProfile(actorName, 'mofos'); } module.exports = { - fetchLatest, - fetchScene, - fetchProfile: networkFetchProfile, + fetchLatest, + fetchScene, + fetchProfile: networkFetchProfile, }; diff --git a/src/scrapers/naughtyamerica.js b/src/scrapers/naughtyamerica.js index 95911c523..d16ab7066 100644 --- a/src/scrapers/naughtyamerica.js +++ b/src/scrapers/naughtyamerica.js @@ -9,149 +9,149 @@ const slugify = require('../utils/slugify'); const { ex, get } = require('../utils/q'); function titleExtractor(pathname) { - const components = pathname.split('/')[2].split('-'); - const entryId = components.slice(-1)[0]; + const components = pathname.split('/')[2].split('-'); + const entryId = components.slice(-1)[0]; - const title = components.slice(0, -1).reduce((accTitle, word, index) => `${accTitle}${index > 0 ? ' ' : ''}${word.slice(0, 1).toUpperCase()}${word.slice(1)}`, ''); + const title = components.slice(0, -1).reduce((accTitle, word, index) => `${accTitle}${index > 0 ? ' ' : ''}${word.slice(0, 1).toUpperCase()}${word.slice(1)}`, ''); - return { title, entryId }; + return { title, entryId }; } function scrapeLatest(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const sceneElements = $('.site-list .scene-item').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const sceneElements = $('.site-list .scene-item').toArray(); - return sceneElements.map((item) => { - const element = $(item); + return sceneElements.map((item) => { + const element = $(item); - const sceneLinkElement = element.find('a').first(); - const { protocol, hostname, pathname } = new URL(sceneLinkElement.attr('href')); - const url = `${protocol}//${hostname}${pathname}`; - const { title, entryId } = titleExtractor(pathname); + const sceneLinkElement = element.find('a').first(); + const { protocol, hostname, pathname } = new URL(sceneLinkElement.attr('href')); + const url = `${protocol}//${hostname}${pathname}`; + const { title, entryId } = titleExtractor(pathname); - const date = moment.utc(element.find('.entry-date').text(), 'MMM D, YYYY').toDate(); - const actors = element.find('.contain-actors a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + const date = moment.utc(element.find('.entry-date').text(), 'MMM D, YYYY').toDate(); + const actors = element.find('.contain-actors a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - const duration = Number(element.find('.scene-runtime').text().slice(0, -4)) * 60; + const duration = Number(element.find('.scene-runtime').text().slice(0, -4)) * 60; - const posterString = sceneLinkElement.find('img[data-srcset]').attr('data-srcset') || sceneLinkElement.find('img[data-src]').attr('data-src'); - const poster = `https:${posterString.match(/[\w/.]+$/)[0]}`; + const posterString = sceneLinkElement.find('img[data-srcset]').attr('data-srcset') || sceneLinkElement.find('img[data-src]').attr('data-src'); + const poster = `https:${posterString.match(/[\w/.]+$/)[0]}`; - return { - url, - entryId, - title, - actors, - date, - duration, - poster, - rating: null, - site, - }; - }); + return { + url, + entryId, + title, + actors, + date, + duration, + poster, + rating: null, + site, + }; + }); } function scrapeScene(html, url, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const sceneElement = $('.scene-info'); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const sceneElement = $('.scene-info'); - const { protocol, hostname, pathname } = new URL(url); - const originalUrl = `${protocol}//${hostname}${pathname}`; + const { protocol, hostname, pathname } = new URL(url); + const originalUrl = `${protocol}//${hostname}${pathname}`; - const entryId = originalUrl.split('-').slice(-1)[0]; - const title = sceneElement.find('h1.scene-title.grey-text').text(); - const description = sceneElement.find('.synopsis').contents().slice(2).text().replace(/[\s\n]+/g, ' ').trim(); + const entryId = originalUrl.split('-').slice(-1)[0]; + const title = sceneElement.find('h1.scene-title.grey-text').text(); + const description = sceneElement.find('.synopsis').contents().slice(2).text().replace(/[\s\n]+/g, ' ').trim(); - const date = moment.utc(sceneElement.find('span.entry-date').text(), 'MMM D, YYYY').toDate(); - const actors = $('a.scene-title.grey-text.link').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + const date = moment.utc(sceneElement.find('span.entry-date').text(), 'MMM D, YYYY').toDate(); + const actors = $('a.scene-title.grey-text.link').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - const duration = Number(sceneElement.find('.duration-ratings .duration').text().slice(10, -4)) * 60; + const duration = Number(sceneElement.find('.duration-ratings .duration').text().slice(10, -4)) * 60; - const poster = `https:${$('video, dl8-video').attr('poster')}`; - const photos = $('.contain-scene-images.desktop-only a').map((index, el) => `https:${$(el).attr('href')}`).toArray(); + const poster = `https:${$('video, dl8-video').attr('poster')}`; + const photos = $('.contain-scene-images.desktop-only a').map((index, el) => `https:${$(el).attr('href')}`).toArray(); - const trailerEl = $('source'); - const trailerSrc = trailerEl.attr('src'); - const trailerType = trailerEl.attr('type'); + const trailerEl = $('source'); + const trailerSrc = trailerEl.attr('src'); + const trailerType = trailerEl.attr('type'); - const siteName = sceneElement.find('a.site-title').text(); - const channel = siteName.replace(/[\s']+/g, '').toLowerCase(); + const siteName = sceneElement.find('a.site-title').text(); + const channel = siteName.replace(/[\s']+/g, '').toLowerCase(); - const tags = $('.categories a.cat-tag').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); + const tags = $('.categories a.cat-tag').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); - return { - url, - entryId, - title, - description, - actors, - date, - duration, - tags, - photos, - poster, - trailer: { - src: trailerSrc, - type: trailerType, - }, - rating: null, - site, - channel, - }; + return { + url, + entryId, + title, + description, + actors, + date, + duration, + tags, + photos, + poster, + trailer: { + src: trailerSrc, + type: trailerType, + }, + rating: null, + site, + channel, + }; } async function fetchActorReleases(url) { - const res = await get(url); + const res = await get(url); - return res.ok - ? res.item.qu.urls('.contain-block:not(.live-scenes) .scene-item > a:first-child') // live scenes repeat on all pages - : []; + return res.ok + ? res.item.qu.urls('.contain-block:not(.live-scenes) .scene-item > a:first-child') // live scenes repeat on all pages + : []; } async function scrapeProfile(html) { - const { qu } = ex(html); - const profile = {}; + const { qu } = ex(html); + const profile = {}; - profile.description = qu.q('.bio_about_text', true); + profile.description = qu.q('.bio_about_text', true); - const avatar = qu.q('img.performer-pic', 'src'); - if (avatar) profile.avatar = `https:${avatar}`; + const avatar = qu.q('img.performer-pic', 'src'); + if (avatar) profile.avatar = `https:${avatar}`; - const releases = qu.urls('.scene-item > a:first-child'); - const otherPages = qu.urls('.pagination a:not([rel=next]):not([rel=prev])'); - const olderReleases = await Promise.all(otherPages.map(async page => fetchActorReleases(page))); + const releases = qu.urls('.scene-item > a:first-child'); + const otherPages = qu.urls('.pagination a:not([rel=next]):not([rel=prev])'); + const olderReleases = await Promise.all(otherPages.map(async page => fetchActorReleases(page))); - profile.releases = releases.concat(olderReleases.flat()); + profile.releases = releases.concat(olderReleases.flat()); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const res = await bhttp.get(`${site.url}?page=${page}`); + const res = await bhttp.get(`${site.url}?page=${page}`); - return scrapeLatest(res.body.toString(), site); + return scrapeLatest(res.body.toString(), site); } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - return scrapeScene(res.body.toString(), url, site); + return scrapeScene(res.body.toString(), url, site); } async function fetchProfile(actorName) { - const actorSlug = slugify(actorName); + const actorSlug = slugify(actorName); - const res = await bhttp.get(`https://www.naughtyamerica.com/pornstar/${actorSlug}`); + const res = await bhttp.get(`https://www.naughtyamerica.com/pornstar/${actorSlug}`); - if (res.statusCode === 200) { - return scrapeProfile(res.body.toString()); - } + if (res.statusCode === 200) { + return scrapeProfile(res.body.toString()); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/newsensations.js b/src/scrapers/newsensations.js index 6f2b9a7b4..05ad3a726 100644 --- a/src/scrapers/newsensations.js +++ b/src/scrapers/newsensations.js @@ -3,75 +3,75 @@ const { geta, ed } = require('../utils/q'); function scrapeBlockLatest(scenes) { - return scenes.map(({ html, qu }) => { - const release = {}; + return scenes.map(({ html, qu }) => { + const release = {}; - const entryId = qu.q('div[class*="videothumb"]', 'class').match(/videothumb_(\d+)/) + const entryId = qu.q('div[class*="videothumb"]', 'class').match(/videothumb_(\d+)/) || qu.q('div[id*="videothumb"]', 'id').match(/videothumb_(\d+)/); - release.entryId = entryId[1]; + release.entryId = entryId[1]; - release.title = qu.q('h4 a', true); - release.url = qu.url('h4 a'); - release.date = ed(html, 'MM/DD/YYYY', /\d{2}\/\d{2}\/\d{4}/); + release.title = qu.q('h4 a', true); + release.url = qu.url('h4 a'); + release.date = ed(html, 'MM/DD/YYYY', /\d{2}\/\d{2}\/\d{4}/); - release.actors = qu.all('.tour_update_models a', true); + release.actors = qu.all('.tour_update_models a', true); - release.poster = qu.q('div img').dataset.src; - release.photos = [qu.q('div img', 'src0_4x') || qu.q('div img', 'src0_3x') || qu.q('div img', 'src0_2x')]; + release.poster = qu.q('div img').dataset.src; + release.photos = [qu.q('div img', 'src0_4x') || qu.q('div img', 'src0_3x') || qu.q('div img', 'src0_2x')]; - release.teaser = qu.video(); + release.teaser = qu.video(); - return release; - }); + return release; + }); } function scrapeClassicLatest(scenes) { - return scenes.map(({ el, qu }) => { - const release = {}; + return scenes.map(({ el, qu }) => { + const release = {}; - release.entryId = el.dataset.setid; - release.url = qu.url('a'); + release.entryId = el.dataset.setid; + release.url = qu.url('a'); - release.title = qu.q('.update_title_small', true) || qu.q('a:nth-child(2)', true); + release.title = qu.q('.update_title_small', true) || qu.q('a:nth-child(2)', true); - const description = qu.q('a', 'title'); - if (description) release.description = description; + const description = qu.q('a', 'title'); + if (description) release.description = description; - const date = qu.date('.date_small, .update_date', 'MM/DD/YYYY'); - if (date) release.date = date; + const date = qu.date('.date_small, .update_date', 'MM/DD/YYYY'); + if (date) release.date = date; - const durationLine = qu.q('.update_counts', true); - if (durationLine) release.duration = Number(durationLine.match(/(\d+) min/i)[1]) * 60; + const durationLine = qu.q('.update_counts', true); + if (durationLine) release.duration = Number(durationLine.match(/(\d+) min/i)[1]) * 60; - const actors = qu.all('.update_models a', true); - release.actors = actors.length > 0 ? actors : qu.q('.update_models', true).split(/,\s*/); + const actors = qu.all('.update_models a', true); + release.actors = actors.length > 0 ? actors : qu.q('.update_models', true).split(/,\s*/); - const photoCount = qu.q('.update_thumb', 'cnt'); - [release.poster, ...release.photos] = Array.from({ length: photoCount }) - .map((value, index) => qu.q('.update_thumb', `src${index}_3x`) + const photoCount = qu.q('.update_thumb', 'cnt'); + [release.poster, ...release.photos] = Array.from({ length: photoCount }) + .map((value, index) => qu.q('.update_thumb', `src${index}_3x`) || qu.q('.update_thumb', `src${index}_2x`) || qu.q('.update_thumb', `src${index}_1x`)); - return release; - }); + return release; + }); } async function fetchLatest(site, page = 1) { - if (!site.parameters) { - return null; - } + if (!site.parameters) { + return null; + } - const url = `${site.url}/tour_${site.parameters.siteId}/categories/movies_${page}_d.html`; - const res = await geta(url, '.updatesBlock .movieBlock, .updatesBlock .videoBlock, .latest_updates_block .update_details, .category_listing_block .update_details'); + const url = `${site.url}/tour_${site.parameters.siteId}/categories/movies_${page}_d.html`; + const res = await geta(url, '.updatesBlock .movieBlock, .updatesBlock .videoBlock, .latest_updates_block .update_details, .category_listing_block .update_details'); - if (res.ok && site.parameters.block) { - return scrapeBlockLatest(res.items, site); - } + if (res.ok && site.parameters.block) { + return scrapeBlockLatest(res.items, site); + } - return res.ok ? scrapeClassicLatest(res.items, site) : res.status; + return res.ok ? scrapeClassicLatest(res.items, site) : res.status; } module.exports = { - fetchLatest, + fetchLatest, }; diff --git a/src/scrapers/nubiles.js b/src/scrapers/nubiles.js index a2c4a77c3..a084ef8f3 100644 --- a/src/scrapers/nubiles.js +++ b/src/scrapers/nubiles.js @@ -5,161 +5,161 @@ const slugify = require('../utils/slugify'); const { heightToCm } = require('../utils/convert'); const slugUrlMap = { - nubiles: 'https://www.nubiles.net', - nubilesporn: 'https://www.nubiles-porn.com', + nubiles: 'https://www.nubiles.net', + nubilesporn: 'https://www.nubiles-porn.com', }; async function getPhotos(albumUrl) { - const res = await geta(albumUrl, '.photo-thumb'); + const res = await geta(albumUrl, '.photo-thumb'); - return res.ok - ? res.items.map(({ q }) => q('source').srcset) - : []; + return res.ok + ? res.items.map(({ q }) => q('source').srcset) + : []; } function scrapeAll(scenes, site, origin) { - return scenes.map(({ qu }) => { - const release = {}; + return scenes.map(({ qu }) => { + const release = {}; - release.title = qu.q('.title a', true); + release.title = qu.q('.title a', true); - const url = qu.url('.title a').split('?')[0]; - const channelUrl = qu.url('.site-link'); + const url = qu.url('.title a').split('?')[0]; + const channelUrl = qu.url('.site-link'); - if (/^http/.test(url)) { - const { pathname } = new URL(url); - release.entryId = pathname.split('/')[3]; + if (/^http/.test(url)) { + const { pathname } = new URL(url); + release.entryId = pathname.split('/')[3]; - if (channelUrl) release.url = `${channelUrl}${pathname}`; - else release.url = url; - } else if (!/\/join/.test(url)) { - release.entryId = url.split('/')[3]; + if (channelUrl) release.url = `${channelUrl}${pathname}`; + else release.url = url; + } else if (!/\/join/.test(url)) { + release.entryId = url.split('/')[3]; - if (channelUrl) release.url = `${channelUrl}${url}`; - else if (site?.url) release.url = `${site.url}${url}`; - else if (origin) release.url = `${origin}${url}`; - } else { - release.entryId = qu.q('a img', 'tube_tour_thumb_id'); - } + if (channelUrl) release.url = `${channelUrl}${url}`; + else if (site?.url) release.url = `${site.url}${url}`; + else if (origin) release.url = `${origin}${url}`; + } else { + release.entryId = qu.q('a img', 'tube_tour_thumb_id'); + } - release.date = qu.date('.date', 'MMM D, YYYY'); - release.actors = qu.all('.models a.model', true); + release.date = qu.date('.date', 'MMM D, YYYY'); + release.actors = qu.all('.models a.model', true); - const poster = qu.q('img').dataset.original; - release.poster = [ - poster.replace('_640', '_1280'), - poster, - ]; + const poster = qu.q('img').dataset.original; + release.poster = [ + poster.replace('_640', '_1280'), + poster, + ]; - release.stars = Number(qu.q('.rating', true)); - release.likes = Number(qu.q('.likes', true)); + release.stars = Number(qu.q('.rating', true)); + release.likes = Number(qu.q('.likes', true)); - return release; - }); + return release; + }); } async function scrapeScene({ qu }, url, site) { - const release = {}; + const release = {}; - const { origin, pathname } = new URL(url); - release.url = `${origin}${pathname}`; + const { origin, pathname } = new URL(url); + release.url = `${origin}${pathname}`; - release.entryId = new URL(url).pathname.split('/')[3]; - release.title = qu.q('.content-pane-title h2', true); - release.description = qu.q('.content-pane-column div', true); + release.entryId = new URL(url).pathname.split('/')[3]; + release.title = qu.q('.content-pane-title h2', true); + release.description = qu.q('.content-pane-column div', true); - release.date = qu.q('.date', 'MMM D, YYYY'); + release.date = qu.q('.date', 'MMM D, YYYY'); - release.actors = qu.all('.content-pane-performers .model', true); - release.tags = qu.all('.categories a', true); + release.actors = qu.all('.content-pane-performers .model', true); + release.tags = qu.all('.categories a', true); - release.poster = qu.poster() || qu.img('.fake-video-player img'); - release.trailer = qu.all('source').map(source => ({ - src: source.src, - quality: Number(source.getAttribute('res')), - })); + release.poster = qu.poster() || qu.img('.fake-video-player img'); + release.trailer = qu.all('source').map(source => ({ + src: source.src, + quality: Number(source.getAttribute('res')), + })); - release.stars = Number(qu.q('.score', true)); - release.likes = Number(qu.q('#likecount', true)); + release.stars = Number(qu.q('.score', true)); + release.likes = Number(qu.q('#likecount', true)); - const albumLink = qu.url('.content-pane-related-links a[href*="gallery"]'); - if (albumLink) release.photos = await getPhotos(`${site.url}${albumLink}`); + const albumLink = qu.url('.content-pane-related-links a[href*="gallery"]'); + if (albumLink) release.photos = await getPhotos(`${site.url}${albumLink}`); - return release; + return release; } function scrapeProfile({ qu }, _actorName, origin) { - const profile = {}; + const profile = {}; - const keys = qu.all('.model-profile h5', true); - const values = qu.all('.model-profile h5 + p', true); + const keys = qu.all('.model-profile h5', true); + const values = qu.all('.model-profile h5 + p', true); - const bio = keys.reduce((acc, key, index) => ({ ...acc, [slugify(key, '_')]: values[index] }), {}); + const bio = keys.reduce((acc, key, index) => ({ ...acc, [slugify(key, '_')]: values[index] }), {}); - profile.age = Number(bio.age); - profile.description = qu.q('.model-bio', true); + profile.age = Number(bio.age); + profile.description = qu.q('.model-bio', true); - profile.residencePlace = bio.location; + profile.residencePlace = bio.location; - profile.height = heightToCm(bio.height); - [profile.bust, profile.waist, profile.hip] = bio.figure.split('-').map(v => Number(v) || v); + profile.height = heightToCm(bio.height); + [profile.bust, profile.waist, profile.hip] = bio.figure.split('-').map(v => Number(v) || v); - profile.avatar = qu.img('.model-profile img'); + profile.avatar = qu.img('.model-profile img'); - const releases = qu.all('.content-grid-item').filter(el => /video\//.test(qu.url(el, '.img-wrapper a'))); // filter out photos - profile.releases = scrapeAll(ctxa(releases), null, origin); + const releases = qu.all('.content-grid-item').filter(el => /video\//.test(qu.url(el, '.img-wrapper a'))); // filter out photos + profile.releases = scrapeAll(ctxa(releases), null, origin); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = `${site.url}/video/gallery/${(page - 1) * 12}`; - const res = await geta(url, '.content-grid-item'); + const url = `${site.url}/video/gallery/${(page - 1) * 12}`; + const res = await geta(url, '.content-grid-item'); - return res.ok ? scrapeAll(res.items, site) : res.status; + return res.ok ? scrapeAll(res.items, site) : res.status; } async function fetchUpcoming(site) { - if (site.parameters?.upcoming) { - const url = `${site.url}/video/upcoming`; - const res = await geta(url, '.content-grid-item'); + if (site.parameters?.upcoming) { + const url = `${site.url}/video/upcoming`; + const res = await geta(url, '.content-grid-item'); - return res.ok ? scrapeAll(res.items, site) : res.status; - } + return res.ok ? scrapeAll(res.items, site) : res.status; + } - return []; + return []; } async function fetchScene(url, site) { - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeScene(res.item, url, site) : res.status; + return res.ok ? scrapeScene(res.item, url, site) : res.status; } async function fetchProfile(actorName, siteSlug) { - const firstLetter = actorName.charAt(0).toLowerCase(); - const origin = slugUrlMap[siteSlug] || `https://www.${siteSlug}.com`; + const firstLetter = actorName.charAt(0).toLowerCase(); + const origin = slugUrlMap[siteSlug] || `https://www.${siteSlug}.com`; - const url = `${origin}/model/alpha/${firstLetter}`; - const resModels = await get(url); + const url = `${origin}/model/alpha/${firstLetter}`; + const resModels = await get(url); - if (!resModels.ok) return resModels.status; + if (!resModels.ok) return resModels.status; - const modelPath = resModels.item.qu.all('.content-grid-item a.title').find(el => slugify(el.textContent) === slugify(actorName)); + const modelPath = resModels.item.qu.all('.content-grid-item a.title').find(el => slugify(el.textContent) === slugify(actorName)); - if (modelPath) { - const modelUrl = `${origin}${modelPath}`; - const resModel = await get(modelUrl); + if (modelPath) { + const modelUrl = `${origin}${modelPath}`; + const resModel = await get(modelUrl); - return resModel.ok ? scrapeProfile(resModel.item, actorName, origin) : resModel.status; - } + return resModel.ok ? scrapeProfile(resModel.item, actorName, origin) : resModel.status; + } - return null; + return null; } module.exports = { - fetchLatest, - fetchUpcoming, - fetchScene, - fetchProfile, + fetchLatest, + fetchUpcoming, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/perfectgonzo.js b/src/scrapers/perfectgonzo.js index b70cf824a..7247416eb 100644 --- a/src/scrapers/perfectgonzo.js +++ b/src/scrapers/perfectgonzo.js @@ -7,143 +7,143 @@ const knex = require('../knex'); const { ex, ctxa } = require('../utils/q'); async function getSiteSlugs() { - return knex('sites') - .pluck('sites.slug') - .join('networks', 'networks.id', 'sites.network_id') - .where('networks.slug', 'perfectgonzo'); + return knex('sites') + .pluck('sites.slug') + .join('networks', 'networks.id', 'sites.network_id') + .where('networks.slug', 'perfectgonzo'); } function getHash(identifier) { - const hash = blake2.createHash('blake2b', { digestLength: 8 }); + const hash = blake2.createHash('blake2b', { digestLength: 8 }); - hash.update(Buffer.from(identifier)); + hash.update(Buffer.from(identifier)); - return hash.digest('hex'); + return hash.digest('hex'); } function extractMaleModelsFromTags(tagContainer) { - if (!tagContainer) { - return []; - } + if (!tagContainer) { + return []; + } - const tagEls = Array.from(tagContainer.childNodes, node => ({ type: node.nodeType, text: node.textContent.trim() })).filter(node => node.text.length > 0); - const modelLabelIndex = tagEls.findIndex(node => node.text === 'Male Models'); + const tagEls = Array.from(tagContainer.childNodes, node => ({ type: node.nodeType, text: node.textContent.trim() })).filter(node => node.text.length > 0); + const modelLabelIndex = tagEls.findIndex(node => node.text === 'Male Models'); - if (modelLabelIndex > -1) { - const nextLabelIndex = tagEls.findIndex((node, index) => index > modelLabelIndex && node.type === 3); - const maleModels = tagEls.slice(modelLabelIndex + 1, nextLabelIndex); + if (modelLabelIndex > -1) { + const nextLabelIndex = tagEls.findIndex((node, index) => index > modelLabelIndex && node.type === 3); + const maleModels = tagEls.slice(modelLabelIndex + 1, nextLabelIndex); - return maleModels.map(model => model.text); - } + return maleModels.map(model => model.text); + } - return []; + return []; } async function extractChannelFromPhoto(photo, metaSiteSlugs) { - const siteSlugs = metaSiteSlugs || await getSiteSlugs(); - const channelMatch = photo.match(new RegExp(siteSlugs.join('|'))); + const siteSlugs = metaSiteSlugs || await getSiteSlugs(); + const channelMatch = photo.match(new RegExp(siteSlugs.join('|'))); - if (channelMatch) { - return channelMatch[0]; - } + if (channelMatch) { + return channelMatch[0]; + } - return null; + return null; } async function scrapeLatest(html, site) { - const siteSlugs = await getSiteSlugs(); - const { element } = ex(html); + const siteSlugs = await getSiteSlugs(); + const { element } = ex(html); - return ctxa(element, '#content-main .itemm').map(({ - q, qa, qlength, qdate, qimages, - }) => { - const release = { - site, - meta: { - siteSlugs, - }, - }; + return ctxa(element, '#content-main .itemm').map(({ + q, qa, qlength, qdate, qimages, + }) => { + const release = { + site, + meta: { + siteSlugs, + }, + }; - const sceneLink = q('a'); + const sceneLink = q('a'); - release.title = sceneLink.title; - release.url = `${site.url}${sceneLink.href}`; - release.date = qdate('.nm-date', 'MM/DD/YYYY'); + release.title = sceneLink.title; + release.url = `${site.url}${sceneLink.href}`; + release.date = qdate('.nm-date', 'MM/DD/YYYY'); - const slug = new URL(release.url).pathname.split('/')[2]; - release.entryId = getHash(`${site.slug}${slug}${release.date.toISOString()}`); + const slug = new URL(release.url).pathname.split('/')[2]; + release.entryId = getHash(`${site.slug}${slug}${release.date.toISOString()}`); - release.actors = release.title.split('&').map(actor => actor.trim()); + release.actors = release.title.split('&').map(actor => actor.trim()); - [release.poster, ...release.photos] = qimages('.bloc-link img'); + [release.poster, ...release.photos] = qimages('.bloc-link img'); - release.tags = qa('.dropdown ul a', true).slice(1); - release.duration = qlength('.dropdown p:first-child'); + release.tags = qa('.dropdown ul a', true).slice(1); + release.duration = qlength('.dropdown p:first-child'); - return release; - }); + return release; + }); } async function scrapeScene(html, site, url, metaSiteSlugs) { - const { - q, qa, qlength, qdate, qposter, qtrailer, - } = ex(html); + const { + q, qa, qlength, qdate, qposter, qtrailer, + } = ex(html); - const release = { url, site }; + const release = { url, site }; - release.title = q('#movie-header h2', true); - release.date = qdate('#movie-header div span', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/); + release.title = q('#movie-header h2', true); + release.date = qdate('#movie-header div span', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/); - release.description = q('.container .mg-md', true); - release.duration = qlength('#video-ribbon .container > div > span:nth-child(3)'); + release.description = q('.container .mg-md', true); + release.duration = qlength('#video-ribbon .container > div > span:nth-child(3)'); - release.actors = qa('#video-info a', true).concat(extractMaleModelsFromTags(q('.tag-container'))); - release.tags = qa('.tag-container a', true); + release.actors = qa('#video-info a', true).concat(extractMaleModelsFromTags(q('.tag-container'))); + release.tags = qa('.tag-container a', true); - const uhd = q('#video-ribbon .container > div > span:nth-child(2)', true); - if (/4K/.test(uhd)) release.tags = release.tags.concat('4k'); + const uhd = q('#video-ribbon .container > div > span:nth-child(2)', true); + if (/4K/.test(uhd)) release.tags = release.tags.concat('4k'); - release.photos = qa('.bxslider_pics img').map(el => el.dataset.original || el.src); - release.poster = qposter(); + release.photos = qa('.bxslider_pics img').map(el => el.dataset.original || el.src); + release.poster = qposter(); - const trailer = qtrailer(); - if (trailer) release.trailer = { src: trailer }; + const trailer = qtrailer(); + if (trailer) release.trailer = { src: trailer }; - if (release.photos.length > 0) release.channel = await extractChannelFromPhoto(release.photos[0], metaSiteSlugs); + if (release.photos.length > 0) release.channel = await extractChannelFromPhoto(release.photos[0], metaSiteSlugs); - if (release.channel) { - const { pathname } = new URL(url); - release.url = `https://${release.channel}.com${pathname}`; + if (release.channel) { + const { pathname } = new URL(url); + release.url = `https://${release.channel}.com${pathname}`; - const slug = pathname.split('/')[2]; - release.entryId = getHash(`${release.channel}${slug}${release.date.toISOString()}`); - } + const slug = pathname.split('/')[2]; + release.entryId = getHash(`${release.channel}${slug}${release.date.toISOString()}`); + } - return release; + return release; } async function fetchLatest(site, page = 1) { - const url = `${site.url}/movies/page-${page}`; - const res = await bhttp.get(url); + const url = `${site.url}/movies/page-${page}`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeLatest(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeLatest(res.body.toString(), site); + } - return []; + return []; } async function fetchScene(url, site, release) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeScene(res.body.toString(), site, url, release?.meta.siteSlugs); - } + if (res.statusCode === 200) { + return scrapeScene(res.body.toString(), site, url, release?.meta.siteSlugs); + } - return []; + return []; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/pervcity.js b/src/scrapers/pervcity.js index 17f34fb67..3500ff864 100644 --- a/src/scrapers/pervcity.js +++ b/src/scrapers/pervcity.js @@ -6,135 +6,135 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); async function getTrailer(entryId) { - const trailerRes = await bhttp.post('https://www.pervcity.com/gettoken.php', { - setId: entryId, - }); + const trailerRes = await bhttp.post('https://www.pervcity.com/gettoken.php', { + setId: entryId, + }); - if (trailerRes.statusCode === 200) { - return { - poster: trailerRes.body.TrailerImg, - trailer: trailerRes.body.TrailerPath || trailerRes.body.Trailerfallback, - }; - } + if (trailerRes.statusCode === 200) { + return { + poster: trailerRes.body.TrailerImg, + trailer: trailerRes.body.TrailerPath || trailerRes.body.Trailerfallback, + }; + } - return null; + return null; } function scrapeLatestScene(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); + const $ = cheerio.load(html, { normalizeWhitespace: true }); - const entryId = $('li').attr('id'); - const sceneLinkElement = $('#scene_title_border a'); - const url = `${site.url}/${sceneLinkElement.attr('href')}`; - const title = sceneLinkElement.attr('title').replace(/\u00E2\u0080\u0099/g, '\''); // replace weird apostrophes + const entryId = $('li').attr('id'); + const sceneLinkElement = $('#scene_title_border a'); + const url = `${site.url}/${sceneLinkElement.attr('href')}`; + const title = sceneLinkElement.attr('title').replace(/\u00E2\u0080\u0099/g, '\''); // replace weird apostrophes - const actors = $('.home_model_name a').toArray().map(element => $(element).text().replace(/,[\u0020\u00A0\u202F]/, '')); // replace weird commas - const date = moment.utc($('.add_date').text(), 'DD-MM-YYYY').toDate(); + const actors = $('.home_model_name a').toArray().map(element => $(element).text().replace(/,[\u0020\u00A0\u202F]/, '')); // replace weird commas + const date = moment.utc($('.add_date').text(), 'DD-MM-YYYY').toDate(); - const poster = $('a:nth-child(2) > img').attr('src'); - const photos = $('.sample-picker img').map((index, element) => $(element).attr('src').replace('tourpics', 'trailer')).toArray(); + const poster = $('a:nth-child(2) > img').attr('src'); + const photos = $('.sample-picker img').map((index, element) => $(element).attr('src').replace('tourpics', 'trailer')).toArray(); - const stars = $('img[src*="/star.png"]') - .toArray() - .map(element => $(element).attr('src')) - .length || 0; + const stars = $('img[src*="/star.png"]') + .toArray() + .map(element => $(element).attr('src')) + .length || 0; - return { - url, - entryId, - title, - actors, - date, - poster, - photos, - rating: { - stars, - }, - site, - }; + return { + url, + entryId, + title, + actors, + date, + poster, + photos, + rating: { + stars, + }, + site, + }; } async function scrapeScene(html, url, site) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const release = { url, site }; + const release = { url, site }; - release.entryId = document.querySelector('input#set_ID').value; + release.entryId = document.querySelector('input#set_ID').value; - release.title = document.querySelector('title').textContent; - release.description = document.querySelector('.player_data').textContent.trim(); + release.title = document.querySelector('title').textContent; + release.description = document.querySelector('.player_data').textContent.trim(); - const durationString = document.querySelector('.tag_lineR div:nth-child(2) span').textContent; - const [minutes, seconds] = durationString.match(/\d+/g); + const durationString = document.querySelector('.tag_lineR div:nth-child(2) span').textContent; + const [minutes, seconds] = durationString.match(/\d+/g); - release.duration = Number(minutes) * 60 + Number(seconds); - release.tags = document.querySelector('meta[name="keywords"]').content.split(','); + release.duration = Number(minutes) * 60 + Number(seconds); + release.tags = document.querySelector('meta[name="keywords"]').content.split(','); - const { poster, trailer } = await getTrailer(release.entryId); + const { poster, trailer } = await getTrailer(release.entryId); - release.poster = poster; - release.trailer = { src: trailer }; + release.poster = poster; + release.trailer = { src: trailer }; - return release; + return release; } function scrapeFallbackLanding(html) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - return document.querySelector('input#set_ID').value; + return document.querySelector('input#set_ID').value; } async function scrapeFallbackScene(html, entryId, url, site) { - const { document } = new JSDOM(html).window; - const release = { url, entryId, site }; + const { document } = new JSDOM(html).window; + const release = { url, entryId, site }; - release.title = document.querySelector('.popup_data_set_head label').textContent; - release.description = document.querySelector('.popup_data_set_des p').textContent.trim(); - release.date = moment.utc(document.querySelector('.popup_left_top div span').textContent, 'MM-DD-YYYY').toDate(); - release.actors = Array.from(document.querySelectorAll('.popup_data_set_models a'), el => el.textContent); + release.title = document.querySelector('.popup_data_set_head label').textContent; + release.description = document.querySelector('.popup_data_set_des p').textContent.trim(); + release.date = moment.utc(document.querySelector('.popup_left_top div span').textContent, 'MM-DD-YYYY').toDate(); + release.actors = Array.from(document.querySelectorAll('.popup_data_set_models a'), el => el.textContent); - const { poster, trailer } = await getTrailer(release.entryId); + const { poster, trailer } = await getTrailer(release.entryId); - release.poster = poster; - release.trailer = { src: trailer }; + release.poster = poster; + release.trailer = { src: trailer }; - release.channel = document.querySelector('.popup_left_top div img').alt; + release.channel = document.querySelector('.popup_left_top div img').alt; - return release; + return release; } async function fetchLatest(site, page = 1) { - const res = page === 1 - ? await bhttp.get(`${site.url}/final_latestupdateview.php?limitstart=${(page - 1) * 9}&limitend=9&websiteid=0&deviceview=browser&tourId=${site.parameters.tourId}`) - : await bhttp.get(`${site.url}/final_load_latestupdate_grid_view.php?limitstart=0&limitend=${(page - 1) * 8 + 1}&websiteid=0&deviceview=browser&tourId=${site.parameters.tourId}`); - const elements = JSON.parse(res.body.toString()); + const res = page === 1 + ? await bhttp.get(`${site.url}/final_latestupdateview.php?limitstart=${(page - 1) * 9}&limitend=9&websiteid=0&deviceview=browser&tourId=${site.parameters.tourId}`) + : await bhttp.get(`${site.url}/final_load_latestupdate_grid_view.php?limitstart=0&limitend=${(page - 1) * 8 + 1}&websiteid=0&deviceview=browser&tourId=${site.parameters.tourId}`); + const elements = JSON.parse(res.body.toString()); - const latest = Object.values(elements.total_arr).map(html => scrapeLatestScene(html, site)); // total_arr is a key-value object for final_load_latestupdate_grid_view.php + const latest = Object.values(elements.total_arr).map(html => scrapeLatestScene(html, site)); // total_arr is a key-value object for final_load_latestupdate_grid_view.php - return latest; + return latest; } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - if (site.isFallback) { - const entryId = scrapeFallbackLanding(res.body.toString(), url); + if (res.statusCode === 200) { + if (site.isNetwork) { + const entryId = scrapeFallbackLanding(res.body.toString(), url); - const fallbackRes = await bhttp.post('https://www.pervcity.com/set_popupvideo.php', { - setId: entryId, - }); + const fallbackRes = await bhttp.post('https://www.pervcity.com/set_popupvideo.php', { + setId: entryId, + }); - return scrapeFallbackScene(fallbackRes.body.toString(), entryId, url, site); - } + return scrapeFallbackScene(fallbackRes.body.toString(), entryId, url, site); + } - return scrapeScene(res.body.toString(), url, site); - } + return scrapeScene(res.body.toString(), url, site); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/pornhub.js b/src/scrapers/pornhub.js index 4ec7861f0..a546fc988 100644 --- a/src/scrapers/pornhub.js +++ b/src/scrapers/pornhub.js @@ -5,56 +5,56 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); const ethnicityMap = { - White: 'Caucasian', + White: 'Caucasian', }; const hairMap = { - Brunette: 'brown', + Brunette: 'brown', }; async function scrapeProfile(html, _url, actorName) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const entries = Array.from(document.querySelectorAll('.infoPiece'), el => el.textContent.replace(/\n|\t/g, '').split(':')); - const bio = entries.reduce((acc, [key, value]) => (key ? { ...acc, [key.trim()]: value.trim() } : acc), {}); + const entries = Array.from(document.querySelectorAll('.infoPiece'), el => el.textContent.replace(/\n|\t/g, '').split(':')); + const bio = entries.reduce((acc, [key, value]) => (key ? { ...acc, [key.trim()]: value.trim() } : acc), {}); - const profile = { - name: actorName, - }; + const profile = { + name: actorName, + }; - const descriptionString = document.querySelector('div[itemprop="description"]') || document.querySelector('.longBio'); - const avatarEl = document.querySelector('#getAvatar') || document.querySelector('.thumbImage img'); + const descriptionString = document.querySelector('div[itemprop="description"]') || document.querySelector('.longBio'); + const avatarEl = document.querySelector('#getAvatar') || document.querySelector('.thumbImage img'); - if (bio.Gender) profile.gender = bio.Gender.toLowerCase(); - if (bio.ethnicity) profile.ethnicity = ethnicityMap[bio.Ethnicity] || bio.Ethnicity; + if (bio.Gender) profile.gender = bio.Gender.toLowerCase(); + if (bio.ethnicity) profile.ethnicity = ethnicityMap[bio.Ethnicity] || bio.Ethnicity; - if (descriptionString) profile.description = descriptionString.textContent; + if (descriptionString) profile.description = descriptionString.textContent; - if (bio.Birthday && !/-0001/.test(bio.Birthday)) profile.birthdate = moment.utc(bio.Birthday, 'MMM D, YYYY').toDate(); // birthyear sometimes -0001, see Spencer Bradley as of january 2020 - if (bio.Born) profile.birthdate = moment.utc(bio.Born, 'YYYY-MM-DD').toDate(); + if (bio.Birthday && !/-0001/.test(bio.Birthday)) profile.birthdate = moment.utc(bio.Birthday, 'MMM D, YYYY').toDate(); // birthyear sometimes -0001, see Spencer Bradley as of january 2020 + if (bio.Born) profile.birthdate = moment.utc(bio.Born, 'YYYY-MM-DD').toDate(); - profile.birthPlace = bio['Birth Place'] || bio.Birthplace; - profile.residencePlace = bio['City and Country']; + profile.birthPlace = bio['Birth Place'] || bio.Birthplace; + profile.residencePlace = bio['City and Country']; - if (bio.Measurements && bio.Measurements !== '--') [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-'); - if (bio['Fake Boobs']) profile.naturalBoobs = bio['Fake Boobs'] === 'No'; + if (bio.Measurements && bio.Measurements !== '--') [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-'); + if (bio['Fake Boobs']) profile.naturalBoobs = bio['Fake Boobs'] === 'No'; - if (bio.Height) profile.height = Number(bio.Height.match(/\(\d+/)[0].slice(1)); - if (bio.Weight) profile.weight = Number(bio.Weight.match(/\(\d+/)[0].slice(1)); - if (bio['Hair Color']) profile.hair = hairMap[bio['Hair Color']] || bio['Hair Color'].toLowerCase(); - if (bio.Piercings) profile.hasPiercings = bio.Piercings === 'Yes'; - if (bio.Tattoos) profile.hasTattoos = bio.Tattoos === 'Yes'; + if (bio.Height) profile.height = Number(bio.Height.match(/\(\d+/)[0].slice(1)); + if (bio.Weight) profile.weight = Number(bio.Weight.match(/\(\d+/)[0].slice(1)); + if (bio['Hair Color']) profile.hair = hairMap[bio['Hair Color']] || bio['Hair Color'].toLowerCase(); + if (bio.Piercings) profile.hasPiercings = bio.Piercings === 'Yes'; + if (bio.Tattoos) profile.hasTattoos = bio.Tattoos === 'Yes'; - if (avatarEl && !/default\//.test(avatarEl.src)) profile.avatar = avatarEl.src; - profile.social = Array.from(document.querySelectorAll('.socialList a'), el => el.href).filter(link => link !== 'https://www.twitter.com/'); // PH links to Twitter itself for some reason + if (avatarEl && !/default\//.test(avatarEl.src)) profile.avatar = avatarEl.src; + profile.social = Array.from(document.querySelectorAll('.socialList a'), el => el.href).filter(link => link !== 'https://www.twitter.com/'); // PH links to Twitter itself for some reason - return profile; + return profile; } async function fetchProfile(actorName) { - const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-'); + const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-'); - /* Model pages are not reliably associated with actual porn stars + /* Model pages are not reliably associated with actual porn stars const modelUrl = `https://pornhub.com/model/${actorSlug}`; const pornstarUrl = `https://pornhub.com/pornstar/${actorSlug}`; @@ -74,12 +74,12 @@ async function fetchProfile(actorName) { } */ - const pornstarUrl = `https://pornhub.com/pornstar/${actorSlug}`; - const pornstarRes = await bhttp.get(pornstarUrl); + const pornstarUrl = `https://pornhub.com/pornstar/${actorSlug}`; + const pornstarRes = await bhttp.get(pornstarUrl); - return scrapeProfile(pornstarRes.body.toString(), pornstarUrl, actorName); + return scrapeProfile(pornstarRes.body.toString(), pornstarUrl, actorName); } module.exports = { - fetchProfile, + fetchProfile, }; diff --git a/src/scrapers/private.js b/src/scrapers/private.js index aa8e7bd0d..b6944376f 100644 --- a/src/scrapers/private.js +++ b/src/scrapers/private.js @@ -9,193 +9,193 @@ const { get, geta } = require('../utils/q'); const slugify = require('../utils/slugify'); async function getPhotos(entryId, site) { - const { hostname } = new URL(site.url); + const { hostname } = new URL(site.url); - const res = await bhttp.get(`https://${hostname}/gallery.php?type=highres&id=${entryId}`); - const html = res.body.toString(); + const res = await bhttp.get(`https://${hostname}/gallery.php?type=highres&id=${entryId}`); + const html = res.body.toString(); - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const photos = $('a.fakethumb').map((photoIndex, photoElement) => $(photoElement).attr('data-src') || $(photoElement).attr('href')).toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const photos = $('a.fakethumb').map((photoIndex, photoElement) => $(photoElement).attr('data-src') || $(photoElement).attr('href')).toArray(); - return photos; + return photos; } function scrapeLatest(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const sceneElements = $('.content-wrapper .scene').toArray(); + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const sceneElements = $('.content-wrapper .scene').toArray(); - return sceneElements.map((element) => { - const sceneLinkElement = $(element).find('h3 a'); - const thumbnailElement = $(element).find('a img'); + return sceneElements.map((element) => { + const sceneLinkElement = $(element).find('h3 a'); + const thumbnailElement = $(element).find('a img'); - const url = sceneLinkElement.attr('href'); - // const title = sceneLinkElement.text(); - const entryId = url.split('/').slice(-1)[0]; + const url = sceneLinkElement.attr('href'); + // const title = sceneLinkElement.text(); + const entryId = url.split('/').slice(-1)[0]; - const titleText = thumbnailElement.attr('alt'); - const title = titleText.slice(titleText.indexOf(':') + 1).trim(); + const titleText = thumbnailElement.attr('alt'); + const title = titleText.slice(titleText.indexOf(':') + 1).trim(); - const date = moment.utc($(element).find('.scene-date'), ['MM/DD/YYYY', 'YYYY-MM-DD']).toDate(); + const date = moment.utc($(element).find('.scene-date'), ['MM/DD/YYYY', 'YYYY-MM-DD']).toDate(); - const actors = $(element).find('.scene-models a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - const likes = Number($(element).find('.scene-votes').text()); + const actors = $(element).find('.scene-models a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + const likes = Number($(element).find('.scene-votes').text()); - const photoCount = Number(thumbnailElement.attr('thumbs_num')); - const poster = thumbnailElement.attr('src'); - const photos = Array.from({ length: photoCount }, (val, index) => thumbnailElement.attr(`src${index + 1}`)); + const photoCount = Number(thumbnailElement.attr('thumbs_num')); + const poster = thumbnailElement.attr('src'); + const photos = Array.from({ length: photoCount }, (val, index) => thumbnailElement.attr(`src${index + 1}`)); - const scene = { - url, - entryId, - title, - actors, - date, - poster, - photos, - rating: { - likes, - }, - site, - }; + const scene = { + url, + entryId, + title, + actors, + date, + poster, + photos, + rating: { + likes, + }, + site, + }; - return scene; - }); + return scene; + }); } async function scrapeScene(html, url, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); - const release = { url }; + const $ = cheerio.load(html, { normalizeWhitespace: true }); + const release = { url }; - [release.entryId] = url.split('/').slice(-1); - release.title = $('.video-wrapper meta[itemprop="name"]').attr('content'); - release.description = $('.video-wrapper meta[itemprop="description"]').attr('content'); + [release.entryId] = url.split('/').slice(-1); + release.title = $('.video-wrapper meta[itemprop="name"]').attr('content'); + release.description = $('.video-wrapper meta[itemprop="description"]').attr('content'); - release.date = moment.utc($('.video-wrapper meta[itemprop="uploadDate"]').attr('content'), 'MM/DD/YYYY').toDate(); - release.actors = $('.content-wrapper .scene-models-list a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); + release.date = moment.utc($('.video-wrapper meta[itemprop="uploadDate"]').attr('content'), 'MM/DD/YYYY').toDate(); + release.actors = $('.content-wrapper .scene-models-list a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); - const timestamp = $('.video-wrapper meta[itemprop="duration"]').attr('content'); + const timestamp = $('.video-wrapper meta[itemprop="duration"]').attr('content'); - if (timestamp) { - const [minutes, seconds] = timestamp.match(/\d+/g); - release.duration = Number(minutes) * 60 + Number(seconds); - } + if (timestamp) { + const [minutes, seconds] = timestamp.match(/\d+/g); + release.duration = Number(minutes) * 60 + Number(seconds); + } - release.tags = $('.content-desc .scene-tags a').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); - release.likes = Number($('.content-desc #social-actions #likes').text()); + release.tags = $('.content-desc .scene-tags a').map((tagIndex, tagElement) => $(tagElement).text()).toArray(); + release.likes = Number($('.content-desc #social-actions #likes').text()); - const posterScript = $('script:contains(poster)').html(); - const posterLink = posterScript?.slice(posterScript.indexOf('https://'), posterScript.indexOf('.jpg') + 4); - release.poster = $('meta[property="og:image"]').attr('content') || posterLink || $('#trailer_player_finished img').attr('src'); + const posterScript = $('script:contains(poster)').html(); + const posterLink = posterScript?.slice(posterScript.indexOf('https://'), posterScript.indexOf('.jpg') + 4); + release.poster = $('meta[property="og:image"]').attr('content') || posterLink || $('#trailer_player_finished img').attr('src'); - const trailer = $('meta[property="og:video"]').attr('content') || $('#videojs-trailer source').attr('src'); + const trailer = $('meta[property="og:video"]').attr('content') || $('#videojs-trailer source').attr('src'); - if (trailer) release.trailer = { src: trailer }; + if (trailer) release.trailer = { src: trailer }; - release.photos = await getPhotos(release.entryId, site); - release.movie = $('a[data-track="FULL MOVIE"]').attr('href'); + release.photos = await getPhotos(release.entryId, site); + release.movie = $('a[data-track="FULL MOVIE"]').attr('href'); - const siteElement = $('.content-wrapper .logos-sites a'); - if (siteElement) release.channel = slugify(siteElement.text(), ''); + const siteElement = $('.content-wrapper .logos-sites a'); + if (siteElement) release.channel = slugify(siteElement.text(), ''); - return release; + return release; } function scrapeProfile({ html, q, qa, qtx }) { - const profile = {}; + const profile = {}; - const bio = qa('.model-facts li:not(.model-facts-long)', true).reduce((acc, fact) => { - const [key, value] = fact.split(':'); - const trimmedValue = value.trim(); + const bio = qa('.model-facts li:not(.model-facts-long)', true).reduce((acc, fact) => { + const [key, value] = fact.split(':'); + const trimmedValue = value.trim(); - if (trimmedValue.length === 0 || trimmedValue === '-') return acc; - return { ...acc, [slugify(key, '_')]: trimmedValue }; - }, {}); + if (trimmedValue.length === 0 || trimmedValue === '-') return acc; + return { ...acc, [slugify(key, '_')]: trimmedValue }; + }, {}); - const description = q('.model-facts-long', true); - if (description) profile.description = description; + const description = q('.model-facts-long', true); + if (description) profile.description = description; - const aliases = qtx('.aka')?.split(/,\s*/); - if (aliases) profile.aliases = aliases; + const aliases = qtx('.aka')?.split(/,\s*/); + if (aliases) profile.aliases = aliases; - if (bio.birth_place) profile.birthPlace = bio.birth_place; - if (bio.nationality) profile.nationality = bio.nationality; + if (bio.birth_place) profile.birthPlace = bio.birth_place; + if (bio.nationality) profile.nationality = bio.nationality; - if (bio.measurements) { - const [bust, waist, hip] = bio.measurements.split('-'); + if (bio.measurements) { + const [bust, waist, hip] = bio.measurements.split('-'); - if (bust) profile.bust = bust; - if (waist) profile.waist = Number(waist); - if (hip) profile.hip = Number(hip); - } + if (bust) profile.bust = bust; + if (waist) profile.waist = Number(waist); + if (hip) profile.hip = Number(hip); + } - if (bio.weight) profile.weight = Number(bio.weight.match(/^\d+/)[0]); - if (bio.height) profile.height = Number(bio.height.match(/^\d+/)[0]); + if (bio.weight) profile.weight = Number(bio.weight.match(/^\d+/)[0]); + if (bio.height) profile.height = Number(bio.height.match(/^\d+/)[0]); - if (bio.hair_color) profile.hair = bio.hair_color; - if (bio.eye_color) profile.eye = bio.eye_color; + if (bio.hair_color) profile.hair = bio.hair_color; + if (bio.eye_color) profile.eye = bio.eye_color; - if (bio.tattoos) { - profile.hasTattoos = true; - profile.tattoos = bio.tattoos; - } + if (bio.tattoos) { + profile.hasTattoos = true; + profile.tattoos = bio.tattoos; + } - if (bio.tattoos) { - profile.hasTattoos = true; - profile.tattoos = bio.tattoos; - } + if (bio.tattoos) { + profile.hasTattoos = true; + profile.tattoos = bio.tattoos; + } - if (bio.piercings) { - profile.hasPiercings = true; - profile.piercings = bio.piercings; - } + if (bio.piercings) { + profile.hasPiercings = true; + profile.piercings = bio.piercings; + } - profile.avatar = q('.img-pornstar img').dataset.src; - profile.releases = scrapeLatest(html); + profile.avatar = q('.img-pornstar img').dataset.src; + profile.releases = scrapeLatest(html); - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const { hostname } = new URL(site.url); + const { hostname } = new URL(site.url); - if (hostname.match('private.com')) { - const res = await bhttp.get(`${site.url}/${page}/`); + if (hostname.match('private.com')) { + const res = await bhttp.get(`${site.url}/${page}/`); - return scrapeLatest(res.body.toString(), site); - } + return scrapeLatest(res.body.toString(), site); + } - const res = await bhttp.get(`${site.url}/scenes/${page}/`); + const res = await bhttp.get(`${site.url}/scenes/${page}/`); - return scrapeLatest(res.body.toString(), site); + return scrapeLatest(res.body.toString(), site); } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - return scrapeScene(res.body.toString(), url, site); + return scrapeScene(res.body.toString(), url, site); } async function fetchProfile(actorName) { - const actorSearchSlug = slugify(actorName, '+'); - const url = `https://www.private.com/search.php?query=${actorSearchSlug}`; - const modelRes = await geta(url, '.model h3 a'); + const actorSearchSlug = slugify(actorName, '+'); + const url = `https://www.private.com/search.php?query=${actorSearchSlug}`; + const modelRes = await geta(url, '.model h3 a'); - if (modelRes.ok) { - const actorSlug = slugify(actorName); - const model = modelRes.items.find(({ text }) => slugify(text) === actorSlug); + if (modelRes.ok) { + const actorSlug = slugify(actorName); + const model = modelRes.items.find(({ text }) => slugify(text) === actorSlug); - if (model) { - const res = await get(model.el.href); + if (model) { + const res = await get(model.el.href); - return res.ok ? scrapeProfile(res.item) : res.status; - } - } + return res.ok ? scrapeProfile(res.item) : res.status; + } + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/puretaboo.js b/src/scrapers/puretaboo.js index ede8da8d0..9ae001f9d 100644 --- a/src/scrapers/puretaboo.js +++ b/src/scrapers/puretaboo.js @@ -3,7 +3,7 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchScene, - fetchUpcoming: fetchApiUpcoming, + fetchLatest: fetchApiLatest, + fetchScene, + fetchUpcoming: fetchApiUpcoming, }; diff --git a/src/scrapers/realitykings.js b/src/scrapers/realitykings.js index 0b98badce..fe40d8954 100644 --- a/src/scrapers/realitykings.js +++ b/src/scrapers/realitykings.js @@ -4,49 +4,49 @@ const bhttp = require('bhttp'); const cheerio = require('cheerio'); const { - scrapeLatestX, - fetchLatest, - fetchScene, - fetchProfile, + scrapeLatestX, + fetchLatest, + fetchScene, + fetchProfile, } = require('./mindgeek'); function scrapeLatestClassic(html, site) { - const $ = cheerio.load(html, { normalizeWhitespace: true }); + const $ = cheerio.load(html, { normalizeWhitespace: true }); - const stateTag = $('script:contains("initialState")').html(); - const prefix = 'initialState = {'; - const prefixIndex = stateTag.indexOf('initialState = {'); - const suffix = '};'; - const stateString = stateTag.slice(prefixIndex + prefix.length - 1, stateTag.indexOf('};', prefixIndex) + suffix.length - 1); - const data = JSON.parse(stateString); + const stateTag = $('script:contains("initialState")').html(); + const prefix = 'initialState = {'; + const prefixIndex = stateTag.indexOf('initialState = {'); + const suffix = '};'; + const stateString = stateTag.slice(prefixIndex + prefix.length - 1, stateTag.indexOf('};', prefixIndex) + suffix.length - 1); + const data = JSON.parse(stateString); - return Object.values(data.entities.releases).map(scene => scrapeLatestX(scene, site)); + return Object.values(data.entities.releases).map(scene => scrapeLatestX(scene, site)); } async function fetchClassic(site, page) { - const res = await bhttp.get(`${site.url}/scenes?page=${page}`); + const res = await bhttp.get(`${site.url}/scenes?page=${page}`); - if (res.statusCode === 200) { - return scrapeLatestClassic(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeLatestClassic(res.body.toString(), site); + } - return null; + return null; } async function fetchLatestWrap(site, page = 1) { - if (site.parameters?.classic) { - return fetchClassic(site, page); - } + if (site.parameters?.classic) { + return fetchClassic(site, page); + } - return fetchLatest(site, page); + return fetchLatest(site, page); } async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'realitykings'); + return fetchProfile(actorName, 'realitykings'); } module.exports = { - fetchLatest: fetchLatestWrap, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest: fetchLatestWrap, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/score.js b/src/scrapers/score.js index 509254a39..79c850228 100644 --- a/src/scrapers/score.js +++ b/src/scrapers/score.js @@ -7,255 +7,255 @@ const slugify = require('../utils/slugify'); const { heightToCm, lbsToKg } = require('../utils/convert'); function scrapePhotos(html) { - const { qis } = ex(html, '#photos-page'); - const photos = qis('img'); + const { qis } = ex(html, '#photos-page'); + const photos = qis('img'); - return photos.map(photo => [ - photo - .replace('x_800', 'x_xl') - .replace('_tn', ''), - photo, - ]); + return photos.map(photo => [ + photo + .replace('x_800', 'x_xl') + .replace('_tn', ''), + photo, + ]); } async function fetchPhotos(url) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapePhotos(res.body.toString(), url); - } + if (res.statusCode === 200) { + return scrapePhotos(res.body.toString(), url); + } - return []; + return []; } function scrapeAll(html, site) { - return exa(html, '.container .video, .container-fluid .video').map(({ q, qa, qd, ql }) => { - const release = {}; + return exa(html, '.container .video, .container-fluid .video').map(({ q, qa, qd, ql }) => { + const release = {}; - release.title = q('.title, .i-title', true); + release.title = q('.title, .i-title', true); - const linkEl = q('a'); - const url = new URL(linkEl.href); - release.url = `${url.origin}${url.pathname}`; + const linkEl = q('a'); + const url = new URL(linkEl.href); + release.url = `${url.origin}${url.pathname}`; - // this is a photo album, not a scene (used for profiles) - if (/photos\//.test(url)) return null; + // this is a photo album, not a scene (used for profiles) + if (/photos\//.test(url)) return null; - [release.entryId] = url.pathname.split('/').slice(-2); + [release.entryId] = url.pathname.split('/').slice(-2); - release.date = qd('.i-date', 'MMM DD', /\w+ \d{1,2}$/) + release.date = qd('.i-date', 'MMM DD', /\w+ \d{1,2}$/) || qd('.dt-box', 'MMM.DD YYYY'); - release.actors = site?.parameters?.actors || qa('.model, .i-model', true); - release.duration = ql('.i-amount, .amount'); + release.actors = site?.parameters?.actors || qa('.model, .i-model', true); + release.duration = ql('.i-amount, .amount'); - const posterEl = q('.item-img img'); + const posterEl = q('.item-img img'); - if (posterEl) { - release.poster = `https:${posterEl.src}`; - } + if (posterEl) { + release.poster = `https:${posterEl.src}`; + } - if (posterEl?.dataset.gifPreview) { - release.teaser = { - src: `https:${posterEl.dataset.gifPreview}`, - }; - } + if (posterEl?.dataset.gifPreview) { + release.teaser = { + src: `https:${posterEl.dataset.gifPreview}`, + }; + } - return release; - }).filter(Boolean); + return release; + }).filter(Boolean); } async function scrapeScene(html, url, site) { - const { qu } = ex(html, '#videos-page, #content'); - const release = {}; + const { qu } = ex(html, '#videos-page, #content'); + const release = {}; - [release.entryId] = new URL(url).pathname.split('/').slice(-2); + [release.entryId] = new URL(url).pathname.split('/').slice(-2); - release.title = qu.q('h2.text-uppercase, h2.title, #breadcrumb-top + h1', true) + release.title = qu.q('h2.text-uppercase, h2.title, #breadcrumb-top + h1', true) || qu.q('h1.m-title', true)?.split(/»|\//).slice(-1)[0].trim(); - release.description = qu.text('.p-desc, .desc'); + release.description = qu.text('.p-desc, .desc'); - release.actors = qu.all('.value a[href*=models], .value a[href*=performer], .value a[href*=teen-babes]', true); + release.actors = qu.all('.value a[href*=models], .value a[href*=performer], .value a[href*=teen-babes]', true); - if (release.actors.length === 0) { - const actorEl = qu.all('.stat').find(stat => /Featuring/.test(stat.textContent)); - const actorString = qu.text(actorEl); + if (release.actors.length === 0) { + const actorEl = qu.all('.stat').find(stat => /Featuring/.test(stat.textContent)); + const actorString = qu.text(actorEl); - release.actors = actorString?.split(/,\band\b|,/g).map(actor => actor.trim()) || []; - } + release.actors = actorString?.split(/,\band\b|,/g).map(actor => actor.trim()) || []; + } - if (release.actors.length === 0 && site.parameters?.actors) release.actors = site.parameters.actors; + if (release.actors.length === 0 && site.parameters?.actors) release.actors = site.parameters.actors; - release.tags = qu.all('a[href*=tag]', true); + release.tags = qu.all('a[href*=tag]', true); - const dateEl = qu.all('.value').find(el => /\w+ \d+\w+, \d{4}/.test(el.textContent)); - release.date = qu.date(dateEl, null, 'MMMM Do, YYYY') + const dateEl = qu.all('.value').find(el => /\w+ \d+\w+, \d{4}/.test(el.textContent)); + release.date = qu.date(dateEl, null, 'MMMM Do, YYYY') || qu.date('.date', 'MMMM Do, YYYY', /\w+ \d{1,2}\w+, \d{4}/) || qu.date('.info .holder', 'MM/DD/YYYY', /\d{2}\/\d{2}\/\d{4}/); - const durationEl = qu.all('value').find(el => /\d{1,3}:\d{2}/.test(el.textContent)); - release.duration = qu.dur(durationEl); + const durationEl = qu.all('value').find(el => /\d{1,3}:\d{2}/.test(el.textContent)); + release.duration = qu.dur(durationEl); - release.poster = qu.poster('video') || qu.img('.flowplayer img') || qu.img('img'); // _800.jpg is larger than _xl.jpg in landscape - const photosUrl = qu.url('.stat a[href*=photos]'); + release.poster = qu.poster('video') || qu.img('.flowplayer img') || qu.img('img'); // _800.jpg is larger than _xl.jpg in landscape + const photosUrl = qu.url('.stat a[href*=photos]'); - if (photosUrl) { - release.photos = await fetchPhotos(photosUrl); - } else { - release.photos = qu.imgs('img[src*=ThumbNails], .p-photos .tn img').map(photo => [ - photo.replace('_tn', ''), - photo, - ]); - } + if (photosUrl) { + release.photos = await fetchPhotos(photosUrl); + } else { + release.photos = qu.imgs('img[src*=ThumbNails], .p-photos .tn img').map(photo => [ + photo.replace('_tn', ''), + photo, + ]); + } - const trailers = qu.all('a[href*=Trailers]'); + const trailers = qu.all('a[href*=Trailers]'); - if (trailers) { - release.trailer = trailers.map((trailer) => { - const src = `https:${trailer.href}`; - const format = trailer.textContent.trim().match(/^\w+/)[0].toLowerCase(); - const quality = parseInt(trailer.textContent.trim().match(/\d+([a-zA-Z]+)?$/)[0], 10); + if (trailers) { + release.trailer = trailers.map((trailer) => { + const src = `https:${trailer.href}`; + const format = trailer.textContent.trim().match(/^\w+/)[0].toLowerCase(); + const quality = parseInt(trailer.textContent.trim().match(/\d+([a-zA-Z]+)?$/)[0], 10); - return format === 'mp4' ? { src, quality } : null; - }).filter(Boolean); - } + return format === 'mp4' ? { src, quality } : null; + }).filter(Boolean); + } - const stars = qu.q('.rate-box').dataset.score; - if (stars) release.rating = { stars }; + const stars = qu.q('.rate-box').dataset.score; + if (stars) release.rating = { stars }; - return release; + return release; } function scrapeModels(html, actorName) { - const { qa } = ex(html); - const model = qa('.model a').find(link => link.title === actorName); + const { qa } = ex(html); + const model = qa('.model a').find(link => link.title === actorName); - return model?.href || null; + return model?.href || null; } async function fetchActorReleases(url, accReleases = []) { - const res = await get(url); + const res = await get(url); - if (res.ok) { - const releases = accReleases.concat(scrapeAll(res.item.document.body.outerHTML)); - const nextPage = res.item.qu.url('.next-pg'); + if (res.ok) { + const releases = accReleases.concat(scrapeAll(res.item.document.body.outerHTML)); + const nextPage = res.item.qu.url('.next-pg'); - if (nextPage && new URL(nextPage).searchParams.has('page')) { // last page has 'next' button linking to join page - return fetchActorReleases(nextPage, releases); - } + if (nextPage && new URL(nextPage).searchParams.has('page')) { // last page has 'next' button linking to join page + return fetchActorReleases(nextPage, releases); + } - return releases; - } + return releases; + } - return null; + return null; } async function scrapeProfile(html, actorUrl, withReleases) { - const { q, qa, qi } = ex(html, '#model-page'); - const profile = { gender: 'female' }; + const { q, qa, qi } = ex(html, '#model-page'); + const profile = { gender: 'female' }; - const bio = qa('.stat').reduce((acc, el) => { - const prop = q(el, '.label', true).slice(0, -1); - const key = slugify(prop, '_'); - const value = q(el, '.value', true); + const bio = qa('.stat').reduce((acc, el) => { + const prop = q(el, '.label', true).slice(0, -1); + const key = slugify(prop, '_'); + const value = q(el, '.value', true); - return { - ...acc, - [key]: value, - }; - }, {}); + return { + ...acc, + [key]: value, + }; + }, {}); - if (bio.location) profile.residencePlace = bio.location.replace('Czech Repulic', 'Czech Republic'); // see Laura Lion + if (bio.location) profile.residencePlace = bio.location.replace('Czech Repulic', 'Czech Republic'); // see Laura Lion - if (bio.birthday) { - const birthMonth = bio.birthday.match(/^\w+/)[0].toLowerCase(); - const [birthDay] = bio.birthday.match(/\d+/); + if (bio.birthday) { + const birthMonth = bio.birthday.match(/^\w+/)[0].toLowerCase(); + const [birthDay] = bio.birthday.match(/\d+/); - profile.birthday = [birthMonth, birthDay]; // currently unused, not to be confused with birthdate - } + profile.birthday = [birthMonth, birthDay]; // currently unused, not to be confused with birthdate + } - if (bio.ethnicity) profile.ethnicity = bio.ethnicity; - if (bio.hair_color) profile.hair = bio.hair_color; + if (bio.ethnicity) profile.ethnicity = bio.ethnicity; + if (bio.hair_color) profile.hair = bio.hair_color; - if (bio.height) profile.height = heightToCm(bio.height); - if (bio.weight) profile.weight = lbsToKg(bio.weight); + if (bio.height) profile.height = heightToCm(bio.height); + if (bio.weight) profile.weight = lbsToKg(bio.weight); - if (bio.bra_size) profile.bust = bio.bra_size; - if (bio.measurements) [, profile.waist, profile.hip] = bio.measurements.split('-'); + if (bio.bra_size) profile.bust = bio.bra_size; + if (bio.measurements) [, profile.waist, profile.hip] = bio.measurements.split('-'); - if (bio.occupation) profile.occupation = bio.occupation; + if (bio.occupation) profile.occupation = bio.occupation; - const avatar = qi('img'); - if (avatar) profile.avatar = avatar; + const avatar = qi('img'); + if (avatar) profile.avatar = avatar; - if (withReleases) { - const { origin, pathname } = new URL(actorUrl); - profile.releases = await fetchActorReleases(`${origin}${pathname}/scenes?page=1`); - } + if (withReleases) { + const { origin, pathname } = new URL(actorUrl); + profile.releases = await fetchActorReleases(`${origin}${pathname}/scenes?page=1`); + } - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const latestPath = site.parameters?.path || '/big-boob-videos'; - const url = `${site.url}${latestPath}?page=${page}`; - const res = await bhttp.get(url); + const latestPath = site.parameters?.path || '/big-boob-videos'; + const url = `${site.url}${latestPath}?page=${page}`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeAll(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeAll(res.body.toString(), site); + } - return res.statusCode; + return res.statusCode; } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeScene(res.body.toString(), url, site); - } + if (res.statusCode === 200) { + return scrapeScene(res.body.toString(), url, site); + } - return null; + return null; } async function fetchProfile(actorName, scraperSlug, site, include, page = 1, source = 0) { - const letter = actorName.charAt(0).toUpperCase(); + const letter = actorName.charAt(0).toUpperCase(); - const sources = [ - `https://www.scoreland.com/big-boob-models/browse/${letter}/?page=${page}`, - `https://www.50plusmilfs.com/xxx-milf-models/browse/${letter}/?page=${page}`, - ]; + const sources = [ + `https://www.scoreland.com/big-boob-models/browse/${letter}/?page=${page}`, + `https://www.50plusmilfs.com/xxx-milf-models/browse/${letter}/?page=${page}`, + ]; - const url = sources[source]; + const url = sources[source]; - const res = await bhttp.get(url, { - followRedirects: false, - }); + const res = await bhttp.get(url, { + followRedirects: false, + }); - if (res.statusCode === 200) { - const actorUrl = scrapeModels(res.body.toString(), actorName); + if (res.statusCode === 200) { + const actorUrl = scrapeModels(res.body.toString(), actorName); - if (actorUrl) { - const actorRes = await bhttp.get(actorUrl); + if (actorUrl) { + const actorRes = await bhttp.get(actorUrl); - if (actorRes.statusCode === 200) { - return scrapeProfile(actorRes.body.toString(), actorUrl, include.scenes); - } + if (actorRes.statusCode === 200) { + return scrapeProfile(actorRes.body.toString(), actorUrl, include.scenes); + } - return null; - } + return null; + } - return fetchProfile(actorName, scraperSlug, site, include, page + 1, source); - } + return fetchProfile(actorName, scraperSlug, site, include, page + 1, source); + } - if (sources[source + 1]) { - return fetchProfile(actorName, scraperSlug, site, include, 1, source + 1); - } + if (sources[source + 1]) { + return fetchProfile(actorName, scraperSlug, site, include, 1, source + 1); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, - fetchProfile, + fetchLatest, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/scrapers.js b/src/scrapers/scrapers.js index 2408826ad..41f999724 100644 --- a/src/scrapers/scrapers.js +++ b/src/scrapers/scrapers.js @@ -65,143 +65,143 @@ const freeones = require('./freeones'); // const freeoneslegacy = require('./freeones_legacy'); module.exports = { - releases: { - '21naturals': naturals, - '21sextreme': sextreme, - '21sextury': sextury, - adulttime, - amateurallure, - assylum, - aziani, - babes, - bamvisions, - bang, - bangbros, - blowpass, - brazzers, - burningangel, - cherrypimps, - ddfnetwork, - digitalplayground, - dogfart, - dogfartnetwork: dogfart, - evilangel, - fakehub, - famedigital, - fantasymassage, - fullpornnetwork, - girlsway, - girlgirl: julesjordan, - hussiepass: hush, - hushpass: hush, - insex, - interracialpass: hush, - jayrock, - jesseloadsmonsterfacials, - julesjordan, - kellymadison, - kink, - legalporno, - men, - metrohd, - mikeadriano, - milehighmedia, - mindgeek, - mofos, - naughtyamerica, - newsensations, - nubiles, - perfectgonzo, - pervcity, - pimpxxx: cherrypimps, - pornpros: whalemember, - private: privateNetwork, - puretaboo, - realitykings, - score, - sexyhub: mindgeek, - swallowsalon: julesjordan, - teamskeet, - twistys, - vivid, - vixen, - vogov, - whalemember, - wicked, - xempire, - }, - actors: { - '21sextury': sextury, - analbbc: fullpornnetwork, - analized: fullpornnetwork, - analviolation: fullpornnetwork, - anilos: nubiles, - aziani, - babes, - baddaddypov: fullpornnetwork, - bamvisions, - bangbros, - blacked: vixen, - blackedraw: vixen, - blowpass, - boobpedia, - brattysis: nubiles, - brazzers, - burningangel, - cherrypimps, - ddfnetwork, - deeper: vixen, - deeplush: nubiles, - digitalplayground, - dtfsluts: fullpornnetwork, - evilangel, - eyeontheguy: hush, - fakehub, - famedigital, - freeones, - gangbangcreampie: aziani, - girlfaction: fullpornnetwork, - gloryholesecrets: aziani, - hergape: fullpornnetwork, - homemadeanalwhores: fullpornnetwork, - hotcrazymess: nubiles, - hushpass: hush, - hussiepass: hush, - iconmale, - interracialpass: hush, - interracialpovs: hush, - jamesdeen: fullpornnetwork, - julesjordan, - kellymadison, - legalporno, - men, - metrohd, - milehighmedia, - mofos, - mugfucked: fullpornnetwork, - naughtyamerica, - nfbusty: nubiles, - nubilefilms: nubiles, - nubiles, - nubilesporn: nubiles, - onlyprince: fullpornnetwork, - pervertgallery: fullpornnetwork, - pimpxxx: cherrypimps, - pornhub, - povperverts: fullpornnetwork, - povpornstars: hush, - private: privateNetwork, - realitykings, - score, - seehimfuck: hush, - sexyhub: mindgeek, - thatsitcomshow: nubiles, - transangels, - tushy: vixen, - tushyraw: vixen, - twistys, - vixen, - wicked, - xempire, - }, + releases: { + '21naturals': naturals, + '21sextreme': sextreme, + '21sextury': sextury, + adulttime, + amateurallure, + assylum, + aziani, + babes, + bamvisions, + bang, + bangbros, + blowpass, + brazzers, + burningangel, + cherrypimps, + ddfnetwork, + digitalplayground, + dogfart, + dogfartnetwork: dogfart, + evilangel, + fakehub, + famedigital, + fantasymassage, + fullpornnetwork, + girlsway, + girlgirl: julesjordan, + hussiepass: hush, + hushpass: hush, + insex, + interracialpass: hush, + jayrock, + jesseloadsmonsterfacials, + julesjordan, + kellymadison, + kink, + legalporno, + men, + metrohd, + mikeadriano, + milehighmedia, + mindgeek, + mofos, + naughtyamerica, + newsensations, + nubiles, + perfectgonzo, + pervcity, + pimpxxx: cherrypimps, + pornpros: whalemember, + private: privateNetwork, + puretaboo, + realitykings, + score, + sexyhub: mindgeek, + swallowsalon: julesjordan, + teamskeet, + twistys, + vivid, + vixen, + vogov, + whalemember, + wicked, + xempire, + }, + actors: { + '21sextury': sextury, + analbbc: fullpornnetwork, + analized: fullpornnetwork, + analviolation: fullpornnetwork, + anilos: nubiles, + aziani, + babes, + baddaddypov: fullpornnetwork, + bamvisions, + bangbros, + blacked: vixen, + blackedraw: vixen, + blowpass, + boobpedia, + brattysis: nubiles, + brazzers, + burningangel, + cherrypimps, + ddfnetwork, + deeper: vixen, + deeplush: nubiles, + digitalplayground, + dtfsluts: fullpornnetwork, + evilangel, + eyeontheguy: hush, + fakehub, + famedigital, + freeones, + gangbangcreampie: aziani, + girlfaction: fullpornnetwork, + gloryholesecrets: aziani, + hergape: fullpornnetwork, + homemadeanalwhores: fullpornnetwork, + hotcrazymess: nubiles, + hushpass: hush, + hussiepass: hush, + iconmale, + interracialpass: hush, + interracialpovs: hush, + jamesdeen: fullpornnetwork, + julesjordan, + kellymadison, + legalporno, + men, + metrohd, + milehighmedia, + mofos, + mugfucked: fullpornnetwork, + naughtyamerica, + nfbusty: nubiles, + nubilefilms: nubiles, + nubiles, + nubilesporn: nubiles, + onlyprince: fullpornnetwork, + pervertgallery: fullpornnetwork, + pimpxxx: cherrypimps, + pornhub, + povperverts: fullpornnetwork, + povpornstars: hush, + private: privateNetwork, + realitykings, + score, + seehimfuck: hush, + sexyhub: mindgeek, + thatsitcomshow: nubiles, + transangels, + tushy: vixen, + tushyraw: vixen, + twistys, + vixen, + wicked, + xempire, + }, }; diff --git a/src/scrapers/teamskeet.js b/src/scrapers/teamskeet.js index 6e17590c7..3aefc7dad 100644 --- a/src/scrapers/teamskeet.js +++ b/src/scrapers/teamskeet.js @@ -5,176 +5,176 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); function extractTitle(pathname) { - return pathname - .split('/') - .slice(-2)[0] - .split('_') - .map(seg => `${seg.charAt(0).toUpperCase()}${seg.slice(1)}`) - .join(' '); + return pathname + .split('/') + .slice(-2)[0] + .split('_') + .map(seg => `${seg.charAt(0).toUpperCase()}${seg.slice(1)}`) + .join(' '); } function extractActors(str) { - return str - .split(/,|\band\b/ig) - .filter(actor => !/\.{3}/.test(actor)) - .map(actor => actor.trim()) - .filter(actor => actor.length > 0); + return str + .split(/,|\band\b/ig) + .filter(actor => !/\.{3}/.test(actor)) + .map(actor => actor.trim()) + .filter(actor => actor.length > 0); } function scrapeLatest(html, site) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const scenes = Array.from(document.querySelectorAll('#updatesList li.grey, #updatesList li.white')); + const scenes = Array.from(document.querySelectorAll('#updatesList li.grey, #updatesList li.white')); - return scenes.map((scene) => { - const release = { site }; + return scenes.map((scene) => { + const release = { site }; - const link = scene.querySelector('.info a'); - const poster = scene.querySelector('img'); - const { pathname } = new URL(link); + const link = scene.querySelector('.info a'); + const poster = scene.querySelector('img'); + const { pathname } = new URL(link); - [release.entryId] = poster.id.match(/\d+/); + [release.entryId] = poster.id.match(/\d+/); - release.url = `https://www.teamskeet.com${pathname}`; - release.title = extractTitle(pathname); + release.url = `https://www.teamskeet.com${pathname}`; + release.title = extractTitle(pathname); - release.date = moment.utc(scene.querySelector('strong').textContent, 'MM/DD/YYYY').toDate(); + release.date = moment.utc(scene.querySelector('strong').textContent, 'MM/DD/YYYY').toDate(); - const photos = Array.from({ length: 5 }, (_value, index) => poster.dataset.original.replace(/\d+.jpg/, `${String(index + 1).padStart(2, '0')}.jpg`)); - [release.poster] = photos; - release.photos = photos.slice(1); + const photos = Array.from({ length: 5 }, (_value, index) => poster.dataset.original.replace(/\d+.jpg/, `${String(index + 1).padStart(2, '0')}.jpg`)); + [release.poster] = photos; + release.photos = photos.slice(1); - const actors = scene.querySelector('div span[rel="test"]').textContent; - release.actors = extractActors(actors); + const actors = scene.querySelector('div span[rel="test"]').textContent; + release.actors = extractActors(actors); - return release; - }); + return release; + }); } function scrapeScene(html, site, url) { - const { document } = new JSDOM(html).window; - const release = { site }; + const { document } = new JSDOM(html).window; + const release = { site }; - release.entryId = document.querySelector('#story-and-tags .scene_rater').attributes.rel.value; - release.description = document.querySelector('#story-and-tags td:nth-child(2) div').textContent; - const [actors, title, channel] = document.querySelector('title').textContent.split('|').map(item => item.trim()); + release.entryId = document.querySelector('#story-and-tags .scene_rater').attributes.rel.value; + release.description = document.querySelector('#story-and-tags td:nth-child(2) div').textContent; + const [actors, title, channel] = document.querySelector('title').textContent.split('|').map(item => item.trim()); - release.url = url; - release.title = title; - release.actors = extractActors(actors); - release.channel = channel.toLowerCase(); - release.tags = Array.from(document.querySelectorAll('#story-and-tags tr:nth-child(2) a'), el => el.rel); + release.url = url; + release.title = title; + release.actors = extractActors(actors); + release.channel = channel.toLowerCase(); + release.tags = Array.from(document.querySelectorAll('#story-and-tags tr:nth-child(2) a'), el => el.rel); - const date = document.querySelector('h3 ~ div:nth-child(4), h3 ~ div div.gray:not(.scene_rater)').textContent.split(':')[1].trim(); - release.date = moment.utc(date, 'MMMM Do, YYYY').toDate(); + const date = document.querySelector('h3 ~ div:nth-child(4), h3 ~ div div.gray:not(.scene_rater)').textContent.split(':')[1].trim(); + release.date = moment.utc(date, 'MMMM Do, YYYY').toDate(); - const { poster } = document.querySelector('video'); - if (poster && !/gen/.test(poster)) release.poster = [poster.replace('low', 'hi'), poster]; + const { poster } = document.querySelector('video'); + if (poster && !/gen/.test(poster)) release.poster = [poster.replace('low', 'hi'), poster]; - const siteId = document.querySelector('#story-and-tags img').src.match(/\w+.jpg/)[0].replace('.jpg', ''); - const actorsSlug = document.querySelector('h3 a').href.split('/').slice(-2)[0]; + const siteId = document.querySelector('#story-and-tags img').src.match(/\w+.jpg/)[0].replace('.jpg', ''); + const actorsSlug = document.querySelector('h3 a').href.split('/').slice(-2)[0]; - release.photos = Array.from({ length: 5 }, (value, index) => `https://images.psmcdn.net/teamskeet/${siteId}/${actorsSlug}/shared/scenes/new/${String(index + 1).padStart(2, '0')}.jpg`); + release.photos = Array.from({ length: 5 }, (value, index) => `https://images.psmcdn.net/teamskeet/${siteId}/${actorsSlug}/shared/scenes/new/${String(index + 1).padStart(2, '0')}.jpg`); - const trailer = document.querySelector('div.right.gray a').href; - if (trailer) release.trailer = { src: trailer }; + const trailer = document.querySelector('div.right.gray a').href; + if (trailer) release.trailer = { src: trailer }; - return release; + return release; } function scrapeSceneA(html, site, sceneX, url) { - const scene = sceneX || new JSDOM(html).window.document; - const release = { site }; + const scene = sceneX || new JSDOM(html).window.document; + const release = { site }; - release.description = scene.querySelector('.scene-story').textContent.replace('...read more', '...').trim(); + release.description = scene.querySelector('.scene-story').textContent.replace('...read more', '...').trim(); - release.date = moment.utc(scene.querySelector('.scene-date').textContent, 'MM/DD/YYYY').toDate(); - release.actors = Array.from(scene.querySelectorAll('.starring span'), el => extractActors(el.textContent)).flat(); + release.date = moment.utc(scene.querySelector('.scene-date').textContent, 'MM/DD/YYYY').toDate(); + release.actors = Array.from(scene.querySelectorAll('.starring span'), el => extractActors(el.textContent)).flat(); - const durationString = scene.querySelector('.time').textContent.trim(); - const duration = ['00'].concat(durationString.split(':')).slice(-3).join(':'); // ensure hh:mm:ss - release.duration = moment.duration(duration).asSeconds(); + const durationString = scene.querySelector('.time').textContent.trim(); + const duration = ['00'].concat(durationString.split(':')).slice(-3).join(':'); // ensure hh:mm:ss + release.duration = moment.duration(duration).asSeconds(); - if (sceneX) { - const titleEl = scene.querySelector(':scope > a'); + if (sceneX) { + const titleEl = scene.querySelector(':scope > a'); - release.url = titleEl.href; - release.entryId = titleEl.id; - release.title = titleEl.title; + release.url = titleEl.href; + release.entryId = titleEl.id; + release.title = titleEl.title; - const [poster, ...photos] = Array.from(scene.querySelectorAll('.scene img'), el => el.src); - release.poster = [poster.replace('bio_big', 'video'), poster]; - release.photos = photos; - } + const [poster, ...photos] = Array.from(scene.querySelectorAll('.scene img'), el => el.src); + release.poster = [poster.replace('bio_big', 'video'), poster]; + release.photos = photos; + } - if (!sceneX) { - release.title = scene.querySelector('.title span').textContent; - release.url = url; + if (!sceneX) { + release.title = scene.querySelector('.title span').textContent; + release.url = url; - release.poster = scene.querySelector('video').poster; - release.photos = [release.poster.replace('video', 'bio_small'), release.poster.replace('video', 'bio_small2')]; - } + release.poster = scene.querySelector('video').poster; + release.photos = [release.poster.replace('video', 'bio_small'), release.poster.replace('video', 'bio_small2')]; + } - const [, entryIdA, entryIdB] = new URL(release.url).pathname.split('/'); - release.entryId = entryIdA === 'scenes' ? entryIdB : entryIdA; + const [, entryIdA, entryIdB] = new URL(release.url).pathname.split('/'); + release.entryId = entryIdA === 'scenes' ? entryIdB : entryIdA; - return release; + return release; } function scrapeLatestA(html, site) { - const { document } = new JSDOM(html).window; + const { document } = new JSDOM(html).window; - const scenes = Array.from(document.querySelectorAll('.scenewrapper')); + const scenes = Array.from(document.querySelectorAll('.scenewrapper')); - return scenes.map(scene => scrapeSceneA(null, site, scene)); + return scenes.map(scene => scrapeSceneA(null, site, scene)); } async function fetchLatestTeamSkeet(site, page = 1) { - const url = `https://www.teamskeet.com/t1/updates/load?fltrs[site]=${site.parameters.id}&page=${page}&view=newest&fltrs[time]=ALL&order=DESC`; - const res = await bhttp.get(url); + const url = `https://www.teamskeet.com/t1/updates/load?fltrs[site]=${site.parameters.id}&page=${page}&view=newest&fltrs[time]=ALL&order=DESC`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeLatest(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeLatest(res.body.toString(), site); + } - return null; + return null; } async function fetchLatestA(site) { - const url = `${site.url}/scenes`; - const res = await bhttp.get(url); + const url = `${site.url}/scenes`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeLatestA(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeLatestA(res.body.toString(), site); + } - return null; + return null; } async function fetchLatest(site, page = 1) { - if (site.parameters.id) { - return fetchLatestTeamSkeet(site, page); - } + if (site.parameters.id) { + return fetchLatestTeamSkeet(site, page); + } - if (site.parameters.scraper === 'A') { - return fetchLatestA(site, page); - } + if (site.parameters.scraper === 'A') { + return fetchLatestA(site, page); + } - return null; + return null; } async function fetchScene(url, site) { - const session = bhttp.session(); // resolve redirects - const res = await session.get(url); + const session = bhttp.session(); // resolve redirects + const res = await session.get(url); - if (site.parameters?.scraper === 'A') { - return scrapeSceneA(res.body.toString(), site, null, url); - } + if (site.parameters?.scraper === 'A') { + return scrapeSceneA(res.body.toString(), site, null, url); + } - return scrapeScene(res.body.toString(), site, url); + return scrapeScene(res.body.toString(), site, url); } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/transangels.js b/src/scrapers/transangels.js index 600d4ced3..560dfccd9 100644 --- a/src/scrapers/transangels.js +++ b/src/scrapers/transangels.js @@ -3,9 +3,9 @@ const { fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'transangels'); + return fetchProfile(actorName, 'transangels'); } module.exports = { - fetchProfile: networkFetchProfile, + fetchProfile: networkFetchProfile, }; diff --git a/src/scrapers/twistys.js b/src/scrapers/twistys.js index 2207736a1..e753ac8cd 100644 --- a/src/scrapers/twistys.js +++ b/src/scrapers/twistys.js @@ -3,11 +3,11 @@ const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek'); async function networkFetchProfile(actorName) { - return fetchProfile(actorName, 'twistys'); + return fetchProfile(actorName, 'twistys'); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchScene, }; diff --git a/src/scrapers/vivid.js b/src/scrapers/vivid.js index 450862835..275cc59b2 100644 --- a/src/scrapers/vivid.js +++ b/src/scrapers/vivid.js @@ -8,128 +8,128 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = requir const slugify = require('../utils/slugify'); function scrapeLatestNative(scenes, site) { - return scenes.map((scene) => { - const release = {}; + return scenes.map((scene) => { + const release = {}; - release.entryId = scene.id; - release.url = `${site.url}${scene.url}`; + release.entryId = scene.id; + release.url = `${site.url}${scene.url}`; - release.title = scene.name; - release.date = ed(scene.release_date, 'YYYY-MM-DD'); - release.duration = parseInt(scene.runtime, 10) * 60; + release.title = scene.name; + release.date = ed(scene.release_date, 'YYYY-MM-DD'); + release.duration = parseInt(scene.runtime, 10) * 60; - release.actors = scene.cast?.map(actor => ({ - name: actor.stagename, - gender: actor.gender.toLowerCase(), - avatar: actor.placard, - })) || []; + release.actors = scene.cast?.map(actor => ({ + name: actor.stagename, + gender: actor.gender.toLowerCase(), + avatar: actor.placard, + })) || []; - release.stars = Number(scene.rating); - release.poster = scene.placard_800 || scene.placard; + release.stars = Number(scene.rating); + release.poster = scene.placard_800 || scene.placard; - return release; - }); + return release; + }); } function scrapeSceneNative({ html, q, qa }, url, _site) { - const release = { url }; + const release = { url }; - release.entryId = new URL(url).pathname.split('/')[2]; // eslint-disable-line prefer-destructuring + release.entryId = new URL(url).pathname.split('/')[2]; // eslint-disable-line prefer-destructuring - release.title = q('.scene-h2-heading', true); - release.description = q('.indie-model-p', true); + release.title = q('.scene-h2-heading', true); + release.description = q('.indie-model-p', true); - const dateString = qa('h5').find(el => /Released/.test(el.textContent)).textContent; - release.date = ed(dateString, 'MMM DD, YYYY', /\w+ \d{1,2}, \d{4}/); + const dateString = qa('h5').find(el => /Released/.test(el.textContent)).textContent; + release.date = ed(dateString, 'MMM DD, YYYY', /\w+ \d{1,2}, \d{4}/); - const duration = qa('h5').find(el => /Runtime/.test(el.textContent)).textContent; - const [hours, minutes] = duration.match(/\d+/g); + const duration = qa('h5').find(el => /Runtime/.test(el.textContent)).textContent; + const [hours, minutes] = duration.match(/\d+/g); - if (minutes) release.duration = (hours * 3600) + (minutes * 60); - else release.duration = hours * 60; // scene shorter that 1hr, hour match are minutes + if (minutes) release.duration = (hours * 3600) + (minutes * 60); + else release.duration = hours * 60; // scene shorter that 1hr, hour match are minutes - release.actors = qa('h4 a[href*="/stars"], h4 a[href*="/celebs"]', true); - release.tags = qa('h5 a[href*="/categories"]', true); + release.actors = qa('h4 a[href*="/stars"], h4 a[href*="/celebs"]', true); + release.tags = qa('h5 a[href*="/categories"]', true); - const [poster, trailer] = html.match(/https:\/\/content.vivid.com(.*)(.jpg|.mp4)/g); - release.poster = poster; + const [poster, trailer] = html.match(/https:\/\/content.vivid.com(.*)(.jpg|.mp4)/g); + release.poster = poster; - if (trailer) { - release.trailer = { - src: trailer, - }; - } + if (trailer) { + release.trailer = { + src: trailer, + }; + } - const channel = q('h5 a[href*="/sites"]', true); - if (channel) release.channel = channel.replace(/\.\w+/, ''); + const channel = q('h5 a[href*="/sites"]', true); + if (channel) release.channel = channel.replace(/\.\w+/, ''); - return release; + return release; } async function fetchLatestNative(site, page = 1) { - if (site.parameters?.useGamma) { - return fetchApiLatest(site, page); - } + if (site.parameters?.useGamma) { + return fetchApiLatest(site, page); + } - const apiUrl = `${site.url}/videos/api/?limit=50&offset=${(page - 1) * 50}&sort=datedesc`; - const res = await bhttp.get(apiUrl, { - decodeJSON: true, - }); + const apiUrl = `${site.url}/videos/api/?limit=50&offset=${(page - 1) * 50}&sort=datedesc`; + const res = await bhttp.get(apiUrl, { + decodeJSON: true, + }); - if (res.statusCode === 200 && res.body.code === 200) { - return scrapeLatestNative(res.body.responseData, site); - } + if (res.statusCode === 200 && res.body.code === 200) { + return scrapeLatestNative(res.body.responseData, site); + } - return null; + return null; } async function fetchUpcomingNative(site) { - if (site.parameters?.useGamma) { - return fetchApiUpcoming(site); - } + if (site.parameters?.useGamma) { + return fetchApiUpcoming(site); + } - return null; + return null; } async function fetchSceneNative(url, site, release) { - if (site.parameters?.useGamma) { - return fetchScene(url, site, release); - } + if (site.parameters?.useGamma) { + return fetchScene(url, site, release); + } - const res = await get(url); + const res = await get(url); - return res.ok ? scrapeSceneNative(res.item, url, site) : res.status; + return res.ok ? scrapeSceneNative(res.item, url, site) : res.status; } async function fetchSceneWrapper(url, site, release) { - const scene = await fetchScene(url, site, release); + const scene = await fetchScene(url, site, release); - if (scene.date - new Date(site.parameters?.lastNative) <= 0) { - // scene is probably still available on Vivid site, use search API to get URL and original date - const searchUrl = `${site.url}/videos/api/?limit=10&sort=datedesc&search=${encodeURI(scene.title)}`; - const searchRes = await bhttp.get(searchUrl, { - decodeJSON: true, - }); + if (scene.date - new Date(site.parameters?.lastNative) <= 0) { + // scene is probably still available on Vivid site, use search API to get URL and original date + const searchUrl = `${site.url}/videos/api/?limit=10&sort=datedesc&search=${encodeURI(scene.title)}`; + const searchRes = await bhttp.get(searchUrl, { + decodeJSON: true, + }); - if (searchRes.statusCode === 200 && searchRes.body.code === 200) { - const sceneMatch = searchRes.body.responseData.find(item => slugify(item.name) === slugify(scene.title)); + if (searchRes.statusCode === 200 && searchRes.body.code === 200) { + const sceneMatch = searchRes.body.responseData.find(item => slugify(item.name) === slugify(scene.title)); - if (sceneMatch) { - return { - ...scene, - url: `${site.url}${sceneMatch.url}`, - date: ed(sceneMatch.release_date, 'YYYY-MM-DD'), - }; - } - } - } + if (sceneMatch) { + return { + ...scene, + url: `${site.url}${sceneMatch.url}`, + date: ed(sceneMatch.release_date, 'YYYY-MM-DD'), + }; + } + } + } - return scene; + return scene; } module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchUpcoming: fetchApiUpcoming, - fetchScene: fetchSceneWrapper, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchUpcoming: fetchApiUpcoming, + fetchScene: fetchSceneWrapper, }; diff --git a/src/scrapers/vixen.js b/src/scrapers/vixen.js index ddbd8bc16..cef49b387 100644 --- a/src/scrapers/vixen.js +++ b/src/scrapers/vixen.js @@ -8,246 +8,246 @@ const { get, post } = require('../utils/http'); const slugify = require('../utils/slugify'); const genderMap = { - F: 'female', - M: 'male', - T: 'transsexual', // not yet observed + F: 'female', + M: 'male', + T: 'transsexual', // not yet observed }; function getPosterFallbacks(poster) { - return poster - .filter(image => /landscape/i.test(image.name)) - .sort((imageA, imageB) => imageB.height - imageA.height) - .map((image) => { - const sources = [image.src, image.highdpi?.['2x'], image.highdpi?.['3x']]; - // high DPI images for full HD source are huge, only prefer for smaller fallback sources - return image.height === 1080 ? sources : sources.reverse(); - }) - .flat(); + return poster + .filter(image => /landscape/i.test(image.name)) + .sort((imageA, imageB) => imageB.height - imageA.height) + .map((image) => { + const sources = [image.src, image.highdpi?.['2x'], image.highdpi?.['3x']]; + // high DPI images for full HD source are huge, only prefer for smaller fallback sources + return image.height === 1080 ? sources : sources.reverse(); + }) + .flat(); } function getTeaserFallbacks(teaser) { - return teaser - .filter(video => /landscape/i.test(video.name)) - .map(video => ({ - src: video.src, - type: video.type, - quality: Number(String(video.height).replace('353', '360')), - })); + return teaser + .filter(video => /landscape/i.test(video.name)) + .map(video => ({ + src: video.src, + type: video.type, + quality: Number(String(video.height).replace('353', '360')), + })); } function getAvatarFallbacks(avatar) { - return avatar - .sort((imageA, imageB) => imageB.height - imageA.height) - .map(image => [image.highdpi?.['3x'], image.highdpi?.['2x'], image.src]) - .flat(); + return avatar + .sort((imageA, imageB) => imageB.height - imageA.height) + .map(image => [image.highdpi?.['3x'], image.highdpi?.['2x'], image.src]) + .flat(); } async function getTrailer(scene, site, url) { - const qualities = [360, 480, 720, 1080, 2160]; + const qualities = [360, 480, 720, 1080, 2160]; - const tokenRes = await post(`${site.url}/api/__record_tknreq`, { - file: scene.previewVideoUrl1080P, - sizes: qualities.join('+'), - type: 'trailer', - }, { referer: url }); + const tokenRes = await post(`${site.url}/api/__record_tknreq`, { + file: scene.previewVideoUrl1080P, + sizes: qualities.join('+'), + type: 'trailer', + }, { referer: url }); - if (!tokenRes.ok) { - return null; - } + if (!tokenRes.ok) { + return null; + } - const trailerUrl = `${site.url}/api${tokenRes.body.data.url}`; - const trailersRes = await post(trailerUrl, null, { referer: url }); + const trailerUrl = `${site.url}/api${tokenRes.body.data.url}`; + const trailersRes = await post(trailerUrl, null, { referer: url }); - if (trailersRes.ok) { - return qualities.map(quality => (trailersRes.body[quality] ? { - src: trailersRes.body[quality].token, - quality, - } : null)).filter(Boolean); - } + if (trailersRes.ok) { + return qualities.map(quality => (trailersRes.body[quality] ? { + src: trailersRes.body[quality].token, + quality, + } : null)).filter(Boolean); + } - return null; + return null; } function scrapeAll(scenes, site, origin) { - return scenes.map((scene) => { - const release = {}; + return scenes.map((scene) => { + const release = {}; - release.title = scene.title; + release.title = scene.title; - release.entryId = String(scene.newId); - release.url = `${site?.url || origin}${scene.targetUrl}`; + release.entryId = String(scene.newId); + release.url = `${site?.url || origin}${scene.targetUrl}`; - release.date = moment.utc(scene.releaseDate).toDate(); - release.shootDate = moment.utc(scene.shootDate).toDate(); + release.date = moment.utc(scene.releaseDate).toDate(); + release.shootDate = moment.utc(scene.shootDate).toDate(); - release.actors = scene.models; - release.stars = Number(scene.textRating) / 2; + release.actors = scene.models; + release.stars = Number(scene.textRating) / 2; - release.poster = getPosterFallbacks(scene.images.poster); - release.teaser = getTeaserFallbacks(scene.previews.poster); + release.poster = getPosterFallbacks(scene.images.poster); + release.teaser = getTeaserFallbacks(scene.previews.poster); - return release; - }); + return release; + }); } function scrapeUpcoming(scene, site) { - if (!scene || scene.isPreReleasePeriod) return null; + if (!scene || scene.isPreReleasePeriod) return null; - const release = {}; + const release = {}; - release.title = scene.targetUrl - .slice(1) - .split('-') - .map(component => `${component.charAt(0).toUpperCase()}${component.slice(1)}`) - .join(' '); + release.title = scene.targetUrl + .slice(1) + .split('-') + .map(component => `${component.charAt(0).toUpperCase()}${component.slice(1)}`) + .join(' '); - release.url = `${site.url}${scene.targetUrl}`; + release.url = `${site.url}${scene.targetUrl}`; - release.date = moment.utc(scene.releaseDate).toDate(); - release.shootDate = moment.utc(scene.shootDate).toDate(); + release.date = moment.utc(scene.releaseDate).toDate(); + release.shootDate = moment.utc(scene.shootDate).toDate(); - release.actors = scene.models; + release.actors = scene.models; - release.poster = getPosterFallbacks(scene.images.poster); - release.teaser = getTeaserFallbacks(scene.previews.poster); + release.poster = getPosterFallbacks(scene.images.poster); + release.teaser = getTeaserFallbacks(scene.previews.poster); - release.entryId = (release.poster[0] || release.teaser[0])?.match(/\/(\d+)/)?.[1]; + release.entryId = (release.poster[0] || release.teaser[0])?.match(/\/(\d+)/)?.[1]; - return [release]; + return [release]; } async function scrapeScene(data, url, site, baseRelease) { - const scene = data.video; + const scene = data.video; - const release = { - url, - title: scene.title, - description: scene.description, - actors: scene.models, - director: scene.directorNames, - duration: scene.runLength, - stars: scene.totalRateVal, - tags: scene.tags, - }; + const release = { + url, + title: scene.title, + description: scene.description, + actors: scene.models, + director: scene.directorNames, + duration: scene.runLength, + stars: scene.totalRateVal, + tags: scene.tags, + }; - release.entryId = scene.newId; + release.entryId = scene.newId; - release.date = moment.utc(scene.releaseDate).toDate(); - release.shootDate = moment.utc(scene.shootDate).toDate(); + release.date = moment.utc(scene.releaseDate).toDate(); + release.shootDate = moment.utc(scene.shootDate).toDate(); - release.actors = baseRelease?.actors || scene.models; + release.actors = baseRelease?.actors || scene.models; - release.poster = getPosterFallbacks(scene.images.poster); - release.photos = data.pictureset.map(photo => photo.main[0].src); + release.poster = getPosterFallbacks(scene.images.poster); + release.photos = data.pictureset.map(photo => photo.main[0].src); - release.teaser = getTeaserFallbacks(scene.previews.poster); + release.teaser = getTeaserFallbacks(scene.previews.poster); - const trailer = await getTrailer(scene, site, url); - if (trailer) release.trailer = trailer; + const trailer = await getTrailer(scene, site, url); + if (trailer) release.trailer = trailer; - return release; + return release; } async function fetchActorReleases(pages, model, origin) { - const releasesPerPage = await Promise.map(pages, async (page) => { - const url = `${origin}/api${model.targetUrl}?page=${page}`; - const res = await get(url); + const releasesPerPage = await Promise.map(pages, async (page) => { + const url = `${origin}/api${model.targetUrl}?page=${page}`; + const res = await get(url); - if (res.code === 200) { - return scrapeAll(res.body.data.videos.videos, null, origin); - } + if (res.code === 200) { + return scrapeAll(res.body.data.videos.videos, null, origin); + } - return []; - }, { concurrency: 3 }); + return []; + }, { concurrency: 3 }); - return releasesPerPage.flat(); + return releasesPerPage.flat(); } async function scrapeProfile(data, origin, withReleases) { - const model = data.model; - const profile = {}; + const model = data.model; + const profile = {}; - profile.birthdate = new Date(model.dateOfBirth); - profile.gender = genderMap[model.sex]; + profile.birthdate = new Date(model.dateOfBirth); + profile.gender = genderMap[model.sex]; - profile.hair = model.hairColour; - profile.nationality = model.nationality; + profile.hair = model.hairColour; + profile.nationality = model.nationality; - if (model.biography.trim().length > 0) profile.description = model.biography; + if (model.biography.trim().length > 0) profile.description = model.biography; - if (model.cupSize && model.bustMeasurment) profile.bust = `${model.bustMeasurment}${model.cupSize}`; - if (model.waistMeasurment) profile.waist = model.waistMeasurment; - if (model.hipMeasurment) profile.hip = model.hipMeasurment; + if (model.cupSize && model.bustMeasurment) profile.bust = `${model.bustMeasurment}${model.cupSize}`; + if (model.waistMeasurment) profile.waist = model.waistMeasurment; + if (model.hipMeasurment) profile.hip = model.hipMeasurment; - profile.avatar = getAvatarFallbacks(model.images.listing); - profile.poster = getAvatarFallbacks(model.images.profile); - profile.banner = getAvatarFallbacks(model.images.poster); + profile.avatar = getAvatarFallbacks(model.images.listing); + profile.poster = getAvatarFallbacks(model.images.profile); + profile.banner = getAvatarFallbacks(model.images.poster); - const releases = scrapeAll(data.videos.videos, null, origin); + const releases = scrapeAll(data.videos.videos, null, origin); - if (withReleases) { - const pageCount = Math.ceil(data.videos.count / 6); - const otherReleases = await fetchActorReleases((Array.from({ length: pageCount - 1 }, (value, index) => index + 2)), model, origin); + if (withReleases) { + const pageCount = Math.ceil(data.videos.count / 6); + const otherReleases = await fetchActorReleases((Array.from({ length: pageCount - 1 }, (value, index) => index + 2)), model, origin); - profile.releases = [...releases, ...otherReleases]; - } else { - profile.releases = releases; - } + profile.releases = [...releases, ...otherReleases]; + } else { + profile.releases = releases; + } - return profile; + return profile; } async function fetchLatest(site, page = 1) { - const url = `${site.url}/api/videos?page=${page}`; - const res = await get(url); + const url = `${site.url}/api/videos?page=${page}`; + const res = await get(url); - if (res.code === 200) { - return scrapeAll(res.body.data.videos, site); - } + if (res.code === 200) { + return scrapeAll(res.body.data.videos, site); + } - return res.code; + return res.code; } async function fetchUpcoming(site) { - const apiUrl = `${site.url}/api`; - const res = await get(apiUrl); + const apiUrl = `${site.url}/api`; + const res = await get(apiUrl); - if (res.code === 200) { - return scrapeUpcoming(res.body.data.nextScene, site); - } + if (res.code === 200) { + return scrapeUpcoming(res.body.data.nextScene, site); + } - return res.code; + return res.code; } async function fetchScene(url, site, baseRelease) { - const { origin, pathname } = new URL(url); - const apiUrl = `${origin}/api${pathname}`; + const { origin, pathname } = new URL(url); + const apiUrl = `${origin}/api${pathname}`; - const res = await get(apiUrl); + const res = await get(apiUrl); - if (res.code === 200) { - return scrapeScene(res.body.data, url, site, baseRelease); - } + if (res.code === 200) { + return scrapeScene(res.body.data, url, site, baseRelease); + } - return res.code; + return res.code; } async function fetchProfile(actorName, scraperSlug, site, include) { - const origin = `https://www.${scraperSlug}.com`; - const actorSlug = slugify(actorName); - const url = `${origin}/api/${actorSlug}`; - const res = await get(url); + const origin = `https://www.${scraperSlug}.com`; + const actorSlug = slugify(actorName); + const url = `${origin}/api/${actorSlug}`; + const res = await get(url); - if (res.code === 200) { - return scrapeProfile(res.body.data, origin, include.scenes); - } + if (res.code === 200) { + return scrapeProfile(res.body.data, origin, include.scenes); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchUpcoming, - fetchScene, - fetchProfile, + fetchLatest, + fetchUpcoming, + fetchScene, + fetchProfile, }; diff --git a/src/scrapers/vogov.js b/src/scrapers/vogov.js index 91e8a07b8..8ba4d0c5f 100644 --- a/src/scrapers/vogov.js +++ b/src/scrapers/vogov.js @@ -5,199 +5,199 @@ const { ex, ctxa } = require('../utils/q'); // const slugify = require('../utils/slugify'); function getLicenseCode(html) { - const licensePrefix = 'license_code: \''; - const licenseStart = html.indexOf(licensePrefix); - const licenseCode = html.slice(licenseStart + licensePrefix.length, html.indexOf('\'', licenseStart + licensePrefix.length)); + const licensePrefix = 'license_code: \''; + const licenseStart = html.indexOf(licensePrefix); + const licenseCode = html.slice(licenseStart + licensePrefix.length, html.indexOf('\'', licenseStart + licensePrefix.length)); - const c = '16px'; - let f; - let g; - let h; - let i; - let j; - let k; - let l; - let m; - let n; + const c = '16px'; + let f; + let g; + let h; + let i; + let j; + let k; + let l; + let m; + let n; - for (f = '', g = 1; g < licenseCode.length; g += 1) { - f += parseInt(licenseCode[g], 10) ? parseInt(licenseCode[g], 10) : 1; - } + for (f = '', g = 1; g < licenseCode.length; g += 1) { + f += parseInt(licenseCode[g], 10) ? parseInt(licenseCode[g], 10) : 1; + } - for (j = parseInt(f.length / 2, 10), - k = parseInt(f.substring(0, j + 1), 10), - l = parseInt(f.substring(j), 10), - g = l - k, - g < 0 && (g = -g), - f = g, - g = k - l, - g < 0 && (g = -g), - f += g, - f *= 2, - f = String(f), - i = (parseInt(c, 10) / 2) + 2, - m = '', - g = 0; g < j + 1; g += 1) { - for (h = 1; h <= 4; h += 1) { - n = parseInt(licenseCode[g + h], 10) + parseInt(f[g], 10); + for (j = parseInt(f.length / 2, 10), + k = parseInt(f.substring(0, j + 1), 10), + l = parseInt(f.substring(j), 10), + g = l - k, + g < 0 && (g = -g), + f = g, + g = k - l, + g < 0 && (g = -g), + f += g, + f *= 2, + f = String(f), + i = (parseInt(c, 10) / 2) + 2, + m = '', + g = 0; g < j + 1; g += 1) { + for (h = 1; h <= 4; h += 1) { + n = parseInt(licenseCode[g + h], 10) + parseInt(f[g], 10); - if (n >= i) n -= i; - m += n; - } - } + if (n >= i) n -= i; + m += n; + } + } - return m; + return m; } function decodeTrailerUrl(html, encodedTrailerUrl) { - const licenseCode = getLicenseCode(html); - const i = licenseCode; + const licenseCode = getLicenseCode(html); + const i = licenseCode; - let j; - let k; - let l; - let m; - let n; - let o; + let j; + let k; + let l; + let m; + let n; + let o; - const d = '16px'; - const g = encodedTrailerUrl.split('/').slice(2); + const d = '16px'; + const g = encodedTrailerUrl.split('/').slice(2); - let h = g[5].substring(0, 2 * parseInt(d, 10)); + let h = g[5].substring(0, 2 * parseInt(d, 10)); - for (j = h, k = h.length - 1; k >= 0; k -= 1) { - for (l = k, m = k; m < i.length; m += 1) { - l += parseInt(i[m], 10); - } + for (j = h, k = h.length - 1; k >= 0; k -= 1) { + for (l = k, m = k; m < i.length; m += 1) { + l += parseInt(i[m], 10); + } - for (; l >= h.length;) { - l -= h.length; - } + for (; l >= h.length;) { + l -= h.length; + } - for (n = '', o = 0; o < h.length; o += 1) { - if (o === k) { - n += h[l]; - } else { - n += (o === l ? h[k] : h[o]); - } - } + for (n = '', o = 0; o < h.length; o += 1) { + if (o === k) { + n += h[l]; + } else { + n += (o === l ? h[k] : h[o]); + } + } - h = n; - } + h = n; + } - g[5] = g[5].replace(j, h); - const trailer = g.join('/'); + g[5] = g[5].replace(j, h); + const trailer = g.join('/'); - return trailer; + return trailer; } function scrapeLatest(html) { - const { document } = ex(html); + const { document } = ex(html); - return ctxa(document, '.video-post').map(({ q, qa, qd }) => { - const release = {}; + return ctxa(document, '.video-post').map(({ q, qa, qd }) => { + const release = {}; - // release.entryId = slugify(release.title); - release.entryId = q('.ico-fav-0').dataset.favVideoId; + // release.entryId = slugify(release.title); + release.entryId = q('.ico-fav-0').dataset.favVideoId; - const titleEl = q('.video-title-title'); - release.title = titleEl.title; - release.url = titleEl.href; + const titleEl = q('.video-title-title'); + release.title = titleEl.title; + release.url = titleEl.href; - release.date = qd('.video-data em', 'MMM DD, YYYY'); - release.actors = qa('.video-model-list a', true); + release.date = qd('.video-data em', 'MMM DD, YYYY'); + release.actors = qa('.video-model-list a', true); - const posterData = q('img.thumb').dataset; - release.poster = posterData.src; - release.trailer = posterData.preview; + const posterData = q('img.thumb').dataset; + release.poster = posterData.src; + release.trailer = posterData.preview; - return release; - }); + return release; + }); } function scrapeScene(html, url) { - const { qu } = ex(html); - const release = { url }; + const { qu } = ex(html); + const release = { url }; - // release.entryId = slugify(release.title); - [release.entryId] = qu.q('link[rel="canonical"]').href.match(/\d+/); + // release.entryId = slugify(release.title); + [release.entryId] = qu.q('link[rel="canonical"]').href.match(/\d+/); - release.title = qu.meta('meta[property="og:title"]') || qu.q('.video-page-header h1', true); - release.description = qu.meta('meta[property="og:description"]') || qu.q('.info-video-description', true); + release.title = qu.meta('meta[property="og:title"]') || qu.q('.video-page-header h1', true); + release.description = qu.meta('meta[property="og:description"]') || qu.q('.info-video-description', true); - release.date = qu.date('.info-video-details li:first-child span', 'MMM DD, YYYY'); - release.duration = qu.dur('.info-video-details li:nth-child(2) span'); + release.date = qu.date('.info-video-details li:first-child span', 'MMM DD, YYYY'); + release.duration = qu.dur('.info-video-details li:nth-child(2) span'); - release.actors = qu.all('.info-video-models a', true); - release.tags = qu.all('.info-video-category a', true); + release.actors = qu.all('.info-video-models a', true); + release.tags = qu.all('.info-video-category a', true); - release.photos = qu.urls('.swiper-wrapper .swiper-slide a').map(source => source.replace('.jpg/', '.jpg')); - release.poster = qu.meta('meta[property="og:image"'); + release.photos = qu.urls('.swiper-wrapper .swiper-slide a').map(source => source.replace('.jpg/', '.jpg')); + release.poster = qu.meta('meta[property="og:image"'); - if (!release.poster) { - const previewStart = html.indexOf('preview_url'); - release.poster = html.slice(html.indexOf('http', previewStart), html.indexOf('.jpg', previewStart) + 4); - } + if (!release.poster) { + const previewStart = html.indexOf('preview_url'); + release.poster = html.slice(html.indexOf('http', previewStart), html.indexOf('.jpg', previewStart) + 4); + } - const varsPrefix = 'flashvars = {'; - const varsStart = html.indexOf(varsPrefix); - const varsString = html.slice(varsStart + varsPrefix.length, html.indexOf('};', varsStart)); + const varsPrefix = 'flashvars = {'; + const varsStart = html.indexOf(varsPrefix); + const varsString = html.slice(varsStart + varsPrefix.length, html.indexOf('};', varsStart)); - const vars = varsString.split(',').reduce((acc, item) => { - const [prop, value] = item.split(': '); - acc[prop.trim()] = value.trim().replace(/'/g, ''); + const vars = varsString.split(',').reduce((acc, item) => { + const [prop, value] = item.split(': '); + acc[prop.trim()] = value.trim().replace(/'/g, ''); - return acc; - }, {}); + return acc; + }, {}); - release.trailer = [ - { - src: decodeTrailerUrl(html, vars.video_url), - quality: parseInt(vars.video_url_text, 10), - }, - { - src: decodeTrailerUrl(html, vars.video_alt_url), - quality: parseInt(vars.video_alt_url_text, 10), - }, - { - src: decodeTrailerUrl(html, vars.video_alt_url2), - quality: parseInt(vars.video_alt_url2_text, 10), - }, - { - src: decodeTrailerUrl(html, vars.video_alt_url3), - quality: parseInt(vars.video_alt_url3_text, 10), - }, - { - src: decodeTrailerUrl(html, vars.video_alt_url4), - quality: parseInt(vars.video_alt_url4_text, 10), - }, - ]; + release.trailer = [ + { + src: decodeTrailerUrl(html, vars.video_url), + quality: parseInt(vars.video_url_text, 10), + }, + { + src: decodeTrailerUrl(html, vars.video_alt_url), + quality: parseInt(vars.video_alt_url_text, 10), + }, + { + src: decodeTrailerUrl(html, vars.video_alt_url2), + quality: parseInt(vars.video_alt_url2_text, 10), + }, + { + src: decodeTrailerUrl(html, vars.video_alt_url3), + quality: parseInt(vars.video_alt_url3_text, 10), + }, + { + src: decodeTrailerUrl(html, vars.video_alt_url4), + quality: parseInt(vars.video_alt_url4_text, 10), + }, + ]; - return release; + return release; } async function fetchLatest(site, page = 1) { - const url = `https://vogov.com/latest-videos/?sort_by=post_date&from=${page}`; - const res = await bhttp.get(url); + const url = `https://vogov.com/latest-videos/?sort_by=post_date&from=${page}`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeLatest(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeLatest(res.body.toString(), site); + } - return null; + return null; } async function fetchScene(url) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeScene(res.body.toString(), url); - } + if (res.statusCode === 200) { + return scrapeScene(res.body.toString(), url); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/whalemember.js b/src/scrapers/whalemember.js index 3f9bdb07c..05ff936ad 100644 --- a/src/scrapers/whalemember.js +++ b/src/scrapers/whalemember.js @@ -5,86 +5,86 @@ const { JSDOM } = require('jsdom'); const moment = require('moment'); function scrapeLatest(html, site) { - const { document } = new JSDOM(html).window; - const { origin } = new URL(site.url); + const { document } = new JSDOM(html).window; + const { origin } = new URL(site.url); - const videos = Array.from(document.querySelectorAll('.video-releases-list')).slice(-1)[0]; + const videos = Array.from(document.querySelectorAll('.video-releases-list')).slice(-1)[0]; - return Array.from(videos.querySelectorAll('.card'), (scene) => { - const release = { site }; + return Array.from(videos.querySelectorAll('.card'), (scene) => { + const release = { site }; - release.url = `${origin}${scene.querySelector(':scope > a').href}`; - release.entryId = scene.dataset.videoId; - release.title = scene.querySelector('.card-title').textContent; - release.date = moment.utc(scene.dataset.date, 'MMMM DD, YYYY').toDate(); - release.actors = Array.from(scene.querySelectorAll('.actors a'), el => el.textContent); + release.url = `${origin}${scene.querySelector(':scope > a').href}`; + release.entryId = scene.dataset.videoId; + release.title = scene.querySelector('.card-title').textContent; + release.date = moment.utc(scene.dataset.date, 'MMMM DD, YYYY').toDate(); + release.actors = Array.from(scene.querySelectorAll('.actors a'), el => el.textContent); - release.poster = `https:${scene.querySelector('.single-image').src}`; - release.photos = Array.from(scene.querySelectorAll('.rollover-thumbs img'), el => `https:${el.dataset.src}`); + release.poster = `https:${scene.querySelector('.single-image').src}`; + release.photos = Array.from(scene.querySelectorAll('.rollover-thumbs img'), el => `https:${el.dataset.src}`); - const trailerEl = scene.querySelector('source'); - if (trailerEl) release.trailer = { src: trailerEl.dataset.src }; + const trailerEl = scene.querySelector('source'); + if (trailerEl) release.trailer = { src: trailerEl.dataset.src }; - return release; - }); + return release; + }); } function scrapeScene(html, site, url) { - const { document } = new JSDOM(html).window; - const release = { site }; + const { document } = new JSDOM(html).window; + const release = { site }; - const scene = document.querySelector('#t2019-2col'); + const scene = document.querySelector('#t2019-2col'); - release.url = url; - release.title = scene.querySelector('.t2019-stitle').textContent.trim(); - release.description = scene.querySelector('#t2019-description').textContent.trim(); - release.actors = Array.from(scene.querySelectorAll('#t2019-models a'), el => el.textContent); + release.url = url; + release.title = scene.querySelector('.t2019-stitle').textContent.trim(); + release.description = scene.querySelector('#t2019-description').textContent.trim(); + release.actors = Array.from(scene.querySelectorAll('#t2019-models a'), el => el.textContent); - const durationEls = Array.from(scene.querySelectorAll('#t2019-stime span')); + const durationEls = Array.from(scene.querySelectorAll('#t2019-stime span')); - if (durationEls.length > 1) { - release.date = moment.utc(durationEls[0].textContent, 'MMMM DD, YYYY').toDate(); - release.duration = Number(durationEls[1].textContent.match(/\d+/)[0]) * 60; - } else { - release.duration = Number(durationEls[0].textContent.match(/\d+/)[0]) * 60; - } + if (durationEls.length > 1) { + release.date = moment.utc(durationEls[0].textContent, 'MMMM DD, YYYY').toDate(); + release.duration = Number(durationEls[1].textContent.match(/\d+/)[0]) * 60; + } else { + release.duration = Number(durationEls[0].textContent.match(/\d+/)[0]) * 60; + } - release.photos = Array.from(scene.querySelectorAll('#t2019-main .t2019-thumbs img'), el => `https:${el.src}`); + release.photos = Array.from(scene.querySelectorAll('#t2019-main .t2019-thumbs img'), el => `https:${el.src}`); - const posterEl = scene.querySelector('#no-player-image'); - const videoEl = scene.querySelector('video'); + const posterEl = scene.querySelector('#no-player-image'); + const videoEl = scene.querySelector('video'); - if (posterEl) release.poster = `https:${posterEl.src}`; - else if (videoEl) release.poster = `https:${videoEl.poster}`; + if (posterEl) release.poster = `https:${posterEl.src}`; + else if (videoEl) release.poster = `https:${videoEl.poster}`; - const trailerEl = scene.querySelector('#t2019-video source'); - if (trailerEl) release.trailer = { src: trailerEl.src }; + const trailerEl = scene.querySelector('#t2019-video source'); + if (trailerEl) release.trailer = { src: trailerEl.src }; - return release; + return release; } async function fetchLatest(site, page = 1) { - const url = `${site.url}?page=${page}`; - const res = await bhttp.get(url); + const url = `${site.url}?page=${page}`; + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeLatest(res.body.toString(), site); - } + if (res.statusCode === 200) { + return scrapeLatest(res.body.toString(), site); + } - return []; + return []; } async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - if (res.statusCode === 200) { - return scrapeScene(res.body.toString(), site, url); - } + if (res.statusCode === 200) { + return scrapeScene(res.body.toString(), site, url); + } - return null; + return null; } module.exports = { - fetchLatest, - fetchScene, + fetchLatest, + fetchScene, }; diff --git a/src/scrapers/wicked.js b/src/scrapers/wicked.js index c31fa6e80..562acaa6e 100644 --- a/src/scrapers/wicked.js +++ b/src/scrapers/wicked.js @@ -3,8 +3,8 @@ const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma'); module.exports = { - fetchLatest: fetchApiLatest, - fetchProfile: fetchApiProfile, - fetchScene, - fetchUpcoming: fetchApiUpcoming, + fetchLatest: fetchApiLatest, + fetchProfile: fetchApiProfile, + fetchScene, + fetchUpcoming: fetchApiUpcoming, }; diff --git a/src/scrapers/xempire.js b/src/scrapers/xempire.js index bbcf744ad..f970ba6d6 100644 --- a/src/scrapers/xempire.js +++ b/src/scrapers/xempire.js @@ -5,31 +5,31 @@ const bhttp = require('bhttp'); const { fetchLatest, fetchUpcoming, scrapeScene, fetchProfile } = require('./gamma'); async function fetchScene(url, site) { - const res = await bhttp.get(url); + const res = await bhttp.get(url); - const release = await scrapeScene(res.body.toString(), url, site); + const release = await scrapeScene(res.body.toString(), url, site); - const siteDomain = release.$('meta[name="twitter:domain"]').attr('content') || 'allblackx.com'; // only AllBlackX has no twitter domain, no other useful hints available - const siteSlug = siteDomain && siteDomain.split('.')[0].toLowerCase(); - // const siteUrl = siteDomain && `https://www.${siteDomain}`; + const siteDomain = release.$('meta[name="twitter:domain"]').attr('content') || 'allblackx.com'; // only AllBlackX has no twitter domain, no other useful hints available + const siteSlug = siteDomain && siteDomain.split('.')[0].toLowerCase(); + // const siteUrl = siteDomain && `https://www.${siteDomain}`; - release.channel = siteSlug; - release.director = 'Mason'; + release.channel = siteSlug; + release.director = 'Mason'; - return release; + return release; } function getActorReleasesUrl(actorPath, page = 1) { - return `https://www.xempire.com/en/videos/xempire/latest/${page}/All-Categories/0${actorPath}`; + return `https://www.xempire.com/en/videos/xempire/latest/${page}/All-Categories/0${actorPath}`; } async function networkFetchProfile(actorName, scraperSlug, site, include) { - return fetchProfile(actorName, scraperSlug, null, getActorReleasesUrl, include); + return fetchProfile(actorName, scraperSlug, null, getActorReleasesUrl, include); } module.exports = { - fetchLatest, - fetchProfile: networkFetchProfile, - fetchUpcoming, - fetchScene, + fetchLatest, + fetchProfile: networkFetchProfile, + fetchUpcoming, + fetchScene, }; diff --git a/src/sites.js b/src/sites.js index 1c9a591fc..0d0995c94 100644 --- a/src/sites.js +++ b/src/sites.js @@ -8,189 +8,189 @@ const knex = require('./knex'); const whereOr = require('./utils/where-or'); async function curateSite(site, includeParameters = false, includeTags = true) { - const curatedSite = { - id: site.id, - name: site.name, - url: site.url, - description: site.description, - slug: site.slug, - independent: !!site.parameters && site.parameters.independent, - parameters: includeParameters ? site.parameters : null, - network: { - id: site.network_id, - name: site.network_name, - description: site.network_description, - slug: site.network_slug, - url: site.network_url, - parameters: includeParameters ? site.network_parameters : null, - }, - }; + const curatedSite = { + id: site.id, + name: site.name, + url: site.url, + description: site.description, + slug: site.slug, + independent: !!site.parameters && site.parameters.independent, + parameters: includeParameters ? site.parameters : null, + network: { + id: site.network_id, + name: site.network_name, + description: site.network_description, + slug: site.network_slug, + url: site.network_url, + parameters: includeParameters ? site.network_parameters : null, + }, + }; - if (includeTags) { - curatedSite.tags = await knex('sites_tags') - .select('tags.*', 'sites_tags.inherit') - .where('site_id', site.id) - .join('tags', 'tags.id', 'sites_tags.tag_id'); - } + if (includeTags) { + curatedSite.tags = await knex('sites_tags') + .select('tags.*', 'sites_tags.inherit') + .where('site_id', site.id) + .join('tags', 'tags.id', 'sites_tags.tag_id'); + } - return curatedSite; + return curatedSite; } async function curateSites(sites, includeParameters) { - return Promise.all(sites.map(async site => curateSite(site, includeParameters))); + return Promise.all(sites.map(async site => curateSite(site, includeParameters))); } function destructConfigNetworks(networks = []) { - return networks.reduce((acc, network) => { - if (Array.isArray(network)) { - // network specifies sites - return { - ...acc, - sites: [...acc.sites, ...network[1]], - }; - } + return networks.reduce((acc, network) => { + if (Array.isArray(network)) { + // network specifies sites + return { + ...acc, + sites: [...acc.sites, ...network[1]], + }; + } - return { - ...acc, - networks: [...acc.networks, network], - }; - }, { - networks: [], - sites: [], - }); + return { + ...acc, + networks: [...acc.networks, network], + }; + }, { + networks: [], + sites: [], + }); } async function findSiteByUrl(url) { - const { origin, hostname, pathname } = new URL(url); - // const domain = hostname.replace(/www.|tour./, ''); - const dirUrl = `${origin}${pathname.split('/').slice(0, 2).join('/')}`; // allow for sites on URI directory + const { origin, hostname, pathname } = new URL(url); + // const domain = hostname.replace(/www.|tour./, ''); + const dirUrl = `${origin}${pathname.split('/').slice(0, 2).join('/')}`; // allow for sites on URI directory - const site = await knex('sites') - .leftJoin('networks', 'sites.network_id', 'networks.id') - .select( - 'sites.*', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', - ) - .where('sites.url', url) - .orWhere('sites.url', origin) - .orWhere('sites.url', origin.replace(/www\.|tour\./, '')) - .orWhere('sites.url', `https://www.${hostname}`) - .orWhere('sites.url', `http://www.${hostname}`) - .orWhere('sites.url', dirUrl) - // .orWhere('sites.url', 'like', `%${domain}`) - .first(); + const site = await knex('sites') + .leftJoin('networks', 'sites.network_id', 'networks.id') + .select( + 'sites.*', + 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', + ) + .where('sites.url', url) + .orWhere('sites.url', origin) + .orWhere('sites.url', origin.replace(/www\.|tour\./, '')) + .orWhere('sites.url', `https://www.${hostname}`) + .orWhere('sites.url', `http://www.${hostname}`) + .orWhere('sites.url', dirUrl) + // .orWhere('sites.url', 'like', `%${domain}`) + .first(); - if (site) { - const curatedSite = curateSite(site, true, false); + if (site) { + const curatedSite = curateSite(site, true, false); - return curatedSite; - } + return curatedSite; + } - return null; + return null; } function sitesByNetwork(sites) { - const networks = sites.reduce((acc, site) => { - if (acc[site.network.slug]) { - acc[site.network.slug].sites = acc[site.network.slug].sites.concat(site); + const networks = sites.reduce((acc, site) => { + if (acc[site.network.slug]) { + acc[site.network.slug].sites = acc[site.network.slug].sites.concat(site); - return acc; - } + return acc; + } - acc[site.network.slug] = { - ...site.network, - sites: [site], - }; + acc[site.network.slug] = { + ...site.network, + sites: [site], + }; - return acc; - }, {}); + return acc; + }, {}); - return Object.values(networks); + return Object.values(networks); } async function fetchSitesFromArgv() { - const rawSites = await knex('sites') - .select( - 'sites.*', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', - ) - .whereIn('sites.slug', argv.sites || []) - .orWhereIn('networks.slug', argv.networks || []) - .leftJoin('networks', 'sites.network_id', 'networks.id'); + const rawSites = await knex('sites') + .select( + 'sites.*', + 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', + ) + .whereIn('sites.slug', argv.sites || []) + .orWhereIn('networks.slug', argv.networks || []) + .leftJoin('networks', 'sites.network_id', 'networks.id'); - const curatedSites = await curateSites(rawSites, true); - logger.info(`Found ${curatedSites.length} sites in database`); + const curatedSites = await curateSites(rawSites, true); + logger.info(`Found ${curatedSites.length} sites in database`); - return sitesByNetwork(curatedSites); + return sitesByNetwork(curatedSites); } async function fetchSitesFromConfig() { - const included = destructConfigNetworks(config.include); - const excluded = destructConfigNetworks(config.exclude); + const included = destructConfigNetworks(config.include); + const excluded = destructConfigNetworks(config.exclude); - const rawSites = await knex('sites') - .select( - 'sites.*', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', - ) - .leftJoin('networks', 'sites.network_id', 'networks.id') - .where((builder) => { - if (config.include) { - builder - .whereIn('sites.slug', included.sites) - .orWhereIn('networks.slug', included.networks); - } - }) - .whereNot((builder) => { - builder - .whereIn('sites.slug', excluded.sites) - .orWhereIn('networks.slug', excluded.networks); - }); + const rawSites = await knex('sites') + .select( + 'sites.*', + 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', + ) + .leftJoin('networks', 'sites.network_id', 'networks.id') + .where((builder) => { + if (config.include) { + builder + .whereIn('sites.slug', included.sites) + .orWhereIn('networks.slug', included.networks); + } + }) + .whereNot((builder) => { + builder + .whereIn('sites.slug', excluded.sites) + .orWhereIn('networks.slug', excluded.networks); + }); - const curatedSites = await curateSites(rawSites, true); - logger.info(`Found ${curatedSites.length} sites in database`); + const curatedSites = await curateSites(rawSites, true); + logger.info(`Found ${curatedSites.length} sites in database`); - return sitesByNetwork(curatedSites); + return sitesByNetwork(curatedSites); } async function fetchIncludedSites() { - if (argv.networks || argv.sites) { - return fetchSitesFromArgv(); - } + if (argv.networks || argv.sites) { + return fetchSitesFromArgv(); + } - return fetchSitesFromConfig(); + return fetchSitesFromConfig(); } async function fetchSites(queryObject) { - const sites = await knex('sites') - .where(builder => whereOr(queryObject, 'sites', builder)) - .select( - 'sites.*', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', - ) - .leftJoin('networks', 'sites.network_id', 'networks.id') - .limit(100); + const sites = await knex('sites') + .where(builder => whereOr(queryObject, 'sites', builder)) + .select( + 'sites.*', + 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', + ) + .leftJoin('networks', 'sites.network_id', 'networks.id') + .limit(100); - return curateSites(sites); + return curateSites(sites); } async function fetchSitesFromReleases() { - const sites = await knex('releases') - .select('site_id', '') - .leftJoin('sites', 'sites.id', 'releases.site_id') - .groupBy('sites.id') - .limit(100); + const sites = await knex('releases') + .select('site_id', '') + .leftJoin('sites', 'sites.id', 'releases.site_id') + .groupBy('sites.id') + .limit(100); - return curateSites(sites); + return curateSites(sites); } module.exports = { - curateSite, - curateSites, - fetchIncludedSites, - fetchSites, - fetchSitesFromConfig, - fetchSitesFromArgv, - fetchSitesFromReleases, - findSiteByUrl, + curateSite, + curateSites, + fetchIncludedSites, + fetchSites, + fetchSitesFromConfig, + fetchSitesFromArgv, + fetchSitesFromReleases, + findSiteByUrl, }; diff --git a/src/store-releases.js b/src/store-releases.js index 738a9803e..1d38425a7 100644 --- a/src/store-releases.js +++ b/src/store-releases.js @@ -11,155 +11,164 @@ const { curateSite } = require('./sites'); const { associateReleaseMedia } = require('./media'); function curateReleaseEntry(release, batchId, existingRelease) { - const slug = slugify(release.title || release.actors?.join('-') || null, '-', { - encode: true, - limit: config.titleSlugLength, - }); + const slug = slugify(release.title || release.actors?.join('-') || null, '-', { + encode: true, + limit: config.titleSlugLength, + }); - const curatedRelease = { - title: release.title, - entry_id: release.entryId || null, - site_id: release.site.id, - shoot_id: release.shootId || null, - studio_id: release.studio?.id || null, - url: release.url, - date: release.date, - slug, - description: release.description, - duration: release.duration, - type: release.type, - // director: release.director, - // likes: release.rating && release.rating.likes, - // dislikes: release.rating && release.rating.dislikes, - // rating: release.rating && release.rating.stars && Math.floor(release.rating.stars), - deep: typeof release.deep === 'boolean' ? release.deep : false, - deep_url: release.deepUrl, - updated_batch_id: batchId, - }; + const curatedRelease = { + title: release.title, + entry_id: release.entryId || null, + site_id: release.site?.id, + network_id: release.site ? null : release.network?.id, // prefer site ID if available + shoot_id: release.shootId || null, + studio_id: release.studio?.id || null, + url: release.url, + date: release.date, + slug, + description: release.description, + duration: release.duration, + type: release.type, + // director: release.director, + // likes: release.rating && release.rating.likes, + // dislikes: release.rating && release.rating.dislikes, + // rating: release.rating && release.rating.stars && Math.floor(release.rating.stars), + deep: typeof release.deep === 'boolean' ? release.deep : false, + deep_url: release.deepUrl, + updated_batch_id: batchId, + }; - if (!existingRelease && !release.id) { - curatedRelease.created_batch_id = batchId; - } + if (!existingRelease && !release.id) { + curatedRelease.created_batch_id = batchId; + } - return curatedRelease; + return curatedRelease; } async function attachChannelSites(releases) { - const releasesWithoutSite = releases.filter(release => release.channel && (!release.site || release.site.isFallback)); + const releasesWithoutSite = releases.filter(release => release.channel && (!release.site || release.site.isNetwork)); - const channelSites = await knex('sites') - .leftJoin('networks', 'networks.id', 'sites.network_id') - .select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.parameters as network_parameters', 'networks.description as network_description') - .whereIn('sites.slug', releasesWithoutSite.map(release => release.channel)); + const channelSites = await knex('sites') + .leftJoin('networks', 'networks.id', 'sites.network_id') + .select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.parameters as network_parameters', 'networks.description as network_description') + .whereIn('sites.slug', releasesWithoutSite.map(release => release.channel)); - const channelSitesBySlug = channelSites.reduce((acc, site) => ({ ...acc, [site.slug]: site }), {}); + const channelSitesBySlug = channelSites.reduce((acc, site) => ({ ...acc, [site.slug]: site }), {}); - const releasesWithChannelSite = await Promise.all(releases - .map(async (release) => { - if (release.site && !release.site.isFallback) { - return release; - } + const releasesWithChannelSite = await Promise.all(releases + .map(async (release) => { + if (release.site && !release.site.isNetwork) { + return release; + } - if (release.channel && channelSitesBySlug[release.channel]) { - const curatedSite = await curateSite(channelSitesBySlug[release.channel]); + if (release.channel && channelSitesBySlug[release.channel]) { + const curatedSite = await curateSite(channelSitesBySlug[release.channel]); - return { - ...release, - site: curatedSite, - }; - } + return { + ...release, + site: curatedSite, + }; + } - logger.error(`Unable to match channel '${release.channel?.slug || release.channel}' from generic URL ${release.url}`); + if (release.site && release.site.isNetwork) { + return { + ...release, + site: null, + network: release.site, + }; + } - return null; - })); + logger.error(`Unable to match channel '${release.channel?.slug || release.channel}' from generic URL ${release.url}`); - return releasesWithChannelSite.filter(Boolean); + return null; + })); + + return releasesWithChannelSite.filter(Boolean); } async function attachStudios(releases) { - const studioSlugs = releases.map(release => release.studio).filter(Boolean); + const studioSlugs = releases.map(release => release.studio).filter(Boolean); - const studios = await knex('studios').whereIn('slug', studioSlugs); - const studioBySlug = studios.reduce((acc, studio) => ({ ...acc, [studio.slug]: studio }), {}); + const studios = await knex('studios').whereIn('slug', studioSlugs); + const studioBySlug = studios.reduce((acc, studio) => ({ ...acc, [studio.slug]: studio }), {}); - const releasesWithStudio = releases.map((release) => { - if (release.studio && studioBySlug[release.studio]) { - return { - ...release, - studio: studioBySlug[release.studio], - }; - } + const releasesWithStudio = releases.map((release) => { + if (release.studio && studioBySlug[release.studio]) { + return { + ...release, + studio: studioBySlug[release.studio], + }; + } - if (release.studio) { - logger.warn(`Unable to match studio '${release.studio}' for ${release.url}`); - } + if (release.studio) { + logger.warn(`Unable to match studio '${release.studio}' for ${release.url}`); + } - return release; - }); + return release; + }); - return releasesWithStudio; + return releasesWithStudio; } function attachReleaseIds(releases, storedReleases) { - const storedReleaseIdsBySiteIdAndEntryId = storedReleases.reduce((acc, release) => { - if (!acc[release.site_id]) acc[release.site_id] = {}; - acc[release.site_id][release.entry_id] = release.id; + const storedReleaseIdsBySiteIdAndEntryId = storedReleases.reduce((acc, release) => { + if (!acc[release.site_id]) acc[release.site_id] = {}; + acc[release.site_id][release.entry_id] = release.id; - return acc; - }, {}); + return acc; + }, {}); - const releasesWithId = releases.map(release => ({ - ...release, - id: storedReleaseIdsBySiteIdAndEntryId[release.site.id][release.entryId], - })); + const releasesWithId = releases.map(release => ({ + ...release, + id: storedReleaseIdsBySiteIdAndEntryId[release.site.id][release.entryId], + })); - return releasesWithId; + return releasesWithId; } function filterInternalDuplicateReleases(releases) { - const releasesBySiteIdAndEntryId = releases.reduce((acc, release) => { - if (!acc[release.site.id]) { - acc[release.site.id] = {}; - } + const releasesBySiteIdAndEntryId = releases.reduce((acc, release) => { + if (!acc[release.site.id]) { + acc[release.site.id] = {}; + } - acc[release.site.id][release.entryId] = release; + acc[release.site.id][release.entryId] = release; - return acc; - }, {}); + return acc; + }, {}); - return Object.values(releasesBySiteIdAndEntryId) - .map(siteReleases => Object.values(siteReleases)) - .flat(); + return Object.values(releasesBySiteIdAndEntryId) + .map(siteReleases => Object.values(siteReleases)) + .flat(); } async function filterDuplicateReleases(releases) { - const internalUniqueReleases = filterInternalDuplicateReleases(releases); + const internalUniqueReleases = filterInternalDuplicateReleases(releases); - const duplicateReleaseEntries = await knex('releases') - .whereIn(['entry_id', 'site_id'], internalUniqueReleases.map(release => [release.entryId, release.site.id])); + const duplicateReleaseEntries = await knex('releases') + .whereIn(['entry_id', 'site_id'], internalUniqueReleases.map(release => [release.entryId, release.site.id])); - const duplicateReleasesBySiteIdAndEntryId = duplicateReleaseEntries.reduce((acc, release) => { - if (!acc[release.site_id]) acc[release.site_id] = {}; - acc[release.site_id][release.entry_id] = true; + const duplicateReleasesBySiteIdAndEntryId = duplicateReleaseEntries.reduce((acc, release) => { + if (!acc[release.site_id]) acc[release.site_id] = {}; + acc[release.site_id][release.entry_id] = true; - return acc; - }, {}); + return acc; + }, {}); - const duplicateReleases = internalUniqueReleases.filter(release => duplicateReleasesBySiteIdAndEntryId[release.site.id]?.[release.entryId]); - const uniqueReleases = internalUniqueReleases.filter(release => !duplicateReleasesBySiteIdAndEntryId[release.site.id]?.[release.entryId]); + const duplicateReleases = internalUniqueReleases.filter(release => duplicateReleasesBySiteIdAndEntryId[release.site.id]?.[release.entryId]); + const uniqueReleases = internalUniqueReleases.filter(release => !duplicateReleasesBySiteIdAndEntryId[release.site.id]?.[release.entryId]); - return { - uniqueReleases, - duplicateReleases, - duplicateReleaseEntries, - }; + return { + uniqueReleases, + duplicateReleases, + duplicateReleaseEntries, + }; } async function updateReleasesSearch(releaseIds) { - logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`); + logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`); - const documents = await knex.raw(` + const documents = await knex.raw(` SELECT releases.id AS release_id, TO_TSVECTOR( @@ -190,45 +199,49 @@ async function updateReleasesSearch(releaseIds) { GROUP BY releases.id, sites.name, sites.slug, sites.alias, sites.url, networks.name, networks.slug, networks.url; `, releaseIds && [releaseIds]); - if (documents.rows?.length > 0) { - const query = knex('releases_search').insert(documents.rows).toString(); - await knex.raw(`${query} ON CONFLICT (release_id) DO UPDATE SET document = EXCLUDED.document`); - } + if (documents.rows?.length > 0) { + const query = knex('releases_search').insert(documents.rows).toString(); + await knex.raw(`${query} ON CONFLICT (release_id) DO UPDATE SET document = EXCLUDED.document`); + } } async function storeReleases(releases) { - const [batchId] = await knex('batches').insert({ comment: null }).returning('id'); + if (releases.length === 0) { + return []; + } - const releasesWithSites = await attachChannelSites(releases); - const releasesWithStudios = await attachStudios(releasesWithSites); + const [batchId] = await knex('batches').insert({ comment: null }).returning('id'); - // uniqueness is site ID + entry ID, filter uniques after adding sites - const { uniqueReleases, duplicateReleases, duplicateReleaseEntries } = await filterDuplicateReleases(releasesWithStudios); + const releasesWithSites = await attachChannelSites(releases); + const releasesWithStudios = await attachStudios(releasesWithSites); - const curatedNewReleaseEntries = uniqueReleases.map(release => curateReleaseEntry(release, batchId)); + // uniqueness is site ID + entry ID, filter uniques after adding sites + const { uniqueReleases, duplicateReleases, duplicateReleaseEntries } = await filterDuplicateReleases(releasesWithStudios); - const storedReleases = await knex('releases').insert(curatedNewReleaseEntries).returning('*'); - // TODO: update duplicate releases + const curatedNewReleaseEntries = uniqueReleases.map(release => curateReleaseEntry(release, batchId)); - const storedReleaseEntries = Array.isArray(storedReleases) ? storedReleases : []; - const releasesWithId = attachReleaseIds([].concat(uniqueReleases, duplicateReleases), [].concat(storedReleaseEntries, duplicateReleaseEntries)); + const storedReleases = await knex('releases').insert(curatedNewReleaseEntries).returning('*'); + // TODO: update duplicate releases - await Promise.all([ - associateActors(releasesWithId, batchId), - associateReleaseTags(releasesWithId), - ]); + const storedReleaseEntries = Array.isArray(storedReleases) ? storedReleases : []; + const releasesWithId = attachReleaseIds([].concat(uniqueReleases, duplicateReleases), [].concat(storedReleaseEntries, duplicateReleaseEntries)); - // media is more error-prone, associate separately - await associateReleaseMedia(releasesWithId); + await Promise.all([ + associateActors(releasesWithId, batchId), + associateReleaseTags(releasesWithId), + ]); - logger.info(`Stored ${storedReleaseEntries.length} releases`); + // media is more error-prone, associate separately + await associateReleaseMedia(releasesWithId); - await updateReleasesSearch(releasesWithId.map(release => release.id)); + logger.info(`Stored ${storedReleaseEntries.length} releases`); - return releasesWithId; + await updateReleasesSearch(releasesWithId.map(release => release.id)); + + return releasesWithId; } module.exports = { - storeReleases, - updateReleasesSearch, + storeReleases, + updateReleasesSearch, }; diff --git a/src/tags-legacy.js b/src/tags-legacy.js index c6f974427..1ea74b111 100644 --- a/src/tags-legacy.js +++ b/src/tags-legacy.js @@ -5,106 +5,106 @@ const knex = require('./knex'); const whereOr = require('./utils/where-or'); async function curateTag(tag) { - const [aliases, media] = await Promise.all([ - knex('tags').where({ alias_for: tag.id }), - knex('media') - .where('domain', 'tags') - .andWhere('target_id', tag.id) - .orderBy('index'), - ]); + const [aliases, media] = await Promise.all([ + knex('tags').where({ alias_for: tag.id }), + knex('media') + .where('domain', 'tags') + .andWhere('target_id', tag.id) + .orderBy('index'), + ]); - return { - id: tag.id, - name: tag.name, - slug: tag.slug, - description: tag.description, - poster: media.find(photo => photo.role === 'poster'), - photos: media.filter(photo => photo.role === 'photo'), - group: { - id: tag.group_id, - name: tag.group_name, - description: tag.group_description, - slug: tag.group_slug, - }, - aliases: aliases.map(({ name }) => name), - }; + return { + id: tag.id, + name: tag.name, + slug: tag.slug, + description: tag.description, + poster: media.find(photo => photo.role === 'poster'), + photos: media.filter(photo => photo.role === 'photo'), + group: { + id: tag.group_id, + name: tag.group_name, + description: tag.group_description, + slug: tag.group_slug, + }, + aliases: aliases.map(({ name }) => name), + }; } function curateTags(tags) { - return Promise.all(tags.map(async tag => curateTag(tag))); + return Promise.all(tags.map(async tag => curateTag(tag))); } async function matchTags(rawTags) { - const filteredTags = rawTags.filter(Boolean); + const filteredTags = rawTags.filter(Boolean); - const tags = filteredTags - .concat(filteredTags.map(tag => tag.toLowerCase())) - .concat(filteredTags.map(tag => tag.toUpperCase())); + const tags = filteredTags + .concat(filteredTags.map(tag => tag.toLowerCase())) + .concat(filteredTags.map(tag => tag.toUpperCase())); - const tagEntries = await knex('tags') - .pluck('aliases.id') - .whereIn('tags.name', tags) - .leftJoin('tags as aliases', function join() { - this - .on('tags.alias_for', 'aliases.id') - .orOn('tags.id', 'aliases.id'); - }) - .where(function where() { - this - .whereNull('tags.alias_for') - .orWhereNull('aliases.alias_for'); - }) - .groupBy('aliases.id'); + const tagEntries = await knex('tags') + .pluck('aliases.id') + .whereIn('tags.name', tags) + .leftJoin('tags as aliases', function join() { + this + .on('tags.alias_for', 'aliases.id') + .orOn('tags.id', 'aliases.id'); + }) + .where(function where() { + this + .whereNull('tags.alias_for') + .orWhereNull('aliases.alias_for'); + }) + .groupBy('aliases.id'); - return tagEntries; + return tagEntries; } async function associateTags(release, releaseId) { - const siteTags = release.site?.tags?.filter(tag => tag.inherit === true).map(tag => tag.id) || []; + const siteTags = release.site?.tags?.filter(tag => tag.inherit === true).map(tag => tag.id) || []; - const rawReleaseTags = release.tags?.filter(Boolean) || []; - const releaseTags = rawReleaseTags.some(tag => typeof tag === 'string') - ? await matchTags(release.tags) // scraper returned raw tags - : rawReleaseTags; // tags already matched by (outdated) scraper + const rawReleaseTags = release.tags?.filter(Boolean) || []; + const releaseTags = rawReleaseTags.some(tag => typeof tag === 'string') + ? await matchTags(release.tags) // scraper returned raw tags + : rawReleaseTags; // tags already matched by (outdated) scraper - const tags = Array.from(new Set(releaseTags.concat(siteTags))); + const tags = Array.from(new Set(releaseTags.concat(siteTags))); - if (tags.length === 0) { - logger.info(`No tags available for (${release.site.name}, ${releaseId}) "${release.title}"`); - return; - } + if (tags.length === 0) { + logger.info(`No tags available for (${release.site.name}, ${releaseId}) "${release.title}"`); + return; + } - const associationEntries = await knex('releases_tags') - .where('release_id', releaseId) - .whereIn('tag_id', tags); + const associationEntries = await knex('releases_tags') + .where('release_id', releaseId) + .whereIn('tag_id', tags); - const existingAssociations = new Set(associationEntries.map(association => association.tag_id)); - const newAssociations = tags.filter(tagId => !existingAssociations.has(tagId)); + const existingAssociations = new Set(associationEntries.map(association => association.tag_id)); + const newAssociations = tags.filter(tagId => !existingAssociations.has(tagId)); - await knex('releases_tags').insert(newAssociations.map(tagId => ({ - tag_id: tagId, - release_id: releaseId, - }))); + await knex('releases_tags').insert(newAssociations.map(tagId => ({ + tag_id: tagId, + release_id: releaseId, + }))); } async function fetchTags(queryObject, groupsQueryObject, limit = 100) { - const tags = await knex('tags') - .where(builder => whereOr(queryObject, 'tags', builder)) - .orWhere(builder => whereOr(groupsQueryObject, 'tags_groups', builder)) - .andWhere({ 'tags.alias_for': null }) - .select( - 'tags.*', - 'tags_groups.id as group_id', 'tags_groups.name as group_name', 'tags_groups.slug as group_slug', 'tags_groups.description as groups_description', - ) - .leftJoin('tags_groups', 'tags.group_id', 'tags_groups.id') - .orderBy('name') - .limit(limit); + const tags = await knex('tags') + .where(builder => whereOr(queryObject, 'tags', builder)) + .orWhere(builder => whereOr(groupsQueryObject, 'tags_groups', builder)) + .andWhere({ 'tags.alias_for': null }) + .select( + 'tags.*', + 'tags_groups.id as group_id', 'tags_groups.name as group_name', 'tags_groups.slug as group_slug', 'tags_groups.description as groups_description', + ) + .leftJoin('tags_groups', 'tags.group_id', 'tags_groups.id') + .orderBy('name') + .limit(limit); - return curateTags(tags); + return curateTags(tags); } module.exports = { - associateTags, - fetchTags, - matchTags, + associateTags, + fetchTags, + matchTags, }; diff --git a/src/tags.js b/src/tags.js index 78333fdba..beba770e1 100644 --- a/src/tags.js +++ b/src/tags.js @@ -4,104 +4,104 @@ const knex = require('./knex'); const slugify = require('./utils/slugify'); async function matchReleaseTags(releases) { - const rawTags = releases - .map(release => release.tags).flat() - .filter(Boolean); + const rawTags = releases + .map(release => release.tags).flat() + .filter(Boolean); - const casedTags = [...new Set( - rawTags - .concat(rawTags.map(tag => tag.toLowerCase())) - .concat(rawTags.map(tag => tag.toUpperCase())), - )]; + const casedTags = [...new Set( + rawTags + .concat(rawTags.map(tag => tag.toLowerCase())) + .concat(rawTags.map(tag => tag.toUpperCase())), + )]; - const tagEntries = await knex('tags') - .select('tags.id', 'tags.name', 'tags.alias_for') - .whereIn('tags.name', casedTags); + const tagEntries = await knex('tags') + .select('tags.id', 'tags.name', 'tags.alias_for') + .whereIn('tags.name', casedTags); - const tagIdsBySlug = tagEntries - .reduce((acc, tag) => ({ - ...acc, - [slugify(tag.name)]: tag.alias_for || tag.id, - }), {}); + const tagIdsBySlug = tagEntries + .reduce((acc, tag) => ({ + ...acc, + [slugify(tag.name)]: tag.alias_for || tag.id, + }), {}); - return tagIdsBySlug; + return tagIdsBySlug; } async function getSiteTags(releases) { - const siteIds = releases.map(release => release.site.id); - const siteTags = await knex('sites_tags').whereIn('site_id', siteIds); + const siteIds = releases.map(release => release.site.id); + const siteTags = await knex('sites_tags').whereIn('site_id', siteIds); - const siteTagIdsBySiteId = siteTags.reduce((acc, siteTag) => { - if (!acc[siteTag.site_id]) { - acc[siteTag.site_id] = []; - } + const siteTagIdsBySiteId = siteTags.reduce((acc, siteTag) => { + if (!acc[siteTag.site_id]) { + acc[siteTag.site_id] = []; + } - acc[siteTag.site_id].push(siteTag.tag_id); + acc[siteTag.site_id].push(siteTag.tag_id); - return acc; - }, {}); + return acc; + }, {}); - return siteTagIdsBySiteId; + return siteTagIdsBySiteId; } function buildReleaseTagAssociations(releases, tagIdsBySlug, siteTagIdsBySiteId) { - const tagAssociations = releases - .map((release) => { - const siteTagIds = siteTagIdsBySiteId[release.site.id]; - const releaseTags = release.tags || []; + const tagAssociations = releases + .map((release) => { + const siteTagIds = siteTagIdsBySiteId[release.site.id]; + const releaseTags = release.tags || []; - const releaseTagIds = releaseTags.every(tag => typeof tag === 'number') - ? releaseTags // obsolete scraper returned pre-matched tags - : releaseTags.map(tag => tagIdsBySlug[slugify(tag)]); + const releaseTagIds = releaseTags.every(tag => typeof tag === 'number') + ? releaseTags // obsolete scraper returned pre-matched tags + : releaseTags.map(tag => tagIdsBySlug[slugify(tag)]); - const tags = [...new Set( - // filter duplicates and empties - releaseTagIds - .concat(siteTagIds) - .filter(Boolean), - )] - .map(tagId => ({ - release_id: release.id, - tag_id: tagId, - })); + const tags = [...new Set( + // filter duplicates and empties + releaseTagIds + .concat(siteTagIds) + .filter(Boolean), + )] + .map(tagId => ({ + release_id: release.id, + tag_id: tagId, + })); - return tags; - }) - .flat(); + return tags; + }) + .flat(); - return tagAssociations; + return tagAssociations; } async function filterUniqueAssociations(tagAssociations) { - const duplicateAssociations = await knex('releases_tags') - .whereIn(['release_id', 'tag_id'], tagAssociations.map(association => [association.release_id, association.tag_id])); + const duplicateAssociations = await knex('releases_tags') + .whereIn(['release_id', 'tag_id'], tagAssociations.map(association => [association.release_id, association.tag_id])); - const duplicateAssociationsByReleaseIdAndTagId = duplicateAssociations.reduce((acc, association) => { - if (!acc[association.release_id]) { - acc[association.release_id] = {}; - } + const duplicateAssociationsByReleaseIdAndTagId = duplicateAssociations.reduce((acc, association) => { + if (!acc[association.release_id]) { + acc[association.release_id] = {}; + } - acc[association.release_id][association.tag_id] = true; + acc[association.release_id][association.tag_id] = true; - return acc; - }, {}); + return acc; + }, {}); - const uniqueAssociations = tagAssociations - .filter(association => !duplicateAssociationsByReleaseIdAndTagId[association.release_id]?.[association.tag_id]); + const uniqueAssociations = tagAssociations + .filter(association => !duplicateAssociationsByReleaseIdAndTagId[association.release_id]?.[association.tag_id]); - return uniqueAssociations; + return uniqueAssociations; } async function associateReleaseTags(releases) { - const tagIdsBySlug = await matchReleaseTags(releases); - const siteTagIdsBySiteId = await getSiteTags(releases); + const tagIdsBySlug = await matchReleaseTags(releases); + const siteTagIdsBySiteId = await getSiteTags(releases); - const tagAssociations = buildReleaseTagAssociations(releases, tagIdsBySlug, siteTagIdsBySiteId); - const uniqueAssociations = await filterUniqueAssociations(tagAssociations); + const tagAssociations = buildReleaseTagAssociations(releases, tagIdsBySlug, siteTagIdsBySiteId); + const uniqueAssociations = await filterUniqueAssociations(tagAssociations); - await knex('releases_tags').insert(uniqueAssociations); + await knex('releases_tags').insert(uniqueAssociations); } module.exports = { - associateReleaseTags, + associateReleaseTags, }; diff --git a/src/updates.js b/src/updates.js index 240f5d31c..fe1007ede 100644 --- a/src/updates.js +++ b/src/updates.js @@ -11,228 +11,228 @@ const scrapers = require('./scrapers/scrapers'); const { fetchSitesFromArgv, fetchSitesFromConfig } = require('./sites'); const afterDate = (() => { - if (/\d{2,4}-\d{2}-\d{2,4}/.test(argv.after)) { - // using date - return moment - .utc(argv.after, ['YYYY-MM-DD', 'DD-MM-YYYY']) - .toDate(); - } + if (/\d{2,4}-\d{2}-\d{2,4}/.test(argv.after)) { + // using date + return moment + .utc(argv.after, ['YYYY-MM-DD', 'DD-MM-YYYY']) + .toDate(); + } - // using time distance (e.g. "1 month") - return moment - .utc() - .subtract(...argv.after.split(' ')) - .toDate(); + // using time distance (e.g. "1 month") + return moment + .utc() + .subtract(...argv.after.split(' ')) + .toDate(); })(); async function filterUniqueReleases(latestReleases, accReleases) { - const latestReleaseIdentifiers = latestReleases - .map(release => [release.site.id, release.entryId]); + const latestReleaseIdentifiers = latestReleases + .map(release => [release.site.id, release.entryId]); - const duplicateReleases = await knex('releases') - .whereIn(['site_id', 'entry_id'], latestReleaseIdentifiers); + const duplicateReleases = await knex('releases') + .whereIn(['site_id', 'entry_id'], latestReleaseIdentifiers); - // add entry IDs of accumulated releases to prevent an infinite scrape loop - // when one page contains the same release as the previous - const duplicateReleasesSiteIdAndEntryIds = duplicateReleases - .concat(accReleases) - .reduce((acc, release) => { - const siteId = release.site_id || release.site.id; - const entryId = release.entry_id || release.entryId; + // add entry IDs of accumulated releases to prevent an infinite scrape loop + // when one page contains the same release as the previous + const duplicateReleasesSiteIdAndEntryIds = duplicateReleases + .concat(accReleases) + .reduce((acc, release) => { + const siteId = release.site_id || release.site.id; + const entryId = release.entry_id || release.entryId; - if (!acc[siteId]) acc[siteId] = {}; - acc[siteId][entryId] = true; + if (!acc[siteId]) acc[siteId] = {}; + acc[siteId][entryId] = true; - return acc; - }, {}); + return acc; + }, {}); - const uniqueReleases = latestReleases - .filter(release => !duplicateReleasesSiteIdAndEntryIds[release.site.id]?.[release.entryId]); + const uniqueReleases = latestReleases + .filter(release => !duplicateReleasesSiteIdAndEntryIds[release.site.id]?.[release.entryId]); - return uniqueReleases; + return uniqueReleases; } function needNextPage(uniqueReleases, pageAccReleases) { - if (uniqueReleases.length === 0) { - return false; - } + if (uniqueReleases.length === 0) { + return false; + } - if (argv.last && pageAccReleases.length < argv.last) { - // request for last N releases not yet satisfied - return true; - } + if (argv.last && pageAccReleases.length < argv.last) { + // request for last N releases not yet satisfied + return true; + } - if (uniqueReleases.every(release => !!release.date)) { - const oldestReleaseOnPage = uniqueReleases - .sort((releaseA, releaseB) => releaseB.date - releaseA.date) - .slice(-1)[0]; + if (uniqueReleases.every(release => !!release.date)) { + const oldestReleaseOnPage = uniqueReleases + .sort((releaseA, releaseB) => releaseB.date - releaseA.date) + .slice(-1)[0]; - if (moment(oldestReleaseOnPage.date).isAfter(afterDate)) { - // oldest release on page is newer than the specified date cut-off - return true; - } - } + if (moment(oldestReleaseOnPage.date).isAfter(afterDate)) { + // oldest release on page is newer than the specified date cut-off + return true; + } + } - // dates missing, and limit for scenes without dates not yet reached - return pageAccReleases.length <= argv.nullDateLimit; + // dates missing, and limit for scenes without dates not yet reached + return pageAccReleases.length <= argv.nullDateLimit; } async function scrapeReleases(scraper, site, preData, upcoming = false) { - const scrapePage = async (page = 1, accReleases = []) => { - const latestReleases = upcoming - ? await scraper.fetchUpcoming(site, page, preData, include) - : await scraper.fetchLatest(site, page, preData, include); + const scrapePage = async (page = 1, accReleases = []) => { + const latestReleases = upcoming + ? await scraper.fetchUpcoming(site, page, preData, include) + : await scraper.fetchLatest(site, page, preData, include); - if (!Array.isArray(latestReleases)) { - // scraper is unable to fetch the releases and returned a HTTP code or null - logger.warn(`Scraper returned ${latestReleases} when fetching latest from '${site.name}' (${site.network.name})`); - return accReleases; - } + if (!Array.isArray(latestReleases)) { + // scraper is unable to fetch the releases and returned a HTTP code or null + logger.warn(`Scraper returned ${latestReleases} when fetching latest from '${site.name}' (${site.network.name})`); + return accReleases; + } - if (latestReleases.length === 0) { - // scraper successfully requested releases, but found none - return accReleases; - } + if (latestReleases.length === 0) { + // scraper successfully requested releases, but found none + return accReleases; + } - const latestReleasesWithSite = latestReleases.map(release => ({ ...release, site: release.site || site })); // attach site release is assigned to when stored + const latestReleasesWithSite = latestReleases.map(release => ({ ...release, site: release.site || site })); // attach site release is assigned to when stored - const uniqueReleases = argv.redownload - ? latestReleasesWithSite - : await filterUniqueReleases(latestReleasesWithSite, accReleases); + const uniqueReleases = argv.redownload + ? latestReleasesWithSite + : await filterUniqueReleases(latestReleasesWithSite, accReleases); - const pageAccReleases = accReleases.concat(uniqueReleases); + const pageAccReleases = accReleases.concat(uniqueReleases); - logger.verbose(`Scraped '${site.name}' (${site.network.name}) ${upcoming ? 'upcoming' : 'latest'} page ${page}, found ${uniqueReleases.length} unique updates`); + logger.verbose(`Scraped '${site.name}' (${site.network.name}) ${upcoming ? 'upcoming' : 'latest'} page ${page}, found ${uniqueReleases.length} unique updates`); - if (needNextPage(uniqueReleases, pageAccReleases)) { - return scrapePage(page + 1, pageAccReleases); - } + if (needNextPage(uniqueReleases, pageAccReleases)) { + return scrapePage(page + 1, pageAccReleases); + } - return pageAccReleases; - }; + return pageAccReleases; + }; - const rawReleases = await scrapePage(argv.page || 1, []); - const releases = upcoming - ? rawReleases.map(rawRelease => ({ ...rawRelease, upcoming: true })) - : rawReleases; + const rawReleases = await scrapePage(argv.page || 1, []); + const releases = upcoming + ? rawReleases.map(rawRelease => ({ ...rawRelease, upcoming: true })) + : rawReleases; - if (argv.last) { - return releases.slice(0, argv.last); - } + if (argv.last) { + return releases.slice(0, argv.last); + } - if (releases.every(release => release.date)) { - return releases.filter(release => moment(release.date).isAfter(afterDate)); - } + if (releases.every(release => release.date)) { + return releases.filter(release => moment(release.date).isAfter(afterDate)); + } - return releases.slice(0, argv.nullDateLimit); + return releases.slice(0, argv.nullDateLimit); } async function scrapeLatestReleases(scraper, site, preData) { - if (!scraper.fetchLatest) { - return []; - } + if (!scraper.fetchLatest) { + return []; + } - try { - return await scrapeReleases(scraper, site, preData, false); - } catch (error) { - logger.warn(`Failed to scrape latest updates for '${site.slug}' (${site.network.slug}): ${error.message}`); - } + try { + return await scrapeReleases(scraper, site, preData, false); + } catch (error) { + logger.warn(`Failed to scrape latest updates for '${site.slug}' (${site.network.slug}): ${error.message}`); + } - return []; + return []; } async function scrapeUpcomingReleases(scraper, site, preData) { - if (!scraper.fetchUpcoming) { - return []; - } + if (!scraper.fetchUpcoming) { + return []; + } - try { - return await scrapeReleases(scraper, site, preData, true); - } catch (error) { - logger.warn(`Failed to scrape upcoming updates for '${site.slug}' (${site.network.slug}): ${error.message}`); - } + try { + return await scrapeReleases(scraper, site, preData, true); + } catch (error) { + logger.warn(`Failed to scrape upcoming updates for '${site.slug}' (${site.network.slug}): ${error.message}`); + } - return []; + return []; } async function scrapeSiteReleases(scraper, site, preData) { - const [latestReleases, upcomingReleases] = await Promise.all([ - argv.latest - ? scrapeLatestReleases(scraper, site, preData) - : [], - argv.upcoming - ? scrapeUpcomingReleases(scraper, site, preData) - : [], - ]); + const [latestReleases, upcomingReleases] = await Promise.all([ + argv.latest + ? scrapeLatestReleases(scraper, site, preData) + : [], + argv.upcoming + ? scrapeUpcomingReleases(scraper, site, preData) + : [], + ]); - logger.info(`Fetching ${latestReleases.length} latest and ${upcomingReleases.length} upcoming updates for '${site.name}' (${site.network.name})`); + logger.info(`Fetching ${latestReleases.length} latest and ${upcomingReleases.length} upcoming updates for '${site.name}' (${site.network.name})`); - return [...latestReleases, ...upcomingReleases]; + return [...latestReleases, ...upcomingReleases]; } async function scrapeSite(site, accSiteReleases) { - const scraper = scrapers.releases[site.slug] + const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug] || scrapers.releases[site.network.parent?.slug]; - if (!scraper) { - logger.warn(`No scraper found for '${site.name}' (${site.network.name})`); - return []; - } + if (!scraper) { + logger.warn(`No scraper found for '${site.name}' (${site.network.name})`); + return []; + } - try { - const beforeFetchLatest = await scraper.beforeFetchLatest?.(site); + try { + const beforeFetchLatest = await scraper.beforeFetchLatest?.(site); - const siteReleases = await scrapeSiteReleases(scraper, site, { - accSiteReleases, - beforeFetchLatest, - }); + const siteReleases = await scrapeSiteReleases(scraper, site, { + accSiteReleases, + beforeFetchLatest, + }); - return siteReleases.map(release => ({ ...release, site })); - } catch (error) { - logger.error(`Failed to scrape releases from ${site.name} using ${scraper.slug}: ${error.message}`); + return siteReleases.map(release => ({ ...release, site })); + } catch (error) { + logger.error(`Failed to scrape releases from ${site.name} using ${scraper.slug}: ${error.message}`); - return []; - } + return []; + } } async function scrapeNetworkSequential(network) { - return Promise.reduce( - network.sites, - async (chain, site) => { - const accSiteReleases = await chain; - const siteReleases = await scrapeSite(site, network, accSiteReleases); + return Promise.reduce( + network.sites, + async (chain, site) => { + const accSiteReleases = await chain; + const siteReleases = await scrapeSite(site, network, accSiteReleases); - return accSiteReleases.concat(siteReleases); - }, - Promise.resolve([]), - ); + return accSiteReleases.concat(siteReleases); + }, + Promise.resolve([]), + ); } async function scrapeNetworkParallel(network) { - return Promise.map( - network.sites, - async site => scrapeSite(site, network), - { concurrency: 3 }, - ); + return Promise.map( + network.sites, + async site => scrapeSite(site, network), + { concurrency: 3 }, + ); } async function fetchUpdates() { - const includedNetworks = argv.sites || argv.networks - ? await fetchSitesFromArgv() - : await fetchSitesFromConfig(); + const includedNetworks = argv.sites || argv.networks + ? await fetchSitesFromArgv() + : await fetchSitesFromConfig(); - const scrapedNetworks = await Promise.map( - includedNetworks, - async network => (network.parameters?.sequential - ? scrapeNetworkSequential(network) - : scrapeNetworkParallel(network)), - { concurrency: 5 }, - ); + const scrapedNetworks = await Promise.map( + includedNetworks, + async network => (network.parameters?.sequential + ? scrapeNetworkSequential(network) + : scrapeNetworkParallel(network)), + { concurrency: 5 }, + ); - const releases = scrapedNetworks.flat(2); + const releases = scrapedNetworks.flat(2); - return releases; + return releases; } module.exports = fetchUpdates; diff --git a/src/utils/argv-include.js b/src/utils/argv-include.js index 65935932b..a955bec64 100644 --- a/src/utils/argv-include.js +++ b/src/utils/argv-include.js @@ -1,20 +1,20 @@ 'use strict'; function include(argv) { - return { - covers: argv.media && argv.covers, - media: argv.media, - photos: argv.media && argv.photos, - poster: argv.media && argv.posters, - posters: argv.media && argv.posters, - releases: argv.withReleases, - scenes: argv.withReleases, - teaser: argv.media && argv.videos && argv.teasers, - teasers: argv.media && argv.videos && argv.teasers, - trailer: argv.media && argv.videos && argv.trailers, - trailers: argv.media && argv.videos && argv.trailers, - videos: argv.videos, - }; + return { + covers: argv.media && argv.covers, + media: argv.media, + photos: argv.media && argv.photos, + poster: argv.media && argv.posters, + posters: argv.media && argv.posters, + releases: argv.withReleases, + scenes: argv.withReleases, + teaser: argv.media && argv.videos && argv.teasers, + teasers: argv.media && argv.videos && argv.teasers, + trailer: argv.media && argv.videos && argv.trailers, + trailers: argv.media && argv.videos && argv.trailers, + videos: argv.videos, + }; } module.exports = include; diff --git a/src/utils/buffer.js b/src/utils/buffer.js index bdaa84cd7..67af934fa 100644 --- a/src/utils/buffer.js +++ b/src/utils/buffer.js @@ -13,106 +13,106 @@ const file = 'https://speed.hetzner.de/100MB.bin'; // const file = 'https://speed.hetzner.de/10GB.bin'; function getMemoryUsage() { - return process.memoryUsage().rss / (10 ** 6); + return process.memoryUsage().rss / (10 ** 6); } const stats = { - peakMemoryUsage: getMemoryUsage(), - done: false, - downloads: {}, + peakMemoryUsage: getMemoryUsage(), + done: false, + downloads: {}, }; function render() { - const downloads = Object.entries(stats.downloads); + const downloads = Object.entries(stats.downloads); - process.stdout.clearScreenDown(); + process.stdout.clearScreenDown(); - process.stdout.write(`peak memory: ${stats.peakMemoryUsage.toFixed(2)} MB\n`); + process.stdout.write(`peak memory: ${stats.peakMemoryUsage.toFixed(2)} MB\n`); - downloads.forEach(([download, progress]) => { - process.stdout.write(`${download}: ${progress}${typeof progress === 'string' ? '' : '%'}\n`); - }); + downloads.forEach(([download, progress]) => { + process.stdout.write(`${download}: ${progress}${typeof progress === 'string' ? '' : '%'}\n`); + }); - process.stdout.moveCursor(0, -(downloads.length + 1)); - process.stdout.cursorTo(0); + process.stdout.moveCursor(0, -(downloads.length + 1)); + process.stdout.cursorTo(0); - if (downloads.length === 0 || !downloads.every(([_label, download]) => typeof download === 'string')) { - setTimeout(() => render(), 1000); - return; - } + if (downloads.length === 0 || !downloads.every(([_label, download]) => typeof download === 'string')) { + setTimeout(() => render(), 1000); + return; + } - process.stdout.moveCursor(0, downloads.length + 1); + process.stdout.moveCursor(0, downloads.length + 1); } function setProgress(label, completedBytes, totalBytes, hash) { - const memory = getMemoryUsage(); + const memory = getMemoryUsage(); - stats.peakMemoryUsage = Math.max(memory, stats.peakMemoryUsage); - stats.downloads[label] = hash || Math.round((completedBytes / totalBytes) * 100); + stats.peakMemoryUsage = Math.max(memory, stats.peakMemoryUsage); + stats.downloads[label] = hash || Math.round((completedBytes / totalBytes) * 100); } async function buffered(label) { - const hash = new blake2.Hash('blake2b'); + const hash = new blake2.Hash('blake2b'); - const imageRes = await bhttp.get(file, { - onDownloadProgress(completedBytes, totalBytes) { - setProgress(label, completedBytes, totalBytes); - }, - }); + const imageRes = await bhttp.get(file, { + onDownloadProgress(completedBytes, totalBytes) { + setProgress(label, completedBytes, totalBytes); + }, + }); - hash.update(imageRes.body); - setProgress(label, null, null, hash.digest('hex')); + hash.update(imageRes.body); + setProgress(label, null, null, hash.digest('hex')); - await fsPromises.writeFile(`/mnt/stor/Pictures/traxxx/temp/buffered-${label}.bin`, imageRes.body); + await fsPromises.writeFile(`/mnt/stor/Pictures/traxxx/temp/buffered-${label}.bin`, imageRes.body); } async function streamed(label) { - const hash = new blake2.Hash('blake2b'); - hash.setEncoding('hex'); + const hash = new blake2.Hash('blake2b'); + hash.setEncoding('hex'); - const hashStream = new PassThrough(); - const targetStream = fs.createWriteStream(`/mnt/stor/Pictures/traxxx/temp/streamed-${label}.bin`); + const hashStream = new PassThrough(); + const targetStream = fs.createWriteStream(`/mnt/stor/Pictures/traxxx/temp/streamed-${label}.bin`); - const imageRes = await bhttp.get(file, { - stream: true, - }); + const imageRes = await bhttp.get(file, { + stream: true, + }); - const stream = imageRes - .pipe(hashStream) - .pipe(targetStream); + const stream = imageRes + .pipe(hashStream) + .pipe(targetStream); - imageRes.on('progress', (completedBytes, totalBytes) => { - setProgress(label, completedBytes, totalBytes); - }); + imageRes.on('progress', (completedBytes, totalBytes) => { + setProgress(label, completedBytes, totalBytes); + }); - hashStream.on('data', (chunk) => { - hash.write(chunk); - }); + hashStream.on('data', (chunk) => { + hash.write(chunk); + }); - stream.on('finish', () => { - hash.end(); - setProgress(label, null, null, hash.read()); - }); + stream.on('finish', () => { + hash.end(); + setProgress(label, null, null, hash.read()); + }); } async function init() { - const n = argv.n || 1; + const n = argv.n || 1; - if (argv._.includes('stream')) { - console.log('using streams'); - render(); + if (argv._.includes('stream')) { + console.log('using streams'); + render(); - await Promise.map(Array.from({ length: n }), async (value, index) => streamed(index + 1)); + await Promise.map(Array.from({ length: n }), async (value, index) => streamed(index + 1)); - return; - } + return; + } - if (argv._.includes('buffer')) { - console.log('using buffers'); - render(); + if (argv._.includes('buffer')) { + console.log('using buffers'); + render(); - await Promise.map(Array.from({ length: n }), async (value, index) => buffered(index + 1)); - } + await Promise.map(Array.from({ length: n }), async (value, index) => buffered(index + 1)); + } } init(); diff --git a/src/utils/capitalize.js b/src/utils/capitalize.js index c51655cfd..11eda33e9 100644 --- a/src/utils/capitalize.js +++ b/src/utils/capitalize.js @@ -1,16 +1,16 @@ 'use strict'; function capitalize(string, trim = true) { - if (!string) { - return ''; - } + if (!string) { + return ''; + } - const capitalized = string - .split(/\s+/) - .map(component => `${component.charAt(0).toUpperCase()}${component.slice(1)}`) - .join(' '); + const capitalized = string + .split(/\s+/) + .map(component => `${component.charAt(0).toUpperCase()}${component.slice(1)}`) + .join(' '); - return trim ? capitalized.trim() : capitalized; + return trim ? capitalized.trim() : capitalized; } module.exports = capitalize; diff --git a/src/utils/chunk.js b/src/utils/chunk.js index 32f1d9233..9ed28b1df 100644 --- a/src/utils/chunk.js +++ b/src/utils/chunk.js @@ -1,8 +1,8 @@ 'use strict'; function chunk(array, chunkSize) { - return Array.from({ length: Math.ceil(array.length / chunkSize) }) - .map((value, index) => array.slice(index * chunkSize, (index * chunkSize) + chunkSize)); + return Array.from({ length: Math.ceil(array.length / chunkSize) }) + .map((value, index) => array.slice(index * chunkSize, (index * chunkSize) + chunkSize)); } module.exports = chunk; diff --git a/src/utils/convert.js b/src/utils/convert.js index d8ec9ee40..d90dd5c3e 100644 --- a/src/utils/convert.js +++ b/src/utils/convert.js @@ -1,48 +1,48 @@ 'use strict'; function inchesToCm(inches) { - return Math.round(Number(inches) * 2.54); + return Math.round(Number(inches) * 2.54); } function feetInchesToCm(feet, inches) { - if (typeof feet === 'string' && !inches) { - const [feetPart, inchesPart] = feet.match(/\d+/g); - return feetInchesToCm(feetPart, inchesPart); - } + if (typeof feet === 'string' && !inches) { + const [feetPart, inchesPart] = feet.match(/\d+/g); + return feetInchesToCm(feetPart, inchesPart); + } - return Math.round((Number(feet) * 30.48) + (Number(inches) * 2.54)); + return Math.round((Number(feet) * 30.48) + (Number(inches) * 2.54)); } function cmToFeetInches(centimeters) { - const feet = Math.floor(centimeters / 30.48); - const inches = Math.round((centimeters / 2.54) % (feet * 12)); + const feet = Math.floor(centimeters / 30.48); + const inches = Math.round((centimeters / 2.54) % (feet * 12)); - return { feet, inches }; + return { feet, inches }; } function heightToCm(height) { - const [feet, inches] = height.match(/\d+/g); + const [feet, inches] = height.match(/\d+/g); - return feetInchesToCm(feet, inches); + return feetInchesToCm(feet, inches); } function lbsToKg(lbs) { - const pounds = lbs.toString().match(/\d+/)[0]; + const pounds = lbs.toString().match(/\d+/)[0]; - return Math.round(Number(pounds) * 0.453592); + return Math.round(Number(pounds) * 0.453592); } function kgToLbs(kgs) { - const kilos = kgs.toString().match(/\d+/)[0]; + const kilos = kgs.toString().match(/\d+/)[0]; - return Math.round(Number(kilos) / 0.453592); + return Math.round(Number(kilos) / 0.453592); } module.exports = { - cmToFeetInches, - feetInchesToCm, - heightToCm, - inchesToCm, - lbsToKg, - kgToLbs, + cmToFeetInches, + feetInchesToCm, + heightToCm, + inchesToCm, + lbsToKg, + kgToLbs, }; diff --git a/src/utils/cookies.js b/src/utils/cookies.js index 9fad54fd3..d14e28e69 100644 --- a/src/utils/cookies.js +++ b/src/utils/cookies.js @@ -1,16 +1,16 @@ 'use strict'; function cookieToData(cookieString) { - return cookieString.split('; ').reduce((acc, cookie) => { - const [key, value] = cookie.split('='); + return cookieString.split('; ').reduce((acc, cookie) => { + const [key, value] = cookie.split('='); - return { - ...acc, - [key]: value, - }; - }, {}); + return { + ...acc, + [key]: value, + }; + }, {}); } module.exports = { - cookieToData, + cookieToData, }; diff --git a/src/utils/escape-html.js b/src/utils/escape-html.js index 845f38649..0b66ffaab 100644 --- a/src/utils/escape-html.js +++ b/src/utils/escape-html.js @@ -1,10 +1,10 @@ function escapeHtml(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } module.exports = escapeHtml; diff --git a/src/utils/http.js b/src/utils/http.js index 47afd59eb..226de71b3 100644 --- a/src/utils/http.js +++ b/src/utils/http.js @@ -11,107 +11,107 @@ const pipeline = util.promisify(stream.pipeline); const logger = require('../logger')(__filename); const defaultHeaders = { - 'user-agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1', + 'user-agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1', }; const defaultOptions = { - responseTimeout: 30000, + responseTimeout: 30000, }; const proxyAgent = tunnel.httpsOverHttp({ - proxy: { - host: config.proxy.host, - port: config.proxy.port, - }, + proxy: { + host: config.proxy.host, + port: config.proxy.port, + }, }); function useProxy(url) { - if (!config.proxy.enable) { - return false; - } + if (!config.proxy.enable) { + return false; + } - const { hostname } = new URL(url); - return config.proxy.hostnames.includes(hostname); + const { hostname } = new URL(url); + return config.proxy.hostnames.includes(hostname); } const queue = taskQueue(); queue.on('concurrencyReached:http', () => { - logger.silly('Queueing requests'); + logger.silly('Queueing requests'); }); queue.define('http', async ({ - url, - method = 'GET', - body, - headers = {}, - options = {}, + url, + method = 'GET', + body, + headers = {}, + options = {}, }) => { - if (body) { - logger.silly(`${method.toUpperCase()} ${url} with ${JSON.stringify(body)}`); - } else { - logger.silly(`${method.toUpperCase()} ${url}`); - } + if (body) { + logger.silly(`${method.toUpperCase()} ${url} with ${JSON.stringify(body)}`); + } else { + logger.silly(`${method.toUpperCase()} ${url}`); + } - const reqOptions = { - headers: { - ...(options.defaultHeaders !== false && defaultHeaders), - ...headers, - }, - ...defaultOptions, - ...options, - ...(options.timeout && { responseTimeout: options.timeout }), - }; + const reqOptions = { + headers: { + ...(options.defaultHeaders !== false && defaultHeaders), + ...headers, + }, + ...defaultOptions, + ...options, + ...(options.timeout && { responseTimeout: options.timeout }), + }; - if (useProxy(url)) { - reqOptions.agent = proxyAgent; - } + if (useProxy(url)) { + reqOptions.agent = proxyAgent; + } - const res = ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) - ? await bhttp[method.toLowerCase()](url, body, reqOptions) - : await bhttp[method.toLowerCase()](url, reqOptions); + const res = ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()) + ? await bhttp[method.toLowerCase()](url, body, reqOptions) + : await bhttp[method.toLowerCase()](url, reqOptions); - if (options.stream && options.destination) { - await pipeline(res, ...(options.transforms || []), options.destination); - } + if (options.stream && options.destination) { + await pipeline(res, ...(options.transforms || []), options.destination); + } - const html = Buffer.isBuffer(res.body) ? res.body.toString() : null; - const json = Buffer.isBuffer(res.body) ? null : res.body; + const html = Buffer.isBuffer(res.body) ? res.body.toString() : null; + const json = Buffer.isBuffer(res.body) ? null : res.body; - return { - ...res, - originalRes: res, - html, - json, - pipe: res.pipe, - ok: res.statusCode >= 200 && res.statusCode <= 299, - code: res.statusCode, - status: res.statusCode, - }; + return { + ...res, + originalRes: res, + html, + json, + pipe: res.pipe, + ok: res.statusCode >= 200 && res.statusCode <= 299, + code: res.statusCode, + status: res.statusCode, + }; }, { - concurrency: 20, + concurrency: 20, }); async function get(url, headers, options) { - return queue.push('http', { - method: 'GET', - url, - headers, - options, - }); + return queue.push('http', { + method: 'GET', + url, + headers, + options, + }); } async function post(url, body, headers, options) { - return queue.push('http', { - method: 'POST', - url, - body, - headers, - options, - }); + return queue.push('http', { + method: 'POST', + url, + body, + headers, + options, + }); } module.exports = { - get, - post, + get, + post, }; diff --git a/src/utils/img.js b/src/utils/img.js index d574a617f..59552b5e6 100644 --- a/src/utils/img.js +++ b/src/utils/img.js @@ -7,12 +7,12 @@ const { argv } = require('yargs'); const url = argv.url || 'http://localhost:5000/media/actors/tommy-pistol/1580341442712.jpeg'; async function scan() { - console.log(url); + console.log(url); - const res = await bhttp.get(url); - const stats = await sharp(res.body).stats(); + const res = await bhttp.get(url); + const stats = await sharp(res.body).stats(); - console.log(stats); + console.log(stats); } scan(); diff --git a/src/utils/list.js b/src/utils/list.js index d847cef1e..fa7fc55f1 100644 --- a/src/utils/list.js +++ b/src/utils/list.js @@ -4,33 +4,33 @@ const Promise = require('bluebird'); const knex = require('../knex'); async function listSites() { - const [networks, allSites] = await Promise.all([ - knex('networks').orderBy('name'), - knex('sites').orderBy('name'), - ]); + const [networks, allSites] = await Promise.all([ + knex('networks').orderBy('name'), + knex('sites').orderBy('name'), + ]); - await Promise.each(networks, async (network) => { - console.log(`* **${network.name}**`); + await Promise.each(networks, async (network) => { + console.log(`* **${network.name}**`); - const sites = await knex('sites') - .where({ network_id: network.id }) - .orderBy('name'); + const sites = await knex('sites') + .where({ network_id: network.id }) + .orderBy('name'); - if (sites.length === 1 && sites[0].name === network.name) { - return; - } + if (sites.length === 1 && sites[0].name === network.name) { + return; + } - sites.forEach((site) => { - const rkSpecial = network.id === 'realitykings' + sites.forEach((site) => { + const rkSpecial = network.id === 'realitykings' && (new URL(site.url).hostname === 'www.realitykings.com' || (site.parameters?.altLayout)) - ? '\\*' : ''; // Reality Kings alt layout sites do not support scene fetch by URL + ? '\\*' : ''; // Reality Kings alt layout sites do not support scene fetch by URL - console.log(` * ${site.name}${rkSpecial}`); - }); - }); + console.log(` * ${site.name}${rkSpecial}`); + }); + }); - console.log(`${networks.length} networks with ${allSites.length} sites total`); + console.log(`${networks.length} networks with ${allSites.length} sites total`); } listSites(); diff --git a/src/utils/media.js b/src/utils/media.js index 2b73cc233..4a3fb7e71 100644 --- a/src/utils/media.js +++ b/src/utils/media.js @@ -12,99 +12,99 @@ const { PassThrough } = require('stream'); const http = require('./http'); function getMemoryUsage() { - return process.memoryUsage().rss / (10 ** 6); + return process.memoryUsage().rss / (10 ** 6); } let peakMemoryUsage = getMemoryUsage(); async function fetchSource(link) { - const id = nanoid(); + const id = nanoid(); - const hasher = new blake2.Hash('blake2b'); - hasher.setEncoding('hex'); + const hasher = new blake2.Hash('blake2b'); + hasher.setEncoding('hex'); - const tempFilePath = `/home/niels/Pictures/thumbs/temp/${id}.jpeg`; - const tempFileStream = fs.createWriteStream(tempFilePath); - const hashStream = new PassThrough(); + const tempFilePath = `/home/niels/Pictures/thumbs/temp/${id}.jpeg`; + const tempFileStream = fs.createWriteStream(tempFilePath); + const hashStream = new PassThrough(); - hashStream.on('data', chunk => hasher.write(chunk)); + hashStream.on('data', chunk => hasher.write(chunk)); - try { - const res = await http.get(link, null, { - stream: true, - transforms: [hashStream], - destination: tempFileStream, - timeout: 5000, - }); + try { + const res = await http.get(link, null, { + stream: true, + transforms: [hashStream], + destination: tempFileStream, + timeout: 5000, + }); - if (!res.ok) { - throw new Error(res.status); - } + if (!res.ok) { + throw new Error(res.status); + } - hasher.end(); - const hash = hasher.read(); + hasher.end(); + const hash = hasher.read(); - const memoryUsage = getMemoryUsage(); - peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage); + const memoryUsage = getMemoryUsage(); + peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage); - console.log(`Stored ${tempFilePath}, memory usage: ${memoryUsage.toFixed(2)} MB`); + console.log(`Stored ${tempFilePath}, memory usage: ${memoryUsage.toFixed(2)} MB`); - return { - id, - path: tempFilePath, - hash, - }; - } catch (error) { - await fsPromises.unlink(tempFilePath); + return { + id, + path: tempFilePath, + hash, + }; + } catch (error) { + await fsPromises.unlink(tempFilePath); - throw error; - } + throw error; + } } async function init() { - const linksFile = await fsPromises.readFile('/home/niels/Pictures/photos', 'utf8'); - const links = linksFile.split('\n').filter(Boolean); + const linksFile = await fsPromises.readFile('/home/niels/Pictures/photos', 'utf8'); + const links = linksFile.split('\n').filter(Boolean); - await fsPromises.mkdir('/home/niels/Pictures/thumbs/temp', { recursive: true }); + await fsPromises.mkdir('/home/niels/Pictures/thumbs/temp', { recursive: true }); - console.time('thumbs'); + console.time('thumbs'); - const files = await Promise.map(links, async (link) => { - try { - return await fetchSource(link); - } catch (error) { - console.log(`Failed to fetch ${link}: ${error.message}`); - return null; - } - }); + const files = await Promise.map(links, async (link) => { + try { + return await fetchSource(link); + } catch (error) { + console.log(`Failed to fetch ${link}: ${error.message}`); + return null; + } + }); - await Promise.map(files.filter(Boolean), async (file) => { - const image = sharp(file.path).jpeg(); + await Promise.map(files.filter(Boolean), async (file) => { + const image = sharp(file.path).jpeg(); - const [{ width, height }, { size }] = await Promise.all([ - image.metadata(), - fsPromises.stat(file.path), - ]); + const [{ width, height }, { size }] = await Promise.all([ + image.metadata(), + fsPromises.stat(file.path), + ]); - await Promise.all([ - image - .toFile(`/home/niels/Pictures/thumbs/${file.hash}.jpeg`), - image - .resize({ - height: config.media.thumbnailSize, - withoutEnlargement: true, - }) - .toFile(`/home/niels/Pictures/thumbs/${file.hash}_thumb.jpeg`), - ]); + await Promise.all([ + image + .toFile(`/home/niels/Pictures/thumbs/${file.hash}.jpeg`), + image + .resize({ + height: config.media.thumbnailSize, + withoutEnlargement: true, + }) + .toFile(`/home/niels/Pictures/thumbs/${file.hash}_thumb.jpeg`), + ]); - const memoryUsage = getMemoryUsage(); - peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage); + const memoryUsage = getMemoryUsage(); + peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage); - console.log(`Resized ${file.id} (${width}, ${height}, ${size}), memory usage: ${memoryUsage.toFixed(2)} MB`); - }, { concurrency: 10 }); + console.log(`Resized ${file.id} (${width}, ${height}, ${size}), memory usage: ${memoryUsage.toFixed(2)} MB`); + }, { concurrency: 10 }); - console.log(`Peak memory usage: ${peakMemoryUsage.toFixed(2)} MB`); - console.timeEnd('thumbs'); + console.log(`Peak memory usage: ${peakMemoryUsage.toFixed(2)} MB`); + console.timeEnd('thumbs'); } init(); diff --git a/src/utils/mofos.js b/src/utils/mofos.js index c086cc517..6324c73c6 100644 --- a/src/utils/mofos.js +++ b/src/utils/mofos.js @@ -6,16 +6,16 @@ const bhttp = require('bhttp'); const knex = require('../knex'); async function run() { - const network = await knex('networks').where('slug', 'mofos').first(); - const sites = await knex('sites').where('network_id', network.id); + const network = await knex('networks').where('slug', 'mofos').first(); + const sites = await knex('sites').where('network_id', network.id); - await Promise.map(sites, async (site) => { - const res = await bhttp.get(site.url); + await Promise.map(sites, async (site) => { + const res = await bhttp.get(site.url); - console.log(site.url, res.statusCode); - }, { - concurrency: 5, - }); + console.log(site.url, res.statusCode); + }, { + concurrency: 5, + }); } run(); diff --git a/src/utils/pick-random.js b/src/utils/pick-random.js index 729d5c51e..1e8fb853d 100644 --- a/src/utils/pick-random.js +++ b/src/utils/pick-random.js @@ -1,5 +1,5 @@ function pickRandom(array) { - return array[Math.floor(Math.random() * array.length)]; + return array[Math.floor(Math.random() * array.length)]; } module.exports = pickRandom; diff --git a/src/utils/posters.js b/src/utils/posters.js index d30d6d95c..541aec0f7 100644 --- a/src/utils/posters.js +++ b/src/utils/posters.js @@ -9,32 +9,32 @@ const argv = require('../argv'); const knex = require('../knex'); async function init() { - const posters = await knex('actors') - .select('actors.name as actor_name', 'releases.title', 'releases.date', 'media.path', 'media.index', 'sites.name as site_name', 'networks.name as network_name') - .whereIn('actors.name', (argv.actors || []).concat(argv._)) - .join('releases_actors', 'releases_actors.actor_id', 'actors.id') - .join('releases', 'releases_actors.release_id', 'releases.id') - .join('sites', 'sites.id', 'releases.site_id') - .join('networks', 'networks.id', 'sites.network_id') - .join('releases_posters', 'releases_posters.release_id', 'releases.id') - .join('media', 'releases_posters.media_id', 'media.id'); - // .join('releases_photos', 'releases_photos.release_id', 'releases.id') - // .join('media', 'releases_photos.media_id', 'media.id'); + const posters = await knex('actors') + .select('actors.name as actor_name', 'releases.title', 'releases.date', 'media.path', 'media.index', 'sites.name as site_name', 'networks.name as network_name') + .whereIn('actors.name', (argv.actors || []).concat(argv._)) + .join('releases_actors', 'releases_actors.actor_id', 'actors.id') + .join('releases', 'releases_actors.release_id', 'releases.id') + .join('sites', 'sites.id', 'releases.site_id') + .join('networks', 'networks.id', 'sites.network_id') + .join('releases_posters', 'releases_posters.release_id', 'releases.id') + .join('media', 'releases_posters.media_id', 'media.id'); + // .join('releases_photos', 'releases_photos.release_id', 'releases.id') + // .join('media', 'releases_photos.media_id', 'media.id'); - await Promise.all(posters.map(async (poster) => { - const source = path.join(config.media.path, poster.path); + await Promise.all(posters.map(async (poster) => { + const source = path.join(config.media.path, poster.path); - const directory = path.join(config.media.path, 'extracted', poster.actor_name); - const target = path.join(directory, `${poster.actor_name} - ${poster.network_name}: ${poster.site_name} - ${poster.title.replace(/[/.]/g, '_')} (${moment.utc(poster.date).format('YYYY-MM-DD')})-${poster.index}.jpeg`); - await fs.mkdir(path.join(directory), { recursive: true }); + const directory = path.join(config.media.path, 'extracted', poster.actor_name); + const target = path.join(directory, `${poster.actor_name} - ${poster.network_name}: ${poster.site_name} - ${poster.title.replace(/[/.]/g, '_')} (${moment.utc(poster.date).format('YYYY-MM-DD')})-${poster.index}.jpeg`); + await fs.mkdir(path.join(directory), { recursive: true }); - const file = await fs.readFile(source); - await fs.writeFile(target, file); + const file = await fs.readFile(source); + await fs.writeFile(target, file); - return file; - })); + return file; + })); - knex.destroy(); + knex.destroy(); } init(); diff --git a/src/utils/qu.js b/src/utils/qu.js index 31e76bea8..7d97be2ca 100644 --- a/src/utils/qu.js +++ b/src/utils/qu.js @@ -5,341 +5,341 @@ const moment = require('moment'); const http = require('./http'); function trim(str) { - if (!str) return null; - return str.trim().replace(/\s+/g, ' '); + if (!str) return null; + return str.trim().replace(/\s+/g, ' '); } function extractDate(dateString, format, match) { - if (match) { - const dateStamp = trim(dateString).match(match); + if (match) { + const dateStamp = trim(dateString).match(match); - if (dateStamp) { - const dateValue = moment.utc(dateStamp[0], format); + if (dateStamp) { + const dateValue = moment.utc(dateStamp[0], format); - return dateValue.isValid() ? dateValue.toDate() : null; - } - return null; - } + return dateValue.isValid() ? dateValue.toDate() : null; + } + return null; + } - const dateValue = moment.utc(trim(dateString), format); + const dateValue = moment.utc(trim(dateString), format); - return dateValue.isValid() ? dateValue.toDate() : null; + return dateValue.isValid() ? dateValue.toDate() : null; } function formatDate(dateValue, format, inputFormat) { - if (inputFormat) { - return moment(dateValue, inputFormat).format(format); - } + if (inputFormat) { + return moment(dateValue, inputFormat).format(format); + } - return moment(dateValue).format(format); + return moment(dateValue).format(format); } function prefixUrl(urlValue, origin, protocol = 'https') { - if (protocol && /^\/\//.test(urlValue)) { - return `${protocol}:${urlValue}`; - } + if (protocol && /^\/\//.test(urlValue)) { + return `${protocol}:${urlValue}`; + } - if (origin && /^\//.test(urlValue)) { - return `${origin}${urlValue}`; - } + if (origin && /^\//.test(urlValue)) { + return `${origin}${urlValue}`; + } - return urlValue; + return urlValue; } function q(context, selector, attrArg, applyTrim = true) { - const attr = attrArg === true ? 'textContent' : attrArg; + const attr = attrArg === true ? 'textContent' : attrArg; - if (attr) { - const value = selector - ? context.querySelector(selector)?.[attr] || context.querySelector(selector)?.attributes[attr]?.value - : context[attr] || context.attributes[attr]?.value; + if (attr) { + const value = selector + ? context.querySelector(selector)?.[attr] || context.querySelector(selector)?.attributes[attr]?.value + : context[attr] || context.attributes[attr]?.value; - return applyTrim && value ? trim(value) : value; - } + return applyTrim && value ? trim(value) : value; + } - return selector ? context.querySelector(selector) : context; + return selector ? context.querySelector(selector) : context; } function all(context, selector, attrArg, applyTrim = true) { - const attr = attrArg === true ? 'textContent' : attrArg; + const attr = attrArg === true ? 'textContent' : attrArg; - if (attr) { - return Array.from(context.querySelectorAll(selector), el => q(el, null, attr, applyTrim)); - } + if (attr) { + return Array.from(context.querySelectorAll(selector), el => q(el, null, attr, applyTrim)); + } - return Array.from(context.querySelectorAll(selector)); + return Array.from(context.querySelectorAll(selector)); } function exists(context, selector) { - return !!q(context, selector); + return !!q(context, selector); } function html(context, selector) { - const el = q(context, selector, null, true); + const el = q(context, selector, null, true); - return el && el.innerHTML; + return el && el.innerHTML; } function texts(context, selector, applyTrim = true, filter = true) { - const el = q(context, selector, null, applyTrim); - if (!el) return null; + const el = q(context, selector, null, applyTrim); + if (!el) return null; - const nodes = Array.from(el.childNodes) - .filter(node => node.nodeName === '#text') - .map(node => (applyTrim ? trim(node.textContent) : node.textContent)); + const nodes = Array.from(el.childNodes) + .filter(node => node.nodeName === '#text') + .map(node => (applyTrim ? trim(node.textContent) : node.textContent)); - return filter ? nodes.filter(Boolean) : nodes; + return filter ? nodes.filter(Boolean) : nodes; } function text(context, selector, applyTrim = true) { - const nodes = texts(context, selector, applyTrim, true); - if (!nodes) return null; + const nodes = texts(context, selector, applyTrim, true); + if (!nodes) return null; - const textValue = nodes.join(' '); + const textValue = nodes.join(' '); - return applyTrim ? trim(textValue) : textValue; + return applyTrim ? trim(textValue) : textValue; } function meta(context, selector, attrArg = 'content', applyTrim = true) { - if (/meta\[.*\]/.test(selector)) { - return q(context, selector, attrArg, applyTrim); - } + if (/meta\[.*\]/.test(selector)) { + return q(context, selector, attrArg, applyTrim); + } - return q(context, `meta[${selector}]`, attrArg, applyTrim); + return q(context, `meta[${selector}]`, attrArg, applyTrim); } function date(context, selector, format, match, attr = 'textContent') { - const dateString = q(context, selector, attr, true); + const dateString = q(context, selector, attr, true); - if (!dateString) return null; + if (!dateString) return null; - return extractDate(dateString, format, match); + return extractDate(dateString, format, match); } function image(context, selector = 'img', attr = 'src', origin, protocol = 'https') { - const imageEl = q(context, selector, attr); + const imageEl = q(context, selector, attr); - // no attribute means q output will be HTML element - return attr ? prefixUrl(imageEl, origin, protocol) : imageEl; + // no attribute means q output will be HTML element + return attr ? prefixUrl(imageEl, origin, protocol) : imageEl; } function images(context, selector = 'img', attr = 'src', origin, protocol = 'https') { - const imageEls = all(context, selector, attr); + const imageEls = all(context, selector, attr); - return attr ? imageEls.map(imageEl => prefixUrl(imageEl, origin, protocol)) : imageEls; + return attr ? imageEls.map(imageEl => prefixUrl(imageEl, origin, protocol)) : imageEls; } function url(context, selector = 'a', attr = 'href', origin, protocol = 'https') { - const urlEl = q(context, selector, attr); + const urlEl = q(context, selector, attr); - return attr ? prefixUrl(urlEl, origin, protocol) : urlEl; + return attr ? prefixUrl(urlEl, origin, protocol) : urlEl; } function urls(context, selector = 'a', attr = 'href', origin, protocol = 'https') { - const urlEls = all(context, selector, attr); + const urlEls = all(context, selector, attr); - return attr ? urlEls.map(urlEl => prefixUrl(urlEl, origin, protocol)) : urlEls; + return attr ? urlEls.map(urlEl => prefixUrl(urlEl, origin, protocol)) : urlEls; } function poster(context, selector = 'video', attr = 'poster', origin, protocol = 'https') { - const posterEl = q(context, selector, attr); + const posterEl = q(context, selector, attr); - return attr ? prefixUrl(posterEl, origin, protocol) : posterEl; + return attr ? prefixUrl(posterEl, origin, protocol) : posterEl; } function video(context, selector = 'source', attr = 'src', origin, protocol = 'https') { - const trailerEl = q(context, selector, attr); + const trailerEl = q(context, selector, attr); - return attr ? prefixUrl(trailerEl, origin, protocol) : trailerEl; + return attr ? prefixUrl(trailerEl, origin, protocol) : trailerEl; } function videos(context, selector = 'source', attr = 'src', origin, protocol = 'https') { - const trailerEls = all(context, selector, attr); + const trailerEls = all(context, selector, attr); - return attr ? trailerEls.map(trailerEl => prefixUrl(trailerEl, origin, protocol)) : trailerEls; + return attr ? trailerEls.map(trailerEl => prefixUrl(trailerEl, origin, protocol)) : trailerEls; } function duration(context, selector, match, attr = 'textContent') { - const durationString = q(context, selector, attr); + const durationString = q(context, selector, attr); - if (!durationString) return null; - const durationMatch = durationString.match(match || /(\d+:)?\d+:\d+/); + if (!durationString) return null; + const durationMatch = durationString.match(match || /(\d+:)?\d+:\d+/); - if (durationMatch) { - const segments = ['00'].concat(durationMatch[0].split(':')).slice(-3); + if (durationMatch) { + const segments = ['00'].concat(durationMatch[0].split(':')).slice(-3); - return moment.duration(segments.join(':')).asSeconds(); - } + return moment.duration(segments.join(':')).asSeconds(); + } - return null; + return null; } const legacyFuncs = { - q, - qa: all, - qall: all, - qd: date, - qdate: date, - qh: html, - qhtml: html, - qi: image, - qimage: image, - qimages: images, - qis: images, - ql: duration, - qlength: duration, - qm: meta, - qmeta: meta, - qp: poster, - qposter: poster, - qs: all, - qt: video, - qtext: text, - qtexts: texts, - qtrailer: video, - qtrailers: videos, - qts: videos, - qtx: text, - qtxs: texts, - qtxt: text, - qtxts: texts, - // qu: url, - qurl: url, - qurls: urls, - qus: urls, + q, + qa: all, + qall: all, + qd: date, + qdate: date, + qh: html, + qhtml: html, + qi: image, + qimage: image, + qimages: images, + qis: images, + ql: duration, + qlength: duration, + qm: meta, + qmeta: meta, + qp: poster, + qposter: poster, + qs: all, + qt: video, + qtext: text, + qtexts: texts, + qtrailer: video, + qtrailers: videos, + qts: videos, + qtx: text, + qtxs: texts, + qtxt: text, + qtxts: texts, + // qu: url, + qurl: url, + qurls: urls, + qus: urls, }; const quFuncs = { - all, - html, - date, - dur: duration, - duration, - exists, - image, - images, - img: image, - imgs: images, - length: duration, - meta, - poster, - q, - text, - texts, - trailer: video, - url, - urls, - video, - videos, + all, + html, + date, + dur: duration, + duration, + exists, + image, + images, + img: image, + imgs: images, + length: duration, + meta, + poster, + q, + text, + texts, + trailer: video, + url, + urls, + video, + videos, }; function init(element, window) { - if (!element) return null; + if (!element) return null; - const legacyContextFuncs = Object.entries(legacyFuncs) // dynamically attach methods with context - .reduce((acc, [key, func]) => ({ - ...acc, - [key]: (...args) => (window && args[0] instanceof window.HTMLElement // allow for different context - ? func(...args) - : func(element, ...args)), - }), {}); + const legacyContextFuncs = Object.entries(legacyFuncs) // dynamically attach methods with context + .reduce((acc, [key, func]) => ({ + ...acc, + [key]: (...args) => (window && args[0] instanceof window.HTMLElement // allow for different context + ? func(...args) + : func(element, ...args)), + }), {}); - const quContextFuncs = Object.entries(quFuncs) // dynamically attach methods with context - .reduce((acc, [key, func]) => ({ - ...acc, - [key]: (...args) => (window && args[0] instanceof window.HTMLElement // allow for different context - ? func(...args) - : func(element, ...args)), - }), {}); + const quContextFuncs = Object.entries(quFuncs) // dynamically attach methods with context + .reduce((acc, [key, func]) => ({ + ...acc, + [key]: (...args) => (window && args[0] instanceof window.HTMLElement // allow for different context + ? func(...args) + : func(element, ...args)), + }), {}); - return { - element, - el: element, - html: element.outerHTML || element.body.outerHTML, - text: trim(element.textContent), - ...(window && { - window, - document: window.document, - }), - ...legacyContextFuncs, - qu: quContextFuncs, - }; + return { + element, + el: element, + html: element.outerHTML || element.body.outerHTML, + text: trim(element.textContent), + ...(window && { + window, + document: window.document, + }), + ...legacyContextFuncs, + qu: quContextFuncs, + }; } function initAll(context, selector, window) { - if (Array.isArray(context)) { - return context.map(element => init(element, window)); - } + if (Array.isArray(context)) { + return context.map(element => init(element, window)); + } - return Array.from(context.querySelectorAll(selector)) - .map(element => init(element, window)); + return Array.from(context.querySelectorAll(selector)) + .map(element => init(element, window)); } function extract(htmlValue, selector) { - const { window } = new JSDOM(htmlValue); + const { window } = new JSDOM(htmlValue); - if (selector) { - return init(window.document.querySelector(selector), window); - } + if (selector) { + return init(window.document.querySelector(selector), window); + } - return init(window.document, window); + return init(window.document, window); } function extractAll(htmlValue, selector) { - const { window } = new JSDOM(htmlValue); + const { window } = new JSDOM(htmlValue); - return initAll(window.document, selector, window); + return initAll(window.document, selector, window); } async function get(urlValue, selector, headers, options, queryAll = false) { - const res = await http.get(urlValue, headers); + const res = await http.get(urlValue, headers); - if (res.statusCode === 200) { - const item = queryAll - ? extractAll(res.body.toString(), selector) - : extract(res.body.toString(), selector); + if (res.statusCode === 200) { + const item = queryAll + ? extractAll(res.body.toString(), selector) + : extract(res.body.toString(), selector); - return { - item, - items: all ? item : [item], - res, - ok: true, - status: res.statusCode, - }; - } + return { + item, + items: all ? item : [item], + res, + ok: true, + status: res.statusCode, + }; + } - return { - item: null, - items: [], - res, - ok: false, - status: res.statusCode, - }; + return { + item: null, + items: [], + res, + ok: false, + status: res.statusCode, + }; } async function getAll(urlValue, selector, headers, options) { - return get(urlValue, selector, headers, options, true); + return get(urlValue, selector, headers, options, true); } module.exports = { - extractDate, - extract, - extractAll, - init, - initAll, - formatDate, - get, - getAll, - context: init, - contextAll: initAll, - ed: extractDate, - ex: extract, - exa: extractAll, - fd: formatDate, - parseDate: extractDate, - ctx: init, - ctxa: initAll, - geta: getAll, - qu: quFuncs, - ...legacyFuncs, + extractDate, + extract, + extractAll, + init, + initAll, + formatDate, + get, + getAll, + context: init, + contextAll: initAll, + ed: extractDate, + ex: extract, + exa: extractAll, + fd: formatDate, + parseDate: extractDate, + ctx: init, + ctxa: initAll, + geta: getAll, + qu: quFuncs, + ...legacyFuncs, }; diff --git a/src/utils/rename.js b/src/utils/rename.js deleted file mode 100644 index 95bd7f04d..000000000 --- a/src/utils/rename.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const path = require('path'); -const Promise = require('bluebird'); -const fs = require('fs-extra'); -const fetchScene = require('../scrape-releases'); - -const argv = require('../argv'); - -async function renameFiles() { - const filenames = await fs.readdir(process.cwd()); - - const curated = await Promise.map(filenames, async (filename) => { - const shootId = filename.split(' ')[1]; - const scene = await fetchScene(`https://kink.com/shoot/${shootId}`); - - if (argv.confirm) { - await fs.rename(path.join(process.cwd(), filename), path.join(process.cwd(), `${scene.filename}.mp4`)); - } - - return scene.filename; - }, { - concurrency: 5, - }); - - console.log(curated); -} - -renameFiles(); diff --git a/src/utils/resolve-place.js b/src/utils/resolve-place.js index 09de60731..7470e8720 100644 --- a/src/utils/resolve-place.js +++ b/src/utils/resolve-place.js @@ -3,26 +3,26 @@ const bhttp = require('bhttp'); async function resolvePlace(query) { - if (!query) { - return null; - } + if (!query) { + return null; + } - const res = await bhttp.get(`https://nominatim.openstreetmap.org/search/${encodeURI(query)}?format=json&accept-language=en&addressdetails=1`); - const [item] = res.body; + const res = await bhttp.get(`https://nominatim.openstreetmap.org/search/${encodeURI(query)}?format=json&accept-language=en&addressdetails=1`); + const [item] = res.body; - if (item && item.address) { - const rawPlace = item.address; - const place = {}; + if (item && item.address) { + const rawPlace = item.address; + const place = {}; - if (rawPlace.city) place.city = rawPlace.city; - if (rawPlace.state) place.state = rawPlace.state; - if (rawPlace.country_code) place.country = rawPlace.country_code.toUpperCase(); - if (rawPlace.continent) place.continent = rawPlace.continent; + if (rawPlace.city) place.city = rawPlace.city; + if (rawPlace.state) place.state = rawPlace.state; + if (rawPlace.country_code) place.country = rawPlace.country_code.toUpperCase(); + if (rawPlace.continent) place.continent = rawPlace.continent; - return place; - } + return place; + } - return null; + return null; } module.exports = resolvePlace; diff --git a/src/utils/scorelogos.js b/src/utils/scorelogos.js index 45c555c9f..527c7ff2c 100644 --- a/src/utils/scorelogos.js +++ b/src/utils/scorelogos.js @@ -6,32 +6,32 @@ const fs = require('fs-extra'); const knex = require('../knex'); async function init() { - const sites = await knex('sites') - .select('networks.name', 'sites.slug') - .join('networks', 'networks.id', 'sites.network_id') - .where('networks.slug', 'score'); + const sites = await knex('sites') + .select('networks.name', 'sites.slug') + .join('networks', 'networks.id', 'sites.network_id') + .where('networks.slug', 'score'); - await Promise.map(sites, async (site) => { - const url = `https://cdn77.scoreuniverse.com/${site.slug}/images/logo.png`; + await Promise.map(sites, async (site) => { + const url = `https://cdn77.scoreuniverse.com/${site.slug}/images/logo.png`; - console.log(url); + console.log(url); - const res = await bhttp.get(url, { - responseTimeout: 5000, - }); + const res = await bhttp.get(url, { + responseTimeout: 5000, + }); - if (res.statusCode === 200) { - console.log(`Saving logo for ${site.slug}`); + if (res.statusCode === 200) { + console.log(`Saving logo for ${site.slug}`); - await fs.writeFile(`./score/${site.slug}.png`, res.body); - } + await fs.writeFile(`./score/${site.slug}.png`, res.body); + } - console.log(`No logo found for ${site.slug}`); - }, { - concurrency: 10, - }); + console.log(`No logo found for ${site.slug}`); + }, { + concurrency: 10, + }); - knex.destroy(); + knex.destroy(); } init(); diff --git a/src/utils/shuffle.js b/src/utils/shuffle.js index 5b96bc6e5..13424cb40 100644 --- a/src/utils/shuffle.js +++ b/src/utils/shuffle.js @@ -1,14 +1,14 @@ 'use strict'; function shuffle(array) { - const shuffledArray = [...array]; + const shuffledArray = [...array]; - for (let i = array.length - 1; i > 0; i -= 1) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; - } + for (let i = array.length - 1; i > 0; i -= 1) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; + } - return shuffledArray; + return shuffledArray; } module.exports = shuffle; diff --git a/src/utils/slugify.js b/src/utils/slugify.js index ae5a6d387..a14c76c50 100644 --- a/src/utils/slugify.js +++ b/src/utils/slugify.js @@ -1,30 +1,30 @@ 'use strict'; function slugify(string, delimiter = '-', { - encode = false, - limit = 1000, + encode = false, + limit = 1000, } = {}) { - if (!string) { - return string; - } + if (!string) { + return string; + } - const slugComponents = string.trim().toLowerCase().match(/\w+/g); + const slugComponents = string.trim().toLowerCase().match(/\w+/g); - if (!slugComponents) { - return ''; - } + if (!slugComponents) { + return ''; + } - const slug = slugComponents.reduce((acc, component, index) => { - const accSlug = `${acc}${index > 0 ? delimiter : ''}${component}`; + const slug = slugComponents.reduce((acc, component, index) => { + const accSlug = `${acc}${index > 0 ? delimiter : ''}${component}`; - if (accSlug.length < limit) { - return accSlug; - } + if (accSlug.length < limit) { + return accSlug; + } - return acc; - }, ''); + return acc; + }, ''); - return encode ? encodeURI(slug) : slug; + return encode ? encodeURI(slug) : slug; } module.exports = slugify; diff --git a/src/utils/stream.js b/src/utils/stream.js index e51c820a9..54a532a47 100644 --- a/src/utils/stream.js +++ b/src/utils/stream.js @@ -11,45 +11,45 @@ const sharp = require('sharp'); const url = 'https://thumbs.julesjordan.com/trial/content//upload/dl03/julesjordan/oil_overload_16_scene2//photos/alina_lopez_jules_jordan_com_77.jpg'; async function init() { - const hash = new blake2.Hash('blake2b'); - hash.setEncoding('hex'); + const hash = new blake2.Hash('blake2b'); + hash.setEncoding('hex'); - const res = await bhttp.get(url, { - stream: true, - }); + const res = await bhttp.get(url, { + stream: true, + }); - const metaStream = sharp(); - const hashStream = new PassThrough(); - const target = fs.createWriteStream(path.join(config.media.path, 'temp', 'alina.jpg')); - const thumbTarget = fs.createWriteStream(path.join(config.media.path, 'temp', 'alina_thumb.jpg')); + const metaStream = sharp(); + const hashStream = new PassThrough(); + const target = fs.createWriteStream(path.join(config.media.path, 'temp', 'alina.jpg')); + const thumbTarget = fs.createWriteStream(path.join(config.media.path, 'temp', 'alina_thumb.jpg')); - hashStream.on('data', (chunk) => { - hash.write(chunk); - }); + hashStream.on('data', (chunk) => { + hash.write(chunk); + }); - metaStream.clone() - .resize(320) - .pipe(thumbTarget); + metaStream.clone() + .resize(320) + .pipe(thumbTarget); - const stream = res - .pipe(metaStream) - .pipe(hashStream) - .pipe(target); + const stream = res + .pipe(metaStream) + .pipe(hashStream) + .pipe(target); - stream.on('finish', () => { - hash.end(); - const digest = hash.read(); + stream.on('finish', () => { + hash.end(); + const digest = hash.read(); - console.log('stream', digest); - }); + console.log('stream', digest); + }); - metaStream.on('info', (info) => { - console.log('info', info); - }); + metaStream.on('info', (info) => { + console.log('info', info); + }); - const stats = await metaStream.stats(); + const stats = await metaStream.stats(); - console.log('stats', stats); + console.log('stats', stats); } init(); diff --git a/src/utils/timeout.js b/src/utils/timeout.js index 6327ed904..f07c76176 100644 --- a/src/utils/timeout.js +++ b/src/utils/timeout.js @@ -6,15 +6,15 @@ const sleep = 5000; const timeout = 1000; async function init() { - try { - const res = await bhttp.get(`https://httpstat.us/200?sleep=${sleep}`, { - responseTimeout: timeout, - }); + try { + const res = await bhttp.get(`https://httpstat.us/200?sleep=${sleep}`, { + responseTimeout: timeout, + }); - console.log(res.statusCode); - } catch (error) { - console.log(error); - } + console.log(res.statusCode); + } catch (error) { + console.log(error); + } } /* diff --git a/src/utils/titles.js b/src/utils/titles.js index 8d8c9503b..283b00c7b 100644 --- a/src/utils/titles.js +++ b/src/utils/titles.js @@ -4,18 +4,18 @@ const argv = require('../argv'); const knex = require('../knex'); async function printTitles() { - const titles = await knex('releases') - .where((builder) => { - if (argv.sites) builder.whereIn('sites.slug', argv.sites); - if (argv.networks) builder.orWhereIn('networks.slug', argv.networks); - }) - .join('sites', 'sites.id', 'releases.site_id') - .join('networks', 'networks.id', 'sites.network_id') - .pluck('title'); + const titles = await knex('releases') + .where((builder) => { + if (argv.sites) builder.whereIn('sites.slug', argv.sites); + if (argv.networks) builder.orWhereIn('networks.slug', argv.networks); + }) + .join('sites', 'sites.id', 'releases.site_id') + .join('networks', 'networks.id', 'sites.network_id') + .pluck('title'); - console.log(titles.join('\n')); + console.log(titles.join('\n')); - knex.destroy(); + knex.destroy(); } printTitles(); diff --git a/src/utils/upsert.js b/src/utils/upsert.js index 72a07ab13..230337940 100644 --- a/src/utils/upsert.js +++ b/src/utils/upsert.js @@ -4,54 +4,54 @@ const knex = require('../knex'); const logger = require('../logger')(__filename); async function upsert(table, items, identifier = ['id'], _knex) { - const identifiers = Array.isArray(identifier) ? identifier : [identifier]; + const identifiers = Array.isArray(identifier) ? identifier : [identifier]; - const duplicates = await knex(table).whereIn(identifiers, items.map(item => identifiers.map(identifierX => item[identifierX]))); - const duplicatesByIdentifiers = duplicates.reduce((acc, duplicate) => { - const duplicateIdentifier = identifiers.map(identifierX => duplicate[identifierX]).toString(); + const duplicates = await knex(table).whereIn(identifiers, items.map(item => identifiers.map(identifierX => item[identifierX]))); + const duplicatesByIdentifiers = duplicates.reduce((acc, duplicate) => { + const duplicateIdentifier = identifiers.map(identifierX => duplicate[identifierX]).toString(); - return { ...acc, [duplicateIdentifier]: duplicate }; - }, {}); + return { ...acc, [duplicateIdentifier]: duplicate }; + }, {}); - const { insert, update } = items.reduce((acc, item) => { - const itemIdentifier = identifiers.map(identifierX => item[identifierX]).toString(); + const { insert, update } = items.reduce((acc, item) => { + const itemIdentifier = identifiers.map(identifierX => item[identifierX]).toString(); - if (duplicatesByIdentifiers[itemIdentifier]) { - acc.update.push(item); - return acc; - } + if (duplicatesByIdentifiers[itemIdentifier]) { + acc.update.push(item); + return acc; + } - acc.insert.push(item); - return acc; - }, { - insert: [], - update: [], - }); + acc.insert.push(item); + return acc; + }, { + insert: [], + update: [], + }); - if (knex) { - logger.debug(`${table}: Inserting ${insert.length}`); - logger.debug(`${table}: Updating ${update.length}`); + if (knex) { + logger.debug(`${table}: Inserting ${insert.length}`); + logger.debug(`${table}: Updating ${update.length}`); - const [inserted, updated] = await Promise.all([ - knex(table).returning('*').insert(insert), - knex.transaction(async trx => Promise.all(update.map((item) => { - const clause = identifiers.reduce((acc, identifierX) => ({ ...acc, [identifierX]: item[identifierX] }), {}); + const [inserted, updated] = await Promise.all([ + knex(table).returning('*').insert(insert), + knex.transaction(async trx => Promise.all(update.map((item) => { + const clause = identifiers.reduce((acc, identifierX) => ({ ...acc, [identifierX]: item[identifierX] }), {}); - return trx - .where(clause) - .update(item) - .into(table) - .returning('*'); - }))), - ]); + return trx + .where(clause) + .update(item) + .into(table) + .returning('*'); + }))), + ]); - return { - inserted: Array.isArray(inserted) ? inserted : [], - updated: updated.reduce((acc, updatedItems) => acc.concat(updatedItems), []), - }; - } + return { + inserted: Array.isArray(inserted) ? inserted : [], + updated: updated.reduce((acc, updatedItems) => acc.concat(updatedItems), []), + }; + } - return { insert, update }; + return { insert, update }; } module.exports = upsert; diff --git a/src/utils/where-or.js b/src/utils/where-or.js index bc9049a8f..a3eb5ce47 100644 --- a/src/utils/where-or.js +++ b/src/utils/where-or.js @@ -1,25 +1,25 @@ 'use strict'; function whereOr(query, table, builder) { - if (!query) { - return {}; - } + if (!query) { + return {}; + } - Object.entries(query).forEach(([key, value]) => { - if (value === undefined) { - return builder; - } + Object.entries(query).forEach(([key, value]) => { + if (value === undefined) { + return builder; + } - if (Array.isArray(value)) { - builder.orWhereIn(`${table}.${key}`, value); - return builder; - } + if (Array.isArray(value)) { + builder.orWhereIn(`${table}.${key}`, value); + return builder; + } - builder.orWhere(`${table}.${key}`, value); - return builder; - }); + builder.orWhere(`${table}.${key}`, value); + return builder; + }); - return builder; + return builder; } module.exports = whereOr; diff --git a/src/web/actors.js b/src/web/actors.js index c43cca327..b6cace5c5 100644 --- a/src/web/actors.js +++ b/src/web/actors.js @@ -3,29 +3,29 @@ const { fetchActors } = require('../actors'); async function fetchActorsApi(req, res) { - const actorId = typeof req.params.actorId === 'number' ? req.params.actorId : null; - const actorSlug = typeof req.params.actorId === 'string' ? req.params.actorId : null; + const actorId = typeof req.params.actorId === 'number' ? req.params.actorId : null; + const actorSlug = typeof req.params.actorId === 'string' ? req.params.actorId : null; - if (actorId || actorSlug) { - const actors = await fetchActors({ - id: actorId, - slug: actorSlug, - }); + if (actorId || actorSlug) { + const actors = await fetchActors({ + id: actorId, + slug: actorSlug, + }); - if (actors.length > 0) { - res.send(actors[0]); - return; - } + if (actors.length > 0) { + res.send(actors[0]); + return; + } - res.status(404).send(); - return; - } + res.status(404).send(); + return; + } - const actors = await fetchActors(null, req.query.limit); + const actors = await fetchActors(null, req.query.limit); - res.send(actors); + res.send(actors); } module.exports = { - fetchActors: fetchActorsApi, + fetchActors: fetchActorsApi, }; diff --git a/src/web/networks.js b/src/web/networks.js index 909afd188..d41b38b00 100644 --- a/src/web/networks.js +++ b/src/web/networks.js @@ -3,24 +3,24 @@ const { fetchNetworks, fetchNetworksFromReleases } = require('../networks'); async function fetchNetworksApi(req, res) { - const networkId = typeof req.params.networkId === 'number' ? req.params.networkId : undefined; // null will literally include NULL results - const networkSlug = typeof req.params.networkId === 'string' ? req.params.networkId : undefined; + const networkId = typeof req.params.networkId === 'number' ? req.params.networkId : undefined; // null will literally include NULL results + const networkSlug = typeof req.params.networkId === 'string' ? req.params.networkId : undefined; - const networks = await fetchNetworks({ - id: networkId, - slug: networkSlug, - }); + const networks = await fetchNetworks({ + id: networkId, + slug: networkSlug, + }); - res.send(networks); + res.send(networks); } async function fetchNetworksFromReleasesApi(req, res) { - const networks = await fetchNetworksFromReleases(); + const networks = await fetchNetworksFromReleases(); - res.send(networks); + res.send(networks); } module.exports = { - fetchNetworks: fetchNetworksApi, - fetchNetworksFromReleases: fetchNetworksFromReleasesApi, + fetchNetworks: fetchNetworksApi, + fetchNetworksFromReleases: fetchNetworksFromReleasesApi, }; diff --git a/src/web/plugins/actors.js b/src/web/plugins/actors.js index 9899dce5e..fceea5f6b 100644 --- a/src/web/plugins/actors.js +++ b/src/web/plugins/actors.js @@ -5,7 +5,7 @@ const moment = require('moment'); const { cmToFeetInches, kgToLbs } = require('../../utils/convert'); const schemaExtender = makeExtendSchemaPlugin(_build => ({ - typeDefs: gql` + typeDefs: gql` enum Units { METRIC IMPERIAL @@ -17,32 +17,32 @@ const schemaExtender = makeExtendSchemaPlugin(_build => ({ weight(units:Units): String @requires(columns: ["weight"]) } `, - resolvers: { - Actor: { - age(parent, _args, _context, _info) { - if (!parent.birthdate) return null; + resolvers: { + Actor: { + age(parent, _args, _context, _info) { + if (!parent.birthdate) return null; - return moment().diff(parent.birthdate, 'years'); - }, - height(parent, args, _context, _info) { - if (!parent.height) return null; + return moment().diff(parent.birthdate, 'years'); + }, + height(parent, args, _context, _info) { + if (!parent.height) return null; - if (args.units === 'IMPERIAL') { - const { feet, inches } = cmToFeetInches(parent.height); - return `${feet}' ${inches}"`; - } + if (args.units === 'IMPERIAL') { + const { feet, inches } = cmToFeetInches(parent.height); + return `${feet}' ${inches}"`; + } - return parent.height.toString(); - }, - weight(parent, args, _context, _info) { - if (!parent.weight) return null; + return parent.height.toString(); + }, + weight(parent, args, _context, _info) { + if (!parent.weight) return null; - return args.units === 'IMPERIAL' - ? kgToLbs(parent.weight).toString() - : parent.weight.toString(); - }, - }, - }, + return args.units === 'IMPERIAL' + ? kgToLbs(parent.weight).toString() + : parent.weight.toString(); + }, + }, + }, })); module.exports = [schemaExtender]; diff --git a/src/web/plugins/plugins.js b/src/web/plugins/plugins.js index 5f320ca75..ec09f8590 100644 --- a/src/web/plugins/plugins.js +++ b/src/web/plugins/plugins.js @@ -5,7 +5,7 @@ const SitePlugins = require('./sites'); // const ReleasePlugins = require('./releases'); module.exports = { - ActorPlugins, - SitePlugins, - ReleasePlugins: [], + ActorPlugins, + SitePlugins, + ReleasePlugins: [], }; diff --git a/src/web/plugins/releases.js b/src/web/plugins/releases.js index ec0bf68a3..9ff05f63d 100644 --- a/src/web/plugins/releases.js +++ b/src/web/plugins/releases.js @@ -3,16 +3,16 @@ const { makeExtendSchemaPlugin, gql } = require('graphile-utils'); const schemaExtender = makeExtendSchemaPlugin(_build => ({ - typeDefs: gql` + typeDefs: gql` extend type Release {} `, - resolvers: { - Release: { - async foo(_parent, _args, _context, _info) { - // template - }, - }, - }, + resolvers: { + Release: { + async foo(_parent, _args, _context, _info) { + // template + }, + }, + }, })); module.exports = [schemaExtender]; diff --git a/src/web/plugins/sites.js b/src/web/plugins/sites.js index 5e25dfeeb..634e0aade 100644 --- a/src/web/plugins/sites.js +++ b/src/web/plugins/sites.js @@ -3,18 +3,18 @@ const { makeExtendSchemaPlugin, gql } = require('graphile-utils'); const schemaExtender = makeExtendSchemaPlugin(_build => ({ - typeDefs: gql` + typeDefs: gql` extend type Site { independent: Boolean @requires(columns: ["parameters"]) } `, - resolvers: { - Site: { - independent(parent, _args, _context, _info) { - return !!parent.parameters?.independent; - }, - }, - }, + resolvers: { + Site: { + independent(parent, _args, _context, _info) { + return !!parent.parameters?.independent; + }, + }, + }, })); module.exports = [schemaExtender]; diff --git a/src/web/releases.js b/src/web/releases.js index 2c24c1a35..68e1f70f2 100644 --- a/src/web/releases.js +++ b/src/web/releases.js @@ -3,15 +3,15 @@ const { fetchReleases, searchReleases } = require('../releases'); async function fetchReleasesApi(req, res) { - const query = req.query.query || req.query.q; + const query = req.query.query || req.query.q; - const releases = query - ? await searchReleases(query, req.query.limit) - : await fetchReleases(req.query.limit); + const releases = query + ? await searchReleases(query, req.query.limit) + : await fetchReleases(req.query.limit); - res.send(releases); + res.send(releases); } module.exports = { - fetchReleases: fetchReleasesApi, + fetchReleases: fetchReleasesApi, }; diff --git a/src/web/server.js b/src/web/server.js index 43eee6f9a..48024c62f 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -15,68 +15,68 @@ const logger = require('../logger')(__filename); const { ActorPlugins, SitePlugins, ReleasePlugins } = require('./plugins/plugins'); const { - fetchReleases, + fetchReleases, } = require('./releases'); function initServer() { - const app = express(); - const router = Router(); + const app = express(); + const router = Router(); - const connectionString = `postgres://${config.database.user}:${config.database.password}@${config.database.host}:5432/${config.database.database}`; + const connectionString = `postgres://${config.database.user}:${config.database.password}@${config.database.host}:5432/${config.database.database}`; - app.use(postgraphile( - connectionString, - 'public', - { - // watchPg: true, - dynamicJson: true, - graphiql: true, - enhanceGraphiql: true, - allowExplain: () => true, - simpleCollections: 'only', - graphileBuildOptions: { - pgOmitListSuffix: true, - connectionFilterRelations: true, - }, - appendPlugins: [ - PgSimplifyInflectorPlugin, - PgConnectionFilterPlugin, - PgOrderByRelatedPlugin, - ...ActorPlugins, - ...SitePlugins, - ...ReleasePlugins, - ], - }, - )); + app.use(postgraphile( + connectionString, + 'public', + { + // watchPg: true, + dynamicJson: true, + graphiql: true, + enhanceGraphiql: true, + allowExplain: () => true, + simpleCollections: 'only', + graphileBuildOptions: { + pgOmitListSuffix: true, + connectionFilterRelations: true, + }, + appendPlugins: [ + PgSimplifyInflectorPlugin, + PgConnectionFilterPlugin, + PgOrderByRelatedPlugin, + ...ActorPlugins, + ...SitePlugins, + ...ReleasePlugins, + ], + }, + )); - app.set('view engine', 'ejs'); + app.set('view engine', 'ejs'); - router.use('/media', express.static(config.media.path)); - router.use(express.static('public')); + router.use('/media', express.static(config.media.path)); + router.use(express.static('public')); - router.use('/img', (req, res) => { - res.status(404).send(); - }); + router.use('/img', (req, res) => { + res.status(404).send(); + }); - router.use(bodyParser.json({ strict: false })); + router.use(bodyParser.json({ strict: false })); - router.get('/api/releases', fetchReleases); + router.get('/api/releases', fetchReleases); - router.get('*', (req, res) => { - res.render(path.join(__dirname, '../../assets/index.ejs'), { - env: JSON.stringify({ - sfw: !!req.headers.sfw || Object.prototype.hasOwnProperty.call(req.query, 'sfw'), - }), - }); - }); + router.get('*', (req, res) => { + res.render(path.join(__dirname, '../../assets/index.ejs'), { + env: JSON.stringify({ + sfw: !!req.headers.sfw || Object.prototype.hasOwnProperty.call(req.query, 'sfw'), + }), + }); + }); - app.use(router); + app.use(router); - const server = app.listen(config.web.port, config.web.host, () => { - const { address, port } = server.address(); + const server = app.listen(config.web.port, config.web.host, () => { + const { address, port } = server.address(); - logger.info(`Web server listening on ${address}:${port}`); - }); + logger.info(`Web server listening on ${address}:${port}`); + }); } module.exports = initServer; diff --git a/src/web/sites.js b/src/web/sites.js index 9cbd63be4..f5aebc8ad 100644 --- a/src/web/sites.js +++ b/src/web/sites.js @@ -3,24 +3,24 @@ const { fetchSites, fetchSitesFromReleases } = require('../sites'); async function fetchSitesApi(req, res) { - const siteId = typeof req.params.siteId === 'number' ? req.params.siteId : undefined; - const siteSlug = typeof req.params.siteId === 'string' ? req.params.siteId : undefined; + const siteId = typeof req.params.siteId === 'number' ? req.params.siteId : undefined; + const siteSlug = typeof req.params.siteId === 'string' ? req.params.siteId : undefined; - const sites = await fetchSites({ - id: siteId, - slug: siteSlug, - }); + const sites = await fetchSites({ + id: siteId, + slug: siteSlug, + }); - res.send(sites); + res.send(sites); } async function fetchSitesFromReleasesApi(req, res) { - const sites = await fetchSitesFromReleases(); + const sites = await fetchSitesFromReleases(); - res.send(sites); + res.send(sites); } module.exports = { - fetchSites: fetchSitesApi, - fetchSitesFromReleases: fetchSitesFromReleasesApi, + fetchSites: fetchSitesApi, + fetchSitesFromReleases: fetchSitesFromReleasesApi, }; diff --git a/src/web/tags.js b/src/web/tags.js index 315bc9ce5..b935b5fac 100644 --- a/src/web/tags.js +++ b/src/web/tags.js @@ -3,38 +3,38 @@ const { fetchTags } = require('../tags'); async function fetchTagsApi(req, res) { - const tagId = typeof req.params.tagId === 'number' ? req.params.tagId : undefined; // null will literally include NULL results - const tagSlug = typeof req.params.tagId === 'string' ? req.params.tagId : undefined; + const tagId = typeof req.params.tagId === 'number' ? req.params.tagId : undefined; // null will literally include NULL results + const tagSlug = typeof req.params.tagId === 'string' ? req.params.tagId : undefined; - if (tagId || tagSlug) { - const tags = await fetchTags({ - id: tagId, - slug: tagSlug, - }, null, req.query.limit); + if (tagId || tagSlug) { + const tags = await fetchTags({ + id: tagId, + slug: tagSlug, + }, null, req.query.limit); - if (tags.length > 0) { - res.send(tags[0]); - return; - } + if (tags.length > 0) { + res.send(tags[0]); + return; + } - res.status(404).send(); - return; - } + res.status(404).send(); + return; + } - const query = {}; - const groupsQuery = {}; + const query = {}; + const groupsQuery = {}; - if (req.query.priority) query.priority = req.query.priority.split(','); - if (req.query.slug) query.slug = req.query.slug.split(','); - if (req.query.group) { - groupsQuery.slug = req.query.group.split(','); - } + if (req.query.priority) query.priority = req.query.priority.split(','); + if (req.query.slug) query.slug = req.query.slug.split(','); + if (req.query.group) { + groupsQuery.slug = req.query.group.split(','); + } - const tags = await fetchTags(query, groupsQuery, req.query.limit); + const tags = await fetchTags(query, groupsQuery, req.query.limit); - res.send(tags); + res.send(tags); } module.exports = { - fetchTags: fetchTagsApi, + fetchTags: fetchTagsApi, }; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 11d4ac0f2..52e2650ae 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -4,70 +4,70 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import autoprefixer from 'autoprefixer'; export default { - entry: './assets/js/main.js', - output: { - filename: 'bundle.js', - path: path.join(__dirname, 'public/js'), - }, - module: { - rules: [ - { - test: /\.vue$/, - include: [ - path.resolve(__dirname, 'assets'), - ], - loader: 'vue-loader', - options: { - preserveWhitespace: false, - }, - }, - { - test: /\.js$/, - exclude: /node_modules/, - use: [ - { - loader: 'babel-loader', - options: { - babelrc: false, - plugins: [ - '@babel/plugin-proposal-object-rest-spread', - ], - }, - }, - 'eslint-loader', - ], - }, - { - test: /\.scss$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader?sourceMap', - { - loader: 'postcss-loader', - options: { - plugins: [autoprefixer], - sourceMap: true, - }, - }, - 'sass-loader?sourceMap', - ], - }, - { - test: /\.svg/, - use: 'raw-loader', - }, - ], - }, - plugins: [ - new VueLoaderPlugin(), - new MiniCssExtractPlugin({ - filename: '../css/style.css', - }), - ], - resolve: { - alias: { - theme: path.join(__dirname, 'assets/css/_theme.scss'), - config: path.join(__dirname, `assets/js/config/${process.env.NODE_ENV || 'default'}.js`), - }, - }, + entry: './assets/js/main.js', + output: { + filename: 'bundle.js', + path: path.join(__dirname, 'public/js'), + }, + module: { + rules: [ + { + test: /\.vue$/, + include: [ + path.resolve(__dirname, 'assets'), + ], + loader: 'vue-loader', + options: { + preserveWhitespace: false, + }, + }, + { + test: /\.js$/, + exclude: /node_modules/, + use: [ + { + loader: 'babel-loader', + options: { + babelrc: false, + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + ], + }, + }, + 'eslint-loader', + ], + }, + { + test: /\.scss$/, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader?sourceMap', + { + loader: 'postcss-loader', + options: { + plugins: [autoprefixer], + sourceMap: true, + }, + }, + 'sass-loader?sourceMap', + ], + }, + { + test: /\.svg/, + use: 'raw-loader', + }, + ], + }, + plugins: [ + new VueLoaderPlugin(), + new MiniCssExtractPlugin({ + filename: '../css/style.css', + }), + ], + resolve: { + alias: { + theme: path.join(__dirname, 'assets/css/_theme.scss'), + config: path.join(__dirname, `assets/js/config/${process.env.NODE_ENV || 'default'}.js`), + }, + }, };