diff --git a/seeds/01_networks.js b/seeds/01_networks.js index a5bd4a18..211b7977 100755 --- a/seeds/01_networks.js +++ b/seeds/01_networks.js @@ -783,6 +783,11 @@ const networks = [ parentSession: false, }, }, + { + slug: 'rsghmedia', + name: 'RSGH Media', + hasLogo: false, + }, { slug: 'score', name: 'SCORE', diff --git a/seeds/02_sites.js b/seeds/02_sites.js index 1fd217fc..fd17bb8c 100755 --- a/seeds/02_sites.js +++ b/seeds/02_sites.js @@ -11114,6 +11114,59 @@ const sites = [ slug: 'rickysroom', url: 'https://rickysroom.com', }, + // RSGH MEDIA + { + name: 'Family Therapy XXX', + slug: 'familytherapyxxx', + url: 'https://familytherapyxxx.com', + tags: ['family'], + parent: 'rsghmedia', + independent: true, + }, + { + name: 'Mom Comes First', + slug: 'momcomesfirst', + url: 'https://momcomesfirst.com', + tags: ['family'], + parent: 'rsghmedia', + independent: true, + }, + { + name: 'Perfect Girlfriend', + slug: 'perfectgirlfriend', + url: 'https://perfectgirlfriend.com', + parent: 'rsghmedia', + independent: true, + }, + { + name: 'Anal Therapy XXX', + slug: 'analtherapyxxx', + url: 'https://analtherapyxxx.com', + tags: ['anal'], + parent: 'rsghmedia', + independent: true, + }, + { + name: 'Cock Ninja Studios', + slug: 'cockninjastudios', + url: 'https://cockninjastudios.com', + parent: 'rsghmedia', + independent: true, + }, + { + name: 'Wife Loves Black', + slug: 'wifelovesblack', + url: 'https://wifelovesblack.com', + parent: 'rsghmedia', + independent: true, + }, + { + name: 'Teen Loves Black', + slug: 'teenlovesblack', + url: 'https://teenlovesblack.com', + parent: 'rsghmedia', + independent: true, + }, // SCORE { name: '18 Eighteen', diff --git a/src/scrapers/releases.js b/src/scrapers/releases.js index 04c6b727..e898b358 100644 --- a/src/scrapers/releases.js +++ b/src/scrapers/releases.js @@ -62,6 +62,7 @@ const pornworld = require('./pornworld'); const privateNetwork = require('./private'); // reserved keyword const radical = require('./radical'); const rickysroom = require('./rickysroom'); +const rsghmedia = require('./rsghmedia'); const sexlikereal = require('./sexlikereal'); const score = require('./score'); const sicflics = require('./sicflics'); @@ -172,6 +173,7 @@ module.exports = { private: privateNetwork, radical, rickysroom, + rsghmedia, score, sexlikereal, sicflics, diff --git a/src/scrapers/rsghmedia.js b/src/scrapers/rsghmedia.js new file mode 100644 index 00000000..c91a236f --- /dev/null +++ b/src/scrapers/rsghmedia.js @@ -0,0 +1,158 @@ +'use strict'; + +const unprint = require('unprint'); +const escapeRegexp = require('escape-string-regexp'); + +const slugify = require('../utils/slugify'); +const capitalize = require('../utils/capitalize'); + +function getDate(query, release) { + const date = query.date('.published', 'MMMM D, YYYY') || query.date('meta[itemprop="uploadDate"]', 'YYYY-MM-DD', { attribute: 'content' }); + + if (date) { + return { + date, + datePrecision: 'day', + }; + } + + const poster = Array.isArray(release.poster) + ? release.poster[0] + : release.poster; + + if (poster) { + const [, year, month] = new URL(poster).pathname.match(/\/uploads\/(\d{4})\/(\d{2})/) || []; + + if (year && month) { + return { + date: new Date(Date.UTC(year, month - 1, 1)), + datePrecision: 'month', + }; + } + } + + return {}; +} + +function scrapeAll(scenes) { + return scenes.map(({ query, element }) => { + const release = {}; + + release.url = query.url('.entry-title a, a.entry-featured-image-url'); + release.entryId = new URL(release.url).pathname.match(/\/([\w-]+)/)[1]; + + const postId = unprint.query.number(element, null, { attribute: 'id' }); + + if (postId) { + release.attributes = { postId }; + } + + release.title = query.content('.entry-title a'); + // description is cut off + + release.tags = query.contents('.post-meta a[href*="/category"], .post-meta a[href*="/tag"]'); // tag only seen on scene page, but just in case + release.poster = Array.from(new Set(query.sourceSet('.entry-featured-image-url img'))); + + const { date, datePrecision } = getDate(query, release); + + release.date = date; + release.datePrecision = datePrecision; + + return release; + }).toSorted((sceneA, sceneB) => { + // HTML is laid out in columns that does not align with chronology + if (sceneA.attributes?.postId && sceneB.attributes?.postId) { + return sceneB.attributes.postId - sceneA.attributes.postId; + } + + if (sceneA.date && sceneB.date) { + return sceneB.date - sceneA.date; + } + + return 0; + }); +} + +async function fetchLatest(channel, page = 1) { + const url = `${channel.origin}/page/${page}`; + const res = await unprint.get(url, { selectAll: '.et_pb_blog_grid .et_pb_post' }); + + if (res.ok) { + return scrapeAll(res.context, channel); + } + + return res.status; +} + +function scrapeScene({ query }, { url }) { + const release = {}; + + release.entryId = new URL(url).pathname.match(/\/([\w-]+)/)[1]; + + const postId = query.number('article[id*="post"]', { attribute: 'id' }); + + if (postId) { + release.attributes = { postId }; + } + + release.title = query.content('.entry-title'); + release.description = query.content('.entry-content p'); + + release.actors = query.all('.st-post-tags a[href*="/tag"]').map((actorEl) => ({ + name: unprint.query.content(actorEl), + url: unprint.query.url(actorEl, null), + })); + + release.poster = query.img('meta[property="og:image"], meta[property="twitter:image"]', { attribute: 'content' }) + || query.sourceSet('.et_post_meta_wrapper img'); + + release.trailer = query.video('.entry-content source'); + + const { date, datePrecision } = getDate(query, release); + + release.date = date; + release.datePrecision = datePrecision; + + const tags = query.all('.post-meta a[href*="/category"], .post-meta a[href*="/tag"]'); + const starring = query.content('//div[contains(@class, "entry-content")]/p[contains(text(), "Starring")]') + || query.content('//div[contains(@class, "entry-content")]/p[contains(text(), "***")]') + || release.title?.split(/\s[-–]\s/).at(-1); + + if (tags.length > 0) { + release.tags = tags.map((tagEl) => unprint.query.content(tagEl)); + } else { + release.tags = query.content('article[id*="post"]', { attribute: 'class' }) + ?.match(/(category|tag)-[\w-]+/g) + ?.map((tag) => slugify(tag.replace(/(category|tag)-/, ''), ' ')) + || []; + } + + if (release.actors.length === 0 && release.tags.length > 0 && starring) { + // actors are always added as tags, sometimes they're clearly divided, sometimes they're mixed in with other tags + // the tail end of the description or title contains a *starring* section against which the tags can be matched + release.actors = release.tags + .map((tag) => { + if (tag === 'alex adams') { + // household male performer that is not usually mentioned in the starring section + return capitalize(tag); + } + + // use description text instead of tag name for capitalization + const match = starring.match(new RegExp(`\\b${escapeRegexp(tag)}\\b`, 'i'))?.[0]; + + if (match) { + return match; + } + + return null; + }) + .filter(Boolean); + } + + return release; +} + +module.exports = { + fetchLatest, + scrapeScene, +}; diff --git a/src/scrapers/template.js b/src/scrapers/template.js index 217bf0f8..3b8ef7fa 100755 --- a/src/scrapers/template.js +++ b/src/scrapers/template.js @@ -31,7 +31,7 @@ function scrapeAll(scenes) { } async function fetchLatest(channel, page = 1) { - const url = `${channel.url}/${page}`; + const url = `${channel.origin}/${page}`; const res = await unprint.get(url, { selectAll: '.scene' }); if (res.ok) {