Added Model Media API layout, renamed AsiaM.

This commit is contained in:
DebaucheryLibrarian
2026-02-06 04:55:40 +01:00
parent e91ff659e9
commit 5a210451e0
12 changed files with 178 additions and 24 deletions

View File

@@ -2,6 +2,71 @@
const unprint = require('unprint');
const slugify = require('../utils/slugify');
function scrapeSceneApi(scene, channel, parameters) {
const release = {};
release.entryId = scene.id;
release.shootId = scene.serial_number;
release.url = `${channel.origin}${parameters.basePath || ''}/videos/${release.shootId}`;
release.title = scene.title;
release.altTitles = [scene.title_cn].filter(Boolean);
release.description = scene.description;
release.altDescriptions = [scene.description_cn].filter(Boolean);
release.date = new Date(scene.published_at);
release.duration = scene.duration;
release.actors = scene.models?.map((model) => ({
name: model.name,
alias: [model.name_cn].filter(Boolean),
gender: model.gender,
entryId: model.id,
avatar: Array.from(new Set([
model.avatar,
model.avatar?.replace('_compressed', ''), // this is often a wider image, not just uncompressed
])).filter(Boolean),
}));
release.tags = scene.tags?.map((tag) => tag.name);
release.poster = scene.cover;
release.trailer = scene.preview_video;
return release;
}
async function fetchLatestApi(channel, page, { parameters }) {
const res = await unprint.get(`${parameters.api}/videos?page=${page}&pageSize=12&sort=published_at`);
if (res.ok && res.data?.status) {
return res.data.data.list.map((scene) => scrapeSceneApi(scene, channel, parameters));
}
return res.status;
}
async function fetchSceneApi(url, channel, _baseRelease, { parameters }) {
// shallow data missing actors and tags
const shootId = new URL(url).pathname.match(/\/videos\/([\w-]+)/)?.[1];
if (!shootId) {
return null;
}
const res = await unprint.get(`${parameters.api}/videos/${shootId}`);
if (res.ok) {
return scrapeSceneApi(res.data.data, channel, parameters);
}
return res.status;
}
function scrapeAll(scenes) {
return scenes.map(({ query }) => {
const release = {};
@@ -10,7 +75,7 @@ function scrapeAll(scenes) {
const url = query.url(null);
if (url) {
if (url && !url.includes('/plans')) {
const { origin, pathname, searchParams } = new URL(url);
release.url = `${origin}${pathname}`;
@@ -63,6 +128,78 @@ function scrapeAll(scenes) {
});
}
function scrapeProfileApi(model, channel, parameters) {
const profile = {};
profile.entryId = model.id;
profile.url = `${channel.origin}${parameters.basePath || ''}/models/${model.id}`;
profile.description = model.description || null;
profile.gender = model.gender;
profile.alias = [model.name_cn].filter(Boolean);
if (!model.birth_day?.includes('0001')) {
profile.dateOfBirth = unprint.extractDate(model.birth_day, 'YYYY-MM-DD');
}
profile.birthPlace = model.birth_place || null;
profile.height = model.height_cm || null;
profile.weight = model.weight_kg || null;
profile.bust = model.measurements_chest;
profile.waist = model.measurements_waist;
profile.hip = model.measurements_hips;
profile.avatar = Array.from(new Set([
model.avatar,
model.avatar?.replace('_compressed', ''), // this is often a wider image, not just uncompressed
])).filter(Boolean);
profile.socials = model.socialmedia;
profile.scenes = model.videos.map((scene) => scrapeSceneApi(scene, channel, parameters));
return profile;
}
async function getModelId(actor, parameters) {
if (actor.url) {
const modelId = new URL(actor.url).pathname.match(/\/models\/\d+/)?.[1];
if (modelId) {
return Number(modelId);
}
}
const res = await unprint.get(`${parameters.api}/search?keyword=${slugify(actor.name, '+')}`);
if (res.ok) {
const model = res.data.data?.models?.find((modelResult) => slugify(modelResult.name) === actor.slug);
if (model) {
return model.id;
}
}
return null;
}
async function fetchProfileApi(actor, { entity, parameters }) {
const modelId = await getModelId(actor, parameters);
if (modelId) {
const res = await unprint.get(`${parameters.api}/models/${modelId}`);
if (res.ok && res.data.data) {
return scrapeProfileApi(res.data.data, entity, parameters);
}
}
return null;
}
function scrapeProfile({ query }) {
const profile = {};
const avatar = query.img('div[class*="prof-pic"] > img');
@@ -88,7 +225,7 @@ function scrapeProfile({ query }) {
return profile;
}
async function getCookie(channel) {
async function getCookie(channel, _parameters) {
const tokenRes = await unprint.get(channel.url);
if (!tokenRes.ok) {
@@ -116,8 +253,8 @@ async function getCookie(channel) {
return cookie;
}
async function fetchLatest(channel, page) {
const cookie = await getCookie(channel);
async function fetchLatest(channel, page, context) {
const cookie = await getCookie(channel, context.parameters);
const res = await unprint.get(`${channel.url}/videos?sort=published_at&page=${page}`, {
selectAll: '.row a[video-id]',
@@ -136,7 +273,7 @@ async function fetchLatest(channel, page) {
// deep pages are paywalled
async function searchProfile(actor, context, cookie) {
const searchRes = await unprint.get(`${context.channel.url}/livesearch?keyword=${actor.name}`, {
const searchRes = await unprint.get(`${context.channel.url}${context.parameters.searchPath || '/livesearch'}?${context.parameters.searchParameter || 'keyword'}=${actor.name}`, {
headers: {
cookie,
},
@@ -150,7 +287,7 @@ async function searchProfile(actor, context, cookie) {
}
async function fetchProfile(actor, context) {
const cookie = await getCookie(context.entity);
const cookie = await getCookie(context.entity, context.parameters);
const actorUrl = actor.url || await searchProfile(actor, context, cookie);
if (!actorUrl) {
@@ -173,4 +310,9 @@ async function fetchProfile(actor, context) {
module.exports = {
fetchLatest,
fetchProfile,
api: {
fetchLatest: fetchLatestApi,
fetchScene: fetchSceneApi,
fetchProfile: fetchProfileApi,
},
};