Compare commits

..

14 Commits

Author SHA1 Message Date
85e7887b65 API
Some checks failed
Ik ben Henk python api ftp deploy / Deploy Python api (push) Has been cancelled
2025-12-01 14:52:22 +01:00
a7fe9d8614 API
Some checks failed
Ik ben Henk python api ftp deploy / Deploy Python api (push) Has been cancelled
2025-12-01 13:27:54 +01:00
0bfc2fd9c5 Dockerify
Some checks failed
Ik ben Henk python api ftp deploy / Deploy Python api (push) Has been cancelled
2025-12-01 13:18:53 +01:00
51d4f30de3 first commit
Some checks failed
Ik ben Henk python api ftp deploy / Deploy Python api (push) Has been cancelled
2025-12-01 13:16:37 +01:00
6fe1292086 initial
Some checks failed
Ik ben Henk python api ftp deploy / Deploy Python api (push) Has been cancelled
2025-12-01 13:03:14 +01:00
958000ce36 initial 2025-12-01 13:02:56 +01:00
3a2c744ed6 all 2025-12-01 13:02:25 +01:00
Computerboer
83033c3c8d Merge pull request #1 from ComputerBoer/main
Main
2025-08-05 20:10:30 +02:00
Computerboer
5d5bbb12b2 Merge branch 'master' into main 2025-08-05 20:10:07 +02:00
Computerboer
b0182c606d Adder filter to auctionviewer to have lots 2025-07-30 23:52:53 +02:00
Computerboer
9a67af7480 Adder filter to have lots 2025-07-30 23:48:48 +02:00
Computerboer
86921652e3 Try to remove duplicate auctions from location 2025-07-20 20:38:30 +02:00
Computerboer
5713fcb0cf Made twk auctions show on all locations 2025-07-20 16:27:56 +02:00
Computerboer
bea8a41670 Added auctionport auctions 2025-07-20 15:43:38 +02:00
25 changed files with 2378 additions and 198 deletions

2
.gitignore vendored
View File

@@ -369,3 +369,5 @@ FodyWeavers.xsd
/myenv /myenv
*.tar.gz *.tar.gz
allauctions_NL.json allauctions_NL.json
.idea/

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
# Dockerfile
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# (optioneel: system deps toevoegen als later nodig voor DB/libs)
RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Python deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# App code
COPY . .
EXPOSE 5000
# Simpel: zelfde als startup.bat, maar dan Linux
CMD ["python", "app.py"]

View File

@@ -1 +1,4 @@
# PythonAuctionviewer # PythonAuctionviewer
## Run dev server
To run the development server run `python app.py` in the root of the project

11
app.py
View File

@@ -7,7 +7,7 @@ from utils.auctionutils import getAuctionlocations
from models.location import JsonEncoder from models.location import JsonEncoder
app = Flask(__name__) app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": ["http://localhost:4200","https://auctionviewer.ikbenhenk.nl"]}}) CORS(app, resources={r"/*": {"origins": ["*"]}})
application = app # our hosting requires application in passenger_wsgi application = app # our hosting requires application in passenger_wsgi
@app.route("/") @app.route("/")
@@ -28,7 +28,7 @@ def getAllAuctions(countrycode):
except Exception as e: except Exception as e:
log('something went wrong ') log('something went wrong ')
print_exc(e) print_exc()
return 'internal server error', 500 return 'internal server error', 500
@app.route("/v2/refreshauction/<countrycode>") @app.route("/v2/refreshauction/<countrycode>")
@@ -43,12 +43,13 @@ def refreshAllAuctions(countrycode):
except Exception as e: except Exception as e:
log('something went wrong with refreshing the auctions') log('something went wrong with refreshing the auctions')
print_exc(e) print_exc()
return 'internal server error' + e, 500 return 'internal server error', 500
if __name__ == "__main__": if __name__ == "__main__":
app.run() # run our Flask app app.run(host="0.0.0.0", port=5000) # ipv alleen app.run()

151
bindToInterface.c Normal file
View File

@@ -0,0 +1,151 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
//#define DEBUG
//compile with gcc -nostartfiles -fpic -shared bindToInterface.c -o bindToInterface.so -ldl -D_GNU_SOURCE
//Use with BIND_INTERFACE=<network interface> LD_PRELOAD=./bindInterface.so <your program> like curl ifconfig.me
int bind_to_source_ip(int sockfd, const char *source_ip)
{
struct sockaddr_in source_addr;
memset(&source_addr, 0, sizeof(source_addr));
source_addr.sin_family = AF_INET;
source_addr.sin_addr.s_addr = inet_addr(source_ip);
return bind(sockfd, (struct sockaddr *)&source_addr, sizeof(source_addr));
}
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
int *(*original_connect)(int, const struct sockaddr *, socklen_t);
original_connect = dlsym(RTLD_NEXT, "connect");
static struct sockaddr_in *socketAddress;
socketAddress = (struct sockaddr_in *)addr;
char *dest = inet_ntoa(socketAddress->sin_addr);
if (socketAddress->sin_family == AF_INET)
{
unsigned short port = ntohs(socketAddress->sin_port);
char *DNSIP_env = getenv("DNS_OVERRIDE_IP");
char *DNSPort_env = getenv("DNS_OVERRIDE_PORT");
int port_new = port;
if (port == 53 && DNSIP_env != NULL && strlen(DNSIP_env) > 0)
{
if (DNSPort_env != NULL && strlen(DNSPort_env) > 0)
{
port_new = atoi(DNSPort_env);
socketAddress->sin_port = htons(port_new);
}
#ifdef DEBUG
printf("Detected DNS query to: %s:%i, overwriting with %s:%i \n", dest, port, DNSIP_env, port_new);
#endif
socketAddress->sin_addr.s_addr = inet_addr(DNSIP_env);
}
port = port_new;
dest = inet_ntoa(socketAddress->sin_addr); //with #include <arpa/inet.h>
#ifdef DEBUG
printf("connecting to: %s:%i \n", dest, port);
#endif
bool IPExcluded = false;
char *bind_excludes = getenv("BIND_EXCLUDE");
if (bind_excludes != NULL && strlen(bind_excludes) > 0)
{
bind_excludes = (char*) malloc(strlen(getenv("BIND_EXCLUDE")) * sizeof(char) + 1);
strcpy(bind_excludes,getenv("BIND_EXCLUDE"));
char sep[] = ",";
char *iplist;
iplist = strtok(bind_excludes, sep);
while (iplist != NULL)
{
if(!strncmp(dest,iplist,strlen(iplist)))
{
IPExcluded = true;
#ifdef DEBUG
printf("IP %s excluded by IP-List, not binding to interface %s\n", dest, getenv("BIND_INTERFACE"));
#endif
break;
}
iplist = strtok(NULL, sep);
}
free(bind_excludes);
}
if (!IPExcluded) //Don't bind when destination is localhost, because it couldn't be reached anymore
{
char *bind_addr_env;
bind_addr_env = getenv("BIND_INTERFACE");
char *source_ip_env;
source_ip_env = getenv("BIND_SOURCE_IP");
struct ifreq interface;
int errorCode;
if (bind_addr_env != NULL && strlen(bind_addr_env) > 0)
{
// printf(bind_addr_env);
struct ifreq boundInterface =
{
.ifr_name = "none",
};
socklen_t optionlen = sizeof(boundInterface);
errorCode = getsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &boundInterface, &optionlen);
if (errorCode < 0)
{
//getsockopt should not fail
perror("getsockopt");
return -1;
};
#ifdef DEBUG
printf("Bound Interface: %s.", boundInterface.ifr_name);
#endif
if (!strcmp(boundInterface.ifr_name, "none") || strcmp(boundInterface.ifr_name, bind_addr_env))
{
#ifdef DEBUG
printf(" Socket not bound to desired interface (Bound to: %s). Binding to interface: %s\n", boundInterface.ifr_name, bind_addr_env);
#endif
strcpy(interface.ifr_name, bind_addr_env);
errorCode = setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &interface, sizeof(interface)); //Will fail if socket is already bound to another interface
if (errorCode < 0)
{
perror("setsockopt");
errno = ENETUNREACH; //Let network appear unreachable for maximum security when desired interface is not available
return -1;
};
}
}
if(source_ip_env != NULL && strlen(source_ip_env) > 0){
if (bind_to_source_ip(sockfd, source_ip_env) < 0){
perror("bind_to_source_ip failed");
return -1;
}
}
if(!(source_ip_env != NULL && strlen(source_ip_env) > 0) && !(bind_addr_env != NULL && strlen(bind_addr_env) > 0)){
printf("Warning: Program with LD_PRELOAD started, but BIND_INTERFACE environment variable not set\n");
fprintf(stderr, "Warning: Program with LD_PRELOAD started, but BIND_INTERFACE environment variable not set\n");
}
}
}
return (uintptr_t)original_connect(sockfd, addr, addrlen);
}

View File

