Added A-Cam (Vilde).

This commit is contained in:
DebaucheryLibrarian
2026-06-02 06:26:24 +02:00
parent 3532911292
commit 8f63f6283b
5 changed files with 336 additions and 1 deletions

228
src/scrapers/acam.js Executable file
View File

@@ -0,0 +1,228 @@
'use strict';
const unprint = require('unprint');
const slugify = require('../utils/slugify');
function extractEntryId(poster) {
try {
return slugify(new URL(poster).pathname.match(/\/images\/(.*?)\.jpg/)?.[1]?.replace(/smak.*/i, ''), '');
} catch (error) {
return null;
}
}
function extractTags(title) {
if (!title) {
return [];
}
const firstTagIndex = title.match(/[A-Z]{2}/)?.index;
if (firstTagIndex) {
const tagSection = title
.slice(firstTagIndex)
.match(/([A-Z0-9\s]{2,})/g);
if (tagSection) {
return tagSection
.map((tag) => tag.trim().toLowerCase())
.filter(Boolean) || [];
}
}
return [];
}
// derived photo is usually uncensored and preferred as poster, but not guaranteed to exist, so fall back to original image
function getPhotos(poster) {
const photo = poster?.replace(/(s[ma]{2}kprov\d*)|([._]preview)/i, ''); // sic
if (photo === poster) {
return {
poster,
photos: [],
};
}
return {
poster: [photo, poster],
photos: [poster],
};
}
function scrapeAll(scenes, channel, parameters) {
return scenes.map(({ query }) => {
const release = {};
// Vilde URLs are temporary tokens for some reason, seem to be handled entirely back-end
const url = query.url('a[href*="/show-video"]');
release.token = new URL(url).pathname.match(/\/show-video\/([a-z0-9]+)/)?.[1];
release.forceDeep = true;
release.title = query.content('a h5, .product-content p, .video_text');
release.tags = extractTags(release.title);
const { poster, photos } = getPhotos(query.img('img[src*="/videos/images"], img[src*="/uploads/images"]'));
release.poster = poster;
release.photos = photos;
if (parameters.staticUrl) {
release.url = url;
release.entryId = release.token;
} else {
release.entryId = extractEntryId(release.poster);
}
return release;
});
}
async function setLanguage(parameters) {
if (parameters.languageUrl) {
const langRes = await unprint.post(parameters.languageUrl, {
[parameters.languageKey || 'select_language']: 'english',
}, {
form: true,
});
return langRes.cookies;
}
return null;
}
async function fetchLatest(channel, page = 1, { parameters }) {
const cookies = await setLanguage(parameters);
const res = await unprint.post(`${channel.origin}/pagination`, {
i: page,
status: true,
}, {
selectAll: '.movi-area',
form: true,
cookies,
});
if (res.ok) {
return scrapeAll(res.context, channel, parameters);
}
return res.status;
}
async function fetchLatestHooked(channel, page = 1, { parameters }) {
const cookies = await setLanguage(parameters);
const res = await unprint.get(`${channel.origin}/Welcome/index/${(page - 1) * 9}`, {
selectAll: '.product-main',
cookies,
});
if (res.ok) {
return scrapeAll(res.context, channel, parameters);
}
return res.status;
}
async function fetchLatestKanal(channel, page = 1, { parameters }) {
const cookies = await setLanguage(parameters);
const res = await unprint.post(`${channel.origin}/pagination`, {
k: page,
status: 1,
}, {
selectAll: '.video_bx',
form: true,
cookies,
});
if (res.ok) {
return scrapeAll(res.context, channel, parameters);
}
return res.status;
}
function scrapeScene({ query }, { url, baseRelease, parameters }) {
const release = {};
// URL is temporary token
if (!query.exists('.login-sec.for-browser, .video-description, .video_co_title')) {
// URL likely expired, still returns 200
return null;
}
if (query.exists('.video-description')) {
const descriptions = query.contents('.video-description p').filter(Boolean);
release.title = descriptions[0];
release.description = descriptions.slice(1).join(' ') || null;
} else {
release.title = query.content('.login-sec.for-browser h3, .video_co_title h3');
release.description = query.contents('.login-sec.for-browser h3 ~ *').join(' ') || null;
}
release.tags = extractTags(release.title);
const { poster, photos } = getPhotos(query.poster('.play_video_cont video'));
release.poster = poster;
release.photos = photos;
release.trailer = query.all('.play_video_cont source')
.map((videoEl) => ({
src: unprint.query.url(videoEl, null, { attribute: 'src' }),
quality: unprint.query.number(videoEl, null, { attribute: 'size' }),
referer: url,
}))
.toSorted((videoA, videoB) => videoB.quality - videoA.quality);
if (parameters.staticUrl) {
release.url = url;
release.entryId = baseRelease?.token || new URL(url).pathname.match(/\/show-video\/([a-z0-9]+)/)?.[1];
} else {
release.entryId = extractEntryId(release.poster);
}
return release;
}
async function fetchScene(baseUrl, entity, baseRelease, { parameters }) {
const url = baseUrl || (baseRelease?.token && `${entity.origin}/show-video/${baseRelease.token}`) || null;
if (!url) {
return null;
}
const cookies = await setLanguage(parameters);
const res = await unprint.get(url, {
headers: {
'accept-language': 'en-US,en',
},
cookies,
});
if (res.ok || res.status === 500) { // Anal Hooked returns 500 for valid scene pages
return scrapeScene(res.context, { url, baseRelease, parameters });
}
return res.status;
}
module.exports = {
fetchLatest,
fetchScene,
hooked: {
fetchLatest: fetchLatestHooked,
fetchScene,
},
kanal: {
fetchLatest: fetchLatestKanal,
fetchScene,
},
};

View File

@@ -1,5 +1,6 @@
'use strict';
const acam = require('./acam');
const adultempire = require('./adultempire');
const angelogodshackoriginal = require('./angelogodshackoriginal');
// const archangel = require('./archangel');
@@ -96,6 +97,7 @@ module.exports = {
freeuse: teamskeet,
familystrokes: teamskeet,
// etc
acam,
analvids: pornbox,
pornbox,
kellymadison,