This commit is contained in:
Cat /dev/Nulo 2022-10-08 18:45:43 -03:00
commit a6033f5fe6
4 changed files with 354 additions and 0 deletions

103
main.go Normal file
View file

@ -0,0 +1,103 @@
package main
import (
"encoding/json"
"io"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
type TokenResponse struct {
Error string `json:"error"`
AccessToken string `json:"access_token"`
}
func authorizeRoute(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
if len(code) == 0 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error":"missing code"}`))
return
}
values := make(url.Values)
values["grant_type"] = []string{"authorization_code"}
values["client_id"] = []string{os.Getenv("APP_ID")}
values["client_secret"] = []string{os.Getenv("SECRET_KEY")}
values["code"] = []string{code}
values["redirect_uri"] = []string{os.Getenv("REDIRECT_URI")}
resp, err := http.PostForm("https://api.mercadolibre.com/oauth/token", values)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"ML API error"}`))
return
}
tokenResp := TokenResponse{}
rawJson, err := io.ReadAll(resp.Body)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"ML API error"}`))
return
}
err = resp.Body.Close()
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"ML API error"}`))
return
}
err = json.Unmarshal(rawJson, &tokenResp)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(`{"error":"ML API error"}`))
return
}
// if len(tokenResp.Error) != 0 {
// log.Println(string(rawJson))
// w.WriteHeader(http.StatusInternalServerError)
// w.Write([]byte(`{"error":"ML API error"}`))
// return
// }
w.Write([]byte(rawJson))
}
func proxyRoute(res http.ResponseWriter, req *http.Request) {
// parse the url
url, _ := url.Parse("https://api.mercadolibre.com/")
// create the reverse proxy
proxy := httputil.NewSingleHostReverseProxy(url)
// Update the headers to allow for SSL redirection
req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
req.Header.Set("X-Forwarded-Host", "")
req.Header.Set("X-Forwarded-For", "")
req.Header.Set("Origin", "")
req.Header.Set("Referer", "")
req.Host = url.Host
// Note that ServeHttp is non blocking and uses a go routine under the hood
proxy.ServeHTTP(res, req)
}
func main() {
fs := http.FileServer(http.Dir("static"))
http.Handle("/", fs)
http.HandleFunc("/authorize", authorizeRoute)
http.HandleFunc("/pictures/items/upload", proxyRoute)
http.HandleFunc("/items", proxyRoute)
log.Fatalln(http.ListenAndServe(":6969", nil))
}

5
readme.md Normal file
View file

@ -0,0 +1,5 @@
# Odio nombrar cosas y odio a MercadoLibre
Una forma más rápida de vender libros en MercadoLibre. Lee el ISBN del libro con un escaner de código de barras, lo busca en la API de [openlibrary.org](https://openlibrary.org), consigue los metadatos y de paso te busca los precios. Super beta, proyecto personal, quizás lo mejore.
Requiere un servidor de API para conseguir el access token sin exponer el client secret y porque MeLi no sabe configurar CORS bien.

19
static/html5-qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

227
static/index.html Normal file
View file

@ -0,0 +1,227 @@
<!doctype html>
<meta charset=utf-8>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<style>
html, body {
margin: 0;
}
#files-preview {
height: 50vh;
overflow: scroll;
border: 1px solid;
}
#files-preview img {
width: 50%;
}
</style>
<fieldset>
<legend>Fotos</legend>
<input multiple type=file accept="image/*" capture=environment>
<ul id=files-preview></ul>
</fieldset>
<fieldset>
<legend>ISBN/metadatos</legend>
<div id="reader" width="1800px"></div>
<input name=isbn placeholder=ISBN>
<input name=title placeholder=Titulo>
<input name=author placeholder=Autor>
<input name=publish-year placeholder="Año publicación">
<input name=publisher placeholder=Editorial>
</fieldset>
<fieldset>
<input name=price placeholder=Precio type=number>
<button id=force-prices-preview>Buscar con titulo y autor</button>
<ul id=prices-preview></ul>
</fieldset>
<button id=go>Go!</button>
<ul id=log></ul>
<script src=html5-qrcode.min.js></script>
<script>
const goAuthorize = () =>
location.href = `https://auth.mercadolibre.com.ar/authorization?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${location.protocol}//${location.host}`
const code = (new URLSearchParams(location.search)).get('code')
if (!code && !localStorage.accessToken) goAuthorize()
async function getAccessToken() {
if (localStorage.accessToken) return localStorage.accessToken
const request = await fetch("/authorize?code="+code)
const json = await request.json()
if (json.error === 'invalid_grant') {
goAuthorize()
return
}
localStorage.accessToken = json.access_token
return json.access_token
}
const $ = s => document.querySelector(s)
const filesEl = $('input[type=file]')
const filesPreviewEl = $('#files-preview')
const pricesPreviewEl = $('#prices-preview')
const logEl = $('#log')
function appendLog (msg) {
const itemEl = document.createElement('li')
itemEl.append(msg)
logEl.append(itemEl)
}
const goEl = $('#go')
const inputs = Object.fromEntries([
'isbn',
'title',
'author',
'publish-year',
'publisher',
'price',
'access-token',
].map(
name => [name, $(`input[name=${name}]`)]
))
async function onScanSuccess(decodedText, decodedResult) {
alert(`Code matched = ${decodedText}`);
inputs.isbn.value = decodedText
const bibkey = `ISBN:${decodedText}`
const request = await fetch(`https://openlibrary.org/api/books?bibkeys=${bibkey}&jscmd=details&format=json`)
const json = await request.json()
inputs.title.value = json[bibkey]?.details?.title || ''
inputs.author.value = json[bibkey]?.details?.authors[0]?.name || ''
getPricePreview()
inputs.publisher.value = json[bibkey]?.details?.publishers[0] || ''
inputs['publish-year'].value = json[bibkey]?.details?.publish_date?.match(/\d{4}/)[0] || ''
}
async function getPricePreview() {
let params = new URLSearchParams()
params.set('q', `${inputs.title.value} ${inputs.author.value} usado`)
params.set('limit', '50')
try {
const request = await fetch(`https://api.mercadolibre.com/sites/MLA/search?${params}`)
const json = await request.json()
if (!json.results) throw new Error(JSON.stringify(json))
pricesPreviewEl.innerHTML=''
for (const result of json.results) {
const itemEl = document.createElement('li')
itemEl.append(`${result.title} - ${result.condition} - ${result.price}`)
pricesPreviewEl.append(itemEl)
}
} catch (error) { alert(error) }
}
$('#force-prices-preview').addEventListener('click', getPricePreview)
function onScanFailure(error) {
console.warn(`Code scan error = ${error}`);
}
let html5QrcodeScanner = new Html5QrcodeScanner(
"reader",
{
fps: 10,
formatsToSupport: [Html5QrcodeSupportedFormats.EAN_13],
experimentalFeatures: {
useBarCodeDetectorIfSupported: true
},
rememberLastUsedCamera: true,
aspectRatio: 1.7777778
},
/* verbose= */ false)
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
filesEl.addEventListener('change', async event => {
for (const file of event.target.files) {
const itemEl = document.createElement('li')
const imgEl = new Image
imgEl.src = URL.createObjectURL(file)
itemEl.append(imgEl)
const deleteBtnEl = document.createElement('button')
deleteBtnEl.append("Borrar")
deleteBtnEl.addEventListener('click', event => {
event.target.parentElement.remove()
})
itemEl.append(deleteBtnEl)
const id = await uploadImageToML(file)
imgEl.dataset.id = id
filesPreviewEl.append(itemEl)
}
})
async function uploadImageToML(file) {
const formData = new FormData
formData.set("file", file)
appendLog(`Subiendo ${file.name}`)
//https://api.mercadolibre.com proxy
const request = await fetch('/pictures/items/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${await getAccessToken()}`,
},
body: formData,
})
const json = await request.json()
appendLog(`[${file.name}] Subido con ID ${json.id}`)
return json.id
}
// categoria Libros, Revistas y Comics: MLA3025
goEl.addEventListener('click', async event => {
try {
const imageIds = [...filesPreviewEl.querySelectorAll('img')].map(
img => img.dataset.id
)
// https://developers.mercadolibre.com.ar/es_ar/publica-productos
const publishing = {
"title":inputs.title.value,
"category_id":"MLA412445",
"price":inputs.price.value,
"currency_id":"ARS",
"available_quantity":1,
"buying_mode":"buy_it_now",
"condition":"used",
"listing_type_id":"free",
"sale_terms":[
],
"pictures": imageIds.map(id => ({id})),
"attributes":[
//https://api.mercadolibre.com/categories/MLA3025/attributes
{ "id":"BOOK_TITLE", "value_name":inputs.title.value },
{ "id":"AUTHOR", "value_name":inputs.author.value },
{ "id":"BOOK_PUBLISHER", "value_name":inputs.publisher.value },
{ "id":"GTIN", "value_name":inputs.isbn.value },
{ "id":"FORMAT", "value_name":"Fisico" },
{ "id":"ITEM_CONDITION", "value_name":"Usado" },
]
}
appendLog(`Creando publicación`)
//https://api.mercadolibre.com proxy
const request = await fetch("/items", {
method: 'POST',
headers: {
'Authorization': `Bearer ${await getAccessToken()}`,
},
body: JSON.stringify(publishing),
})
const json = await request.json()
if (!json.permalink) throw new Error(JSON.stringify(json))
location.href = json.permalink
} catch (error) {
alert(error)
}
})
</script>