@@ -1,4 +1,5 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import os
import os.path import os.path
from pathlib import Path from pathlib import Path
import time import time
@@ -9,19 +10,20 @@ from utils.helperutils import log
cache = {} cache = {}
class Cache(): class Cache():
def get(key, notOlderThanHours = 24): def get(key, notOlderThanHours=24):
#print('get key ' + key) # print('get key ' + key)
if key in cache: if key in cache:
cacheobj = cache[key] cacheobj = cache[key]
if(not cache): if (not cache):
return None return None
if(cacheobj.isOlderThanHours(notOlderThanHours)): if (cacheobj.isOlderThanHours(notOlderThanHours)):
log('removing cacheobject ' + key) log('removing cacheobject ' + key)
del cache[key] del cache[key]
return None return None
# log( 'returning cacheobject ' + key) # log( 'returning cacheobject ' + key)
return cacheobj.obj return cacheobj.obj
def add(key, obj): def add(key, obj):
log('adding cacheobject ' + key) log('adding cacheobject ' + key)
@@ -33,7 +35,7 @@ class CacheObj:
def __init__(self, key, obj): def __init__(self, key, obj):
self.key = key self.key = key
self.obj = obj self.obj = obj
self.time=datetime.now() self.time = datetime.now()
def isOlderThanHours(self, hours): def isOlderThanHours(self, hours):
# log('checking time cacheobject ' + self.key + ': ' + str(self.time) + " < " + str(datetime.now() - timedelta(hours=hours))) # log('checking time cacheobject ' + self.key + ': ' + str(self.time) + " < " + str(datetime.now() - timedelta(hours=hours)))
@@ -41,31 +43,36 @@ class CacheObj:
class FileCache(): class FileCache():
def get(key, notOlderThanHours = None): def get(key, notOlderThanHours=None):
filepath = "./filecache/" + key + ".json" filepath = "./filecache/" + key + ".json"
cachefile = Path(filepath) cachefile = Path(filepath)
if cachefile.is_file(): if cachefile.is_file():
ti_m = os.path.getmtime(filepath) ti_m = os.path.getmtime(filepath)
if(notOlderThanHours is not None): if (notOlderThanHours is not None):
#checks last modified age of file, and removes it if it is too old # checks last modified age of file, and removes it if it is too old
log('checking time cachefile ' + filepath + ': ' + str(ti_m) + " < " + str(time.time() - (3600 * notOlderThanHours))) log('checking time cachefile ' + filepath + ': ' + str(ti_m) + " < " + str(time.time() - (3600 * notOlderThanHours)))
if( ti_m < time.time() - (3600 * notOlderThanHours)): if (ti_m < time.time() - (3600 * notOlderThanHours)):
log('removing old filecache') log('removing old filecache')
os.remove(filepath) os.remove(filepath)
return None return None
with open(filepath) as json_file: with open(filepath) as json_file:
json_data = json.load(json_file) json_data = json.load(json_file)
log('returning json data from cachefile: ' + key) log('returning json data from cachefile: ' + key)
return json_data return json_data
return None return None
def add(key, obj): def add(key, obj):
log('adding filecacheobject ' + key) log('adding filecacheobject ' + key)
json_data = JsonEncoder().encode(obj) json_data = JsonEncoder().encode(obj)
# json_data = json.dumps(obj, cls=JsonEncoder, indent=2)
# Ensure the filecache directory exists
os.makedirs("./filecache", exist_ok=True)
with open("./filecache/" + key + ".json", 'w+') as f: with open("./filecache/" + key + ".json", 'w+') as f:
f.write(json_data) f.write(json_data)

View File

@@ -465,6 +465,7 @@
2743947 Zevenbergen Zevenbergen Zevenbergen,ze fen bei heng,zefenberugen,Зевенберген,ゼーフェンベルゲン,澤芬貝亨 51.645 4.60417 P PPL NL 06 1709 0 5 Europe/Amsterdam 2017-10-17 2743947 Zevenbergen Zevenbergen Zevenbergen,ze fen bei heng,zefenberugen,Зевенберген,ゼーフェンベルゲン,澤芬貝亨 51.645 4.60417 P PPL NL 06 1709 0 5 Europe/Amsterdam 2017-10-17
2743948 Gemeente Zevenaar Gemeente Zevenaar Gemeente Zevenaar,Zevenaar 51.95376 6.07727 A ADM2 NL 03 0299 44096 9 Europe/Amsterdam 2023-01-31 2743948 Gemeente Zevenaar Gemeente Zevenaar Gemeente Zevenaar,Zevenaar 51.95376 6.07727 A ADM2 NL 03 0299 44096 9 Europe/Amsterdam 2023-01-31
2743949 Zevenaar Zevenaar Zevenar,Зевенар 51.93 6.07083 P PPL NL 03 0299 26063 12 Europe/Amsterdam 2017-10-17 2743949 Zevenaar Zevenaar Zevenar,Зевенар 51.93 6.07083 P PPL NL 03 0299 26063 12 Europe/Amsterdam 2017-10-17
2743949 omgeving zevenaar omgeving zevenaar Zevenar,Зевенар 51.93 6.07083 P PPL NL 03 0299 26063 12 Europe/Amsterdam 2017-10-17
2743950 Zeumersche Beek Zeumersche Beek 52.16452 5.53953 H STM NL 03 0 5 Europe/Amsterdam 2011-04-19 2743950 Zeumersche Beek Zeumersche Beek 52.16452 5.53953 H STM NL 03 0 5 Europe/Amsterdam 2011-04-19
2743951 Zeumeren Zeumeren 52.17256 5.60208 P PPL NL 03 0203 0 12 Europe/Amsterdam 2009-01-17 2743951 Zeumeren Zeumeren 52.17256 5.60208 P PPL NL 03 0203 0 12 Europe/Amsterdam 2009-01-17
2743952 Zetten Zetten 51.92833 5.71389 P PPL NL 03 1734 2985 10 Europe/Amsterdam 2017-10-17 2743952 Zetten Zetten 51.92833 5.71389 P PPL NL 03 1734 2985 10 Europe/Amsterdam 2017-10-17
@@ -13230,6 +13231,7 @@
2757247 Den Bramel Den Bramel 52.12176 6.3111 S CSTL NL 03 1876 0 15 Europe/Amsterdam 2011-06-04 2757247 Den Bramel Den Bramel 52.12176 6.3111 S CSTL NL 03 1876 0 15 Europe/Amsterdam 2011-06-04
2757248 Den Braam Den Braam 52.1375 6.84028 P PPL NL 15 0158 0 38 Europe/Amsterdam 2007-06-03 2757248 Den Braam Den Braam 52.1375 6.84028 P PPL NL 15 0158 0 38 Europe/Amsterdam 2007-06-03
2757251 Den Bosch Den Bosch 52.07795 6.13621 S EST NL 03 0213 0 13 Europe/Amsterdam 2011-06-04 2757251 Den Bosch Den Bosch 52.07795 6.13621 S EST NL 03 0213 0 13 Europe/Amsterdam 2011-06-04
2757251 's hertogenbosch 's hertogenbosch 52.07795 6.13621 S EST NL 03 0213 0 13 Europe/Amsterdam 2011-06-04
2757252 Polder Den Bommel Polder Den Bommel 51.70597 4.28369 T PLDR NL 11 0 -2 Europe/Amsterdam 2011-04-19 2757252 Polder Den Bommel Polder Den Bommel 51.70597 4.28369 T PLDR NL 11 0 -2 Europe/Amsterdam 2011-04-19
2757253 Den Bommel Den Bommel Bommel 51.71583 4.28472 P PPL NL 11 1924 0 -1 Europe/Amsterdam 2017-10-17 2757253 Den Bommel Den Bommel Bommel 51.71583 4.28472 P PPL NL 11 1924 0 -1 Europe/Amsterdam 2017-10-17
2757254 Polder Den Bol Polder Den Bol 51.71665 4.75829 T PLDR NL 06 0 -1 Europe/Amsterdam 2011-04-19 2757254 Polder Den Bol Polder Den Bol 51.71665 4.75829 T PLDR NL 06 0 -1 Europe/Amsterdam 2011-04-19

6
haha.bat Normal file
View File

@@ -0,0 +1,6 @@
@echo off
:: adjust path below to your install location
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
:: compile the given file(s)
cl /EHsc /W4 /O2 %*

View File

@@ -1,19 +1,25 @@
from enum import Enum from enum import Enum
import enum
from json import JSONEncoder from json import JSONEncoder
import json
class Countrycode(Enum): class Countrycode(Enum):
NL = "NL", NL = "NL",
DE = "DE", DE = "DE",
BE = "BE" BE = "BE"
class Auctionbrand(str, Enum): class Auctionbrand(str, Enum):
NONE = "NONE", NONE = "NONE",
TWK = "TWK" TWK = "TWK"
OVM = "OVM" OVM = "OVM"
AP = "AP"
class GeonameLocation: class GeonameLocation:
def __init__(self, geonameid = 0, name = "", asciiname = "", alternatenames = [], latitude = 0, longitude = 0, countrycode:Countrycode = Countrycode.NL, modificationdate = "") : def __init__(self, geonameid=0, name="", asciiname="", alternatenames=[], latitude=0, longitude=0,
countrycode: Countrycode = Countrycode.NL, modificationdate=""):
self.geonameid = geonameid self.geonameid = geonameid
self.name = name self.name = name
self.asciiname = asciiname self.asciiname = asciiname
@@ -23,16 +29,20 @@ class GeonameLocation:
self.countrycode = countrycode self.countrycode = countrycode
self.modificationdate = modificationdate self.modificationdate = modificationdate
class Maplocation: class Maplocation:
def __init__(self, lat = 0, long = 0, numberofauctions = 0, geonamelocation:GeonameLocation = None, auctions = []): def __init__(self, lat=0, long=0, numberofauctions=0, geonamelocation: GeonameLocation = None, auctions=[]):
self.lat = lat self.lat = lat
self.long = long self.long = long
self.numberofauctions = numberofauctions self.numberofauctions = numberofauctions
self.geonamelocation = geonamelocation self.geonamelocation = geonamelocation
self.auctions = auctions self.auctions = auctions
class Auction: class Auction:
def __init__(self, auctionbrand: Auctionbrand = Auctionbrand.NONE, city = "", countrycode:Countrycode = Countrycode.NL, name = "", starttime = None, closingtime = None, url = "", imageurl = "", numberoflots = 0, geonamelocation: GeonameLocation = None): def __init__(self, auctionbrand: Auctionbrand = Auctionbrand.NONE, city="", countrycode: Countrycode = Countrycode.NL, name="",
starttime=None, closingtime=None, url="", imageurl="", numberoflots=0, geonamelocation: GeonameLocation = None,
multiplelocations=False):
self.city = city self.city = city
self.countrycode = countrycode self.countrycode = countrycode
self.name = name self.name = name
@@ -43,7 +53,35 @@ class Auction:
self.numberoflots = numberoflots self.numberoflots = numberoflots
self.geonamelocation = geonamelocation self.geonamelocation = geonamelocation
self.brand = auctionbrand self.brand = auctionbrand
self.multiplelocations = multiplelocations
class JsonEncoder(JSONEncoder): class JsonEncoder(JSONEncoder):
def default(self, o): # def default(self, o):
return o.__dict__ # return o.__dict__
# try 2
def default(self, obj):
# Only serialize public, instance-level attributes
if hasattr(obj, '__dict__'):
return {
key: self.serialize(value)
for key, value in obj.__dict__.items()
if not key.startswith('_') # skip private/protected
}
return super().default(obj)
def serialize(self, value):
if isinstance(value, list):
return [self.serialize(item) for item in value]
elif isinstance(value, dict):
return {k: self.serialize(v) for k, v in value.items()}
elif isinstance(value, enum.Enum):
return value.name # or value.value
elif hasattr(value, '__dict__'):
return self.default(value) # dive into nested object
else:
try:
json.dumps(value)
return value
except (TypeError, OverflowError):
return str(value)

