@bytebrand/export-cars 1.26.1 → 1.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/config.d.ts +2 -1
- package/dist/configs/config.js +2 -0
- package/dist/configs/config.js.map +1 -1
- package/dist/exporters/fb.exporter.js +1 -1
- package/dist/exporters/fb.exporter.js.map +1 -1
- package/dist/services/images.service.d.ts +2 -0
- package/dist/services/images.service.js +17 -2
- package/dist/services/images.service.js.map +1 -1
- package/dist/transformers/fb.transformer.js +6 -0
- package/dist/transformers/fb.transformer.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/configs/config.ts +2 -0
- package/src/exporters/fb.exporter.ts +1 -1
- package/src/formats/facebook-shop.builder.ts +59 -59
- package/src/services/images.service.ts +28 -2
- package/src/transformers/facebook-shop.transformer.ts +259 -259
- package/src/transformers/fb.transformer.ts +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/export-cars",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "bytebrand",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@bytebrand/car-schema": "^3.14.15",
|
|
20
20
|
"@bytebrand/chunkable": "^5.19.8",
|
|
21
21
|
"@bytebrand/cronicle-helper": "^1.21.8",
|
|
22
|
-
"@bytebrand/fe-financing": "^5.14.
|
|
22
|
+
"@bytebrand/fe-financing": "^5.14.12",
|
|
23
23
|
"@bytebrand/i18n-dictionaries": "^1.5.0",
|
|
24
24
|
"@bytebrand/import-helper": "^0.45.0",
|
|
25
25
|
"@bytebrand/schema-tools": "^3.18.8",
|
package/src/configs/config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export class Config {
|
|
|
16
16
|
isNewCar: boolean;
|
|
17
17
|
isCriteo: boolean;
|
|
18
18
|
exportType: ExportType;
|
|
19
|
+
environment: Environment;
|
|
19
20
|
domain: string;
|
|
20
21
|
input: {
|
|
21
22
|
stringIN: string;
|
|
@@ -59,6 +60,7 @@ export class Config {
|
|
|
59
60
|
constructor(params: Params) {
|
|
60
61
|
this.apikey = params.apikey;
|
|
61
62
|
this.exportType = params.exportType;
|
|
63
|
+
this.environment = params.environment;
|
|
62
64
|
this.cpc = params.cpc;
|
|
63
65
|
this.domain = this.environmentConfig[params.environment].domain;
|
|
64
66
|
this.baseImageUrl = this.environmentConfig[params.environment].baseImageUrl;
|
|
@@ -42,7 +42,7 @@ export class FbExporter implements Exporter {
|
|
|
42
42
|
console.timeEnd(`Transform ${data.car._id}`);
|
|
43
43
|
|
|
44
44
|
if (!car) {
|
|
45
|
-
console.log(`Skip car ${data.car._id}
|
|
45
|
+
console.log(`Skip car ${data.car._id} — no real images, or dealer ${ownerId} not in the priority list`);
|
|
46
46
|
continue;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import { escape, isNil, isNumber, isString } from 'lodash';
|
|
2
|
-
import { XMLBuilderBase } from '@bytebrand/cronicle-helper';
|
|
3
|
-
import { FacebookShopItem } from '../interfaces';
|
|
4
|
-
|
|
5
|
-
const MAX_IMAGES = 2;
|
|
6
|
-
|
|
7
|
-
export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
8
|
-
// No xmlns:g / g: prefixes here — that namespace belongs to the old Google Merchant /
|
|
9
|
-
// Facebook Shop product-catalog schema. Meta's Automotive Model Ads Vehicle Offers Feed
|
|
10
|
-
// uses plain, undecorated tag names (see facebook-shop.builder field reference above).
|
|
11
|
-
async buildStart(selfUrl?: string): Promise<void> {
|
|
12
|
-
await this.write(
|
|
13
|
-
'<?xml version="1.0" encoding="utf-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">' +
|
|
14
|
-
'\n<channel>\n<title>auto.de Vehicle Offers Feed</title>' +
|
|
15
|
-
'\n<description>Automotive Model Ads Vehicle Offers Feed für auto.de</description>' +
|
|
16
|
-
'\n<link>https://www.auto.de/</link>' +
|
|
17
|
-
`\n<atom:link href="${escape(selfUrl)}" rel="self" type="application/rss+xml" />\n`,
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async addItem(car: FacebookShopItem): Promise<void> {
|
|
22
|
-
let item = '\n<item>\n';
|
|
23
|
-
|
|
24
|
-
const { image, ...tags } = car;
|
|
25
|
-
|
|
26
|
-
for (const [tag, value] of Object.entries(tags)) {
|
|
27
|
-
item += this.buildTagElement(tag, value);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
item += image
|
|
31
|
-
.slice(0, MAX_IMAGES)
|
|
32
|
-
.map((url: string) => `<image><url>${escape(url)}</url></image>\n`)
|
|
33
|
-
.join('');
|
|
34
|
-
|
|
35
|
-
item += '</item>';
|
|
36
|
-
|
|
37
|
-
await this.write(item);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private buildTagElement(tag: string, value: unknown): string {
|
|
41
|
-
if (isNil(value) || value === '') {
|
|
42
|
-
return '';
|
|
43
|
-
}
|
|
44
|
-
// getElement() treats falsy values as "missing" — stringify numbers first so a
|
|
45
|
-
// legitimate 0 (e.g. downpayment/term_length) still renders instead of an empty tag.
|
|
46
|
-
if (isString(value)) {
|
|
47
|
-
return this.getElement(tag, escape(value));
|
|
48
|
-
}
|
|
49
|
-
if (isNumber(value)) {
|
|
50
|
-
return this.getElement(tag, String(value));
|
|
51
|
-
}
|
|
52
|
-
return this.getElement(tag, value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async buildEnd(): Promise<void> {
|
|
56
|
-
await this.write('\n</channel>\n</rss>\n');
|
|
57
|
-
await this.end();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import { escape, isNil, isNumber, isString } from 'lodash';
|
|
2
|
+
import { XMLBuilderBase } from '@bytebrand/cronicle-helper';
|
|
3
|
+
import { FacebookShopItem } from '../interfaces';
|
|
4
|
+
|
|
5
|
+
const MAX_IMAGES = 2;
|
|
6
|
+
|
|
7
|
+
export class FacebookShopXmlBuilder extends XMLBuilderBase {
|
|
8
|
+
// No xmlns:g / g: prefixes here — that namespace belongs to the old Google Merchant /
|
|
9
|
+
// Facebook Shop product-catalog schema. Meta's Automotive Model Ads Vehicle Offers Feed
|
|
10
|
+
// uses plain, undecorated tag names (see facebook-shop.builder field reference above).
|
|
11
|
+
async buildStart(selfUrl?: string): Promise<void> {
|
|
12
|
+
await this.write(
|
|
13
|
+
'<?xml version="1.0" encoding="utf-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">' +
|
|
14
|
+
'\n<channel>\n<title>auto.de Vehicle Offers Feed</title>' +
|
|
15
|
+
'\n<description>Automotive Model Ads Vehicle Offers Feed für auto.de</description>' +
|
|
16
|
+
'\n<link>https://www.auto.de/</link>' +
|
|
17
|
+
`\n<atom:link href="${escape(selfUrl)}" rel="self" type="application/rss+xml" />\n`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async addItem(car: FacebookShopItem): Promise<void> {
|
|
22
|
+
let item = '\n<item>\n';
|
|
23
|
+
|
|
24
|
+
const { image, ...tags } = car;
|
|
25
|
+
|
|
26
|
+
for (const [tag, value] of Object.entries(tags)) {
|
|
27
|
+
item += this.buildTagElement(tag, value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
item += image
|
|
31
|
+
.slice(0, MAX_IMAGES)
|
|
32
|
+
.map((url: string) => `<image><url>${escape(url)}</url></image>\n`)
|
|
33
|
+
.join('');
|
|
34
|
+
|
|
35
|
+
item += '</item>';
|
|
36
|
+
|
|
37
|
+
await this.write(item);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private buildTagElement(tag: string, value: unknown): string {
|
|
41
|
+
if (isNil(value) || value === '') {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
// getElement() treats falsy values as "missing" — stringify numbers first so a
|
|
45
|
+
// legitimate 0 (e.g. downpayment/term_length) still renders instead of an empty tag.
|
|
46
|
+
if (isString(value)) {
|
|
47
|
+
return this.getElement(tag, escape(value));
|
|
48
|
+
}
|
|
49
|
+
if (isNumber(value)) {
|
|
50
|
+
return this.getElement(tag, String(value));
|
|
51
|
+
}
|
|
52
|
+
return this.getElement(tag, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async buildEnd(): Promise<void> {
|
|
56
|
+
await this.write('\n</channel>\n</rss>\n');
|
|
57
|
+
await this.end();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -189,9 +189,15 @@ export class ImagesService {
|
|
|
189
189
|
const imageCode = marketingImage.marketingImageCodes[i];
|
|
190
190
|
const imageId = transformedImages[imageCode];
|
|
191
191
|
if (imageId) {
|
|
192
|
+
const url = this.getImagePathWithExtension(imageUrl, carId, imageId, config);
|
|
193
|
+
// meta_aia cards are composited async and may not exist yet for a given car —
|
|
194
|
+
// verify the file is real before publishing the link, not just the placeholder.
|
|
195
|
+
if (config.channelCode === 'meta_aia' && !(await this.imageExists(url))) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
192
198
|
imageWithUrls.push({
|
|
193
199
|
imageCodePredicted: imageCode,
|
|
194
|
-
url
|
|
200
|
+
url,
|
|
195
201
|
});
|
|
196
202
|
break;
|
|
197
203
|
}
|
|
@@ -208,6 +214,22 @@ export class ImagesService {
|
|
|
208
214
|
return imageWithUrls.map(i => i.url);
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
// Imbo serves a 400x300 JPEG placeholder (measured 7996 bytes on both auto.de and
|
|
218
|
+
// autode-dev.de) with a 404 status when a transformation isn't ready yet. nginx in front of
|
|
219
|
+
// Imbo has been seen caching that placeholder body regardless of status code (see
|
|
220
|
+
// nginx-image-cache-negative-caching), so check Content-Length rather than trusting the
|
|
221
|
+
// HTTP status alone.
|
|
222
|
+
private readonly PLACEHOLDER_MAX_BYTES = 8000;
|
|
223
|
+
|
|
224
|
+
private async imageExists(url: string): Promise<boolean> {
|
|
225
|
+
try {
|
|
226
|
+
const { status, headers } = await axios.head(url, { timeout: 5000 });
|
|
227
|
+
return status === 200 && Number(headers['content-length'] ?? 0) > this.PLACEHOLDER_MAX_BYTES;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
211
233
|
private transformImagesArrayIntoObject(images: ImboImageResponseInterface[]): { [imageCode: string]: string } {
|
|
212
234
|
return images.reduce((result, image) => {
|
|
213
235
|
const imageCode = image?.imageCodePredicted?.split('-')[0];
|
|
@@ -224,7 +246,11 @@ export class ImagesService {
|
|
|
224
246
|
|
|
225
247
|
protected getImagePathWithExtension(imageUrl: string, carId: string, imageId: string, config: MarketingImageConfigsInterface): string {
|
|
226
248
|
const { channelCode, imageExtension } = config;
|
|
227
|
-
|
|
249
|
+
// image-generator's MetaAiaImagesService writes files as `meta-aia-financing-{imageId}` /
|
|
250
|
+
// `meta-aia-leasing-{imageId}` (hyphenated, mode baked into the name) — not derived from
|
|
251
|
+
// channelCode the way other channels are. FB only wants the financing variant.
|
|
252
|
+
const filenamePrefix = channelCode === 'meta_aia' ? 'meta-aia-financing' : channelCode.replace(/ /g, '-');
|
|
253
|
+
return `${this.getChannelPath(imageUrl, carId)}/${filenamePrefix}-${imageId}.${imageExtension}`;
|
|
228
254
|
}
|
|
229
255
|
|
|
230
256
|
sortImages(images: ImboImageResponseInterface[]): ImboImageResponseInterface[] {
|