Added support for Family Strokes.
|  | @ -119,7 +119,7 @@ | ||||||
|                         title="bust-waist-hip" |                         title="bust-waist-hip" | ||||||
|                         class="bio-item" |                         class="bio-item" | ||||||
|                     > |                     > | ||||||
|                         <dfn class="bio-label"><Icon icon="ruler" />Sizes</dfn> |                         <dfn class="bio-label"><Icon icon="ruler" />Figure</dfn> | ||||||
|                         <span> |                         <span> | ||||||
|                             <Icon |                             <Icon | ||||||
|                                 v-if="actor.naturalBoobs === false" |                                 v-if="actor.naturalBoobs === false" | ||||||
|  |  | ||||||
|  | @ -21,6 +21,14 @@ | ||||||
|                 > |                 > | ||||||
|             </a> |             </a> | ||||||
| 
 | 
 | ||||||
|  |             <ul class="tags nolist"> | ||||||
|  |                 <li | ||||||
|  |                     v-for="tag in site.tags" | ||||||
|  |                     :key="`tag-${tag.slug}`" | ||||||
|  |                     class="tag" | ||||||
|  |                 >{{ tag.name }}</li> | ||||||
|  |             </ul> | ||||||
|  | 
 | ||||||
|             <a |             <a | ||||||
|                 v-tooltip.bottom="`Go to ${site.network.name} overview`" |                 v-tooltip.bottom="`Go to ${site.network.name} overview`" | ||||||
|                 :href="`/network/${site.network.slug}`" |                 :href="`/network/${site.network.slug}`" | ||||||
|  | @ -115,6 +123,12 @@ export default { | ||||||
|     filter: $logo-highlight; |     filter: $logo-highlight; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .tag { | ||||||
|  |     background: $shadow; | ||||||
|  |     padding: .5rem; | ||||||
|  |     margin: 0 .5rem .5rem 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @media(max-width: $breakpoint) { | @media(max-width: $breakpoint) { | ||||||
|     .link { |     .link { | ||||||
|         padding: .5rem 1rem; |         padding: .5rem 1rem; | ||||||
|  | @ -123,5 +137,9 @@ export default { | ||||||
|     .logo { |     .logo { | ||||||
|         max-height: 2.5rem; |         max-height: 2.5rem; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     .tags { | ||||||
|  |         display: none; | ||||||
|  |     } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ function curateSite(site, network) { | ||||||
| 
 | 
 | ||||||
|     if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release)); |     if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release)); | ||||||
|     if (site.network || network) curatedSite.network = site.network || network; |     if (site.network || network) curatedSite.network = site.network || network; | ||||||
|  |     if (site.tags) curatedSite.tags = site.tags.map(({ tag }) => tag); | ||||||
| 
 | 
 | ||||||
|     return curatedSite; |     return curatedSite; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,6 +16,13 @@ function initSitesActions(store, _router) { | ||||||
|                     name |                     name | ||||||
|                     slug |                     slug | ||||||
|                     url |                     url | ||||||
|  |                     tags: sitesTags { | ||||||
|  |                         tag { | ||||||
|  |                             id | ||||||
|  |                             slug | ||||||
|  |                             name | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|                     network { |                     network { | ||||||
|                         id |                         id | ||||||
|                         name |                         name | ||||||
|  |  | ||||||
|  | @ -159,6 +159,9 @@ exports.up = knex => Promise.resolve() | ||||||
|             .references('id') |             .references('id') | ||||||
|             .inTable('sites'); |             .inTable('sites'); | ||||||
| 
 | 
 | ||||||
|  |         table.boolean('inherit') | ||||||
|  |             .defaultTo(false); | ||||||
|  | 
 | ||||||
|         table.unique(['tag_id', 'site_id']); |         table.unique(['tag_id', 'site_id']); | ||||||
|     })) |     })) | ||||||
|     .then(() => knex.schema.createTable('sites_social', (table) => { |     .then(() => knex.schema.createTable('sites_social', (table) => { | ||||||
|  |  | ||||||
|  | @ -4875,6 +4875,14 @@ | ||||||
|         "flat-cache": "^2.0.1" |         "flat-cache": "^2.0.1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "file-stream-rotator": { | ||||||
|  |       "version": "0.5.5", | ||||||
|  |       "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.5.5.tgz", | ||||||
|  |       "integrity": "sha512-XzvE1ogpxUbARtZPZLICaDRAeWxoQLFMKS3ZwADoCQmurKEwuDD2jEfDVPm/R1HeKYsRYEl9PzVIezjQ3VTTPQ==", | ||||||
|  |       "requires": { | ||||||
|  |         "moment": "^2.11.2" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "fill-range": { |     "fill-range": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", | ||||||
|  | @ -12274,6 +12282,24 @@ | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "winston-daily-rotate-file": { | ||||||
|  |       "version": "4.4.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-4.4.1.tgz", | ||||||
|  |       "integrity": "sha512-516bL4IDjgX5mPEsTPXNVNzZtJkrUFY2IvPhj8n5xSKyy804xadp4TUlhxEZLL/Jbs8CF+rESfq95QXFLFTzKA==", | ||||||
|  |       "requires": { | ||||||
|  |         "file-stream-rotator": "^0.5.5", | ||||||
|  |         "object-hash": "^2.0.1", | ||||||
|  |         "triple-beam": "^1.3.0", | ||||||
|  |         "winston-transport": "^4.2.0" | ||||||
|  |       }, | ||||||
|  |       "dependencies": { | ||||||
|  |         "object-hash": { | ||||||
|  |           "version": "2.0.1", | ||||||
|  |           "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.1.tgz", | ||||||
|  |           "integrity": "sha512-HgcGMooY4JC2PBt9sdUdJ6PMzpin+YtY3r/7wg0uTifP+HJWW8rammseSEHuyt0UeShI183UGssCJqm1bJR7QA==" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "winston-transport": { |     "winston-transport": { | ||||||
|       "version": "4.3.0", |       "version": "4.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", |       "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", | ||||||
|  |  | ||||||
|  | @ -109,6 +109,7 @@ | ||||||
|         "vue-router": "^3.1.3", |         "vue-router": "^3.1.3", | ||||||
|         "vuex": "^3.1.2", |         "vuex": "^3.1.2", | ||||||
|         "winston": "^3.2.1", |         "winston": "^3.2.1", | ||||||
|  |         "winston-daily-rotate-file": "^4.4.1", | ||||||
|         "yargs": "^13.3.0" |         "yargs": "^13.3.0" | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -597,6 +597,11 @@ | ||||||
|   -webkit-filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.5)); |   -webkit-filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.5)); | ||||||
|           filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.5)); |           filter: drop-shadow(0 0 1px rgba(255, 255, 255, 0.5)); | ||||||
| } | } | ||||||
|  | .tag[data-v-194630f6] { | ||||||
|  |   background: rgba(0, 0, 0, 0.5); | ||||||
|  |   padding: .5rem; | ||||||
|  |   margin: 0 .5rem .5rem 0; | ||||||
|  | } | ||||||
| @media (max-width: 720px) { | @media (max-width: 720px) { | ||||||
| .link[data-v-194630f6] { | .link[data-v-194630f6] { | ||||||
|     padding: .5rem 1rem; |     padding: .5rem 1rem; | ||||||
|  | @ -604,6 +609,9 @@ | ||||||
| .logo[data-v-194630f6] { | .logo[data-v-194630f6] { | ||||||
|     max-height: 2.5rem; |     max-height: 2.5rem; | ||||||
| } | } | ||||||
|  | .tags[data-v-194630f6] { | ||||||
|  |     display: none; | ||||||
|  | } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /* $primary: #ff886c; */ | /* $primary: #ff886c; */ | ||||||
|  |  | ||||||
| After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB | 
| Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB | 
| Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB | 
| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB | 
| After Width: | Height: | Size: 9.6 KiB | 
| Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 1.3 KiB | 
| Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB | 
| Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB | 
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
| Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB | 
| Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB | 
| Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB | 
| Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB | 
| Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 182 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB | 
| Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB | 
| Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB | 
| Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB | 
| Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB | 
| Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB | 
| Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 213 KiB | 
| Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 112 KiB | 
|  | @ -2704,6 +2704,22 @@ function getSites(networksMap) { | ||||||
|             parameters: JSON.stringify({ id: 'sss' }), |             parameters: JSON.stringify({ id: 'sss' }), | ||||||
|             network_id: networksMap.teamskeet, |             network_id: networksMap.teamskeet, | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             slug: 'submissived', | ||||||
|  |             name: 'Submissived', | ||||||
|  |             description: '', | ||||||
|  |             url: 'https://www.submissived.com', | ||||||
|  |             parameters: JSON.stringify({ scraper: 'A' }), | ||||||
|  |             network_id: networksMap.teamskeet, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             slug: 'familystrokes', | ||||||
|  |             name: 'Family Strokes', | ||||||
|  |             description: '', | ||||||
|  |             url: 'https://www.familystrokes.com', | ||||||
|  |             parameters: JSON.stringify({ scraper: 'A' }), | ||||||
|  |             network_id: networksMap.teamskeet, | ||||||
|  |         }, | ||||||
|         // VIXEN
 |         // VIXEN
 | ||||||
|         { |         { | ||||||
|             slug: 'vixen', |             slug: 'vixen', | ||||||
|  |  | ||||||
|  | @ -404,6 +404,11 @@ function getTags(groupsMap) { | ||||||
|             alias_for: null, |             alias_for: null, | ||||||
|             group_id: groupsMap.finish, |             group_id: groupsMap.finish, | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             name: 'family taboo', | ||||||
|  |             slug: 'family', | ||||||
|  |             alias_for: null, | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             name: 'feet', |             name: 'feet', | ||||||
|             slug: 'feet', |             slug: 'feet', | ||||||
|  | @ -1322,6 +1327,14 @@ function getTagAliases(tagsMap) { | ||||||
|             name: 'huge toys', |             name: 'huge toys', | ||||||
|             alias_for: tagsMap.toys, |             alias_for: tagsMap.toys, | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             name: 'incest', | ||||||
|  |             alias_for: tagsMap.family, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: 'incest fantasy', | ||||||
|  |             alias_for: tagsMap.family, | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|             name: 'innie', |             name: 'innie', | ||||||
|             alias_for: tagsMap['innie-pussy'], |             alias_for: tagsMap['innie-pussy'], | ||||||
|  | @ -1553,6 +1566,21 @@ function getTagAliases(tagsMap) { | ||||||
|     ]; |     ]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function getSiteTags() { | ||||||
|  |     return { | ||||||
|  |         allanal: ['anal', 'mff'], | ||||||
|  |         boundgods: ['gay'], | ||||||
|  |         buttmachineboys: ['gay'], | ||||||
|  |         divinebitches: ['femdom'], | ||||||
|  |         familystrokes: ['family'], | ||||||
|  |         menonedge: ['gay'], | ||||||
|  |         submissived: ['bdsm'], | ||||||
|  |         swallowed: ['blowjob', 'deepthroat', 'facefucking'], | ||||||
|  |         trueanal: ['anal'], | ||||||
|  |         tspussyhunters: ['transsexual'], | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| exports.seed = knex => Promise.resolve() | exports.seed = knex => Promise.resolve() | ||||||
|     .then(async () => upsert('tags_groups', groups, 'slug', knex)) |     .then(async () => upsert('tags_groups', groups, 'slug', knex)) | ||||||
|     .then(async () => { |     .then(async () => { | ||||||
|  | @ -1561,7 +1589,7 @@ exports.seed = knex => Promise.resolve() | ||||||
| 
 | 
 | ||||||
|         const tags = getTags(groupsMap); |         const tags = getTags(groupsMap); | ||||||
| 
 | 
 | ||||||
|         return upsert('tags', tags, 'slug', knex); |         return upsert('tags', tags, 'slug'); | ||||||
|     }) |     }) | ||||||
|     .then(async () => { |     .then(async () => { | ||||||
|         const tags = await knex('tags').select('*').where({ alias_for: null }); |         const tags = await knex('tags').select('*').where({ alias_for: null }); | ||||||
|  | @ -1569,5 +1597,22 @@ exports.seed = knex => Promise.resolve() | ||||||
| 
 | 
 | ||||||
|         const tagAliases = getTagAliases(tagsMap); |         const tagAliases = getTagAliases(tagsMap); | ||||||
| 
 | 
 | ||||||
|         return upsert('tags', tagAliases, 'name', knex); |         return upsert('tags', tagAliases, 'name'); | ||||||
|  |     }) | ||||||
|  |     .then(async () => { | ||||||
|  |         const siteTags = getSiteTags(); | ||||||
|  |         const sites = await knex('sites').whereIn('slug', Object.keys(siteTags)); | ||||||
|  | 
 | ||||||
|  |         const tags = await knex('tags').whereIn('slug', Object.values(siteTags).flat()); | ||||||
|  |         const tagsMap = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.id }), {}); | ||||||
|  | 
 | ||||||
|  |         const tagAssociations = sites | ||||||
|  |             .map(site => siteTags[site.slug].map(tagSlug => ({ | ||||||
|  |                 tag_id: tagsMap[tagSlug], | ||||||
|  |                 site_id: site.id, | ||||||
|  |                 inherit: true, | ||||||
|  |             }))) | ||||||
|  |             .flat(); | ||||||
|  | 
 | ||||||
|  |         return upsert('sites_tags', tagAssociations, ['tag_id', 'site_id']); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ | ||||||
| const util = require('util'); | const util = require('util'); | ||||||
| const winston = require('winston'); | const winston = require('winston'); | ||||||
| const args = require('./argv'); | const args = require('./argv'); | ||||||
|  | require('winston-daily-rotate-file'); | ||||||
| 
 | 
 | ||||||
| const logger = winston.createLogger({ | const logger = winston.createLogger({ | ||||||
|     format: winston.format.combine( |     format: winston.format.combine( | ||||||
|  | @ -19,6 +20,11 @@ const logger = winston.createLogger({ | ||||||
|             ), |             ), | ||||||
|             timestamp: true, |             timestamp: true, | ||||||
|         }), |         }), | ||||||
|  |         new winston.transports.DailyRotateFile({ | ||||||
|  |             datePattern: 'YYYY-MM-DD', | ||||||
|  |             filename: 'log/%DATE%.log', | ||||||
|  |             level: 'silly', | ||||||
|  |         }), | ||||||
|     ], |     ], | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -320,7 +320,6 @@ async function storeReleaseAssets(releases) { | ||||||
| 
 | 
 | ||||||
|         await createMediaDirectory('releases', subpath); |         await createMediaDirectory('releases', subpath); | ||||||
| 
 | 
 | ||||||
|         try { |  | ||||||
|             // don't use Promise.all to prevent concurrency issues with duplicate detection
 |             // don't use Promise.all to prevent concurrency issues with duplicate detection
 | ||||||
|             if (release.poster) { |             if (release.poster) { | ||||||
|                 await storePhotos([release.poster], { |                 await storePhotos([release.poster], { | ||||||
|  | @ -346,9 +345,6 @@ async function storeReleaseAssets(releases) { | ||||||
|                 targetId: release.id, |                 targetId: release.id, | ||||||
|                 subpath, |                 subpath, | ||||||
|             }, identifier); |             }, identifier); | ||||||
|         } catch (error) { |  | ||||||
|             console.log(release.url, error); |  | ||||||
|         } |  | ||||||
|     }, { |     }, { | ||||||
|         concurrency: 10, |         concurrency: 10, | ||||||
|     }); |     }); | ||||||
|  | @ -409,7 +405,7 @@ async function storeReleases(releases) { | ||||||
|                 ...releaseWithChannelSite, |                 ...releaseWithChannelSite, | ||||||
|             }; |             }; | ||||||
|         } catch (error) { |         } catch (error) { | ||||||
|             logger.error(error); |             logger.error(error.message); | ||||||
| 
 | 
 | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -79,7 +79,7 @@ async function deepFetchReleases(baseReleases) { | ||||||
|                     deep: true, |                     deep: true, | ||||||
|                 }; |                 }; | ||||||
|             } catch (error) { |             } catch (error) { | ||||||
|                 logger.error(error); |                 logger.error(error.message); | ||||||
| 
 | 
 | ||||||
|                 return { |                 return { | ||||||
|                     ...release, |                     ...release, | ||||||
|  |  | ||||||
|  | @ -6,15 +6,6 @@ const { JSDOM } = require('jsdom'); | ||||||
| const cheerio = require('cheerio'); | const cheerio = require('cheerio'); | ||||||
| const moment = require('moment'); | const moment = require('moment'); | ||||||
| 
 | 
 | ||||||
| const { matchTags } = require('../tags'); |  | ||||||
| 
 |  | ||||||
| const defaultTags = { |  | ||||||
|     swallowed: ['blowjob', 'deepthroat', 'facefuck'], |  | ||||||
|     trueanal: ['anal'], |  | ||||||
|     allanal: ['anal', 'fmf'], |  | ||||||
|     nympho: [], |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const descriptionTags = { | const descriptionTags = { | ||||||
|     'anal cream pie': 'anal creampie', |     'anal cream pie': 'anal creampie', | ||||||
|     'ass to mouth': 'ass to mouth', |     'ass to mouth': 'ass to mouth', | ||||||
|  | @ -55,7 +46,7 @@ async function scrapeLatestA(html, site) { | ||||||
|         const actors = Array.from(element.querySelectorAll('h4.models a'), actorElement => actorElement.textContent); |         const actors = Array.from(element.querySelectorAll('h4.models a'), actorElement => actorElement.textContent); | ||||||
| 
 | 
 | ||||||
|         const durationString = element.querySelector('.total-time').textContent.trim(); |         const durationString = element.querySelector('.total-time').textContent.trim(); | ||||||
|         // timestamp is somethines 00:00, sometimes 0:00:00
 |         // timestamp is sometimes 00:00, sometimes 0:00:00
 | ||||||
|         const duration = durationString.split(':').length === 3 |         const duration = durationString.split(':').length === 3 | ||||||
|             ? moment.duration(durationString).asSeconds() |             ? moment.duration(durationString).asSeconds() | ||||||
|             : moment.duration(`00:${durationString}`).asSeconds(); |             : moment.duration(`00:${durationString}`).asSeconds(); | ||||||
|  | @ -70,7 +61,7 @@ async function scrapeLatestA(html, site) { | ||||||
|             .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); |             .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); | ||||||
| 
 | 
 | ||||||
|         const photos = [...primaryPhotos, ...secondaryPhotos]; |         const photos = [...primaryPhotos, ...secondaryPhotos]; | ||||||
|         const tags = await matchTags([...defaultTags[site.slug], ...deriveTagsFromDescription(description)]); |         const tags = deriveTagsFromDescription(description); | ||||||
| 
 | 
 | ||||||
|         const scene = { |         const scene = { | ||||||
|             url, |             url, | ||||||
|  | @ -124,7 +115,7 @@ async function scrapeLatestB(html, site) { | ||||||
|             .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); |             .map(photoUrl => photoUrl.slice(photoUrl.indexOf('http'), photoUrl.indexOf('.jpg') + 4)); | ||||||
| 
 | 
 | ||||||
|         const photos = [...primaryPhotos, ...secondaryPhotos]; |         const photos = [...primaryPhotos, ...secondaryPhotos]; | ||||||
|         const tags = await matchTags([...defaultTags[site.slug], ...deriveTagsFromDescription(description)]); |         const tags = deriveTagsFromDescription(description); | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
|             url, |             url, | ||||||
|  | @ -155,7 +146,7 @@ async function scrapeSceneA(html, url, site) { | ||||||
|     const actors = Array.from(element.querySelectorAll('.models a'), actorElement => actorElement.textContent); |     const actors = Array.from(element.querySelectorAll('.models a'), actorElement => actorElement.textContent); | ||||||
| 
 | 
 | ||||||
|     const durationString = element.querySelector('.total-time').textContent.trim(); |     const durationString = element.querySelector('.total-time').textContent.trim(); | ||||||
|     // timestamp is somethines 00:00, sometimes 0:00:00
 |     // timestamp is sometimes 00:00, sometimes 0:00:00
 | ||||||
|     const duration = durationString.split(':').length === 3 |     const duration = durationString.split(':').length === 3 | ||||||
|         ? moment.duration(durationString).asSeconds() |         ? moment.duration(durationString).asSeconds() | ||||||
|         : moment.duration(`00:${durationString}`).asSeconds(); |         : moment.duration(`00:${durationString}`).asSeconds(); | ||||||
|  | @ -163,7 +154,7 @@ async function scrapeSceneA(html, url, site) { | ||||||
|     const { poster } = document.querySelector('.content-page-header video'); |     const { poster } = document.querySelector('.content-page-header video'); | ||||||
|     const { src, type } = document.querySelector('.content-page-header source'); |     const { src, type } = document.querySelector('.content-page-header source'); | ||||||
| 
 | 
 | ||||||
|     const tags = await matchTags([...defaultTags[site.slug], ...deriveTagsFromDescription(description)]); |     const tags = deriveTagsFromDescription(description); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         url, |         url, | ||||||
|  | @ -204,7 +195,7 @@ async function scrapeSceneB(html, url, site) { | ||||||
|     const { poster } = document.querySelector('.content-page-header-inner video'); |     const { poster } = document.querySelector('.content-page-header-inner video'); | ||||||
|     const { src, type } = document.querySelector('.content-page-header-inner source'); |     const { src, type } = document.querySelector('.content-page-header-inner source'); | ||||||
| 
 | 
 | ||||||
|     const tags = await matchTags([...defaultTags[site.slug], ...deriveTagsFromDescription(description)]); |     const tags = deriveTagsFromDescription(description); | ||||||
| 
 | 
 | ||||||
|     const scene = { |     const scene = { | ||||||
|         url, |         url, | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ async function scrapeProfile(html, _url, actorName) { | ||||||
| 
 | 
 | ||||||
|     if (descriptionString) profile.description = descriptionString.textContent; |     if (descriptionString) profile.description = descriptionString.textContent; | ||||||
| 
 | 
 | ||||||
|     if (bio.Birthday) profile.birthdate = moment.utc(bio.Birthday, 'MMM D, YYYY').toDate(); |     if (bio.Birthday && !/-0001/.test(bio.Birthday)) profile.birthdate = moment.utc(bio.Birthday, 'MMM D, YYYY').toDate(); // birthyear sometimes -0001, see Spencer Bradley as of january 2020
 | ||||||
|     if (bio.Born) profile.birthdate = moment.utc(bio.Born, 'YYYY-MM-DD').toDate(); |     if (bio.Born) profile.birthdate = moment.utc(bio.Born, 'YYYY-MM-DD').toDate(); | ||||||
| 
 | 
 | ||||||
|     profile.birthPlace = bio['Birth Place'] || bio.Birthplace; |     profile.birthPlace = bio['Birth Place'] || bio.Birthplace; | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ function extractTitle(pathname) { | ||||||
| 
 | 
 | ||||||
| function extractActors(str) { | function extractActors(str) { | ||||||
|     return str |     return str | ||||||
|         .split(/,|\band/) |         .split(/,|\band\b/ig) | ||||||
|         .filter(actor => !/\.{3}/.test(actor)) |         .filter(actor => !/\.{3}/.test(actor)) | ||||||
|         .map(actor => actor.trim()) |         .map(actor => actor.trim()) | ||||||
|         .filter(actor => actor.length > 0); |         .filter(actor => actor.length > 0); | ||||||
|  | @ -81,7 +81,54 @@ function scrapeScene(html, site) { | ||||||
|     return release; |     return release; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchLatest(site, page = 1) { | function scrapeSceneA(html, site, sceneX, url) { | ||||||
|  |     const scene = sceneX || new JSDOM(html).window.document; | ||||||
|  |     const release = { site }; | ||||||
|  | 
 | ||||||
|  |     release.description = scene.querySelector('.scene-story').textContent.replace('...read more', '...').trim(); | ||||||
|  | 
 | ||||||
|  |     release.date = moment.utc(scene.querySelector('.scene-date').textContent, 'MM/DD/YYYY').toDate(); | ||||||
|  |     release.actors = Array.from(scene.querySelectorAll('.starring span'), el => extractActors(el.textContent)).flat(); | ||||||
|  | 
 | ||||||
|  |     const durationString = scene.querySelector('.time').textContent.trim(); | ||||||
|  |     const duration = ['00'].concat(durationString.split(':')).slice(-3).join(':'); // ensure hh:mm:ss
 | ||||||
|  |     release.duration = moment.duration(duration).asSeconds(); | ||||||
|  | 
 | ||||||
|  |     if (sceneX) { | ||||||
|  |         const titleEl = scene.querySelector(':scope > a'); | ||||||
|  | 
 | ||||||
|  |         release.url = titleEl.href; | ||||||
|  |         release.entryId = titleEl.id; | ||||||
|  |         release.title = titleEl.title; | ||||||
|  | 
 | ||||||
|  |         const [poster, ...photos] = Array.from(scene.querySelectorAll('.scene img'), el => el.src); | ||||||
|  |         release.poster = [poster.replace('bio_big', 'video'), poster]; | ||||||
|  |         release.photos = photos; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!sceneX) { | ||||||
|  |         release.title = scene.querySelector('.title span').textContent; | ||||||
|  |         release.url = url; | ||||||
|  | 
 | ||||||
|  |         release.poster = scene.querySelector('video').poster; | ||||||
|  |         release.photos = [release.poster.replace('video', 'bio_small'), release.poster.replace('video', 'bio_small2')]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const [, entryIdA, entryIdB] = new URL(release.url).pathname.split('/'); | ||||||
|  |     release.entryId = entryIdA === 'scenes' ? entryIdB : entryIdA; | ||||||
|  | 
 | ||||||
|  |     return release; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function scrapeLatestA(html, site) { | ||||||
|  |     const { document } = new JSDOM(html).window; | ||||||
|  | 
 | ||||||
|  |     const scenes = Array.from(document.querySelectorAll('.scenewrapper')); | ||||||
|  | 
 | ||||||
|  |     return scenes.map(scene => scrapeSceneA(null, site, scene)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchLatestTeamSkeet(site, page = 1) { | ||||||
|     const url = `https://www.teamskeet.com/t1/updates/load?fltrs[site]=${site.parameters.id}&page=${page}&view=newest&fltrs[time]=ALL&order=DESC`; |     const url = `https://www.teamskeet.com/t1/updates/load?fltrs[site]=${site.parameters.id}&page=${page}&view=newest&fltrs[time]=ALL&order=DESC`; | ||||||
|     const res = await bhttp.get(url); |     const res = await bhttp.get(url); | ||||||
| 
 | 
 | ||||||
|  | @ -92,10 +139,37 @@ async function fetchLatest(site, page = 1) { | ||||||
|     return null; |     return null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | async function fetchLatestA(site) { | ||||||
|  |     const url = `${site.url}/scenes`; | ||||||
|  |     const res = await bhttp.get(url); | ||||||
|  | 
 | ||||||
|  |     if (res.statusCode === 200) { | ||||||
|  |         return scrapeLatestA(res.body.toString(), site); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchLatest(site, page = 1) { | ||||||
|  |     if (site.parameters.id) { | ||||||
|  |         return fetchLatestTeamSkeet(site, page); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (site.parameters.scraper === 'A') { | ||||||
|  |         return fetchLatestA(site, page); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| async function fetchScene(url, site) { | async function fetchScene(url, site) { | ||||||
|     const session = bhttp.session(); // resolve redirects
 |     const session = bhttp.session(); // resolve redirects
 | ||||||
|     const res = await session.get(url); |     const res = await session.get(url); | ||||||
| 
 | 
 | ||||||
|  |     if (site.parameters.scraper === 'A') { | ||||||
|  |         return scrapeSceneA(res.body.toString(), site, null, url); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return scrapeScene(res.body.toString(), site); |     return scrapeScene(res.body.toString(), site); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,11 @@ const knex = require('./knex'); | ||||||
| const whereOr = require('./utils/where-or'); | const whereOr = require('./utils/where-or'); | ||||||
| 
 | 
 | ||||||
| async function curateSite(site, includeParameters = false) { | async function curateSite(site, includeParameters = false) { | ||||||
|  |     const tags = await knex('sites_tags') | ||||||
|  |         .select('tags.*', 'sites_tags.inherit') | ||||||
|  |         .where('site_id', site.id) | ||||||
|  |         .join('tags', 'tags.id', 'sites_tags.tag_id'); | ||||||
|  | 
 | ||||||
|     const parameters = JSON.parse(site.parameters); |     const parameters = JSON.parse(site.parameters); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|  | @ -16,6 +21,7 @@ async function curateSite(site, includeParameters = false) { | ||||||
|         url: site.url, |         url: site.url, | ||||||
|         description: site.description, |         description: site.description, | ||||||
|         slug: site.slug, |         slug: site.slug, | ||||||
|  |         tags, | ||||||
|         independent: !!parameters && parameters.independent, |         independent: !!parameters && parameters.independent, | ||||||
|         parameters: includeParameters ? parameters : null, |         parameters: includeParameters ? parameters : null, | ||||||
|         network: { |         network: { | ||||||
|  | @ -55,7 +61,7 @@ function destructConfigNetworks(networks) { | ||||||
| 
 | 
 | ||||||
| async function findSiteByUrl(url) { | async function findSiteByUrl(url) { | ||||||
|     const { hostname } = new URL(url); |     const { hostname } = new URL(url); | ||||||
|     const domain = hostname.replace(/^www./, ''); |     const domain = hostname.replace(/www.|tour./, ''); | ||||||
| 
 | 
 | ||||||
|     const site = await knex('sites') |     const site = await knex('sites') | ||||||
|         .leftJoin('networks', 'sites.network_id', 'networks.id') |         .leftJoin('networks', 'sites.network_id', 'networks.id') | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								src/tags.js
								
								
								
								
							
							
						
						|  | @ -42,6 +42,7 @@ async function matchTags(rawTags) { | ||||||
|     const tagEntries = await knex('tags') |     const tagEntries = await knex('tags') | ||||||
|         .pluck('aliases.id') |         .pluck('aliases.id') | ||||||
|         .whereIn('tags.name', tags) |         .whereIn('tags.name', tags) | ||||||
|  |         .orWhereIn('tags.slug', tags) | ||||||
|         .where(function where() { |         .where(function where() { | ||||||
|             this |             this | ||||||
|                 .whereNull('tags.alias_for') |                 .whereNull('tags.alias_for') | ||||||
|  | @ -58,15 +59,20 @@ async function matchTags(rawTags) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function associateTags(release, releaseId) { | async function associateTags(release, releaseId) { | ||||||
|     if (!release.tags || release.tags.length === 0) { |     const siteTags = release.site.tags.filter(tag => tag.inherit === true).map(tag => tag.id); | ||||||
|  | 
 | ||||||
|  |     const rawReleaseTags = release.tags || []; | ||||||
|  |     const releaseTags = rawReleaseTags.some(tag => typeof tag === 'string') | ||||||
|  |         ? await matchTags(release.tags) // scraper returned raw tags
 | ||||||
|  |         : rawReleaseTags; // tags already matched by (outdated) scraper
 | ||||||
|  | 
 | ||||||
|  |     const tags = releaseTags.concat(siteTags); | ||||||
|  | 
 | ||||||
|  |     if (tags.length === 0) { | ||||||
|         logger.info(`No tags available for (${release.site.name}, ${releaseId}) "${release.title}"`); |         logger.info(`No tags available for (${release.site.name}, ${releaseId}) "${release.title}"`); | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const tags = release.tags.some(tag => typeof tag === 'string') |  | ||||||
|         ? await matchTags(release.tags) // scraper returned raw tags
 |  | ||||||
|         : release.tags; // tags already matched by (outdated) scraper
 |  | ||||||
| 
 |  | ||||||
|     const associationEntries = await knex('releases_tags') |     const associationEntries = await knex('releases_tags') | ||||||
|         .where('release_id', releaseId) |         .where('release_id', releaseId) | ||||||
|         .whereIn('tag_id', tags); |         .whereIn('tag_id', tags); | ||||||
|  |  | ||||||