View File

@@ -1,7 +1,7 @@
from app import app as application from app import app as application
import importlib.util import importlib.util
spec = importlib.util.spec_from_file_location("wsgi", "app.py") spec = importlib.util.spec_from_file_location("wsgi", "app.py")
wsgi = importlib.util.module_from_spec(spec) wsgi = importlib.util.module_from_spec(spec)
spec.loader.exec_module(wsgi) spec.loader.exec_module(wsgi)

View File

@@ -1,7 +1,7 @@
pip==9.0.1 pip>=23.3
setuptools==28.8.0 setuptools>=78.1.1
flask===3.0.3 flask===3.1.1
werkzeug==3.0.0 werkzeug>=3.0.6
flask_cors===5.0.0 flask_cors>=6.0.0
requests===2.27.1 requests>=2.32.4
pathlib===1.0.1 pathlib===1.0.1

View File

@@ -11,6 +11,6 @@ import requests
response = requests.get('https://api.auctionviewer.ikbenhenk.nl//v2/refreshauction/NL') response = requests.get('https://api.auctionviewer.ikbenhenk.nl//v2/refreshauction/NL')
if(response.status_code ==200): if(response.status_code ==200):
print('ran getauctions request successfull') print('ran getauctions request successful')
else: else:
print('A error occurred while running the getauctions request') print('A error occurred while running the getauctions request')

26
startup.bat Normal file
View File

@@ -0,0 +1,26 @@
@echo off
echo ====================================
echo Auctionviewer API Startup Script
echo ====================================
echo.
echo [1/3] Installing dependencies...
python -m pip install --upgrade pip
pip install -r requirements.txt
echo.
echo [2/3] Starting API server...
start /B python app.py
timeout /t 3 /nobreak >nul
echo.
echo [3/3] Fetching auctions...
echo.
echo --- NL Auctions ---
curl http://localhost:5000/v2/auction/NL
echo.
echo.
echo ====================================
echo Startup complete!
echo API running at http://localhost:5000
echo ====================================

79
utils/APutils.py Normal file
View File

@@ -0,0 +1,79 @@
from datetime import datetime
import json
import re
from time import ctime
from traceback import print_exc
import requests
from cache import Cache
from models.location import Auction, Auctionbrand, Countrycode, JsonEncoder
from utils.helperutils import log
def getAPAuctions():
cachename = 'AuctionPort_'
res = Cache.get(cachename)
if (res):
return res
try:
response = requests.get("https://api.auctionport.be/auctions/small?size=100&page=1")
except:
log("The Auctionport auctions call threw a error")
if (response is None):
return []
if (response.status_code == 200):
log('Got AP Auctions')
try:
data = response.json()
pages = data['pages']
auctions = []
for i in range(0, pages - 1, 1):
log("getting page " + str(i) + ' of ' + str(pages))
# if(i > 1):
response = requests.get("https://api.auctionport.be/auctions/small?size=100&page=" + str(i))
if (response is None): continue
if (response.status_code != 200): continue
data = response.json()
for PAauction in data['data']:
# makes sure that the locations array is filled with at least one location
if PAauction['locations'] == []:
PAauction['locations'] = [PAauction['location']]
# makes sure that the auction is still active
closingdate = datetime.fromisoformat(PAauction['closingDate'])
if (closingdate.date() < datetime.now().date()): continue
if (PAauction['lotCount'] <= 0): continue
multipleLocations = len(PAauction['locations']) > 1
for location in PAauction['locations']:
if not location.endswith('Nederland'): continue
loc = re.sub('Nederland', '', location)
# loc = location.split(",")
postalcodeRegex = r'(.*?)[1-9][0-9]{3} ?(?!sa|sd|ss)[a-zA-Z]{2}'
city = re.sub(postalcodeRegex, '', loc) # removes postalcode and everything in front of it
city = city.strip() # removes whitespace
city = city.strip(',') # removes trailing and leading ,
city = city.split(',') # splits on , to overcome not matching regex
city = city[len(city) - 1]
city = city.strip()
newauction = Auction(Auctionbrand.AP, city, Countrycode.NL, PAauction['title'],
datetime.fromisoformat(PAauction['openDate']),
datetime.fromisoformat(PAauction['closingDate']), 'https://www.auctionport.be/auction/' + str(PAauction['id']),
PAauction['imageUrl'], PAauction['lotCount'], None, multipleLocations)
auctions.append(newauction)
Cache.add(cachename, auctions)
return auctions
except Exception as e:
log(e.__cause__ + '-- Something went wrong in the mapping of AP auctions to auctionviewer objects. The reason was: ' + response.reason + '. The response was: ' + JsonEncoder().encode(
response.json()))
print_exc(e)
else:
log("The AP auctions call didn't gave a 200 response but a " + str(response.status_code) + ". With the reason: " + response.reason)
return []

55
utils/OVMutils.py Normal file
View File

@@ -0,0 +1,55 @@
from traceback import print_exc
import requests
from cache import Cache
from models.location import Auction, Auctionbrand, JsonEncoder
from utils.helperutils import log
def getOVMAuctions():
cachename = 'OnlineVeiling_'
res = Cache.get(cachename)
if (res):
return res
try:
response = requests.get("https://onlineveilingmeester.nl/rest/nl/veilingen?status=open&domein=ONLINEVEILINGMEESTER")
except:
log("The OVM auctions call threw a error")
if (response is None):
return []
if (response.status_code == 200):
log('Got Ovm Auctions')
try:
data = response.json()
auctions = []
for result in data['veilingen']:
cityname = "Nederland" if result['isBezorgVeiling'] else result['afgifteAdres']['plaats']
cityname = "Nederland" if cityname is None else cityname # there can be auctions where you have to make an appointment to retrieve the lots
startdatetime = result['openingsDatumISO'].replace("T", " ").replace("Z", "")
enddatetime = result['sluitingsDatumISO'].replace("T", " ").replace("Z", "")
image = ""
# if hasattr(result, 'image') : #result['image'] :
image = result.get('image', "") # ['image']
if image == "":
images = result.get('imageList')
if (len(images) > 0):
image = images[0]
else:
log("No image found for OVM auction: " + result['naam'])
a = Auction(Auctionbrand.OVM, cityname, result['land'], result['naam'], startdatetime, enddatetime,
'https://onlineveilingmeester.nl/' + str(result['land']).lower() + '/veilingen/' + str(result['id']) + '/kavels', 'https://onlineveilingmeester.nl/images/150x150/' + image,
result['totaalKavels'])
auctions.append(a)
Cache.add(cachename, auctions)
return auctions
except Exception as e:
log(e.__cause__ + '-- Something went wrong in the mapping of OVM auctions to auctionviewer objects. The reason was: ' + response.reason + '. The response was: ' + JsonEncoder().encode(
response.json()))
print_exc(e)
else:
log("The OVM auctions call didn't gave a 200 response but a " + str(response.status_code) + ". With the reason: " + response.reason)
return []

83
utils/TWKutils.py Normal file
View File

