From 8733fdc657a377d56b306bc91e110f2c0200b4b9 Mon Sep 17 00:00:00 2001 From: Niels Simenon Date: Mon, 18 May 2020 01:22:56 +0200 Subject: [PATCH] Improved actor scraping and display. --- assets/components/actors/actor.vue | 12 +- assets/components/actors/actors.vue | 176 +++++----- assets/components/actors/photos.vue | 11 +- assets/components/header/header.vue | 284 ++++++++-------- assets/components/header/search.vue | 93 +++--- assets/components/releases/releases.vue | 106 +++--- assets/components/search/search.vue | 112 +++++-- assets/components/tile/actor.vue | 88 +++-- assets/components/tile/release.vue | 306 +++++++++--------- assets/components/tile/tag.vue | 106 +++--- assets/img/icons/boobjob.svg | 6 + assets/img/icons/forward.svg | 5 + assets/img/icons/redo2.svg | 5 + assets/img/icons/reply-all.svg | 6 + assets/img/icons/reply.svg | 5 + assets/img/icons/undo2.svg | 5 + assets/img/icons/users2.svg | 31 ++ assets/img/icons/users3.svg | 6 + assets/js/actors/actions.js | 85 ++--- assets/js/curate.js | 39 ++- assets/js/releases/actions.js | 70 ---- assets/js/ui/actions.js | 129 ++++++++ migrations/20190325001339_releases.js | 13 +- .../logos/penthouse/misc/penthouse_dark.png | Bin 0 -> 21593 bytes public/img/logos/penthouse/penthouse.png | Bin 0 -> 15050 bytes seeds/04_media.js | 1 - src/actors.js | 118 +++---- src/scrapers/bang.js | 8 +- 28 files changed, 1033 insertions(+), 793 deletions(-) create mode 100644 assets/img/icons/boobjob.svg create mode 100644 assets/img/icons/forward.svg create mode 100644 assets/img/icons/redo2.svg create mode 100644 assets/img/icons/reply-all.svg create mode 100644 assets/img/icons/reply.svg create mode 100644 assets/img/icons/undo2.svg create mode 100644 assets/img/icons/users2.svg create mode 100644 assets/img/icons/users3.svg create mode 100644 public/img/logos/penthouse/misc/penthouse_dark.png create mode 100644 public/img/logos/penthouse/penthouse.png diff --git a/assets/components/actors/actor.vue b/assets/components/actors/actor.vue index 231de1c0..3f983288 100644 --- a/assets/components/actors/actor.vue +++ b/assets/components/actors/actor.vue @@ -44,7 +44,7 @@ class="avatar-link" > @@ -153,7 +153,7 @@ {{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }} @@ -271,6 +271,10 @@ async function fetchActor() { }); } +function sfw() { + return this.$store.state.ui.sfw; +} + async function route() { await this.fetchActor(); } @@ -303,6 +307,9 @@ export default { expanded: false, }; }, + computed: { + sfw, + }, watch: { $route: route, }, @@ -495,6 +502,7 @@ export default { .enhanced.icon { fill: $primary; padding: 0 .5rem; + transform: scaleX(-1); } .ethnicity { diff --git a/assets/components/actors/actors.vue b/assets/components/actors/actors.vue index 260eedb3..6e58a500 100644 --- a/assets/components/actors/actors.vue +++ b/assets/components/actors/actors.vue @@ -1,60 +1,60 @@ @@ -226,7 +226,7 @@ export default { } } -@media(max-width: $breakpoint) { +@media(max-width: $breakpoint0) { .tiles { grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); } diff --git a/assets/components/actors/photos.vue b/assets/components/actors/photos.vue index af54d184..c10226c0 100644 --- a/assets/components/actors/photos.vue +++ b/assets/components/actors/photos.vue @@ -11,7 +11,7 @@ class="avatar-link photo-link" > @@ -26,7 +26,7 @@ class="photo-link" > @@ -35,6 +35,10 @@ diff --git a/assets/components/header/header.vue b/assets/components/header/header.vue index 95d4e867..931767a0 100644 --- a/assets/components/header/header.vue +++ b/assets/components/header/header.vue @@ -1,128 +1,128 @@ diff --git a/assets/components/header/search.vue b/assets/components/header/search.vue index 8d14a1bd..cc01941c 100644 --- a/assets/components/header/search.vue +++ b/assets/components/header/search.vue @@ -1,57 +1,64 @@ diff --git a/assets/components/releases/releases.vue b/assets/components/releases/releases.vue index ba33be91..979dffb3 100644 --- a/assets/components/releases/releases.vue +++ b/assets/components/releases/releases.vue @@ -1,72 +1,72 @@ diff --git a/assets/components/search/search.vue b/assets/components/search/search.vue index 10f70ce9..4aae2bbb 100644 --- a/assets/components/search/search.vue +++ b/assets/components/search/search.vue @@ -1,47 +1,89 @@ @@ -54,4 +96,12 @@ export default { color: $shadow; font-weight: bold; } + +.tiles { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + grid-gap: 0 .5rem; + flex-grow: 1; + margin: 0 0 1rem 0; +} diff --git a/assets/components/tile/actor.vue b/assets/components/tile/actor.vue index 4f568900..714da5df 100644 --- a/assets/components/tile/actor.vue +++ b/assets/components/tile/actor.vue @@ -8,29 +8,37 @@ class="link" > - {{ actor.name }} + + - {{ actor.name }} - + + - - {{ actor.name }} +
@@ -45,7 +53,9 @@ - + + + {{ actor.ageThen }} - - import Gender from '../actors/gender.vue'; +function sfw() { + return this.$store.state.ui.sfw; +} + export default { components: { Gender, @@ -95,6 +107,13 @@ export default { type: Object, default: null, }, + alias: { + type: Object, + default: null, + }, + }, + computed: { + sfw, }, }; @@ -137,18 +156,29 @@ export default { display: flex; align-items: center; justify-content: center; - padding: .5rem; font-weight: bold; + + .name { + padding: .5rem; + } + + .alias { + fill: var(--highlight); + } } .favicon { + font-size: 0; + padding: .5rem .25rem; + + &:last-child { + padding: .5rem; + } +} + +.favicon-icon { width: 1rem; height: 1rem; - margin: 0 .5rem 0 0; - - & + .name { - padding: 0 1rem 0 0; - } } .name { @@ -156,13 +186,13 @@ export default { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - text-align: center; } .avatar-container { display: flex; flex-grow: 1; position: relative; + overflow: hidden; } .avatar { @@ -191,6 +221,7 @@ export default { height: 1.75rem; display: flex; align-items: center; + justify-content: space-between; box-sizing: border-box; padding: .5rem; position: absolute; @@ -199,14 +230,13 @@ export default { font-weight: bold; } -.age, -.country, -.gender { - flex: 1; +.gender-age { + display: flex; + align-items: center; } .gender { - text-align: center; + margin: .25rem .25rem 0 0; } .country { @@ -216,7 +246,7 @@ export default { } .flag { - height: 1rem; + height: .75rem; margin: 0 0 0 .5rem; } diff --git a/assets/components/tile/release.vue b/assets/components/tile/release.vue index b61f8824..281ff4a3 100644 --- a/assets/components/tile/release.vue +++ b/assets/components/tile/release.vue @@ -1,175 +1,175 @@ diff --git a/assets/components/tile/tag.vue b/assets/components/tile/tag.vue index 885d4daf..82ce165b 100644 --- a/assets/components/tile/tag.vue +++ b/assets/components/tile/tag.vue @@ -1,68 +1,68 @@ diff --git a/assets/img/icons/boobjob.svg b/assets/img/icons/boobjob.svg new file mode 100644 index 00000000..e4db8562 --- /dev/null +++ b/assets/img/icons/boobjob.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/img/icons/forward.svg b/assets/img/icons/forward.svg new file mode 100644 index 00000000..69d294d5 --- /dev/null +++ b/assets/img/icons/forward.svg @@ -0,0 +1,5 @@ + + +forward + + diff --git a/assets/img/icons/redo2.svg b/assets/img/icons/redo2.svg new file mode 100644 index 00000000..becaa9b8 --- /dev/null +++ b/assets/img/icons/redo2.svg @@ -0,0 +1,5 @@ + + +redo2 + + diff --git a/assets/img/icons/reply-all.svg b/assets/img/icons/reply-all.svg new file mode 100644 index 00000000..f5e3581e --- /dev/null +++ b/assets/img/icons/reply-all.svg @@ -0,0 +1,6 @@ + + +reply-all + + + diff --git a/assets/img/icons/reply.svg b/assets/img/icons/reply.svg new file mode 100644 index 00000000..94db7312 --- /dev/null +++ b/assets/img/icons/reply.svg @@ -0,0 +1,5 @@ + + +reply + + diff --git a/assets/img/icons/undo2.svg b/assets/img/icons/undo2.svg new file mode 100644 index 00000000..a1135fe8 --- /dev/null +++ b/assets/img/icons/undo2.svg @@ -0,0 +1,5 @@ + + +undo2 + + diff --git a/assets/img/icons/users2.svg b/assets/img/icons/users2.svg new file mode 100644 index 00000000..80900d73 --- /dev/null +++ b/assets/img/icons/users2.svg @@ -0,0 +1,31 @@ + + +users2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/icons/users3.svg b/assets/img/icons/users3.svg new file mode 100644 index 00000000..ec2d3cec --- /dev/null +++ b/assets/img/icons/users3.svg @@ -0,0 +1,6 @@ + + +users3 + + + diff --git a/assets/js/actors/actions.js b/assets/js/actors/actions.js index 47a61e6e..2350052c 100644 --- a/assets/js/actors/actions.js +++ b/assets/js/actors/actions.js @@ -4,53 +4,9 @@ import { releaseActorsFragment, releaseTagsFragment, } from '../fragments'; -import { curateRelease } from '../curate'; +import { curateActor, curateRelease } from '../curate'; import getDateRange from '../get-date-range'; -function curateActor(actor) { - if (!actor) { - return null; - } - - const curatedActor = { - ...actor, - height: actor.heightMetric && { - metric: actor.heightMetric, - imperial: actor.heightImperial, - }, - weight: actor.weightMetric && { - metric: actor.weightMetric, - imperial: actor.weightImperial, - }, - origin: actor.birthCountry && { - city: actor.birthCity, - state: actor.birthState, - country: actor.birthCountry, - }, - residence: actor.residenceCountry && { - city: actor.residenceCity, - state: actor.residenceState, - country: actor.residenceCountry, - }, - scrapedAt: new Date(actor.createdAt), - updatedAt: new Date(actor.updatedAt), - }; - - if (actor.profiles && actor.profiles.length > 0) { - const photos = actor.profiles - .map(profile => profile.avatar) - .filter(avatar => avatar && (!curatedActor.avatar || avatar.hash !== curatedActor.avatar.hash)); - - curatedActor.photos = Object.values(photos.reduce((acc, photo) => ({ ...acc, [photo.hash]: photo }), {})); - } - - if (actor.releases) { - curatedActor.releases = actor.releases.map(release => curateRelease(release.release)); - } - - return curatedActor; -} - function initActorActions(store, _router) { async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) { const { before, after, orderBy } = getDateRange(range); @@ -110,6 +66,12 @@ function initActorActions(store, _router) { hash comment copyright + sfw: sfwMedia { + id + thumbnail + path + comment + } } profiles: actorsProfiles { description @@ -121,6 +83,12 @@ function initActorActions(store, _router) { hash comment copyright + sfw: sfwMedia { + id + thumbnail + path + comment + } } } birthCity @@ -202,7 +170,7 @@ function initActorActions(store, _router) { exclude: store.state.ui.filter, }); - return curateActor(actor); + return curateActor(actor, null, curateRelease); } async function fetchActors({ _commit }, { @@ -223,13 +191,16 @@ function initActorActions(store, _router) { first:$limit, orderBy: NAME_ASC, filter: { + aliasFor: { + isNull: true + } name: { startsWith: $letter - }, + } gender: { ${genderFilter} - }, - }, + } + } ) { id name @@ -249,17 +220,13 @@ function initActorActions(store, _router) { lazy comment copyright + sfw: sfwMedia { + id + thumbnail + path + comment + } } - actorsProfiles { - actorsAvatarByProfileId { - media { - id - path - thumbnail - copyright - } - } - } birthCountry: countryByBirthCountryAlpha2 { alpha2 name diff --git a/assets/js/curate.js b/assets/js/curate.js index 22c2dc48..ebb7666b 100644 --- a/assets/js/curate.js +++ b/assets/js/curate.js @@ -1,17 +1,50 @@ import dayjs from 'dayjs'; -function curateActor(actor, release) { +function curateActor(actor, release, curateActorRelease) { + if (!actor) { + return null; + } + const curatedActor = { ...actor, - origin: actor.originCountry && { - country: actor.originCountry, + height: actor.heightMetric && { + metric: actor.heightMetric, + imperial: actor.heightImperial, }, + weight: actor.weightMetric && { + metric: actor.weightMetric, + imperial: actor.weightImperial, + }, + origin: actor.birthCountry && { + city: actor.birthCity, + state: actor.birthState, + country: actor.birthCountry, + }, + residence: actor.residenceCountry && { + city: actor.residenceCity, + state: actor.residenceState, + country: actor.residenceCountry, + }, + scrapedAt: new Date(actor.createdAt), + updatedAt: new Date(actor.updatedAt), }; + if (actor.profiles && actor.profiles.length > 0) { + const photos = actor.profiles + .map(profile => profile.avatar) + .filter(avatar => avatar && (!curatedActor.avatar || avatar.hash !== curatedActor.avatar.hash)); + + curatedActor.photos = Object.values(photos.reduce((acc, photo) => ({ ...acc, [photo.hash]: photo }), {})); + } + if (release && release.date && curatedActor.birthdate) { curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year'); } + if (actor.releases) { + curatedActor.releases = actor.releases.map(actorRelease => curateActorRelease(actorRelease.release)); + } + return curatedActor; } diff --git a/assets/js/releases/actions.js b/assets/js/releases/actions.js index 11054a96..9ea26acd 100644 --- a/assets/js/releases/actions.js +++ b/assets/js/releases/actions.js @@ -28,75 +28,6 @@ function initReleasesActions(store, _router) { return releases.map(release => curateRelease(release)); } - async function searchReleases({ _commit }, { query, limit = 20 }) { - const res = await graphql(` - query SearchReleases( - $query: String! - $limit: Int = 20 - ) { - releases: searchReleases( - query: $query - first: $limit - ) { - id - title - slug - date - url - type - isNew - site { - id - slug - name - url - network { - id - slug - name - url - } - } - actors: releasesActors { - actor { - id - slug - name - } - } - tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) { - tag { - id - name - slug - } - } - poster: releasesPosterByReleaseId { - media { - id - thumbnail - lazy - } - } - covers: releasesCovers { - media { - id - thumbnail - lazy - } - } - } - } - `, { - query, - limit, - }); - - if (!res) return []; - - return res.releases.map(release => curateRelease(release)); - } - async function fetchReleaseById({ _commit }, releaseId) { // const release = await get(`/releases/${releaseId}`); @@ -114,7 +45,6 @@ function initReleasesActions(store, _router) { return { fetchReleases, fetchReleaseById, - searchReleases, }; } diff --git a/assets/js/ui/actions.js b/assets/js/ui/actions.js index a53d2b3a..5364b7eb 100644 --- a/assets/js/ui/actions.js +++ b/assets/js/ui/actions.js @@ -1,3 +1,6 @@ +import { graphql } from '../api'; +import { curateRelease, curateActor } from '../curate'; + function initUiActions(_store, _router) { function setFilter({ commit }, filter) { commit('setFilter', filter); @@ -23,7 +26,133 @@ function initUiActions(_store, _router) { localStorage.setItem('sfw', sfw); } + async function search({ _commit }, { query, limit = 20 }) { + const res = await graphql(` + query SearchReleases( + $query: String! + $limit: Int = 20 + ) { + releases: searchReleases( + query: $query + first: $limit + ) { + id + title + slug + date + url + type + isNew + site { + id + slug + name + url + network { + id + slug + name + url + } + } + actors: releasesActors { + actor { + id + slug + name + } + } + tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) { + tag { + id + name + slug + } + } + poster: releasesPosterByReleaseId { + media { + id + thumbnail + lazy + } + } + covers: releasesCovers { + media { + id + thumbnail + lazy + } + } + } + actors: searchActors( + search: $query, + first: $limit + ) { + id + name + slug + age + dateOfBirth + gender + aliasFor: actorByAliasFor { + id + name + slug + age + dateOfBirth + gender + network { + id + name + slug + } + avatar: avatarMedia { + id + path + thumbnail + lazy + comment + copyright + } + birthCountry: countryByBirthCountryAlpha2 { + alpha2 + name + alias + } + } + network { + id + name + slug + } + avatar: avatarMedia { + id + path + thumbnail + lazy + comment + copyright + } + birthCountry: countryByBirthCountryAlpha2 { + alpha2 + name + alias + } + } + } + `, { + query, + limit, + }); + + return { + releases: res.releases.map(release => curateRelease(release)), + actors: res.actors.map(actor => curateActor(actor)), + }; + } + return { + search, setFilter, setRange, setBatch, diff --git a/migrations/20190325001339_releases.js b/migrations/20190325001339_releases.js index 91b8ede5..13ce67e8 100644 --- a/migrations/20190325001339_releases.js +++ b/migrations/20190325001339_releases.js @@ -270,8 +270,6 @@ exports.up = knex => Promise.resolve() .references('id') .inTable('networks'); - table.unique(['slug', 'network_id']); - table.integer('alias_for', 12) .references('id') .inTable('actors'); @@ -794,6 +792,9 @@ exports.up = knex => Promise.resolve() ALTER TABLE releases_search ADD COLUMN document tsvector; + CREATE UNIQUE INDEX unique_actor_slugs_network ON actors (slug, network_id); + CREATE UNIQUE INDEX unique_actor_slugs ON actors (slug, (network_id IS NULL)); + CREATE TEXT SEARCH DICTIONARY traxxx_dict ( TEMPLATE = pg_catalog.simple, stopwords = traxxx @@ -825,6 +826,12 @@ exports.up = knex => Promise.resolve() url ILIKE ('%' || search || '%') $$ LANGUAGE SQL STABLE; + CREATE FUNCTION search_actors(search text, min_length numeric DEFAULT 2) RETURNS SETOF actors AS $$ + SELECT * FROM actors + WHERE length(search) >= min_length + AND name ILIKE ('%' || search || '%') + $$ LANGUAGE SQL STABLE; + CREATE FUNCTION releases_is_new(release releases) RETURNS boolean AS $$ SELECT NOT EXISTS(SELECT true FROM batches WHERE batches.id = release.created_batch_id + 1 LIMIT 1); $$ LANGUAGE sql STABLE; @@ -894,8 +901,8 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style DROP TABLE IF EXISTS countries CASCADE; DROP TABLE IF EXISTS networks CASCADE; - DROP FUNCTION IF EXISTS releases_by_tag_slugs; DROP FUNCTION IF EXISTS search_sites; + DROP FUNCTION IF EXISTS search_actors; DROP FUNCTION IF EXISTS get_random_sfw_media_id; DROP TEXT SEARCH CONFIGURATION IF EXISTS traxxx; diff --git a/public/img/logos/penthouse/misc/penthouse_dark.png b/public/img/logos/penthouse/misc/penthouse_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a2b225ffa6d3af60fddcb4cd8540a4f5daa0014d GIT binary patch literal 21593 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 literal 0 HcmV?d00001 diff --git a/public/img/logos/penthouse/penthouse.png b/public/img/logos/penthouse/penthouse.png new file mode 100644 index 0000000000000000000000000000000000000000..b4427e0a4e2f885ad8eaf19ea8ea9926ea0cfeb6 GIT binary patch literal 15050 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 E9|^{+eE { - try { - return await [].concat(source).reduce(async (outcome, scraperSlug) => outcome.catch(async () => { - const scraper = scrapers[scraperSlug]; - const siteOrNetwork = networksBySlug[scraperSlug] || sitesBySlug[scraperSlug]; - - if (!scraper?.fetchProfile) { - logger.warn(`No profile profile scraper available for ${scraperSlug}`); - throw new Error(`No profile profile scraper available for ${scraperSlug}`); - } - - if (!siteOrNetwork) { - logger.warn(`No site or network found for ${scraperSlug}`); - throw new Error(`No site or network found for ${scraperSlug}`); - } - - logger.verbose(`Searching profile for '${actor.name}' on '${scraperSlug}'`); - - const profile = await scraper.fetchProfile(actor.name, scraperSlug, siteOrNetwork, include); - - if (!profile || typeof profile === 'number') { // scraper returns HTTP code on request failure - logger.verbose(`Profile for '${actor.name}' not available on ${scraperSlug}, scraper returned ${profile}`); - throw Object.assign(new Error(`Profile for '${actor.name}' not available on ${scraperSlug}`), { code: 'PROFILE_NOT_AVAILABLE' }); - } - - return { - ...actor, - ...profile, - scraper: scraperSlug, - site: siteOrNetwork, - }; - }), Promise.reject(new Error())); - } catch (error) { - if (error.code !== 'PROFILE_NOT_AVAILABLE') { - logger.error(`Failed to fetch profile for '${actor.name}': ${error.message}`); - } - } - - return null; - }); - - return profiles.filter(Boolean); -} - async function upsertProfiles(profiles) { const curatedProfileEntries = profiles.map(profile => curateProfileEntry(profile)); @@ -403,6 +363,51 @@ async function upsertProfiles(profiles) { } } +async function scrapeProfiles(actor, sources, networksBySlug, sitesBySlug) { + const profiles = Promise.map(sources, async (source) => { + try { + return await [].concat(source).reduce(async (outcome, scraperSlug) => outcome.catch(async () => { + const scraper = scrapers[scraperSlug]; + const siteOrNetwork = networksBySlug[scraperSlug] || sitesBySlug[scraperSlug]; + + if (!scraper?.fetchProfile) { + logger.warn(`No profile profile scraper available for ${scraperSlug}`); + throw new Error(`No profile profile scraper available for ${scraperSlug}`); + } + + if (!siteOrNetwork) { + logger.warn(`No site or network found for ${scraperSlug}`); + throw new Error(`No site or network found for ${scraperSlug}`); + } + + logger.verbose(`Searching profile for '${actor.name}' on '${scraperSlug}'`); + + const profile = await scraper.fetchProfile(actor.name, scraperSlug, siteOrNetwork, include); + + if (!profile || typeof profile === 'number') { // scraper returns HTTP code on request failure + logger.verbose(`Profile for '${actor.name}' not available on ${scraperSlug}, scraper returned ${profile}`); + throw Object.assign(new Error(`Profile for '${actor.name}' not available on ${scraperSlug}`), { code: 'PROFILE_NOT_AVAILABLE' }); + } + + return { + ...actor, + ...profile, + scraper: scraperSlug, + site: siteOrNetwork, + }; + }), Promise.reject(new Error())); + } catch (error) { + if (error.code !== 'PROFILE_NOT_AVAILABLE') { + logger.error(`Failed to fetch profile for '${actor.name}': ${error.message}`); + } + } + + return null; + }); + + return profiles.filter(Boolean); +} + async function scrapeActors(actorNames) { const baseActors = toBaseActors(actorNames); @@ -438,7 +443,8 @@ async function scrapeActors(actorNames) { { concurrency: 10 }, ); - const profiles = await Promise.all(profilesPerActor.flat().map(profile => curateProfile(profile))); + const curatedProfiles = await Promise.all(profilesPerActor.flat().map(profile => curateProfile(profile))); + const profiles = curatedProfiles.filter(Boolean); if (argv.inspect) { console.log(profiles); @@ -495,31 +501,25 @@ async function associateActors(releases, batchId) { return null; } - const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({ + const baseActorsBySlug = baseActors.reduce((acc, baseActor) => ({ ...acc, - [baseActor.slug]: { - ...acc[baseActor.slug], - [baseActor.network.id]: baseActor, - }, + [baseActor.slug]: baseActor, }), {}); - const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat(); + const uniqueBaseActors = Object.values(baseActorsBySlug); const actors = await getOrCreateActors(uniqueBaseActors, batchId); - const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({ + const actorIdsBySlug = actors.reduce((acc, actor) => ({ ...acc, - [actor.network_id]: { - ...acc[actor.network_id], - [actor.slug]: actor.alias_for || actor.id, - }, + [actor.slug]: actor.alias_for || actor.id, }), {}); const releaseActorAssociations = Object.entries(baseActorsByReleaseId) .map(([releaseId, releaseActors]) => releaseActors .map(releaseActor => ({ release_id: releaseId, - actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug], + actor_id: actorIdsBySlug[releaseActor.slug], }))) .flat(); diff --git a/src/scrapers/bang.js b/src/scrapers/bang.js index 7dc51fce..df8246da 100644 --- a/src/scrapers/bang.js +++ b/src/scrapers/bang.js @@ -252,9 +252,13 @@ async function fetchProfile(actorName) { }, { encodeJSON: true }); if (res.ok) { - const actor = res.body.hits.hits.find(hit => hit._source.name === actorName); + const actor = res.body.hits.hits.find(hit => hit._source.name.toLowerCase() === actorName.toLowerCase()); - return scrapeProfile(actor._source); + if (actor) { + return scrapeProfile(actor._source); + } + + return null; } return res.status;