@bytebrand/export-cars 1.20.0 → 1.21.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/export-cars",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "",
5
5
  "author": "bytebrand",
6
6
  "license": "UNLICENSED",
@@ -28,6 +28,7 @@
28
28
  "@nestjs/cache-manager": "^2.1.0",
29
29
  "@nestjs/common": "^10.2.7",
30
30
  "@nestjs/core": "^10.2.7",
31
+ "axios": "^1.5.1",
31
32
  "bottleneck": "^2.19.5",
32
33
  "cache-manager": "^5.2.4",
33
34
  "csv-write-stream": "^2.0.0",
@@ -61,5 +62,5 @@
61
62
  "engines": {
62
63
  "node": ">=14.0.0"
63
64
  },
64
- "gitHead": "e755a559caae3bd7088cfed459bb8f80e8adc012"
65
+ "gitHead": "3ef72636ca23cee78df87f16e66089e1a532b053"
65
66
  }
@@ -12,6 +12,7 @@ export class Config {
12
12
  baseImageUrl: string;
13
13
  marketingImageUrl: string;
14
14
  imageType: ImageType;
15
+ downloadImage: boolean;
15
16
  isNewCar: boolean;
16
17
  isCriteo: boolean;
17
18
  exportType: ExportType;
@@ -10,6 +10,7 @@ export class EviumConfig extends Config {
10
10
  this.imageUrl = `https://images.${this.domain}/carimage`;
11
11
  this.mappingSpreadsheetTitle = 'mobile_yescar';
12
12
  this.imageType = ImageType.default;
13
+ this.downloadImage = true;
13
14
  this.outputFiles = [
14
15
  `${this.pathesConfig.localConvertedDirectory}/api_export_cars__${ExportType.EVIUM}.${FileType.CSV}`,
15
16
  `${this.pathesConfig.localConvertedDirectory}/api_export_cars_${ExportType.EVIUM}.${FileType.ZIP}`,
@@ -1,5 +1,5 @@
1
1
  import { writeFile } from 'fs/promises';
2
- import { basename, normalize, parse } from 'path';
2
+ import { normalize, parse } from 'path';
3
3
 
4
4
  import { Injectable } from '@nestjs/common';
5
5
  import { ModuleRef } from '@nestjs/core';
@@ -8,10 +8,10 @@ import { CronicleUtils } from '@bytebrand/cronicle-helper';
8
8
  import { FsChunkInterface } from '@bytebrand/chunkable';
9
9
  import { Zipper } from '@bytebrand/zipper';
10
10
 
11
- import { CarService, RulesService, LoaderService, ConfigService } from '../services';
11
+ import { CarService, RulesService, LoaderService, ConfigService, ImagesService } from '../services';
12
12
  import { MobileTransformer } from '../transformers/mobile.transformer';
13
- import { convertToCSV } from '../utils/converter.utils';
14
13
  import { Exporter } from '../interfaces/exporter.interface';
14
+ import { convertToCSV } from '../utils/converter.utils';
15
15
  import { FileType } from '../enums';
16
16
 
17
17
  @Injectable()
@@ -21,6 +21,7 @@ export class MobileExporter implements Exporter {
21
21
  private configService: ConfigService,
22
22
  private rulesService: RulesService,
23
23
  private loaderService: LoaderService,
24
+ private imageService: ImagesService,
24
25
  private moduleRef: ModuleRef,
25
26
  ) { }
26
27
 
@@ -30,6 +31,7 @@ export class MobileExporter implements Exporter {
30
31
  await CronicleUtils.emptyDir(this.configService.config.pathesConfig.localConvertedDirectory);
31
32
 
32
33
  const mobileTransformer = await this.moduleRef.create(MobileTransformer);
34
+ const imagesPath = [];
33
35
 
34
36
  await this.loaderService.process(async (chunk: FsChunkInterface) => {
35
37
  chunk.data = chunk.data.filter(e => this.carService.filterCar(e, this.rulesService.rules));
@@ -38,9 +40,12 @@ export class MobileExporter implements Exporter {
38
40
  for (let i = 0; i < chunk.data.length; i += 1) {
39
41
  const data = chunk.data[i];
40
42
  const { car, images } = data;
41
- // console.time(`Transform ${data.car._id}`);
42
43
  const transformedData = await mobileTransformer.transform(car, images);
43
- // console.timeEnd(`Transform ${data.car._id}`);
44
+
45
+ if (this.configService.config.downloadImage) {
46
+ const urls = await this.imageService.getImages(car, images, this.configService.config.imageType, true);
47
+ imagesPath.push(...await this.imageService.downloadInParallel(car.metaData.vehicleId, urls, this.configService.config, 10));
48
+ }
44
49
 
45
50
  await writeFile(fileName, Buffer.from(`${convertToCSV(transformedData)}\n`), { flag: 'a' });
46
51
 
@@ -50,9 +55,9 @@ export class MobileExporter implements Exporter {
50
55
  });
51
56
 
52
57
  const fileZIP = normalize(`${parse(fileName).dir}/${parse(fileName).name}.${FileType.ZIP}`);
53
- console.log(fileZIP);
58
+
54
59
  if (await CronicleUtils.exists(fileName)) {
55
- await Zipper.zip({ fileList: [fileName], dest: fileZIP });
60
+ await Zipper.zip({ fileList: [fileName, ...imagesPath], dest: fileZIP });
56
61
  }
57
62
  console.log(this.carService.counter.getCountedCars());
58
63
  }
@@ -1,4 +1,6 @@
1
- import { get } from 'lodash';
1
+ import { createWriteStream } from 'fs';
2
+ import { extname } from 'path';
3
+ import axios from 'axios';
2
4
 
3
5
  import { Injectable } from '@nestjs/common';
4
6
 
@@ -7,9 +9,10 @@ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
7
9
  import { DataSchemes } from '@bytebrand/car-schema';
8
10
 
9
11
  import { FB_MAX_IMAGE_COUNT_FOR_DEAFULT } from '../constants/fb.constants';
10
- import { isNotEmptyArray } from '../utils';
11
12
  import { ConfigService } from './config.service';
12
13
  import { UserService } from './user.service';
14
+ import { isNotEmptyArray } from '../utils';
15
+ import { Config } from '../configs/config';
13
16
  import { ImageType } from '../enums';
14
17
 
15
18
  @Injectable()
@@ -84,30 +87,21 @@ export class ImagesService {
84
87
 
85
88
  constructor(private readonly configService: ConfigService, private readonly userService: UserService) { }
86
89
 
87
- async getImages(
88
- car: DataSchemes.Car,
89
- images: ImboImageResponseInterface[],
90
- imageType: ImageType,
91
- ignoreDefImages: boolean,
92
- ): Promise<Array<string>> {
90
+ async getImages(car: DataSchemes.Car, images: ImboImageResponseInterface[], imageType: ImageType, ignoreDefImages: boolean): Promise<Array<string>> {
93
91
  if (imageType === ImageType.default) {
94
- return this.getDefaultImages(car, images, ignoreDefImages);
92
+ return this.getDefaultImages(car._id, images, ignoreDefImages);
95
93
  } else {
96
- return await this.getMarketingImages(car, images, ignoreDefImages);
94
+ return await this.getMarketingImages(car._id, images, ignoreDefImages);
97
95
  }
98
96
  }
99
97
 
100
- getDefaultImages(
101
- car: DataSchemes.Car,
102
- images: ImboImageResponseInterface[],
103
- ignoreDefImages: boolean,
104
- ): Array<string> {
98
+ getDefaultImages(carId: string, images: ImboImageResponseInterface[], ignoreDefImages: boolean): Array<string> {
105
99
  if (!isNotEmptyArray(images)) {
106
100
  return this.DEFAULT_IMAGES;
107
101
  }
108
102
 
109
103
  const sortedImages = this.sortImages(images);
110
- const transformedImages = this.generateImageLinks(sortedImages, get(car, '_id'));
104
+ const transformedImages = this.generateImageLinks(sortedImages, carId);
111
105
 
112
106
  if (!ignoreDefImages && transformedImages.length < 17) {
113
107
  transformedImages.push(...this.DEFAULT_IMAGES);
@@ -122,11 +116,7 @@ export class ImagesService {
122
116
  });
123
117
  }
124
118
 
125
- async getMarketingImages(
126
- car: DataSchemes.Car,
127
- images: ImboImageResponseInterface[],
128
- ignoreDefImages: boolean,
129
- ): Promise<string[]> {
119
+ async getMarketingImages(carId: string, images: ImboImageResponseInterface[], ignoreDefImages: boolean): Promise<string[]> {
130
120
  const imageUrl = this.configService.config.marketingImageUrl;
131
121
 
132
122
  const transformedImages = this.transformImagesArrayIntoObject(this.sortImages(images));
@@ -140,7 +130,7 @@ export class ImagesService {
140
130
  const imageCode = marketingImage.marketingImageCodes[i];
141
131
  const imageId = transformedImages[imageCode];
142
132
  if (imageId) {
143
- imageUrls.push(this.getImagePathWithExtension(imageUrl, get(car, '_id', ''), imageId, config));
133
+ imageUrls.push(this.getImagePathWithExtension(imageUrl, carId, imageId, config));
144
134
  break;
145
135
  }
146
136
  }
@@ -167,24 +157,48 @@ export class ImagesService {
167
157
  return `${imageUrl}/${[carId[0], carId[1], carId[2], carId].join('/')}`;
168
158
  }
169
159
 
170
- protected getImagePathWithExtension(
171
- imageUrl: string,
172
- carId: string,
173
- imageId: string,
174
- config: MarketingImageConfigsInterface,
175
- ): string {
160
+ protected getImagePathWithExtension(imageUrl: string, carId: string, imageId: string, config: MarketingImageConfigsInterface): string {
176
161
  const { channelCode, imageExtension } = config;
177
162
  return `${this.getChannelPath(imageUrl, carId)}/${channelCode.replace(/ /g, '-')}-${imageId}.${imageExtension}`;
178
163
  }
179
164
 
180
165
  sortImages(images: ImboImageResponseInterface[]): ImboImageResponseInterface[] {
181
166
  return images.sort((a, b) => {
182
- const imageCodePredictedA = a.imageCodePredicted.includes('-') ? a.imageCodePredicted.split('-')[1] : a.imageCodePredicted;
183
- const imageCodePredictedB = b.imageCodePredicted.includes('-') ? b.imageCodePredicted.split('-')[1] : b.imageCodePredicted;
167
+ const imageCodePredictedA = a.imageCodePredicted.includes('-') ? a.imageCodePredicted.split('-')[0] : a.imageCodePredicted;
168
+ const imageCodePredictedB = b.imageCodePredicted.includes('-') ? b.imageCodePredicted.split('-')[0] : b.imageCodePredicted;
184
169
  const orderA = this.IMAGES_SORT_ORDER[imageCodePredictedA] || 100;
185
170
  const orderB = this.IMAGES_SORT_ORDER[imageCodePredictedB] || 100;
186
-
171
+
187
172
  return orderA - orderB;
188
173
  });
189
174
  }
175
+
176
+ async downloadInParallel(vehicleId: string, urls: string[], config: Config, concurrency = 1): Promise<string[]> {
177
+ for (let i = 0; i < urls.length; i += concurrency) {
178
+ const downloadPromises = urls.map(
179
+ (url: string, j: number) => this.download(
180
+ url,
181
+ `${config.pathesConfig.localConvertedDirectory}/${vehicleId}_${(i + 1).toLocaleString('en-US', { minimumIntegerDigits: 2 })}.jpeg`
182
+ )
183
+ );
184
+ return Promise.all(downloadPromises);
185
+ }
186
+ return [];
187
+ }
188
+
189
+ private async download(url: string, imagePath: string) {
190
+ return new Promise<string>(async resolve => {
191
+ try {
192
+ const response = await axios.get(url, { responseType: 'stream' });
193
+ response.data.pipe(
194
+ createWriteStream(imagePath)
195
+ .on('finish', () => { console.log(`Downloaded: ${url}`); resolve(imagePath); })
196
+ .on('error', (err) => { console.error(`Failed to download: ${url}`); resolve(null); })
197
+ );
198
+ } catch (error) {
199
+ console.error(`Failed to download: ${url}`);
200
+ resolve(null);
201
+ }
202
+ });
203
+ };
190
204
  }
@@ -12,6 +12,7 @@ import { MappingRules } from '../interfaces/mapping-rules.interface';
12
12
  import { isNotEmptyArray } from '../utils';
13
13
  import { ConfigService } from '../services';
14
14
  import { ExportType } from '../enums';
15
+ import axios from "axios";
15
16
 
16
17
  const vinGenerator = require('vin-generator');
17
18
 
@@ -24,7 +25,7 @@ export class MobileTransformer {
24
25
  private userService: UserService,
25
26
  private configService: ConfigService,
26
27
  private imagesService: ImagesService,
27
- ) {}
28
+ ) { }
28
29
 
29
30
  async transform(carData: DataSchemes.Car, images: ImboImageResponseInterface[]): Promise<unknown> {
30
31
  this.currentCar = carData;
@@ -364,6 +365,9 @@ export class MobileTransformer {
364
365
  false,
365
366
  );
366
367
  let imagesAsALine;
368
+ // console.log(' --> transformedImages', transformedImages)
369
+ if (this.configService.config.campaign == ExportType.EVIUM) return null;
370
+
367
371
  if (isNotEmptyArray(transformedImages)) {
368
372
  imagesAsALine = transformedImages.map((el: string) => el).join(';');
369
373
  }