@@ -0,0 +1,83 @@
from datetime import datetime
import math
import re
import requests
from cache import Cache
from models.location import Auction, Auctionbrand
from utils.helperutils import log
def getTWKUrl():
response = requests.get('https://www.troostwijkauctions.com/')
if (response.status_code == 200):
buildid = re.search(r'"buildId":"([^"]*)', response.text, re.MULTILINE)
twkDataUrl = 'https://www.troostwijkauctions.com/_next/data/' + str(buildid[1]) + '/nl/'
log('buildid: ' + str(buildid[1]))
log('twkDataUrl: ' + twkDataUrl)
return twkDataUrl
return None
def getTwkAuctions(countrycode):
cachename = 'TwkAuctions_' + countrycode
res = Cache.get(cachename)
if (res):
return res
# buildidresponse = requests.get('https://www.troostwijkauctions.com/')
twkDataUrl = getTWKUrl()
if (twkDataUrl is None):
return []
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode)
if (response.status_code == 200):
log('Got Twk Auctions')
data = response.json()
auctions = []
totalAuctionCount = data['pageProps']['totalSize'];
pages = math.ceil(totalAuctionCount / data['pageProps']['pageSize'])
# for result in data['pageProps']['auctionList']:
for i in range(1, pages, 1):
log("getting page " + str(i) + ' of ' + str(pages))
if (i > 1):
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode + "&page=" + str(i))
data = response.json()
for twka in data['pageProps']['listData']:
# print(twka['urlSlug'])
auctionlocations = getTWKAuction(twka)
for auction in auctionlocations:
auctions.append(auction)
Cache.add(cachename, auctions)
return auctions
return []
def getTWKAuction(twka):
locations = []
cities = []
if (len(twka['collectionDays']) > 0):
cities = twka['collectionDays']
elif (len(twka['deliveryCountries']) > 0):
cities = [{'countryCode': 'nl', 'city': 'Nederland'}]
image = ''
if (len(twka['images']) > 0 and twka['images'][0]['url']):
image = twka['images'][0]['url']
for city in cities:
if (city['countryCode'].upper() != 'NL'): continue
a = Auction(Auctionbrand.TWK, city['city'], city['countryCode'].upper(), twka['name'], datetime.fromtimestamp(twka['startDate']),
datetime.fromtimestamp(twka['minEndDate']), 'https://www.troostwijkauctions.com/a/' + twka['urlSlug'], image, twka['lotCount'], None, len(cities) > 1)
locations.append(a)
return locations

481
utils/__auction_items.py Normal file
View File

@@ -0,0 +1,481 @@
"""
Utility functions to fetch auction items (lots) from different providers.
This module defines three functions that make HTTP calls to the public APIs of
Troostwijk Auctions (TWK), AuctionPort (AP) and Online Veilingmeester (OVM)
and normalises their responses into Python dictionaries. Each function
returns a list of dictionaries where each dictionary represents an
individual lot and includes standardised keys: ``title``, ``description``,
``bids`` (the number of bids if available), ``current_bid`` (current price
and currency if available), ``image_url`` and ``end_time``.
The implementations rely on the ``requests`` library for HTTP transport
and include basic error handling. They raise ``requests.HTTPError``
when the remote server responds with a non200 status code.
Note: the APIs these functions call are subject to change. Endpoints and
field names may differ depending on the auction status or provider version.
These functions are intended as a starting point for integrating with
multiple auction platforms; you may need to adjust query parameters,
header values or JSON field names if the provider updates their API.
Examples
--------
```
from auction_items import get_items_twk, get_items_ap, get_items_ovm
# Troostwijk Auctions (TWK): pass the visible auction identifier
lots = get_items_twk(display_id="35563")
for lot in lots:
print(lot['lot_number'], lot['title'], lot['current_bid'])
# AuctionPort (AP): pass the auction ID from the AuctionPort website
ap_lots = get_items_ap(auction_id=1323)
# Online Veilingmeester (OVM): the country code is required to build the
# endpoint path (e.g. ``'nederland'`` or ``'belgie'``) along with the
# numeric auction ID.
ovm_lots = get_items_ovm(country="nederland", auction_id=7713)
```
"""
from __future__ import annotations
import json
import logging
from typing import Dict, List, Optional
import requests
logger = logging.getLogger(__name__)
def get_items_twk(
display_id: str,
*,
page: int = 1,
page_size: int = 200,
locale: str = "nl",
platform: str = "WEB",
request_session: Optional[requests.Session] = None,
) -> List[Dict[str, Optional[str]]]:
"""Fetch lots (items) for a Troostwijk auction using the GraphQL API.
Troostwijk Auctions exposes its public data through a GraphQL endpoint at
``https://storefront.tbauctions.com/storefront/graphql``. The
``auctionWithLotsV5`` query returns a list of lots for a given auction.
According to the GraphQL documentation, the query accepts a
``request`` object of type ``AuctionWithLotsInputV3`` and a
``platform`` argument. The ``request`` object requires the auction's
``displayId`` (a string identifier visible in the URL of the auction
page), ``locale`` (language code), ``pageNumber``, ``pageSize`` and
two lists for range and value facets. The return type
``AuctionWithListingLots`` contains an ``auction`` and a list of
``lots`` with details such as the lot number, title, description,
current bid and images【561575328263299†screenshot】. Fields included in
this function's query correspond to those documented in the schema.
Parameters
----------
display_id: str
The humanreadable identifier of the auction (e.g. ``"35563"``).
page: int, optional
The page number of results (defaults to 1). The API uses
1based page numbering. A page size of 200 appears sufficient
for most auctions.
page_size: int, optional
The maximum number of lots to fetch per page (defaults to 200).
locale: str, optional
Language/locale code for the content (defaults to ``"nl"``).
platform: str, optional
Platform enumeration value required by the API (default
``"WEB"``). Other values may include ``"B2B"`` or ``"B2C"``;
consult the GraphQL documentation if you encounter an error.
request_session: Optional[requests.Session], optional
An existing requests session to reuse connections. If omitted,
a temporary session is created for this call.
Returns
-------
List[Dict[str, Optional[str]]]
A list of dictionaries. Each dictionary represents a lot and
contains the keys ``lot_number``, ``title``, ``description``,
``bids`` (number of bids, if provided), ``current_bid`` (a
dictionary with ``amount`` and ``currency`` or ``None`` if no bid),
``image_url`` (first image) and ``end_time`` (auction end time in
ISO 8601 format).
Raises
------
requests.HTTPError
If the HTTP response has a non200 status code.
Exception
For other errors such as JSON decoding failures.
"""
session = request_session or requests.Session()
url = "https://storefront.tbauctions.com/storefront/graphql"
# GraphQL query string. The fields selected here mirror those
# described in the GraphQL documentation for the ``auctionWithLotsV5``
# operation【561575328263299†screenshot】. Additional fields can be added
# if necessary.
graphql_query = """
query AuctionWithLots($request: AuctionWithLotsInputV3!, $platform: Platform!) {
auctionWithLotsV5(request: $request, platform: $platform) {
lots {
lotNumber
id
title
description
numberOfBids
currentBid {
amount
currency
}
endDateISO
images {
url
}
}
}
}
"""
# Build the variables for the query. The request object must include
# ``displayId``, ``locale``, ``pageNumber``, ``pageSize``, and two empty
# lists for range and value facets as required by the schema【835513158978214†screenshot】.
variables = {
"request": {
"displayId": str(display_id),
"locale": locale,
"pageNumber": page,
"pageSize": page_size,
# These facets are optional; empty lists mean no filters
"rangeFacetInputs": [],
"valueFacetInputs": [],
},
"platform": platform,
}
headers = {
# A typical browser may send JSON content; set an Accept header
"Accept": "application/json",
"Content-Type": "application/json",
# The GraphQL service uses a CSRF protection token; a random
# ``x-csrf-token`` header can be supplied if needed. Leaving it
# empty usually works for public queries.
"x-csrf-token": "",
}
response = session.post(
url,
json={"query": graphql_query, "variables": variables},
headers=headers,
timeout=30,
)
# Raise an HTTPError for non200 responses
try:
response.raise_for_status()
except requests.HTTPError:
logger.error("Troostwijk API returned status %s: %s", response.status_code, response.text)
raise
# Parse the JSON body
data = response.json()
# Check for GraphQL errors
if "errors" in data and data["errors"]:
message = data["errors"]
logger.error("GraphQL returned errors: %s", message)
raise Exception(f"GraphQL returned errors: {message}")
lots = []
# Navigate the nested structure to the list of lots. The path
# matches the GraphQL selection set defined above.
try:
lot_items = data["data"]["auctionWithLotsV5"]["lots"]
except (KeyError, TypeError) as e:
logger.error("Unexpected response structure from Troostwijk API: %s", data)
raise Exception(f"Unexpected response structure: {e}")
for item in lot_items:
# Some fields may be missing; use dict.get with defaults
lot_number = item.get("lotNumber")
title = item.get("title")
description = item.get("description")
bids = item.get("numberOfBids")
current_bid = item.get("currentBid")
end_time = item.get("endDateISO")
images = item.get("images", []) or []
image_url = images[0]["url"] if images else None
lots.append(
{
"lot_number": lot_number,
"title": title,
"description": description,
"bids": bids,
"current_bid": current_bid,
"image_url": image_url,
"end_time": end_time,
}
)
return lots
def get_items_ap(
auction_id: int,
*,
request_session: Optional[requests.Session] = None,
) -> List[Dict[str, Optional[str]]]:
"""Retrieve items (lots) from an AuctionPort auction.
AuctionPort operates a JSON API on ``https://api.auctionport.be``. While
official documentation for the lot endpoints is scarce, community code
suggests that auctions can be fetched via ``/auctions/small``【461010206788258†L10-L39】. The
corresponding lot information appears to reside under an
``/auctions/{id}/lots`` or ``/lots?auctionId={id}`` endpoint (the
platform uses XML internally for some pages as observed when visiting
``/auctions/{id}/lots`` in a browser). This function attempts to call
these endpoints in order and parse their JSON responses. If the
response is not JSON, it falls back to a simple text scrape looking
for lot numbers, titles, descriptions and current bid amounts.
Parameters
----------
auction_id: int
The numeric identifier of the auction on AuctionPort.
request_session: Optional[requests.Session], optional
An existing requests session.
Returns
-------
List[Dict[str, Optional[str]]]
A list of lot dictionaries with the keys ``lot_number``, ``title``,
``description``, ``bids`` (if available), ``current_bid`` (amount and
currency if provided), ``image_url`` and ``end_time``. If no lots
could be parsed, an empty list is returned.
Raises
------
requests.HTTPError
If both endpoint attempts return non200 responses.
"""
session = request_session or requests.Session()
# Candidate endpoints for AuctionPort lots. The first URL follows the
# pattern used by the AuctionPort website; the second is a query by
# parameter. Additional endpoints can be added if discovered.
url_candidates = [
f"https://api.auctionport.be/auctions/{auction_id}/lots",
f"https://api.auctionport.be/lots?auctionId={auction_id}",
]
last_error: Optional[Exception] = None
for url in url_candidates:
try:
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
except Exception as exc:
# Capture connection errors and continue with the next endpoint
last_error = exc
continue
if response.status_code == 404:
# Try the next candidate
continue
if response.status_code >= 400:
last_error = requests.HTTPError(
f"AuctionPort API error {response.status_code} for {url}",
response=response,
)
continue
# If the response is OK, attempt to parse JSON
try:
data = response.json()
except json.JSONDecodeError:
# Not JSON: fallback to naive parsing of plain text/XML. AuctionPort
# sometimes returns XML for lots pages. We'll attempt to extract
# structured information using simple patterns.
text = response.text
lots: List[Dict[str, Optional[str]]] = []
# Split by <div class="lot"> like markers (not guaranteed). In the
# absence of a stable API specification, heuristics must be used.
# Here we use a very simple split on "Lot " followed by a number.
import re
pattern = re.compile(r"\bLot\s+(\d+)\b", re.IGNORECASE)
for match in pattern.finditer(text):
lot_number = match.group(1)
# Attempt to extract the title and description following the
# lot number. This heuristic looks for a line break or
# sentence after the lot label; adjust as necessary.
start = match.end()
segment = text[start:start + 300] # arbitrary slice length
# Title is the first sentence or line
title_match = re.search(r"[:\-]\s*(.*?)\.(?=\s|<)", segment)
title = title_match.group(1).strip() if title_match else segment.strip()
lots.append({
"lot_number": lot_number,
"title": title,
"description": None,
"bids": None,
"current_bid": None,
"image_url": None,
"end_time": None,
})
if lots:
return lots
else:
# If no lots were extracted, continue to the next candidate
last_error = Exception("Unable to parse AuctionPort lots from nonJSON response")
continue
# If JSON parsing succeeded, inspect the structure. Some endpoints
# return a toplevel object with a ``data`` field containing a list.
lots: List[Dict[str, Optional[str]]] = []
# Attempt to locate the list of lots: it might be in ``data``, ``lots`` or
# another property.
candidate_keys = ["lots", "data", "items"]
lot_list: Optional[List[Dict[str, any]]] = None
for key in candidate_keys:
if isinstance(data, dict) and isinstance(data.get(key), list):
lot_list = data[key]
break
# If the response is a list itself (not a dict), treat it as the lot list
if lot_list is None and isinstance(data, list):
lot_list = data
if lot_list is None:
# Unknown structure; return empty list
return []
for item in lot_list:
# Map fields according to common names; adjust if your endpoint
# uses different property names. Use dict.get to avoid KeyError.
lot_number = item.get("lotNumber") or item.get("lotnumber") or item.get("id")
title = item.get("title") or item.get("naam")
description = item.get("description") or item.get("beschrijving")
bids = item.get("numberOfBids") or item.get("bidCount")
# Determine current bid: AuctionPort might provide ``currentBid`` or
# ``currentPrice`` as an object or numeric value.
current_bid_obj = item.get("currentBid") or item.get("currentPrice")
current_bid: Optional[Dict[str, any]] = None
if isinstance(current_bid_obj, dict):
current_bid = {
"amount": current_bid_obj.get("amount"),
"currency": current_bid_obj.get("currency"),
}
elif current_bid_obj is not None:
current_bid = {"amount": current_bid_obj, "currency": None}
# Image
image_url = None
if isinstance(item.get("images"), list) and item["images"]:
image_url = item["images"][0].get("url")
elif isinstance(item.get("image"), str):
image_url = item.get("image")
# End time
end_time = item.get("endDateISO") or item.get("closingDateISO") or item.get("closingDate")
lots.append(
{
"lot_number": lot_number,
"title": title,
"description": description,
"bids": bids,
"current_bid": current_bid,
"image_url": image_url,
"end_time": end_time,
}
)
return lots
# All candidates failed
if last_error:
raise last_error
raise requests.HTTPError(f"Could not fetch lots for AuctionPort auction {auction_id}")
def get_items_ovm(
country: str,
auction_id: int,
*,
request_session: Optional[requests.Session] = None,
) -> List[Dict[str, Optional[str]]]:
"""Fetch lots from an Online Veilingmeester auction.
Online Veilingmeester's REST API exposes auctions and their lots via
endpoints under ``https://onlineveilingmeester.nl/rest/nl``. The
AuctionViewer project's source code constructs lot URLs as
``{land}/veilingen/{id}/kavels``, where ``land`` is the lowercased
country name (e.g. ``nederland`` or ``belgie``)【366543684390870†L13-L50】.
Therefore, the full path for retrieving the lots of a specific auction
is ``https://onlineveilingmeester.nl/rest/nl/{country}/veilingen/{auction_id}/kavels``.
Parameters
----------
country: str
Lowercase country name used in the API path (for example
``"nederland"`` or ``"belgie"``). The value should correspond to
the ``land`` property returned by the OVM auctions endpoint【366543684390870†L13-L50】.
auction_id: int
The numeric identifier of the auction.
request_session: Optional[requests.Session], optional
A ``requests`` session to reuse connections.
Returns
-------
List[Dict[str, Optional[str]]]
A list of lot dictionaries with keys ``lot_number``, ``title``,
``description``, ``bids``, ``current_bid`` (if available),
``image_url`` and ``end_time``. If the endpoint returns no items,
an empty list is returned.
Raises
------
requests.HTTPError
If the HTTP call returns a non200 response.
Exception
If the response cannot be decoded as JSON.
"""
session = request_session or requests.Session()
base_url = "https://onlineveilingmeester.nl/rest/nl"
url = f"{base_url}/{country}/veilingen/{auction_id}/kavels"
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
try:
response.raise_for_status()
except requests.HTTPError:
logger.error("OVM API returned status %s: %s", response.status_code, response.text)
raise
# Parse the JSON body; expect a list of lots
data = response.json()
lots: List[Dict[str, Optional[str]]] = []
# The response may be a dictionary containing a ``kavels`` key or a list
if isinstance(data, dict) and isinstance(data.get("kavels"), list):
lot_list = data["kavels"]
elif isinstance(data, list):
lot_list = data
else:
logger.error("Unexpected response structure from OVM API: %s", data)
raise Exception("Unexpected response structure for OVM lots")
for item in lot_list:
lot_number = item.get("kavelnummer") or item.get("lotNumber") or item.get("id")
title = item.get("naam") or item.get("title")
description = item.get("beschrijving") or item.get("description")
bids = item.get("aantalBiedingen") or item.get("numberOfBids")
# Current bid is nested in ``hoogsteBod`` or ``currentBid``
current_bid_obj = item.get("hoogsteBod") or item.get("currentBid")
current_bid: Optional[Dict[str, any]] = None
if isinstance(current_bid_obj, dict):
current_bid = {
"amount": current_bid_obj.get("bodBedrag") or current_bid_obj.get("amount"),
"currency": current_bid_obj.get("valuta") or current_bid_obj.get("currency"),
}
image_url = None
# OVM may provide a list of image URLs under ``afbeeldingen`` or ``images``
if isinstance(item.get("afbeeldingen"), list) and item["afbeeldingen"]:
image_url = item["afbeeldingen"][0]
elif isinstance(item.get("images"), list) and item["images"]:
image_url = item["images"][0].get("url") if isinstance(item["images"][0], dict) else item["images"][0]
end_time = item.get("eindDatumISO") or item.get("endDateISO") or item.get("eindDatum")
lots.append(
{
"lot_number": lot_number,
"title": title,
"description": description,
"bids": bids,
"current_bid": current_bid,
"image_url": image_url,
"end_time": end_time,
}
)
return lots

