diff --git a/assets/components/releases/clips.vue b/assets/components/releases/clips.vue index a17ce45c..6e683560 100644 --- a/assets/components/releases/clips.vue +++ b/assets/components/releases/clips.vue @@ -86,12 +86,14 @@ export default { } .clip-duration { + background: var(--darken); color: var(--text-light); display: block; position: absolute; - bottom: 0; - left: 0; - padding: .5rem .5rem .75rem 1rem; + top: 0; + right: 0; + padding: .25rem .5rem; + font-size: .9rem; font-weight: bold; text-shadow: 0 0 2px var(--darken-strong); } diff --git a/assets/components/releases/details.vue b/assets/components/releases/details.vue index 5f2a0a11..561e481c 100644 --- a/assets/components/releases/details.vue +++ b/assets/components/releases/details.vue @@ -3,7 +3,6 @@
- {{ formatDate(release.date, 'MMM D, YYYY', release.datePrecision) }} - {{ formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) }} + {{ release.date ? formatDate(release.date, 'MMM D, YYYY', release.datePrecision) : 'Date N/A' }} + {{ release.date ? formatDate(release.date, 'MMMM D, YYYY', release.datePrecision) : 'Date unknown' }} { const release = {}; release.url = query.url('a', 'href', { origin: channel.url }); - release.entryId = new URL(release.url).pathname.match(/\/Collection\/(\d+)/)[1]; + // release.entryId = new URL(release.url).pathname.match(/\/Collection\/(\d+)/)[1]; can't be matched with upcoming scenes release.shootId = query.cnt('a span:nth-of-type(1)').match(/^\d+/)?.[0]; - release.date = query.date('a span:nth-of-type(2)', 'YYYY-MM-DD'); + release.entryId = release.shootId; + release.date = query.date('a span:nth-of-type(2)', 'YYYY-MM-DD'); release.actors = (query.q('a img', 'alt') || query.cnt('a span:nth-of-type(1)'))?.match(/[a-zA-Z]+(\s[A-Za-z]+)*/g); release.poster = release.shootId @@ -25,13 +27,145 @@ function scrapeAll(scenes, channel) { }); } -function scrapeScene({ query, html }, url, channel) { +function scrapeUpcoming(scenes, channel) { + return scenes.map(({ query }) => { + const release = {}; + + const title = query.cnt('span'); + + release.entryId = title.match(/^\d+/)[0]; + release.actors = title.slice(0, title.indexOf('-')).match(/[a-zA-Z]+(\s[a-zA-Z]+)*/g); + + const date = moment.utc(title.match(/\w+ \d+\w+$/)[0], 'MMM Do'); + + if (date.isBefore()) { + // date is next year + release.date = date.add(1, 'year').toDate(); + } else { + release.date = date.toDate(); + } + + release.poster = [ + `https://inthecrack.com/assets/images/posters/collections/${release.entryId}.jpg`, + query.img('img', 'src', { origin: channel.url }), + ]; + + return release; + }); +} + +function scrapeProfileScenes(items, actorName, channel) { + return items.map(({ query }) => { + const release = {}; + + if (slugify(query.cnt()) === 'no-other-collections') { + return null; + } + + const details = query.cnts('figure p').reduce((acc, info) => { + const [key, value] = info.split(':'); + + return { + ...acc, + [slugify(key, '_')]: value?.trim(), + }; + }, {}); + + release.url = query.url('a', 'href', { origin: channel.url }); + + release.shootId = details.collection.match(/\d+/)[0]; + release.entryId = release.shootId; + + release.date = qu.parseDate(details.release_date, 'YYYY-MM-DD'); + release.actors = [actorName]; + + /* rely on clip length + const durationString = Object.keys(details).find(info => /\d+_min_video/.test(info)); + release.duration = durationString && Number(durationString.match(/^\d+/)?.[0]) * 60; + */ + + release.productionLocation = details.shoot_location; + + release.poster = [ + `https://inthecrack.com/assets/images/posters/collections/${release.entryId}.jpg`, + query.img('img', 'src', { origin: channel.url }), + ]; + + return release; + }).filter(Boolean); +} + +function scrapeProfile({ query }, actorName, actorAvatar, channel, releasesFromScene) { + const profile = {}; + + const bio = query.cnts(releasesFromScene ? 'ul li' : 'div.modelInfo li').reduce((acc, info) => { + const [key, value] = info.split(':'); + + return { + ...acc, + [slugify(key, '_')]: value.trim(), + }; + }, {}); + + profile.name = actorName || bio.name; + profile.gender = 'female'; + profile.birthPlace = bio.nationality; + + if (bio.height) profile.height = feetInchesToCm(bio.height); + if (bio.weight) profile.weight = lbsToKg(bio.weight); + + profile.releases = releasesFromScene?.[profile.name] || scrapeProfileScenes(qu.initAll(query.all('.Models li')), actorName, channel); + + // avatar is the poster of a scene, find scene and use its high quality poster instead + const avatarRelease = profile.releases.find(release => new URL(release.poster[1]).pathname === new URL(actorAvatar).pathname); + profile.avatar = avatarRelease?.poster[0]; + + return profile; +} + +async function fetchSceneActors(entryId, _release, channel) { + const url = `https://inthecrack.com/Collection/Biography/${entryId}`; + const res = await qu.get(url); + + if (res.ok) { + const actorTabs = qu.initAll(res.item.query.all('#ModelTabs li')).map(({ query }) => ({ + name: query.cnt('a'), + id: query.q('a', 'data-model'), + })); + + const actorReleasesByActorName = actorTabs.reduce((acc, { name, id }) => { + const releaseEls = qu.initAll(res.item.query.all(`#Model-${id} li`)); + const releases = scrapeProfileScenes(releaseEls, name, channel); + + return { + ...acc, + [name]: releases, + }; + }, {}); + + const actors = qu.initAll(res.item.query.all('.modelInfo > li')).map((item) => { + const avatar = item.query.img('img', 'src', { origin: channel.url }); + const profile = scrapeProfile(item, null, avatar, channel, actorReleasesByActorName); + + return profile; + }); + + return actors; + } + + return null; +} + +async function scrapeScene({ query, html }, url, channel) { const release = {}; - release.entryId = new URL(url).pathname.match(/\/Collection\/(\d+)/)[1]; - release.shootId = query.cnt('h2 span').match(/^\d+/)?.[0]; + const entryId = new URL(url).pathname.match(/\/Collection\/(\d+)/)[1]; - release.actors = query.cnt('h2 span')?.match(/[a-zA-Z]+(\s[A-Za-z]+)*/g); + release.shootId = query.cnt('h2 span').match(/^\d+/)?.[0]; + release.entryId = release.shootId; // site entry ID can't be matched with upcoming scenes + + const actors = await fetchSceneActors(entryId, release, channel); + release.actors = actors || query.cnt('h2 span')?.match(/[a-zA-Z]+(\s[A-Za-z]+)*/g); release.description = query.cnt('p#CollectionDescription'); release.productionLocation = query.cnt('.modelCollectionHeader p')?.match(/Shoot Location: (.*)/)?.[1]; @@ -67,22 +201,6 @@ function scrapeScene({ query, html }, url, channel) { return release; } -function scrapeProfile({ query, el }, actorName, entity, include) { - const profile = {}; - - profile.description = query.cnt('.bio-text'); - profile.birthPlace = query.cnt('.birth-place span'); - - profile.avatar = query.img('.actor-photo img'); - - if (include.releases) { - return scrapeAll(qu.initAll(el, '.scene')); - } - - console.log(profile); - return profile; -} - async function fetchLatest(channel, page = 1) { const year = moment().subtract(page - 1, ' year').year(); @@ -96,6 +214,16 @@ async function fetchLatest(channel, page = 1) { return res.status; } +async function fetchUpcoming(channel) { + const res = await qu.getAll(channel.url, '#ComingSoon li'); + + if (res.ok) { + return scrapeUpcoming(res.items, channel); + } + + return res.status; +} + async function fetchScene(url, channel) { const res = await qu.get(url); @@ -106,12 +234,27 @@ async function fetchScene(url, channel) { return res.status; } -async function fetchProfile({ name: actorName }, entity, include) { - const url = `${entity.url}/actors/${slugify(actorName, '_')}`; - const res = await qu.get(url); +async function fetchProfile({ name: actorName }, channel, _include) { + const firstLetter = actorName.charAt(0).toUpperCase(); + const url = `${channel.url}/Collections/Name/${firstLetter}`; + const res = await qu.getAll(url, '.collectionGridLayout li'); if (res.ok) { - return scrapeProfile(res.item, actorName, entity, include); + const actorItem = res.items.find(({ query }) => slugify(query.cnt('span')) === slugify(actorName)); + + if (actorItem) { + const actorUrl = actorItem.query.url('a', 'href', { origin: channel.url }); + const actorAvatar = actorItem.query.img('img', 'src', { origin: channel.url }); + const actorRes = await qu.get(actorUrl); + + if (actorRes.ok) { + return scrapeProfile(actorRes.item, actorName, actorAvatar, channel); + } + + return actorRes.status; + } + + return null; } return res.status; @@ -119,6 +262,7 @@ async function fetchProfile({ name: actorName }, entity, include) { module.exports = { fetchLatest, + fetchUpcoming, fetchScene, - // fetchProfile, + fetchProfile, }; diff --git a/src/scrapers/scrapers.js b/src/scrapers/scrapers.js index f67add47..a1a0ec5c 100644 --- a/src/scrapers/scrapers.js +++ b/src/scrapers/scrapers.js @@ -197,6 +197,7 @@ module.exports = { iconmale, interracialpass: hush, interracialpovs: hush, + inthecrack, jamesdeen: fullpornnetwork, julesjordan, kellymadison, diff --git a/src/store-releases.js b/src/store-releases.js index 6562422a..b977a25c 100644 --- a/src/store-releases.js +++ b/src/store-releases.js @@ -263,7 +263,7 @@ async function storeClips(releases) { clip: clip.clip, })); - const storedClips = await bulkInsert('clips', curatedClipEntries); + const storedClips = await bulkInsert('clips', curatedClipEntries, ['release_id', 'clip']); const clipIdsByReleaseIdAndClip = storedClips.reduce((acc, clip) => ({ ...acc, [clip.release_id]: {