Generating and using URL slugs for releases, improver slugify module. Added 'extract' parameter to MindGeek scraper to get scenes not associate with a channel (see Digital Playground). Added various high res logos.
|
@ -48,7 +48,7 @@
|
|||
</span>
|
||||
|
||||
<a
|
||||
:href="`/${release.type || 'scene'}/${release.id}`"
|
||||
:href="`/${release.type || 'scene'}/${release.id}/${release.slug || ''}`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="link"
|
||||
|
|
|
@ -112,6 +112,7 @@ const releaseFields = `
|
|||
id
|
||||
title
|
||||
date
|
||||
slug
|
||||
createdAt
|
||||
url
|
||||
${releaseActorsFragment}
|
||||
|
|
|
@ -21,7 +21,7 @@ const routes = [
|
|||
name: 'home',
|
||||
},
|
||||
{
|
||||
path: '/scene/:releaseId',
|
||||
path: '/scene/:releaseId/:releaseTitle?',
|
||||
component: Release,
|
||||
name: 'scene',
|
||||
},
|
||||
|
|
|
@ -54,4 +54,5 @@ module.exports = {
|
|||
trailerQuality: [480, 540],
|
||||
limit: 25, // max number of photos per release
|
||||
},
|
||||
titleSlugLength: 50,
|
||||
};
|
||||
|
|
|
@ -332,6 +332,7 @@ exports.up = knex => Promise.resolve()
|
|||
|
||||
table.string('url', 1000);
|
||||
table.string('title');
|
||||
table.string('slug');
|
||||
table.date('date');
|
||||
table.text('description');
|
||||
|
||||
|
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 377 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1308 112" style="enable-background:new 0 0 1308 112;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{clip-path:url(#SVGID_2_);}
|
||||
.st1{fill:#EE2A34;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<defs>
|
||||
<rect id="SVGID_1_" width="1308" height="112"/>
|
||||
</defs>
|
||||
<clipPath id="SVGID_2_">
|
||||
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
|
||||
</clipPath>
|
||||
<g class="st0">
|
||||
<path class="st1" d="M87.5,93.5c3.7,0.1,7.4-0.2,11-0.8c2.8-0.5,5.8-1.3,9.1-2.2L98.3,112H0c1.8-2.4,3.4-5,4.6-7.8
|
||||
c1.5-3.7,2.7-7.5,3.6-11.5l18.3-73.5c1.2-4.3,1.9-8.7,2.1-13.1c0-2.1-0.3-4.1-0.7-6.1h96.6l-1.9,21.5l-0.3-0.2
|
||||
c-2.5-0.9-5.2-1.5-7.8-2c-3.4-0.5-6.9-0.8-10.4-0.7H69.2l-6.4,26.2h30.3c3.5,0.1,6.9-0.2,10.4-0.7c3.1-0.6,6.2-1.3,9.2-2.3h0.3
|
||||
l-6.1,24.5l-0.2-0.2c-2.6-0.9-5.4-1.6-8.1-2.1c-3.4-0.5-6.8-0.8-10.2-0.7H58l-7.6,30H87.5z"/>
|
||||
<path class="st1" d="M171,112L159.4,10.3c-0.3-3.8-1.8-7.4-4.2-10.3h53c-1.1,1.9-1.9,4-2.4,6.1c-0.6,2.1-0.9,4.3-0.9,6.5
|
||||
c0.1,2.7,0.3,5.4,0.7,8l7.7,67.5l42.9-67.5c2.1-3.4,3.7-6.1,4.7-8.1c1-2,1.8-4.2,2.4-6.4c0.4-2,0.6-4.1,0.7-6.1h28.8
|
||||
c-2.7,2.3-5.2,4.8-7.4,7.6c-3.2,4-6.1,8.3-8.7,12.7L218.5,112H171z"/>
|
||||
<path class="st1" d="M297.4,112c1.8-2.4,3.4-5,4.6-7.8c1.5-3.7,2.7-7.5,3.6-11.5l18.3-73.5c1.2-4.3,1.9-8.7,2.1-13.1
|
||||
c0-2.1-0.3-4.1-0.7-6.1h49.4c-1.8,2.4-3.4,5-4.6,7.8c-1.5,3.8-2.6,7.6-3.5,11.6l-18.3,73.5c-1.1,4.3-1.8,8.7-1.9,13.1
|
||||
c0,2.1,0.3,4.1,0.7,6.1L297.4,112z"/>
|
||||
<path class="st1" d="M484.9,93.5c3.4,0,6.9-0.3,10.3-0.9c3.2-0.5,6.2-1.5,9.1-2.9l-9.3,22.3h-96.2c1.8-2.4,3.4-5,4.6-7.8
|
||||
c1.5-3.7,2.7-7.5,3.6-11.5l18.2-73.4c1.2-4.3,1.9-8.7,2.1-13.1c0-2.1-0.3-4.1-0.7-6.1H476c-1.7,2.4-3.2,4.9-4.3,7.6
|
||||
c-1.5,3.7-2.8,7.5-3.7,11.5l-18.6,74.3H484.9z"/>
|
||||
<path class="st2" d="M575,81.9l-11.3,15.4c-1.6,1.9-2.6,4.2-3.1,6.7c-0.5,1.3-0.7,2.7-0.7,4.1c0.1,1.4,0.4,2.7,1,4h-31.5
|
||||
c2.2-2.1,4.1-3.9,5.6-5.5c1.5-1.6,2.9-3.1,4.2-4.5c1.3-1.5,2.5-2.9,3.7-4.4s2.6-3.4,4.4-5.8L609,6.3L605.7,0h50.4l15.7,96.6
|
||||
c0.9,5.5,2.9,10.7,5.8,15.4h-55.8c1.2-1.4,2.3-2.9,3.2-4.5c0.5-0.9,0.9-1.9,1.1-2.9c0.5-1.9,0.7-3.9,0.7-5.8
|
||||
c0-0.5-0.1-0.9-0.2-1.4c-0.1-0.4-0.2-0.8-0.2-1.2L624,81.8L575,81.9z M621.2,63.4l-6.4-37.9l-26.5,37.9H621.2z"/>
|
||||
<path class="st2" d="M760.6,19.4h-0.3l-18,72.6c-1.2,4.4-2.1,8.9-2.4,13.5c0.1,2.3,0.6,4.6,1.6,6.6h-26.8c1.1-1.1,2.2-2.3,3.1-3.6
|
||||
c0.9-1.2,1.6-2.4,2.2-3.8c0.7-1.6,1.3-3.3,1.8-5.1c0.6-1.9,1.3-4.5,2.1-7.7l17.8-71.8c0.8-3.5,1.4-6.3,1.8-8.4
|
||||
c0.4-1.7,0.6-3.4,0.6-5.1c-0.1-2.3-0.6-4.6-1.6-6.6h59.8l22.5,91.3h0.5l17.8-71.2c0.8-3.5,1.4-6.3,1.8-8.4
|
||||
c0.4-1.7,0.6-3.4,0.6-5.1c-0.1-2.3-0.6-4.6-1.6-6.6h26.8c-1.1,1.1-2.2,2.3-3.1,3.6c-0.8,1.2-1.6,2.4-2.2,3.7
|
||||
c-0.7,1.6-1.4,3.3-1.8,5.1c-0.6,1.9-1.3,4.5-2.1,7.8l-22.8,91.9h-55.2L760.6,19.4z"/>
|
||||
<path class="st2" d="M1028.7,44.8c-2.4,2.6-4.6,5.5-6.5,8.6c-1.4,2.8-2.4,5.8-3,8.9l-7.1,27.7c-0.8,3.6-1.5,6.6-1.9,8.9
|
||||
c-0.4,1.8-0.6,3.7-0.7,5.6c0.1,2.6,0.7,5.1,1.7,7.5h-37.5l1.2-10.3h-0.3c-1.5,3.2-3.8,5.9-6.5,8c-3.6,1.8-7.6,2.6-11.6,2.3h-37.8
|
||||
c-7.3,0-12.8-1.5-16.5-4.6c-3.7-3-5.5-7.9-5.5-14.6c0.1-4.7,0.7-9.3,1.9-13.8l11.6-46.2c2.9-11.7,7.3-20.1,13.2-25.2
|
||||
c6.5-5.4,14.8-8.1,23.2-7.7h81.6l-2.1,21.5c-2.3-1-4.7-1.8-7.2-2.3c-3-0.5-6.1-0.8-9.1-0.7h-45.1c-2.4-0.1-4.7,0.4-6.8,1.7
|
||||
c-1.8,1.3-2.9,3.3-3.3,5.5l-15,60.5c-0.2,0.9-0.3,1.7-0.3,2.6c0,3,2.3,4.6,6.8,4.6h14.3c2.4,0.1,4.7-0.4,6.8-1.7
|
||||
c1.8-1.4,3-3.3,3.5-5.5l5.6-22.9h-15.9c-2.5,0.1-5,0.8-7.3,1.9l-0.3,0.2l5.2-20.7L1028.7,44.8z"/>
|
||||
<path class="st2" d="M1143.6,93.5c3.7,0.1,7.3-0.2,11-0.8c2.8-0.5,5.8-1.3,9.1-2.2l-9.2,21.5h-98.3c1.8-2.4,3.4-5,4.6-7.8
|
||||
c1.5-3.7,2.7-7.5,3.6-11.5l18.3-73.5c1.2-4.3,1.9-8.7,2.1-13.1c0-2.1-0.2-4.1-0.7-6.1h96.6l-1.9,21.5l-0.3-0.2
|
||||
c-2.6-0.9-5.2-1.6-7.9-2.1c-3.4-0.5-6.9-0.8-10.4-0.7h-34.6l-6.4,26.2h30.3c3.5,0.1,6.9-0.2,10.4-0.7c3.1-0.6,6.1-1.3,9.1-2.3h0.2
|
||||
l-6.1,24.5l-0.2-0.2c-2.7-0.9-5.4-1.6-8.1-2.1c-3.4-0.5-6.8-0.8-10.2-0.7h-30.2l-7.5,30.1H1143.6z"/>
|
||||
<path class="st2" d="M1288.6,93.5c3.4,0,6.9-0.3,10.3-0.9c3.2-0.5,6.2-1.5,9.1-2.9l-9.2,22.2h-96.2c1.8-2.4,3.4-5,4.6-7.8
|
||||
c1.5-3.7,2.7-7.5,3.6-11.5l18.3-73.5c1.2-4.3,1.9-8.7,2.1-13.1c0-2.1-0.2-4.1-0.7-6.1h49.4c-1.8,2.4-3.4,5-4.6,7.8
|
||||
c-1.5,3.7-2.8,7.5-3.8,11.5l-18.2,74.3H1288.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 13 KiB |
|
@ -1007,7 +1007,15 @@ const sites = [
|
|||
description: 'Fantasy Blowjobs & POV Cock Sucking Videos and Photos Produced in VR, 4K and full HD featuring Sexy European Pornstars',
|
||||
network: 'ddfnetwork',
|
||||
},
|
||||
// FAKE HUB
|
||||
// DIGITAL PLAYGROUND
|
||||
{
|
||||
slug: 'digitalplayground',
|
||||
name: 'Digital Playground',
|
||||
url: 'https://www.digitalplayground.com/scenes',
|
||||
description: '',
|
||||
parameters: { extract: true },
|
||||
network: 'digitalplayground',
|
||||
},
|
||||
{
|
||||
slug: 'episodes',
|
||||
name: 'Episodes',
|
||||
|
|
|
@ -367,6 +367,7 @@ async function scrapeActors(actorNames) {
|
|||
} catch (error) {
|
||||
if (error.warn !== false) {
|
||||
logger.warn(`Error in scraper ${source}: ${error.message}`);
|
||||
logger.error(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ const { argv } = yargs
|
|||
describe: 'Scrape profiles for new actors after fetching scenes',
|
||||
type: 'boolean',
|
||||
alias: 'with-actors',
|
||||
default: true,
|
||||
default: false,
|
||||
})
|
||||
.option('scene', {
|
||||
describe: 'Scrape scene info from URL',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const Promise = require('bluebird');
|
||||
const moment = require('moment');
|
||||
|
||||
|
@ -16,6 +17,7 @@ const {
|
|||
storeTrailer,
|
||||
} = require('./media');
|
||||
const { fetchSites, findSiteByUrl } = require('./sites');
|
||||
const slugify = require('./utils/slugify');
|
||||
|
||||
function commonQuery(queryBuilder, {
|
||||
filter = [],
|
||||
|
@ -204,6 +206,11 @@ async function attachStudio(release) {
|
|||
}
|
||||
|
||||
async function curateReleaseEntry(release) {
|
||||
const slug = slugify(release.title, {
|
||||
encode: true,
|
||||
limit: config.titleSlugLength,
|
||||
});
|
||||
|
||||
const curatedRelease = {
|
||||
site_id: release.site.id,
|
||||
studio_id: release.studio ? release.studio.id : null,
|
||||
|
@ -213,6 +220,7 @@ async function curateReleaseEntry(release) {
|
|||
type: release.type,
|
||||
url: release.url,
|
||||
title: release.title,
|
||||
slug,
|
||||
date: release.date,
|
||||
description: release.description,
|
||||
// director: release.director,
|
||||
|
@ -397,7 +405,7 @@ async function storeRelease(release) {
|
|||
|
||||
logger.info(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`);
|
||||
|
||||
return releaseEntry.id;
|
||||
return releaseEntry;
|
||||
}
|
||||
|
||||
async function storeReleases(releases) {
|
||||
|
@ -405,10 +413,11 @@ async function storeReleases(releases) {
|
|||
try {
|
||||
const releaseWithChannelSite = await attachChannelSite(release);
|
||||
const releaseWithStudio = await attachStudio(releaseWithChannelSite);
|
||||
const releaseId = await storeRelease(releaseWithStudio);
|
||||
const { id, slug } = await storeRelease(releaseWithStudio);
|
||||
|
||||
return {
|
||||
id: releaseId,
|
||||
id,
|
||||
slug,
|
||||
...releaseWithChannelSite,
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
|
@ -97,7 +97,7 @@ async function scrapeReleases(sources, release = null, type = 'scene') {
|
|||
const { releases: storedReleases } = await storeReleases(curatedReleases);
|
||||
|
||||
if (storedReleases) {
|
||||
console.log(storedReleases.map(storedRelease => `http://${config.web.host}:${config.web.port}/scene/${storedRelease.id}`).join('\n'));
|
||||
logger.info(storedReleases.map(storedRelease => `\nhttp://${config.web.host}:${config.web.port}/scene/${storedRelease.id}/${storedRelease.slug}`).join(''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ async function scrapeScene(scene, site, tokens) {
|
|||
},
|
||||
};
|
||||
|
||||
release.url = `${site.url}/scene/${release.entryId}/${slugify(release.title, true)}`;
|
||||
release.url = `${site.url}/scene/${release.entryId}/${slugify(release.title, { encode: true })}`;
|
||||
release.date = new Date(scene.sites.collection[scene.id].publishDate);
|
||||
release.poster = scene._resources.primary[0].url;
|
||||
|
||||
|
|
|
@ -26,6 +26,16 @@ function getThumbs(scene) {
|
|||
}
|
||||
|
||||
function scrapeLatestX(data, site) {
|
||||
if (site.parameters?.extract === true && data.collections.length > 0) {
|
||||
// release should not belong to any channel
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof site.parameters?.extract === 'string' && !data.collections.some(collection => collection.shortName === site.parameters.extract)) {
|
||||
// release should belong to specific channel
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id: entryId, title, description } = data;
|
||||
const hostname = site.parameters?.native ? site.url : site.network.url;
|
||||
const url = `${hostname}/scene/${entryId}/`;
|
||||
|
@ -58,7 +68,9 @@ function scrapeLatestX(data, site) {
|
|||
}
|
||||
|
||||
async function scrapeLatest(items, site) {
|
||||
return Promise.all(items.map(async data => scrapeLatestX(data, site)));
|
||||
const latestReleases = await Promise.all(items.map(async data => scrapeLatestX(data, site)));
|
||||
|
||||
return latestReleases.filter(Boolean);
|
||||
}
|
||||
|
||||
function scrapeScene(data, url, _site) {
|
||||
|
@ -85,10 +97,10 @@ function scrapeScene(data, url, _site) {
|
|||
};
|
||||
}
|
||||
|
||||
const siteName = data.collections[0].name;
|
||||
const siteName = data.collections[0]?.name || data.brand;
|
||||
release.channel = siteName.replace(/\s+/g, '').toLowerCase();
|
||||
|
||||
release.url = url || `https://www.realitykings.com/scene/${entryId}/`;
|
||||
release.url = url || `https://www.${data.brand}.com/scene/${entryId}/`;
|
||||
|
||||
return release;
|
||||
}
|
||||
|
@ -104,6 +116,9 @@ function getUrl(site) {
|
|||
return `${site.url}/scenes`;
|
||||
}
|
||||
|
||||
if (site.parameters?.extract) {
|
||||
return `${site.url}/scenes`;
|
||||
}
|
||||
|
||||
if (site.parameters?.siteId) {
|
||||
return `${site.network.url}/scenes?site=${site.parameters.siteId}`;
|
||||
|
@ -144,7 +159,7 @@ function scrapeProfile(data, html, releases = []) {
|
|||
if (data.height) profile.height = inchesToCm(data.height);
|
||||
if (data.weight) profile.weight = lbsToKg(data.weight);
|
||||
|
||||
if (data.images.card_main_rect && data.images.card_main_rect[0]) {
|
||||
if (data.images.card_main_rect?.[0]) {
|
||||
profile.avatar = data.images.card_main_rect[0].xl?.url
|
||||
|| data.images.card_main_rect[0].lg?.url
|
||||
|| data.images.card_main_rect[0].md?.url
|
||||
|
@ -169,7 +184,7 @@ async function fetchLatest(site, page = 1) {
|
|||
|
||||
const beforeDate = moment().add('1', 'day').format('YYYY-MM-DD');
|
||||
const limit = 10;
|
||||
const apiUrl = site.parameters?.native
|
||||
const apiUrl = site.parameters?.native || site.parameters?.extract
|
||||
? `https://site-api.project1service.com/v2/releases?dateReleased=<${beforeDate}&limit=${limit}&offset=${limit * (page - 1)}&orderBy=-dateReleased&type=scene`
|
||||
: `https://site-api.project1service.com/v2/releases?collectionId=${siteId}&dateReleased=<${beforeDate}&limit=${limit}&offset=${limit * (page - 1)}&orderBy=-dateReleased&type=scene`;
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ async function scrapeScene(html, url, site) {
|
|||
release.actors = qa('.value a[href*=models], .value a[href*=performer], .value a[href*=teen-babes]', true);
|
||||
|
||||
if (release.actors.length === 0) {
|
||||
const actorEl = qa('.stat').find(stat => /Featuring/.test(stat.textContent))
|
||||
const actorEl = qa('.stat').find(stat => /Featuring/.test(stat.textContent));
|
||||
const actorString = qtext(actorEl);
|
||||
|
||||
console.log(actorString);
|
||||
|
@ -147,7 +147,7 @@ function scrapeProfile(html) {
|
|||
|
||||
const bio = qa('.stat').reduce((acc, el) => {
|
||||
const prop = q(el, '.label', true).slice(0, -1);
|
||||
const key = slugify(prop, false, '_');
|
||||
const key = slugify(prop, { delimiter: '_' });
|
||||
const value = q(el, '.value', true);
|
||||
|
||||
return {
|
||||
|
|
|
@ -60,7 +60,7 @@ function destructConfigNetworks(networks = []) {
|
|||
}
|
||||
|
||||
async function findSiteByUrl(url) {
|
||||
const { origin, pathname } = new URL(url);
|
||||
const { origin, hostname, pathname } = new URL(url);
|
||||
// const domain = hostname.replace(/www.|tour./, '');
|
||||
const dirUrl = `${origin}${pathname.split('/').slice(0, 2).join('/')}`; // allow for sites on URI directory
|
||||
|
||||
|
@ -72,6 +72,9 @@ async function findSiteByUrl(url) {
|
|||
)
|
||||
.where('sites.url', url)
|
||||
.orWhere('sites.url', origin)
|
||||
.orWhere('sites.url', origin.replace(/www\.|tour\./, ''))
|
||||
.orWhere('sites.url', `https://www.${hostname}`)
|
||||
.orWhere('sites.url', `http://www.${hostname}`)
|
||||
.orWhere('sites.url', dirUrl)
|
||||
// .orWhere('sites.url', 'like', `%${domain}`)
|
||||
.first();
|
||||
|
|
|
@ -9,16 +9,18 @@ const knex = require('../knex');
|
|||
|
||||
async function init() {
|
||||
const posters = await knex('actors')
|
||||
.select('actors.name', 'releases.title', 'media.path')
|
||||
.whereIn('name', argv.actors)
|
||||
.select('actors.name as actor_name', 'releases.title', 'media.path', 'sites.name as site_name', 'networks.name as network_name')
|
||||
.whereIn('actors.name', argv.actors)
|
||||
.join('releases_actors', 'releases_actors.actor_id', 'actors.id')
|
||||
.join('releases', 'releases_actors.release_id', 'releases.id')
|
||||
.join('releases_posters', 'releases_posters.release_id', 'releases.id')
|
||||
.join('sites', 'sites.id', 'releases.site_id')
|
||||
.join('networks', 'networks.id', 'sites.network_id')
|
||||
.join('media', 'releases_posters.media_id', 'media.id');
|
||||
|
||||
await Promise.all(posters.map(async (poster) => {
|
||||
const source = path.join(config.media.path, poster.path);
|
||||
const target = path.join(config.media.path, 'posters', `${poster.title.replace('/', '_')}.${poster.name}.jpeg`);
|
||||
const target = path.join(config.media.path, 'posters', `${poster.actor_name} - ${poster.network_name}: ${poster.site_name} - ${poster.title.replace(/[/.]/g, '_')}.jpeg`);
|
||||
|
||||
const file = await fs.readFile(source);
|
||||
await fs.writeFile(target, file);
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
function slugify(string, encode = false, delimiter = '-') {
|
||||
const slug = string.trim().toLowerCase().match(/\w+/g).join(delimiter);
|
||||
function slugify(string, {
|
||||
encode = false,
|
||||
delimiter = '-',
|
||||
limit = 1000,
|
||||
} = {}) {
|
||||
const slugComponents = string.trim().toLowerCase().match(/\w+/g);
|
||||
|
||||
const slug = slugComponents.reduce((acc, component, index) => {
|
||||
const accSlug = `${acc}${index > 0 ? delimiter : ''}${component}`;
|
||||
|
||||
if (accSlug.length < limit) {
|
||||
return accSlug;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, '');
|
||||
|
||||
return encode ? encodeURI(slug) : slug;
|
||||
}
|
||||
|
|