forked from DebaucheryLibrarian/traxxx
180 lines
4.0 KiB
JavaScript
180 lines
4.0 KiB
JavaScript
'use strict';
|
|
|
|
const unprint = require('unprint');
|
|
|
|
const slugify = require('../utils/slugify');
|
|
|
|
function curateTitle(title) {
|
|
return title?.replace(/^tokyo hot\s*/i, '');
|
|
}
|
|
|
|
function scrapeAll(scenes, channel) {
|
|
return scenes.map(({ query }) => {
|
|
const release = {};
|
|
|
|
const pathname = query.url();
|
|
|
|
release.url = unprint.prefixUrl(pathname, channel.url);
|
|
release.entryId = pathname.match(/product\/(\w+)/)?.[1];
|
|
release.shootId = query.attribute('img', 'title');
|
|
|
|
release.title = curateTitle(query.content('.title'));
|
|
release.description = query.content('.text');
|
|
|
|
const poster = query.img();
|
|
|
|
if (poster) {
|
|
release.poster = [
|
|
poster.replace('220x124', '820x462'),
|
|
poster,
|
|
];
|
|
}
|
|
|
|
return release;
|
|
});
|
|
}
|
|
|
|
function scrapeScene({ query }, url, channel) {
|
|
const release = {};
|
|
|
|
release.entryId = new URL(url).pathname.match(/product\/(\w+)/)?.[1];
|
|
release.shootId = query.content('//dt[contains(text(), "Product ID")]/following-sibling::dd[1]');
|
|
|
|
release.title = curateTitle(query.content('.contents h2'));
|
|
release.description = query.content('.contents .sentence');
|
|
release.date = query.date('//dt[contains(text(), "Release Date")]/following-sibling::dd[1]', 'YYYY/MM/DD');
|
|
release.duration = query.duration('//dt[contains(text(), "Duration")]/following-sibling::dd[1]');
|
|
|
|
release.actors = query.all('.info a[href*="/cast"]').map((el) => ({
|
|
name: unprint.query.content(el),
|
|
url: unprint.query.url(el, null, { origin: channel.url }),
|
|
}));
|
|
|
|
release.tags = query.contents('.info a[href*="type=play"]');
|
|
|
|
const poster = query.poster('.movie video') || query.img('.movie img');
|
|
|
|
if (poster) {
|
|
release.poster = [
|
|
poster,
|
|
poster.replace('820x462', '220x124'),
|
|
];
|
|
}
|
|
|
|
release.trailer = query.video('.movie source');
|
|
|
|
release.photos = query.imgs('.scap a', { attribute: 'href' }).map((img) => [
|
|
img,
|
|
img?.replace('640x480_wlimited', '150x150_default'),
|
|
]);
|
|
|
|
release.caps = query.imgs('.vcap a', { attribute: 'href' }).map((img) => [
|
|
img,
|
|
img?.replace('640x480_wlimited', '120x120_default'),
|
|
]);
|
|
|
|
return release;
|
|
}
|
|
|
|
// measurements are specified as a range in centimeters 85 ~ 89cm
|
|
function getMeasurement(string, inches = false) {
|
|
if (!string) {
|
|
return null;
|
|
}
|
|
|
|
const value = Array.from(string.matchAll(/(\d+(?:\.\d+)?)\s*cm/g)).at(-1)?.[1];
|
|
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
if (inches) {
|
|
return Math.round(Number(value) * 0.393701);
|
|
}
|
|
|
|
return Number(value);
|
|
}
|
|
|
|
function scrapeProfile({ query }) {
|
|
const profile = {};
|
|
|
|
const keys = query.contents('.info dt');
|
|
const values = query.contents('.info dd');
|
|
|
|
const bio = Object.fromEntries(keys.map((key, index) => [slugify(key, '_'), values[index]]));
|
|
|
|
profile.birthPlace = bio.home_town;
|
|
|
|
profile.height = getMeasurement(bio.height);
|
|
|
|
profile.cup = bio.cup_size?.replace('cup', '').trim();
|
|
profile.bust = getMeasurement(bio.bust_size, true);
|
|
profile.waist = getMeasurement(bio.waist_size, true);
|
|
profile.hip = getMeasurement(bio.hip_size || bio.hip, true);
|
|
|
|
profile.hairStyle = bio.hair_style;
|
|
profile.shoeSize = getMeasurement(bio.shoes_size);
|
|
|
|
profile.bloodType = bio.blood_type?.replace('type', '').trim();
|
|
|
|
profile.avatar = query.img('#profile img');
|
|
|
|
return profile;
|
|
}
|
|
|
|
async function fetchLatest(channel, page) {
|
|
const url = `${channel.url}/product/?vendor=Tokyo-Hot&page=${page}&order=published_at`;
|
|
|
|
const res = await unprint.get(url, {
|
|
selectAll: '#main .list .detail',
|
|
agent: {
|
|
rejectUnauthorized: false,
|
|
},
|
|
});
|
|
|
|
if (res.ok) {
|
|
return scrapeAll(res.context, channel);
|
|
}
|
|
|
|
return res.status;
|
|
}
|
|
|
|
async function fetchScene(url, channel) {
|
|
const res = await unprint.get(url, {
|
|
agent: {
|
|
rejectUnauthorized: false,
|
|
},
|
|
});
|
|
|
|
if (res.ok) {
|
|
return scrapeScene(res.context, url, channel);
|
|
}
|
|
|
|
return res.status;
|
|
}
|
|
|
|
async function fetchProfile(actor, context) {
|
|
if (!actor.url) {
|
|
// search is cumbersome
|
|
return null;
|
|
}
|
|
|
|
const res = await unprint.get(actor.url, {
|
|
agent: {
|
|
rejectUnauthorized: false,
|
|
},
|
|
});
|
|
|
|
if (res.ok) {
|
|
return scrapeProfile(res.context, context);
|
|
}
|
|
|
|
return res.status;
|
|
}
|
|
|
|
module.exports = {
|
|
fetchLatest,
|
|
fetchScene,
|
|
fetchProfile,
|
|
};
|