481
utils/auction_items.py Normal file
View File

@@ -0,0 +1,481 @@
"""
Utility functions to fetch auction items (lots) from different providers.
This module defines three functions that make HTTP calls to the public APIs of
Troostwijk Auctions (TWK), AuctionPort (AP) and Online Veilingmeester (OVM)
and normalises their responses into Python dictionaries. Each function
returns a list of dictionaries where each dictionary represents an
individual lot and includes standardised keys: ``title``, ``description``,
``bids`` (the number of bids if available), ``current_bid`` (current price
and currency if available), ``image_url`` and ``end_time``.
The implementations rely on the ``requests`` library for HTTP transport
and include basic error handling. They raise ``requests.HTTPError``
when the remote server responds with a non200 status code.
Note: the APIs these functions call are subject to change. Endpoints and
field names may differ depending on the auction status or provider version.
These functions are intended as a starting point for integrating with
multiple auction platforms; you may need to adjust query parameters,
header values or JSON field names if the provider updates their API.
Examples
--------
```
from auction_items import get_items_twk, get_items_ap, get_items_ovm
# Troostwijk Auctions (TWK): pass the visible auction identifier
lots = get_items_twk(display_id="35563")
for lot in lots:
print(lot['lot_number'], lot['title'], lot['current_bid'])
# AuctionPort (AP): pass the auction ID from the AuctionPort website
ap_lots = get_items_ap(auction_id=1323)
# Online Veilingmeester (OVM): the country code is required to build the
# endpoint path (e.g. ``'nederland'`` or ``'belgie'``) along with the
# numeric auction ID.
ovm_lots = get_items_ovm(country="nederland", auction_id=7713)
```
"""
from __future__ import annotations
import json
import logging
from typing import Dict, List, Optional
import requests
logger = logging.getLogger(__name__)
def get_items_twk(
display_id: str,
*,
page: int = 1,
page_size: int = 200,
locale: str = "nl",
sort_by: str = "LOT_NUMBER_ASC",
platform: str = "TWK",
request_session: Optional[requests.Session] = None,
) -> List[Dict[str, Optional[str]]]:
"""Fetch lots (items) for a Troostwijk auction using the current GraphQL API.
Troostwijks public GraphQL API exposes a ``lotsByAuctionDisplayId`` query
that returns the lots belonging to a specific auction. The input
``LotsByAuctionDisplayIdInput`` requires an ``auctionDisplayId``,
``locale``, ``pageNumber``, ``pageSize`` and a nonnull ``sortBy``
parameter【566587544277629†screenshot】. The platform is provided as the
``Platform`` enum value and must be set to ``"TWK"`` for Troostwijk
auctions【622367855745945†screenshot】.
Each result in the ``results`` list is a ``ListingLot`` object that
includes fields such as ``number`` (the lot/kavel number), ``title``,
``description``, ``bidsCount``, ``currentBidAmount`` (Money object with
``cents`` and ``currency``), ``endDate`` (a scalar ``TbaDate``) and
``image`` with a ``url`` property【819766814773156†screenshot】. This
function builds a query that selects these fields and converts them
into a normalised Python dictionary per lot.
Parameters
----------
display_id: str
The auction display identifier (e.g., the number visible in the
auction URL).
page: int, optional
The page number to retrieve (1based). Defaults to 1.
page_size: int, optional
The number of lots per page. Defaults to 200.
locale: str, optional
Language code for the returned content (default ``"nl"``).
sort_by: str, optional
Sorting option from the ``LotsSortingOption`` enum (for example
``"LOT_NUMBER_ASC"`` or ``"END_DATE_DESC"``)【566587544277629†screenshot】.
Defaults to ``"LOT_NUMBER_ASC"``.
platform: str, optional
Platform code as defined in the ``Platform`` enum. Use ``"TWK"`` for
Troostwijk auctions【622367855745945†screenshot】.
request_session: Optional[requests.Session], optional
Optional session object for connection pooling.
Returns
-------
List[Dict[str, Optional[str]]]
A list of dictionaries representing lots. Each dictionary
contains the keys ``lot_number``, ``title``, ``description``,
``bids`` (number of bids), ``current_bid`` (amount in major units
and currency), ``image_url`` and ``end_time``.
Raises
------
requests.HTTPError
If the HTTP call does not return a 2xx status code.
Exception
If GraphQL returns errors or the response structure is unexpected.
"""
session = request_session or requests.Session()
url = "https://storefront.tbauctions.com/storefront/graphql"
# GraphQL query for lots by auction display ID. We request the
# ListingLots wrapper (hasNext, pageNumber etc.) and select relevant
# fields from each ListingLot【819766814773156†screenshot】. The money
# amounts are represented in cents; we convert them to major units.
graphql_query = """
query LotsByAuctionDisplayId($request: LotsByAuctionDisplayIdInput!, $platform: Platform!) {
lotsByAuctionDisplayId(request: $request, platform: $platform) {
pageNumber
pageSize
totalSize
hasNext
results {
number
title
description
bidsCount
currentBidAmount {
cents
currency
}
endDate
image {
url
}
}
}
}
"""
variables = {
"request": {
"auctionDisplayId": str(display_id),
"locale": locale,
"pageNumber": page,
"pageSize": page_size,
"sortBy": sort_by,
},
"platform": platform,
}
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
# Use GET request because the GraphQL endpoint does not accept POST
# from unauthenticated clients. We encode the query and variables as
# query parameters.
try:
response = session.get(
url,
params={"query": graphql_query, "variables": json.dumps(variables)},
headers=headers,
timeout=30,
)
except Exception as exc:
logger.error("Error contacting Troostwijk GraphQL endpoint: %s", exc)
raise
try:
response.raise_for_status()
except requests.HTTPError:
logger.error("Troostwijk API returned status %s: %s", response.status_code, response.text)
raise
data = response.json()
if "errors" in data and data["errors"]:
message = data["errors"]
logger.error("GraphQL returned errors: %s", message)
raise Exception(f"GraphQL returned errors: {message}")
# Extract the list of lots
try:
lot_items = data["data"]["lotsByAuctionDisplayId"]["results"]
except (KeyError, TypeError) as e:
logger.error("Unexpected response structure from Troostwijk API: %s", data)
raise Exception(f"Unexpected response structure: {e}")
lots: List[Dict[str, Optional[str]]] = []
for item in lot_items:
lot_number = item.get("number")
title = item.get("title")
description = item.get("description")
bids = item.get("bidsCount")
bid_amount = item.get("currentBidAmount") or {}
# Convert cents to major units if available
current_bid = None
if bid_amount and isinstance(bid_amount, dict):
cents = bid_amount.get("cents")
currency = bid_amount.get("currency")
if cents is not None:
current_bid = {
"amount": cents / 100.0,
"currency": currency,
}
end_time = item.get("endDate")
image_obj = item.get("image") or {}
image_url = image_obj.get("url") if isinstance(image_obj, dict) else None
lots.append(
{
"lot_number": lot_number,
"title": title,
"description": description,
"bids": bids,
"current_bid": current_bid,
"image_url": image_url,
"end_time": end_time,
}
)
return lots
def get_items_ap(
auction_id: int,
*,
request_session: Optional[requests.Session] = None,
) -> List[Dict[str, Optional[str]]]:
"""Retrieve items (lots) from an AuctionPort auction.
AuctionPort operates a JSON API on ``https://api.auctionport.be``. While
official documentation for the lot endpoints is scarce, community code
suggests that auctions can be fetched via ``/auctions/small``【461010206788258†L10-L39】. The
corresponding lot information appears to reside under an
``/auctions/{id}/lots`` or ``/lots?auctionId={id}`` endpoint (the
platform uses XML internally for some pages as observed when visiting
``/auctions/{id}/lots`` in a browser). This function attempts to call
these endpoints in order and parse their JSON responses. If the
response is not JSON, it falls back to a simple text scrape looking
for lot numbers, titles, descriptions and current bid amounts.
Parameters
----------
auction_id: int
The numeric identifier of the auction on AuctionPort.
request_session: Optional[requests.Session], optional
An existing requests session.
Returns
-------
List[Dict[str, Optional[str]]]
A list of lot dictionaries with the keys ``lot_number``, ``title``,
``description``, ``bids`` (if available), ``current_bid`` (amount and
currency if provided), ``image_url`` and ``end_time``. If no lots
could be parsed, an empty list is returned.
Raises
------
requests.HTTPError
If both endpoint attempts return non200 responses.
"""
session = request_session or requests.Session()
# Candidate endpoints for AuctionPort lots. The first URL follows the
# pattern used by the AuctionPort website; the second is a query by
# parameter. Additional endpoints can be added if discovered.
url_candidates = [
f"https://api.auctionport.be/auctions/{auction_id}/lots",
f"https://api.auctionport.be/lots?auctionId={auction_id}",
]
last_error: Optional[Exception] = None
for url in url_candidates:
try:
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
except Exception as exc:
# Capture connection errors and continue with the next endpoint
last_error = exc
continue
if response.status_code == 404:
# Try the next candidate
continue
if response.status_code >= 400:
last_error = requests.HTTPError(
f"AuctionPort API error {response.status_code} for {url}",
response=response,
)
continue
# If the response is OK, attempt to parse JSON
try:
data = response.json()
except json.JSONDecodeError:
# Not JSON: fallback to naive parsing of plain text/XML. AuctionPort
# sometimes returns XML for lots pages. We'll attempt to extract
# structured information using simple patterns.
text = response.text
lots: List[Dict[str, Optional[str]]] = []
# Split by <div class="lot"> like markers (not guaranteed). In the
# absence of a stable API specification, heuristics must be used.
# Here we use a very simple split on "Lot " followed by a number.
import re
pattern = re.compile(r"\bLot\s+(\d+)\b", re.IGNORECASE)
for match in pattern.finditer(text):
lot_number = match.group(1)
# Attempt to extract the title and description following the
# lot number. This heuristic looks for a line break or
# sentence after the lot label; adjust as necessary.
start = match.end()
segment = text[start:start + 300] # arbitrary slice length
# Title is the first sentence or line
title_match = re.search(r"[:\-]\s*(.*?)\.(?=\s|<)", segment)
title = title_match.group(1).strip() if title_match else segment.strip()
lots.append({
"lot_number": lot_number,
"title": title,
"description": None,
"bids": None,
"current_bid": None,
"image_url": None,
"end_time": None,
})
if lots:
return lots
else:
# If no lots were extracted, continue to the next candidate
last_error = Exception("Unable to parse AuctionPort lots from nonJSON response")
continue
# If JSON parsing succeeded, inspect the structure. Some endpoints
# return a toplevel object with a ``data`` field containing a list.
lots: List[Dict[str, Optional[str]]] = []
# Attempt to locate the list of lots: it might be in ``data``, ``lots`` or
# another property.
candidate_keys = ["lots", "data", "items"]
lot_list: Optional[List[Dict[str, any]]] = None
for key in candidate_keys:
if isinstance(data, dict) and isinstance(data.get(key), list):
lot_list = data[key]
break
# If the response is a list itself (not a dict), treat it as the lot list
if lot_list is None and isinstance(data, list):
lot_list = data
if lot_list is None:
# Unknown structure; return empty list
return []
for item in lot_list:
# Map fields according to common names; adjust if your endpoint
# uses different property names. Use dict.get to avoid KeyError.
lot_number = item.get("lotNumber") or item.get("lotnumber") or item.get("id")
title = item.get("title") or item.get("naam")
description = item.get("description") or item.get("beschrijving")
bids = item.get("numberOfBids") or item.get("bidCount")
# Determine current bid: AuctionPort might provide ``currentBid`` or
# ``currentPrice`` as an object or numeric value.
current_bid_obj = item.get("currentBid") or item.get("currentPrice")
current_bid: Optional[Dict[str, any]] = None
if isinstance(current_bid_obj, dict):
current_bid = {
"amount": current_bid_obj.get("amount"),
"currency": current_bid_obj.get("currency"),
}
elif current_bid_obj is not None:
current_bid = {"amount": current_bid_obj, "currency": None}
# Image
image_url = None
if isinstance(item.get("images"), list) and item["images"]:
image_url = item["images"][0].get("url")
elif isinstance(item.get("image"), str):
image_url = item.get("image")
# End time
end_time = item.get("endDateISO") or item.get("closingDateISO") or item.get("closingDate")
lots.append(
{
"lot_number": lot_number,
"title": title,
"description": description,
"bids": bids,
"current_bid": current_bid,
"image_url": image_url,
"end_time": end_time,
}
)
return lots
# All candidates failed
if last_error:
raise last_error
raise requests.HTTPError(f"Could not fetch lots for AuctionPort auction {auction_id}")
def get_items_ovm(
country: str,
auction_id: int,
*,
request_session: Optional[requests.Session] = None,
) -> List[Dict[str, Optional[str]]]:
"""Fetch lots from an Online Veilingmeester auction.
Online Veilingmeester's REST API exposes auctions and their lots via
endpoints under ``https://onlineveilingmeester.nl/rest/nl``. The
AuctionViewer project's source code constructs lot URLs as
``{land}/veilingen/{id}/kavels``, where ``land`` is the lowercased
country name (e.g. ``nederland`` or ``belgie``)【366543684390870†L13-L50】.
Therefore, the full path for retrieving the lots of a specific auction
is ``https://onlineveilingmeester.nl/rest/nl/{country}/veilingen/{auction_id}/kavels``.
Parameters
----------
country: str
Lowercase country name used in the API path (for example
``"nederland"`` or ``"belgie"``). The value should correspond to
the ``land`` property returned by the OVM auctions endpoint【366543684390870†L13-L50】.
auction_id: int
The numeric identifier of the auction.
request_session: Optional[requests.Session], optional
A ``requests`` session to reuse connections.
Returns
-------
List[Dict[str, Optional[str]]]
A list of lot dictionaries with keys ``lot_number``, ``title``,
``description``, ``bids``, ``current_bid`` (if available),
``image_url`` and ``end_time``. If the endpoint returns no items,
an empty list is returned.
Raises
------
requests.HTTPError
If the HTTP call returns a non200 response.
Exception
If the response cannot be decoded as JSON.
"""
session = request_session or requests.Session()
base_url = "https://onlineveilingmeester.nl/rest/nl"
url = f"{base_url}/{country}/veilingen/{auction_id}/kavels"
response = session.get(url, headers={"Accept": "application/json"}, timeout=30)
try:
response.raise_for_status()
except requests.HTTPError:
logger.error("OVM API returned status %s: %s", response.status_code, response.text)
raise
# Parse the JSON body; expect a list of lots
data = response.json()
lots: List[Dict[str, Optional[str]]] = []
# The response may be a dictionary containing a ``kavels`` key or a list
if isinstance(data, dict) and isinstance(data.get("kavels"), list):
lot_list = data["kavels"]
elif isinstance(data, list):
lot_list = data
else:
logger.error("Unexpected response structure from OVM API: %s", data)
raise Exception("Unexpected response structure for OVM lots")
for item in lot_list:
lot_number = item.get("kavelnummer") or item.get("lotNumber") or item.get("id")
title = item.get("naam") or item.get("title")
description = item.get("beschrijving") or item.get("description")
bids = item.get("aantalBiedingen") or item.get("numberOfBids")
# Current bid is nested in ``hoogsteBod`` or ``currentBid``
current_bid_obj = item.get("hoogsteBod") or item.get("currentBid")
current_bid: Optional[Dict[str, any]] = None
if isinstance(current_bid_obj, dict):
current_bid = {
"amount": current_bid_obj.get("bodBedrag") or current_bid_obj.get("amount"),
"currency": current_bid_obj.get("valuta") or current_bid_obj.get("currency"),
}
image_url = None
# OVM may provide a list of image URLs under ``afbeeldingen`` or ``images``
if isinstance(item.get("afbeeldingen"), list) and item["afbeeldingen"]:
image_url = item["afbeeldingen"][0]
elif isinstance(item.get("images"), list) and item["images"]:
image_url = item["images"][0].get("url") if isinstance(item["images"][0], dict) else item["images"][0]
end_time = item.get("eindDatumISO") or item.get("endDateISO") or item.get("eindDatum")
lots.append(
{
"lot_number": lot_number,
"title": title,
"description": description,
"bids": bids,
"current_bid": current_bid,
"image_url": image_url,
"end_time": end_time,
}
)
return lots

