This commit is contained in:
2020-05-25 16:07:30 +03:00
parent 0753359f07
commit 13971100cf
55 changed files with 32279 additions and 6964 deletions

8
api/.babelrc.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
presets: [
"@babel/preset-env"
],
plugins: [
'@babel/plugin-proposal-class-properties',
],
};

12
api/.eslintrc.js Normal file
View 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
View File

@@ -0,0 +1,4 @@
node_modules/
.vscode/
tmp/
.env

7118
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
api/package.json Normal file
View 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
View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB