commas
This commit is contained in:
8
api/.babelrc.js
Normal file
8
api/.babelrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
"@babel/preset-env"
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
],
|
||||
};
|
||||
12
api/.eslintrc.js
Normal file
12
api/.eslintrc.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2019,
|
||||
sourceType: 'module',
|
||||
},
|
||||
extends: ['airbnb'],
|
||||
rules: {
|
||||
'lines-between-class-members': 0,
|
||||
'no-console': 0,
|
||||
},
|
||||
};
|
||||
4
api/.gitignore
vendored
Normal file
4
api/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.vscode/
|
||||
tmp/
|
||||
.env
|
||||
7118
api/package-lock.json
generated
Normal file
7118
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
api/package.json
Normal file
39
api/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "maanteeamet-fetch",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": "14.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "babel-node src/index.js",
|
||||
"dev": "nodemon -r dotenv/config --watch src --exec babel-node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cacheman": "2.2.1",
|
||||
"cacheman-file": "0.2.1",
|
||||
"compression": "1.7.4",
|
||||
"dotenv": "8.2.0",
|
||||
"express": "4.17.1",
|
||||
"form-data": "3.0.0",
|
||||
"helmet": "^3.22.0",
|
||||
"jsdom": "16.2.2",
|
||||
"node-fetch": "2.6.0",
|
||||
"puppeteer": "3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.8.4",
|
||||
"@babel/core": "7.9.6",
|
||||
"@babel/node": "7.8.7",
|
||||
"@babel/plugin-proposal-class-properties": "7.8.3",
|
||||
"@babel/preset-env": "7.9.6",
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "18.1.0",
|
||||
"eslint-plugin-import": "2.20.2",
|
||||
"eslint-plugin-jsx-a11y": "6.2.3",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"eslint-plugin-react-hooks": "2.5.0",
|
||||
"nodemon": "2.0.4"
|
||||
}
|
||||
}
|
||||
62
api/src/api/Fetcher.js
Normal file
62
api/src/api/Fetcher.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import {
|
||||
SEARCH_URL,
|
||||
NAVIGATION_TIMEOUT,
|
||||
TEMP_DIR,
|
||||
SITE_COOKIES,
|
||||
} from '../util/Constants';
|
||||
import Selectors from '../util/Selectors';
|
||||
|
||||
class CookieMonster {
|
||||
cache;
|
||||
browser;
|
||||
page;
|
||||
|
||||
constructor(cache) {
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
async submitForm(plate) {
|
||||
await this.page.focus(Selectors.form.plate);
|
||||
await this.page.keyboard.type(plate);
|
||||
await this.page.evaluate(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
PrimeFaces.ab({
|
||||
s: 'j_idt104:j_idt131',
|
||||
u: 'j_idt104',
|
||||
});
|
||||
});
|
||||
await this.page.waitForNavigation({
|
||||
timeout: NAVIGATION_TIMEOUT,
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
}
|
||||
|
||||
async launchPage() {
|
||||
this.browser = await puppeteer.launch();
|
||||
this.page = await this.browser.newPage();
|
||||
await this.page.goto(SEARCH_URL);
|
||||
await this.page.setCookie(...SITE_COOKIES);
|
||||
}
|
||||
|
||||
async cleanup(plate) {
|
||||
await this.page.screenshot({
|
||||
path: `${TEMP_DIR.screenshots}/${plate}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
await this.browser.close();
|
||||
}
|
||||
|
||||
async init(plate) {
|
||||
console.log(`Fetching data for ${plate}`);
|
||||
await this.launchPage();
|
||||
await this.submitForm(plate);
|
||||
const pageContent = await this.page
|
||||
.$eval(Selectors.container.main, (element) => element.innerHTML);
|
||||
await this.cleanup(plate);
|
||||
console.log(`Successfully fetched fresh data for ${plate}`);
|
||||
return pageContent;
|
||||
}
|
||||
}
|
||||
|
||||
export default CookieMonster;
|
||||
30
api/src/api/PlateRecognizr.js
Normal file
30
api/src/api/PlateRecognizr.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import fetch from 'node-fetch';
|
||||
import FormData from 'form-data';
|
||||
import fs from 'fs';
|
||||
|
||||
const ALPR_API_PATH = 'https://api.platerecognizer.com/v1/plate-reader/';
|
||||
const API_KEY = process.env.ALPR_API_KEY;
|
||||
|
||||
class PlateRecognizr {
|
||||
async handle(image) {
|
||||
const response = await this.fetch(image);
|
||||
console.log(await response.json());
|
||||
return response;
|
||||
}
|
||||
|
||||
fetch(image) {
|
||||
const body = new FormData();
|
||||
body.append('upload', fs.createReadStream('test.jpg'));
|
||||
body.append('regions', 'gb'); // Change to your country
|
||||
return fetch(ALPR_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${API_KEY}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default PlateRecognizr;
|
||||
46
api/src/components/Hack.js
Normal file
46
api/src/components/Hack.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import Scraper from './Scraper';
|
||||
import Cache from '../util/Cache';
|
||||
import Fetcher from '../api/Fetcher';
|
||||
|
||||
class Hack {
|
||||
scraper;
|
||||
cache;
|
||||
fetcher;
|
||||
api;
|
||||
|
||||
constructor() {
|
||||
this.scraper = new Scraper();
|
||||
this.cache = new Cache();
|
||||
this.fetcher = new Fetcher(this.cache);
|
||||
}
|
||||
|
||||
async getData(plate) {
|
||||
const cached = await this.cache.get(plate);
|
||||
if (cached) {
|
||||
console.log(`Using cached data for ${plate}`);
|
||||
return cached;
|
||||
}
|
||||
const data = await this.fetcher.init(plate);
|
||||
this.cache.save(plate, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
async init(plate) {
|
||||
try {
|
||||
const data = await this.getData(plate);
|
||||
this.scraper.setContent(data);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
this.scraper.scrapeBasicProperties();
|
||||
this.scraper.scrapeMainProperties();
|
||||
console.log(this.scraper.car);
|
||||
}
|
||||
|
||||
getCar() {
|
||||
return this.scraper.car;
|
||||
}
|
||||
}
|
||||
|
||||
export default Hack;
|
||||
59
api/src/components/Scraper.js
Normal file
59
api/src/components/Scraper.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import Car from '../model/Car';
|
||||
import Selectors from '../util/Selectors';
|
||||
|
||||
class Scraper {
|
||||
document;
|
||||
car;
|
||||
|
||||
setContent(text) {
|
||||
const parsedContent = new JSDOM(text).window.document;
|
||||
if (parsedContent.querySelector(Selectors.properties.main.container) === null) {
|
||||
throw Error('No data was received.. Something went wrong.');
|
||||
}
|
||||
this.document = parsedContent;
|
||||
}
|
||||
|
||||
getTextBySelector(selector) {
|
||||
return this.document.querySelector(selector).innerHTML;
|
||||
}
|
||||
|
||||
scrapeMainProperties() {
|
||||
const {
|
||||
main: selector,
|
||||
} = Selectors.properties;
|
||||
this.document
|
||||
.querySelector(selector.container)
|
||||
.querySelectorAll(selector.rows)
|
||||
.forEach((field) => {
|
||||
const value = field.querySelectorAll(selector.cell);
|
||||
let data;
|
||||
if (value[1].childElementCount > 0) {
|
||||
data = value[1].querySelector(selector.irregularText).innerHTML;
|
||||
} else {
|
||||
data = value[1].innerHTML;
|
||||
}
|
||||
let key = value[0].innerHTML;
|
||||
if (key.charAt(key.length - 1) === ':') {
|
||||
key = key.substring(0, key.length - 1).toLowerCase();
|
||||
}
|
||||
this.car[key] = data;
|
||||
});
|
||||
}
|
||||
|
||||
scrapeBasicProperties() {
|
||||
if (!this.document) {
|
||||
throw Error('No data to scrape.');
|
||||
}
|
||||
const {
|
||||
properties: selector,
|
||||
} = Selectors;
|
||||
const plate = this.getTextBySelector(selector.plate);
|
||||
const carName = this.getTextBySelector(selector.name);
|
||||
const vin = this.getTextBySelector(selector.vin);
|
||||
this.car = new Car(plate, carName, vin.substring(5));
|
||||
return this.car;
|
||||
}
|
||||
}
|
||||
|
||||
export default Scraper;
|
||||
20
api/src/index.js
Normal file
20
api/src/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import RestApi from './rest';
|
||||
import PlateRecognizr from './api/PlateRecognizr';
|
||||
|
||||
class Main {
|
||||
api;
|
||||
alpr;
|
||||
|
||||
constructor() {
|
||||
this.api = new RestApi();
|
||||
this.alpr = new PlateRecognizr();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.api.init();
|
||||
this.alpr.handle();
|
||||
}
|
||||
}
|
||||
|
||||
const main = new Main();
|
||||
main.init();
|
||||
13
api/src/model/Car.js
Normal file
13
api/src/model/Car.js
Normal file
@@ -0,0 +1,13 @@
|
||||
class Car {
|
||||
plate;
|
||||
name;
|
||||
vin;
|
||||
|
||||
constructor(plate, name, vin) {
|
||||
this.plate = plate;
|
||||
this.name = name;
|
||||
this.vin = vin;
|
||||
}
|
||||
}
|
||||
|
||||
export default Car;
|
||||
40
api/src/rest/index.js
Normal file
40
api/src/rest/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import compression from 'compression';
|
||||
import helmet from 'helmet';
|
||||
import Hack from '../components/Hack';
|
||||
|
||||
const PORT = process.env.PORT || "8000";
|
||||
|
||||
class BasicApi {
|
||||
app;
|
||||
server;
|
||||
hack;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.app.use(compression());
|
||||
this.app.use(helmet());
|
||||
this.server = http.createServer(this.app);
|
||||
this.hack = new Hack();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.server.listen(PORT, () => {
|
||||
console.log(`Server running at port ${PORT}`);
|
||||
});
|
||||
this.registerEndpoints();
|
||||
}
|
||||
|
||||
registerEndpoints() {
|
||||
this.app.get('/:plate', async ({ params }, response) => {
|
||||
const { plate } = params;
|
||||
await this.hack.init(plate);
|
||||
response
|
||||
.status(200)
|
||||
.send(this.hack.getCar());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicApi;
|
||||
34
api/src/util/Cache.js
Normal file
34
api/src/util/Cache.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import Cacheman from 'cacheman';
|
||||
import { CACHE } from './Constants';
|
||||
|
||||
const formatKey = (name) => {
|
||||
if (!name) {
|
||||
throw Error('No number plate specified');
|
||||
}
|
||||
return `${CACHE.PREFIX.plate}${name}`;
|
||||
};
|
||||
|
||||
class Cache {
|
||||
manager;
|
||||
|
||||
constructor() {
|
||||
this.manager = new Cacheman({
|
||||
ttl: CACHE.ttl,
|
||||
engine: CACHE.engine,
|
||||
tmpDir: CACHE.directory,
|
||||
});
|
||||
}
|
||||
|
||||
async get(name) {
|
||||
return this.manager.get(formatKey(name));
|
||||
}
|
||||
|
||||
save(name, data) {
|
||||
if (!data) {
|
||||
throw Error(`No data for caching car ${name}`);
|
||||
}
|
||||
this.manager.set(formatKey(name), data);
|
||||
}
|
||||
}
|
||||
|
||||
export default Cache;
|
||||
23
api/src/util/Constants.js
Normal file
23
api/src/util/Constants.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export const BASE_URL = 'https://eteenindus.mnt.ee/public/soidukDetailvaadeAvalik.jsf';
|
||||
export const SEARCH_URL = 'https://eteenindus.mnt.ee/public/soidukTaustakontroll.jsf';
|
||||
|
||||
export const SITE_COOKIES = [{
|
||||
name: 'eteenindus_lang',
|
||||
value: 'en',
|
||||
}];
|
||||
|
||||
export const NAVIGATION_TIMEOUT = 3500;
|
||||
|
||||
const TMP_DIR = 'tmp';
|
||||
export const CACHE = {
|
||||
ttl: 600,
|
||||
engine: 'file',
|
||||
directory: `${TMP_DIR}/cache`,
|
||||
PREFIX: {
|
||||
plate: 'car-',
|
||||
},
|
||||
};
|
||||
|
||||
export const TEMP_DIR = {
|
||||
screenshots: `${TMP_DIR}/screenshots`,
|
||||
};
|
||||
20
api/src/util/Selectors.js
Normal file
20
api/src/util/Selectors.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
form: {
|
||||
plate: '#j_idt104\\:regMark',
|
||||
},
|
||||
container: {
|
||||
main: '#content',
|
||||
form: '#j_idt104',
|
||||
},
|
||||
properties: {
|
||||
plate: '.content-title h1',
|
||||
name: '.content-title p:first-of-type',
|
||||
vin: '.content-title p:nth-of-type(2)',
|
||||
main: {
|
||||
container: '.asset',
|
||||
rows: '.asset-details table tbody tr',
|
||||
cell: 'td',
|
||||
irregularText: 'span:first-child', // to get past spans, superscripts and such
|
||||
},
|
||||
},
|
||||
};
|
||||
BIN
api/test.jpg
Normal file
BIN
api/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 469 KiB |
Reference in New Issue
Block a user