View File

@@ -2,27 +2,30 @@ import requests
from traceback import print_exc from traceback import print_exc
from cache import Cache, FileCache from cache import Cache, FileCache
from models.location import Auction, Auctionbrand, Countrycode, Maplocation, JsonEncoder from models.location import Auction, Auctionbrand, Countrycode, Maplocation, JsonEncoder
from utils.APutils import getAPAuctions
from utils.OVMutils import getOVMAuctions
from utils.TWKutils import getTwkAuctions
from utils.locationutils import getGeoLocationByCity from utils.locationutils import getGeoLocationByCity
from utils.helperutils import log from utils.helperutils import log
from datetime import datetime from datetime import datetime
import re
import math
def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
def getAuctionlocations(countrycode: Countrycode, clearcache: bool = False):
cachename = 'allauctions_' + countrycode cachename = 'allauctions_' + countrycode
log("should clear chache with cachename: " + str(clearcache) + ", " + cachename) log("should clear chache with cachename: " + str(clearcache) + ", " + cachename)
if(clearcache): if (clearcache):
res = FileCache.get(cachename, 1) res = FileCache.get(cachename, 1)
else: else:
res = FileCache.get(cachename) res = FileCache.get(cachename)
if(res): if (res):
return res return res
twkauctions = [] twkauctions = []
ovmauctions = [] ovmauctions = []
apauctions = []
try: try:
twkauctions = getTwkAuctions(countrycode) twkauctions = getTwkAuctions(countrycode)
@@ -31,159 +34,55 @@ def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
print_exc(e) print_exc(e)
try: try:
ovmauctions = getOVMAuctions() ovmauctions = getOVMAuctions()
except Exception as e: except Exception as e:
log('something went wrong while running the OVM auctions request') log('something went wrong while running the OVM auctions request')
print_exc(e) print_exc(e)
try:
apauctions = getAPAuctions()
except Exception as e:
log('something went wrong while running the OVM auctions request')
print_exc(e)
auctions = [*twkauctions, *ovmauctions] auctions = [*twkauctions, *ovmauctions, *apauctions]
# filters all auctions for this geonameid
auctions = list(filter(lambda a: a.numberoflots > 0, auctions))
for auction in auctions: for auction in auctions:
auction.geonamelocation = getGeoLocationByCity(auction.city, countrycode) auction.geonamelocation = getGeoLocationByCity(auction.city, countrycode)
# filters all auctions for this geonameid
auctions = list(filter(lambda a: a.numberoflots > 0, auctions))
geonameids = map(get_geonameid, auctions) geonameids = map(get_geonameid, auctions)
uniquegeonameids = (list(set(geonameids))) uniquegeonameids = (list(set(geonameids)))
maplocations = [] maplocations = []
#loops through the uniques geonameids # loops through the uniques geonameids
for geoid in uniquegeonameids: for geoid in uniquegeonameids:
#filters all auctions for this geonameid
geoauctions = list(filter(lambda a: get_geonameid(a) == geoid , auctions)) # filters all auctions for this geonameid
if(geoauctions): geoauctions = list(filter(lambda a: get_geonameid(a) == geoid, auctions))
#gets the location (if it has any) for the geolocation if (geoauctions):
geoauctions = list({object_.url: object_ for object_ in geoauctions}.values())
# gets the location (if it has any) for the geolocation
location = geoauctions[0].geonamelocation location = geoauctions[0].geonamelocation
if(location): if (location):
maplocation = Maplocation(location.latitude, location.longitude, len(geoauctions), location, geoauctions) maplocation = Maplocation(location.latitude, location.longitude, len(geoauctions), location, geoauctions)
maplocations.append(maplocation) maplocations.append(maplocation)
for location in maplocations: for location in maplocations:
del location.geonamelocation #removes object which is not used anymore del location.geonamelocation # removes object which is not used anymore
for auction in location.auctions: for auction in location.auctions:
del auction.geonamelocation #removes object to not have duplicate data send to the server del auction.geonamelocation # removes object to not have duplicate data send to the server
FileCache.add(cachename, maplocations) FileCache.add(cachename, maplocations)
return maplocations return maplocations
def get_geonameid(auction): def get_geonameid(auction):
if(auction.geonamelocation): if (auction.geonamelocation):
return auction.geonamelocation.geonameid return auction.geonamelocation.geonameid
return None return None
# global twkDataUrl; # = ''; # 'https://www.troostwijkauctions.com/_next/data/' #e6-N0pLHv12LVGS0oYzx6/nl/'
def getTWKUrl():
response = requests.get('https://www.troostwijkauctions.com/')
if(response.status_code ==200):
buildid = re.search(r'"buildId":"([^"]*)', response.text, re.MULTILINE )
twkDataUrl = 'https://www.troostwijkauctions.com/_next/data/' + str(buildid[1]) + '/nl/'
log('buildid: ' + str(buildid[1]))
log('twkDataUrl: ' + twkDataUrl)
return twkDataUrl
return None
def getTwkAuctions(countrycode):
cachename = 'TwkAuctions_'+ countrycode
res = Cache.get(cachename)
if(res):
return res
# buildidresponse = requests.get('https://www.troostwijkauctions.com/')
twkDataUrl = getTWKUrl()
if(twkDataUrl is None):
return []
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode)
if(response.status_code ==200):
log('Got Twk Auctions')
data = response.json()
auctions = []
totalAuctionCount = data['pageProps']['totalSize'];
pages = math.ceil(totalAuctionCount / data['pageProps']['pageSize'])
# for result in data['pageProps']['auctionList']:
for i in range(1,pages,1):
log("getting page " + str(i) + ' of ' + str(pages))
if(i > 1):
response = requests.get(twkDataUrl + "auctions.json?countries=" + countrycode + "&page=" + str(i));
data = response.json()
for twka in data['pageProps']['listData']:
# print(twka['urlSlug'])
auction = getTWKAuction(twkDataUrl, twka['urlSlug'])
if(auction):
auctions.append(auction)
Cache.add(cachename, auctions)
return auctions
return []
def getTWKAuction(twkDataUrl, auctionurlslug):
log("getting TWK auctiondetails:" + twkDataUrl + "a/" + auctionurlslug + ".json")
response = requests.get(twkDataUrl + "a/" + auctionurlslug + '.json')
if(response.status_code == 200):
data = response.json()
if(len(data['pageProps']['lots']['results']) ==0):
return None
twka = data['pageProps']['auction']
firstlot = data['pageProps']['lots']['results'][0]
city = "Nederland" if firstlot['location']['city'].lower() == 'online' or firstlot['location']['city'].lower() == "free delivery" else firstlot['location']['city']
a = Auction(Auctionbrand.TWK, city, firstlot['location']['countryCode'].upper(), twka['name'], datetime.fromtimestamp(twka['startDate']), datetime.fromtimestamp(twka['minEndDate']), '/a/' + auctionurlslug, twka['image']['url'], twka['lotCount'] )
# print(a);
return a
return None
def getOVMAuctions():
cachename = 'OnlineVeiling_'
res = Cache.get(cachename)
if(res):
return res
try:
response = requests.get("https://onlineveilingmeester.nl/rest/nl/veilingen?status=open&domein=ONLINEVEILINGMEESTER")
except:
log("The OVM auctions call threw a error")
if(response is None):
return []
if(response.status_code ==200):
log('Got Ovm Auctions')
try:
data = response.json()
auctions = []
for result in data['veilingen']:
cityname ="Nederland" if result['isBezorgVeiling'] else result['afgifteAdres']['plaats']
cityname = "Nederland" if cityname is None else cityname #there can be auctions where you have to make an appointment to retrieve the lots
startdatetime = result['openingsDatumISO'].replace("T", " ").replace("Z", "")
enddatetime = result['sluitingsDatumISO'].replace("T", " ").replace("Z", "")
image = ""
#if hasattr(result, 'image') : #result['image'] :
image = result.get('image', "") #['image']
if image == "":
images = result.get('imageList')
if(len(images) >0):
image = images[0]
else:
log("No image found for OVM auction: " + result['naam'])
a = Auction(Auctionbrand.OVM, cityname,result['land'], result['naam'],startdatetime, enddatetime, str(result['land']).lower() + '/veilingen/' + str(result['id']) + '/kavels', 'images/150x150/' + image, result['totaalKavels'] )
auctions.append(a)
Cache.add(cachename, auctions)
return auctions
except Exception as e:
log(e.__cause__ + '-- Something went wrong in the mapping of OVM auctions to auctionviewer objects. The reason was: ' + response.reason + '. The response was: ' + JsonEncoder().encode(response.json()))
print_exc(e)
else:
log("The OVM auctions call didn't gave a 200 response but a " + str(response.status_code) + ". With the reason: " + response.reason)
return []

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
def log(value): def log(value):
value = value.encode('ascii', errors='ignore') value = value.encode('ascii', errors='ignore')
value = value.decode() value = value.decode()

