Compare commits
10 Commits
173dcb0f50
...
6fe1292086
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fe1292086 | |||
| 958000ce36 | |||
| 3a2c744ed6 | |||
|
|
83033c3c8d | ||
|
|
5d5bbb12b2 | ||
|
|
b0182c606d | ||
|
|
9a67af7480 | ||
|
|
86921652e3 | ||
|
|
5713fcb0cf | ||
|
|
bea8a41670 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -369,3 +369,5 @@ FodyWeavers.xsd
|
||||
/myenv
|
||||
*.tar.gz
|
||||
allauctions_NL.json
|
||||
|
||||
.idea/
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
# PythonAuctionviewer
|
||||
|
||||
## Run dev server
|
||||
To run the development server run `python app.py` in the root of the project
|
||||
2
app.py
2
app.py
@@ -7,7 +7,7 @@ from utils.auctionutils import getAuctionlocations
|
||||
from models.location import JsonEncoder
|
||||
|
||||
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
|
||||
|
||||
@app.route("/")
|
||||
|
||||
151
bindToInterface.c
Normal file
151
bindToInterface.c
Normal 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);
|
||||
|
||||
}
|
||||
2
cache.py
2
cache.py
@@ -9,6 +9,7 @@ from utils.helperutils import log
|
||||
|
||||
cache = {}
|
||||
|
||||
|
||||
class Cache():
|
||||
def get(key, notOlderThanHours=24):
|
||||
# print('get key ' + key)
|
||||
@@ -67,5 +68,6 @@ class FileCache():
|
||||
def add(key, obj):
|
||||
log('adding filecacheobject ' + key)
|
||||
json_data = JsonEncoder().encode(obj)
|
||||
# json_data = json.dumps(obj, cls=JsonEncoder, indent=2)
|
||||
with open("./filecache/" + key + ".json", 'w+') as f:
|
||||
f.write(json_data)
|
||||
@@ -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
|
||||
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 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
|
||||
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
|
||||
@@ -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
|
||||
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 '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
|
||||
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
|
||||
|
||||
6
haha.bat
Normal file
6
haha.bat
Normal 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 %*
|
||||
@@ -1,19 +1,25 @@
|
||||
from enum import Enum
|
||||
import enum
|
||||
from json import JSONEncoder
|
||||
import json
|
||||
|
||||
|
||||
class Countrycode(Enum):
|
||||
NL = "NL",
|
||||
DE = "DE",
|
||||
BE = "BE"
|
||||
|
||||
|
||||
class Auctionbrand(str, Enum):
|
||||
NONE = "NONE",
|
||||
TWK = "TWK"
|
||||
OVM = "OVM"
|
||||
AP = "AP"
|
||||
|
||||
|
||||
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.name = name
|
||||
self.asciiname = asciiname
|
||||
@@ -23,6 +29,7 @@ class GeonameLocation:
|
||||
self.countrycode = countrycode
|
||||
self.modificationdate = modificationdate
|
||||
|
||||
|
||||
class Maplocation:
|
||||
def __init__(self, lat=0, long=0, numberofauctions=0, geonamelocation: GeonameLocation = None, auctions=[]):
|
||||
self.lat = lat
|
||||
@@ -31,8 +38,11 @@ class Maplocation:
|
||||
self.geonamelocation = geonamelocation
|
||||
self.auctions = auctions
|
||||
|
||||
|
||||
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.countrycode = countrycode
|
||||
self.name = name
|
||||
@@ -43,7 +53,35 @@ class Auction:
|
||||
self.numberoflots = numberoflots
|
||||
self.geonamelocation = geonamelocation
|
||||
self.brand = auctionbrand
|
||||
self.multiplelocations = multiplelocations
|
||||
|
||||
|
||||
class JsonEncoder(JSONEncoder):
|
||||
def default(self, o):
|
||||
return o.__dict__
|
||||
# def default(self, o):
|
||||
# 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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from app import app as application
|
||||
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location("wsgi", "app.py")
|
||||
wsgi = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(wsgi)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pip==9.0.1
|
||||
setuptools==28.8.0
|
||||
flask===3.0.3
|
||||
werkzeug==3.0.0
|
||||
flask_cors===5.0.0
|
||||
requests===2.27.1
|
||||
pip>=23.3
|
||||
setuptools>=78.1.1
|
||||
flask===3.1.1
|
||||
werkzeug>=3.0.6
|
||||
flask_cors>=6.0.0
|
||||
requests>=2.32.4
|
||||
pathlib===1.0.1
|
||||
26
startup.bat
Normal file
26
startup.bat
Normal 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
79
utils/APutils.py
Normal 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
55
utils/OVMutils.py
Normal 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
83
utils/TWKutils.py
Normal 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
481
utils/__auction_items.py
Normal 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 non‑200 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 human‑readable identifier of the auction (e.g. ``"35563"``).
|
||||
page: int, optional
|
||||
The page number of results (defaults to 1). The API uses
|
||||
1‑based 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 non‑200 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 non‑200 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 non‑200 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 non‑JSON response")
|
||||
continue
|
||||
# If JSON parsing succeeded, inspect the structure. Some endpoints
|
||||
# return a top‑level 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 lower‑cased
|
||||
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
|
||||
Lower‑case 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 non‑200 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
481
utils/auction_items.py
Normal 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 non‑200 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.
|
||||
|
||||
Troostwijk’s 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 non‑null ``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 (1‑based). 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 non‑200 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 non‑JSON response")
|
||||
continue
|
||||
# If JSON parsing succeeded, inspect the structure. Some endpoints
|
||||
# return a top‑level 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 lower‑cased
|
||||
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
|
||||
Lower‑case 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 non‑200 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
|
||||
@@ -2,11 +2,13 @@ import requests
|
||||
from traceback import print_exc
|
||||
from cache import Cache, FileCache
|
||||
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.helperutils import log
|
||||
from datetime import datetime
|
||||
import re
|
||||
import math
|
||||
|
||||
|
||||
def getAuctionlocations(countrycode: Countrycode, clearcache: bool = False):
|
||||
cachename = 'allauctions_' + countrycode
|
||||
@@ -23,6 +25,7 @@ def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
|
||||
|
||||
twkauctions = []
|
||||
ovmauctions = []
|
||||
apauctions = []
|
||||
|
||||
try:
|
||||
twkauctions = getTwkAuctions(countrycode)
|
||||
@@ -36,12 +39,22 @@ def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
|
||||
log('something went wrong while running the OVM auctions request')
|
||||
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:
|
||||
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)
|
||||
uniquegeonameids = (list(set(geonameids)))
|
||||
|
||||
@@ -49,9 +62,11 @@ def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
|
||||
|
||||
# loops through the uniques geonameids
|
||||
for geoid in uniquegeonameids:
|
||||
|
||||
# filters all auctions for this geonameid
|
||||
geoauctions = list(filter(lambda a: get_geonameid(a) == geoid, auctions))
|
||||
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
|
||||
if (location):
|
||||
@@ -66,124 +81,8 @@ def getAuctionlocations(countrycode: Countrycode, clearcache:bool = False):
|
||||
FileCache.add(cachename, maplocations)
|
||||
return maplocations
|
||||
|
||||
|
||||
def get_geonameid(auction):
|
||||
if (auction.geonamelocation):
|
||||
return auction.geonamelocation.geonameid
|
||||
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 []
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def log(value):
|
||||
value = value.encode('ascii', errors='ignore')
|
||||
value = value.decode()
|
||||
|
||||
6
utils/lots.py
Normal file
6
utils/lots.py
Normal 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
160
web/auctionviewer (1).html
Normal 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: '© <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
661
web/leaflet.css
Normal 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
5
web/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
10
web/test.html
Normal file
10
web/test.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user