diff --git a/app/src/api.js b/app/src/api.js
index 43467bc8..9294b826 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -825,3 +825,15 @@ export async function uploadFiles(files, destination) {
data.append('destination', destination);
console.log(data);
}
+
+export async function fetchDecoded(vinNumber) {
+ const body = {vin_number: vinNumber};
+ const response = await apiPost(`${API_URI}/vin_number_decoder`, body);
+ if (response.status === 200) {
+ const content = await response.json();
+ return content['vin'];
+ } else if (response.status === 400) {
+ console.error('Invalid VIN Number');
+ return null;
+ }
+}
diff --git a/app/src/components/Apps.js b/app/src/components/Apps.js
index 2117ac46..e6936ecd 100644
--- a/app/src/components/Apps.js
+++ b/app/src/components/Apps.js
@@ -1,11 +1,11 @@
import React, {useContext} from 'react';
-import {Divider, SegmentGroup, StatisticLabel, StatisticValue} from "semantic-ui-react";
+import {Divider, Input, SegmentGroup, StatisticLabel, StatisticValue, TableCell, TableRow} from "semantic-ui-react";
import {Link, Route, Routes} from "react-router-dom";
import {decryptOTP, encryptOTP} from "../api";
import {humanFileSize, mimetypeColor, PageContainer, toLocaleString, useTitle} from "./Common";
import {ThemeContext} from "../contexts/contexts";
-import {Button, Header, Loader, Segment, Statistic, StatisticGroup, TextArea} from "./Theme";
-import {useStatistics} from "../hooks/customHooks";
+import {Button, Header, Loader, Segment, Statistic, StatisticGroup, Table, TextArea} from "./Theme";
+import {useStatistics, useVINDecoder} from "../hooks/customHooks";
class Encrypt extends React.Component {
constructor(props) {
@@ -145,6 +145,57 @@ function OTP() {
>
}
+function VINDecoder() {
+ useTitle('VIN Number Decoder');
+ const {t} = useContext(ThemeContext);
+
+ const basicKeys = ['Country', 'Manufacturer', 'Region', 'Years'];
+ const detailsKeys = ['Body', 'Engine', 'Model', 'Plant', 'Transmission', 'Serial'];
+
+ const {value, setValue, vin} = useVINDecoder();
+ console.debug(vin);
+
+ let body =
Enter a VIN number above
;
+ if (value && !vin) {
+ body = VIN number is invalid
;
+ }
+ if (vin && vin['country']) {
+ let details = No details
;
+ if (vin['body']) {
+ details =
+ {detailsKeys.map(i =>
+ {i}
+ {vin[i.toLowerCase()] || '(Unknown)'}
+ )}
+
;
+ }
+ body = <>
+
+ {basicKeys.map(i =>
+ {i}
+ {vin[i.toLowerCase()]}
+ )}
+
+
+ {details}
+ >;
+ }
+
+ return <>
+
+ setValue(e.target.value)}
+ />
+
+
+ {body}
+ >
+}
+
+
function StatisticsPage() {
useTitle('Statistics');
@@ -251,6 +302,7 @@ export function MoreRoute(props) {
}/>
}/>
+ }/>
}
diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js
index fb0c20ab..9d47cb3b 100644
--- a/app/src/components/Nav.js
+++ b/app/src/components/Nav.js
@@ -17,6 +17,7 @@ const links = [
links: [
{to: '/more/otp', text: 'One Time Pad', end: true},
{to: '/more/statistics', text: 'Statistics', end: true},
+ {to: '/more/vin', text: 'VIN Decoder', end: true},
]
},
];
diff --git a/app/src/hooks/customHooks.js b/app/src/hooks/customHooks.js
index 6ef990d5..cba63f24 100644
--- a/app/src/hooks/customHooks.js
+++ b/app/src/hooks/customHooks.js
@@ -1,5 +1,6 @@
import {useContext, useEffect, useRef, useState} from "react";
import {
+ fetchDecoded,
fetchDomains,
fetchFilesProgress,
filesSearch,
@@ -928,3 +929,29 @@ export const useUploadFile = () => {
return {files, setFiles: handleFilesChange, progresses, destination, setDestination, doClear, doUpload}
}
+
+export const useVINDecoder = (defaultVINNumber = '') => {
+ const [value, setValue] = useState(defaultVINNumber);
+ // The response vin from the API.
+ const [vin, setVin] = useState(null);
+
+ const localFetchDecoded = async () => {
+ if (!value) {
+ return;
+ }
+
+ try {
+ const vin = await fetchDecoded(value);
+ setVin(vin);
+ } catch (e) {
+ console.error('Failed to decode VIN number');
+ setVin(null);
+ }
+ }
+
+ useEffect(() => {
+ localFetchDecoded();
+ }, [value]);
+
+ return {value, setValue, vin}
+}
diff --git a/requirements.txt b/requirements.txt
index a49f06bd..a75af6b3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,6 +22,7 @@ sanic-testing==22.6.0
sanic==22.6.2
selenium==4.9.1
srt==3.5.3
+vininfo[cli]==1.7.0
websockets==10.1
webvtt-py==0.4.6
-yt-dlp==2023.3.4
+yt-dlp==2023.3.4
\ No newline at end of file
diff --git a/wrolpi/root_api.py b/wrolpi/root_api.py
index d4aeb682..9e4d6970 100644
--- a/wrolpi/root_api.py
+++ b/wrolpi/root_api.py
@@ -7,12 +7,15 @@ from http import HTTPStatus
from pathlib import Path
from typing import Union
+import vininfo.exceptions
from sanic import Sanic, response, Blueprint, __version__ as sanic_version
from sanic.blueprint_group import BlueprintGroup
from sanic.request import Request
from sanic.response import HTTPResponse
from sanic_ext import validate
from sanic_ext.extensions.openapi import openapi
+from vininfo import Vin
+from vininfo.details._base import VinDetails
from wrolpi import admin, status, flags, schema
from wrolpi import tags
@@ -421,6 +424,39 @@ async def post_notify(_: Request, body: schema.NotifyRequest):
return response.empty(HTTPStatus.CREATED)
+@api_bp.post('/vin_number_decoder')
+@validate(schema.VINDecoderRequest)
+async def post_vin_number_decoder(_: Request, body: schema.VINDecoderRequest):
+ try:
+ vin = Vin(body.vin_number)
+ except vininfo.exceptions.VininfoException:
+ return json_response({}, HTTPStatus.BAD_REQUEST)
+
+ def detail_to_json(details: VinDetails, key: str):
+ if details:
+ attr = getattr(details, key)
+ if attr and attr.name:
+ name = attr.name
+ if isinstance(name, list):
+ return ', '.join(str(i) for i in name)
+ else:
+ return name
+
+ vin = schema.VIN(
+ country=vin.country,
+ manufacturer=vin.manufacturer,
+ region=vin.region,
+ years=','.join(map(str, vin.years)),
+ body=detail_to_json(vin.details, 'body'),
+ engine=detail_to_json(vin.details, 'engine'),
+ model=detail_to_json(vin.details, 'model'),
+ plant=detail_to_json(vin.details, 'plant'),
+ transmission=detail_to_json(vin.details, 'transmission'),
+ serial=detail_to_json(vin.details, 'serial'),
+ )
+ return json_response(dict(vin=vin))
+
+
class CustomJSONEncoder(json.JSONEncoder):
def default(self, obj):
diff --git a/wrolpi/schema.py b/wrolpi/schema.py
index 16d83751..c3c5c82b 100644
--- a/wrolpi/schema.py
+++ b/wrolpi/schema.py
@@ -82,3 +82,41 @@ class DeleteTagRequest:
class NotifyRequest:
message: str
url: str = None
+
+
+@dataclass
+class VINDecoderRequest:
+ vin_number: str
+
+@dataclass
+class VIN:
+ country: str
+ manufacturer: str
+ region: str
+ years: str
+ body: str = None
+ engine: str = None
+ model: str = None
+ plant: str = None
+ transmission: str = None
+ serial: str = None
+
+ def __json__(self):
+ d = dict(
+ country=self.country,
+ manufacturer=self.manufacturer,
+ region=self.region,
+ years=self.years,
+ body=self.body,
+ engine=self.engine,
+ model=self.model,
+ plant=self.plant,
+ transmission=self.transmission,
+ serial=self.serial,
+ )
+ return d
+
+
+@dataclass
+class VINDecoderResponse:
+ vin: VIN
diff --git a/wrolpi/test/test_root_api.py b/wrolpi/test/test_root_api.py
index f307590f..7b4c6d9b 100644
--- a/wrolpi/test/test_root_api.py
+++ b/wrolpi/test/test_root_api.py
@@ -358,3 +358,27 @@ def test_get_downloaders(test_client):
def test_get_global_statistics(test_session, test_client):
request, response = test_client.get('/api/statistics')
assert response.json['global_statistics']['db_size'] > 1
+
+
+def test_post_vin_number_decoder(test_client):
+ """A VIN number can be decoded."""
+ # Invalid VIN number.
+ content = dict(vin_number='5N1NJ01CXST00000')
+ request, response = test_client.post('/api/vin_number_decoder', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.BAD_REQUEST
+ assert response.json == dict()
+
+ # Test VIN from vininfo.
+ content = dict(vin_number='5N1NJ01CXST000001')
+ request, response = test_client.post('/api/vin_number_decoder', content=json.dumps(content))
+ assert response.status_code == HTTPStatus.OK
+ assert response.json['vin'] == {'body': 'Sedan, 4-Door, Standard Body Truck',
+ 'country': 'United States',
+ 'engine': 'VH45DE',
+ 'manufacturer': 'Nissan',
+ 'model': 'Maxima',
+ 'plant': 'Tochigi, Oppama',
+ 'region': 'North America',
+ 'serial': None,
+ 'transmission': None,
+ 'years': '1995'}