6
utils/lots.py Normal file
View File

@@ -0,0 +1,6 @@
from auction_items import get_items_twk
# Replace "35563" with a real auction ID from troostwijkauctions.com
lots = get_items_twk(display_id="35563")
for lot in lots:
print(lot['lot_number'], lot['title'], lot['current_bid'])

160
web/auctionviewer (1).html Normal file
View File

@@ -0,0 +1,160 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Auctionviewer Map</title>
<!-- Leaflet CSS from alternative CDN -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css"
/>
<style>
html, body { height: 100%; margin: 0; padding: 0; }
#map { height: 100%; width: 100%; }
.auction-marker {
background: #2563eb;
color: #fff;
font-weight: bold;
border: 2px solid #fff;
border-radius: 50%;
text-align: center;
line-height: 30px;
width: 30px;
height: 30px;
}
#loading {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
z-index: 1000; background: white; padding: 20px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
#error {
position: absolute; top: 10px; left: 10px; right: 10px;
z-index: 1000; background: #fee; color: #c00; padding: 15px;
border-radius: 4px; display: none;
}
</style>
</head>
<body>
<div id="loading">Loading map library...</div>
<div id="error"></div>
<div id="map"></div>
<!-- Leaflet JS from alternative CDN -->
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// DEBUG: Check if Leaflet loaded immediately after script tag
console.log("Leaflet immediate check:", typeof L !== 'undefined');
const locations = [
{
lat: 52.36861,
long: 5.2375,
numberofauctions: 2,
auctions: [
{
city: "Almere",
countrycode: "NL",
name: "Verzamelveiling van verschillende goederen",
starttime: "2025-11-14 14:00:00",
closingtime: "2025-11-27 14:00:00",
url: "/a/verzamelveiling-van-verschillende-goederen-A1-34291",
imageurl: "https://media.tbauctions.com/image-media/d4d302ab-513d-4f6a-8cdf-e8259612dc9e/file",
numberoflots: 198,
brand: "TWK",
},
{
city: "Almere",
countrycode: "NL",
name: "Veiling bedrijfsovertrekking",
starttime: "2025-11-21 14:00:00",
closingtime: "2025-12-04 14:00:00",
url: "/a/veiling-bedrijfsovertrekking-A1-34292",
imageurl: "https://media.tbauctions.com/image-media/c5bbc716-3de8-4860-a29d-6e1587f7d42e/file",
numberoflots: 247,
brand: "TWK",
},
],
},
{
lat: 51.92442,
long: 4.47773,
numberofauctions: 1,
auctions: [
{
city: "Rotterdam",
countrycode: "NL",
name: "Rotterdam Havenveiling",
starttime: "2025-11-20 10:00:00",
closingtime: "2025-12-03 18:00:00",
url: "/a/rotterdam-havenveiling-A1-34293",
imageurl: "https://media.tbauctions.com/image-media/placeholder/placeholder/file",
numberoflots: 156,
brand: "RHA",
},
],
},
];
// Use window.load to ensure ALL resources are loaded
window.addEventListener("load", function () {
console.log("Window loaded, L is:", typeof L);
// CRITICAL: Check if Leaflet is actually available
if (typeof L === 'undefined') {
const errorMsg = "ERROR: Leaflet library failed to load. Check:<br>1. Internet connection<br>2. Browser console (F12)<br>3. Try clearing browser cache<br>4. Ensure not blocking CDN with ad-blocker";
document.getElementById('error').innerHTML = errorMsg;
document.getElementById('error').style.display = 'block';
document.getElementById('loading').style.display = 'none';
console.error("Leaflet not loaded. Check network tab in developer tools.");
return;
}
// Hide loading indicator
document.getElementById('loading').style.display = 'none';
// Initialize map
const map = L.map("map").setView([52.1326, 5.2913], 8);
// Add tile layer
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 18,
}).addTo(map);
// Add markers
locations.forEach((location) => {
const { lat, long, numberofauctions, auctions } = location;
const icon = L.divIcon({
className: "auction-marker",
html: numberofauctions.toString(),
iconSize: [30, 30],
iconAnchor: [15, 15],
});
const marker = L.marker([lat, long], { icon });
let popupHtml = `<strong>${numberofauctions} veiling(en) in ${auctions[0].city}</strong>`;
popupHtml += "<ul style='padding-left: 1em;'>";
auctions.forEach((a) => {
popupHtml += `<li style='margin-bottom: 0.5em;'>`;
popupHtml += `<span style='font-weight: bold;'>${a.name}</span><br />`;
popupHtml += `Start: ${a.starttime}<br />`;
popupHtml += `Sluiting: ${a.closingtime}<br />`;
popupHtml += `Lots: ${a.numberoflots}<br />`;
popupHtml += `<a href="${a.url}" target="_blank" rel="noopener">Bekijk veiling</a>`;
popupHtml += `</li>`;
});
popupHtml += "</ul>";
marker.bindPopup(popupHtml);
marker.addTo(map);
});
console.log("Map initialized successfully with", locations.length, "locations");
});
</script>
</body>
</html>

661
web/leaflet.css Normal file
View File

@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

5
web/leaflet.js Normal file

File diff suppressed because one or more lines are too long

10
web/test.html Normal file
View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>