Search.query + Offer.price
This commit is contained in:
parent
2b2bbf9182
commit
ec8ebf3280
@ -1,7 +1,10 @@
|
|||||||
package de.ph87.kleinanzeigen.kleinanzeigen;
|
package de.ph87.kleinanzeigen.kleinanzeigen;
|
||||||
|
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.LocationNotFound;
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferCreate;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferCreate;
|
||||||
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferService;
|
import de.ph87.kleinanzeigen.kleinanzeigen.offer.OfferService;
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.search.SearchDto;
|
||||||
|
import de.ph87.kleinanzeigen.kleinanzeigen.search.SearchService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
@ -13,6 +16,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.time.DateTimeException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -21,25 +25,44 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class KleinanzeigenApi {
|
public class KleinanzeigenApi {
|
||||||
|
|
||||||
private static final String VERSCHENKEN_EPPELBORN_30KM = "https://www.kleinanzeigen.de/s-zu-verschenken/66571/seite:%d/c192l339r%d";
|
private static final String VERSCHENKEN_EPPELBORN_RADIUS = "https://www.kleinanzeigen.de/s-zu-verschenken/66571/c192l339r%d";
|
||||||
|
|
||||||
|
private static final String QUERY_EPPELBORN_RADIUS = "https://www.kleinanzeigen.de/s-66571/preis:%s:%s/%s/k0l339r%d";
|
||||||
|
|
||||||
private final OfferService offerService;
|
private final OfferService offerService;
|
||||||
|
|
||||||
|
private final SearchService searchService;
|
||||||
|
|
||||||
private final KleinanzeigenConfig config;
|
private final KleinanzeigenConfig config;
|
||||||
|
|
||||||
@Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
@Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
public void fetch() {
|
public void fetch() {
|
||||||
final URI uri = URI.create(VERSCHENKEN_EPPELBORN_30KM.formatted(1, config.getRadiusKm()));
|
fetch(VERSCHENKEN_EPPELBORN_RADIUS.formatted(config.getRadiusKm()), config.getRadiusKm());
|
||||||
|
searchService.findAllEnabledDto().forEach(this::fetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetch(final SearchDto search) {
|
||||||
|
final String url = QUERY_EPPELBORN_RADIUS.formatted(
|
||||||
|
search.getPriceMinEscaped(),
|
||||||
|
search.getPriceMaxEscaped(),
|
||||||
|
search.getQueryEscaped(),
|
||||||
|
search.getRadius()
|
||||||
|
);
|
||||||
|
fetch(url, search.getRadius());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetch(final String url, final int radius) {
|
||||||
|
final URI uri = URI.create(url);
|
||||||
try {
|
try {
|
||||||
log.debug("Fetching page: {}", uri);
|
log.debug("Fetching page: {}", uri);
|
||||||
final Document document = Jsoup.parse(uri.toURL(), 3000);
|
final Document document = Jsoup.parse(uri.toURL(), 3000);
|
||||||
document.select("li.ad-listitem:not(.is-topad) article.aditem").forEach(article -> tryParse(article, uri));
|
document.select("li.ad-listitem:not(.is-topad) article.aditem").forEach(article -> tryParse(article, uri, radius));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Failed to fetch Kleinanzeigen: {}", e.toString());
|
log.error("Failed to fetch Kleinanzeigen: {}", e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryParse(final Element article, final URI uri) {
|
private void tryParse(final Element article, final URI uri, final int radius) {
|
||||||
try {
|
try {
|
||||||
final OfferCreate create = new OfferCreate(article, uri);
|
final OfferCreate create = new OfferCreate(article, uri);
|
||||||
if (create.getDistance() <= radius) {
|
if (create.getDistance() <= radius) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -68,6 +69,13 @@ public class Offer {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private String imageURL = null;
|
private String imageURL = null;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private BigDecimal price = null;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean negotiable;
|
||||||
|
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
@OneToMany(mappedBy = "offer")
|
@OneToMany(mappedBy = "offer")
|
||||||
private List<Message> messages = new ArrayList<>();
|
private List<Message> messages = new ArrayList<>();
|
||||||
@ -108,6 +116,14 @@ public class Offer {
|
|||||||
this.imageURL = create.getImageURL();
|
this.imageURL = create.getImageURL();
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
if ((this.price == null) != (create.getPrice() == null) || (this.price != null && this.price.compareTo(create.getPrice()) != 0)) {
|
||||||
|
this.price = create.getPrice();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(this.negotiable, create.isNegotiable())) {
|
||||||
|
this.negotiable = create.isNegotiable();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import org.jsoup.nodes.Document;
|
|||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
@ -52,7 +53,12 @@ public class OfferCreate {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final String imageURL;
|
private final String imageURL;
|
||||||
|
|
||||||
public OfferCreate(final Element article, final URI uri) {
|
@Nullable
|
||||||
|
private final BigDecimal price;
|
||||||
|
|
||||||
|
private final boolean negotiable;
|
||||||
|
|
||||||
|
public OfferCreate(final Element article, final URI uri) throws LocationNotFound {
|
||||||
articleId = article.attr("data-adid");
|
articleId = article.attr("data-adid");
|
||||||
title = article.select(".text-module-begin").text();
|
title = article.select(".text-module-begin").text();
|
||||||
description = article.select(".aditem-main--middle--description").text();
|
description = article.select(".aditem-main--middle--description").text();
|
||||||
@ -67,6 +73,15 @@ public class OfferCreate {
|
|||||||
location = locationMatcher.group("location");
|
location = locationMatcher.group("location");
|
||||||
distance = Integer.parseInt(locationMatcher.group("distance"));
|
distance = Integer.parseInt(locationMatcher.group("distance"));
|
||||||
imageURL = getImageURL(articleURL);
|
imageURL = getImageURL(articleURL);
|
||||||
|
|
||||||
|
final Matcher priceMatcher = Pattern.compile("(?<price>\\d+(?:[,.]\\d+)?) €(?<negotiable> VB)?").matcher(article.select(".aditem-main--middle--price-shipping--price").text());
|
||||||
|
if (priceMatcher.find()) {
|
||||||
|
price = new BigDecimal(priceMatcher.group("price"));
|
||||||
|
negotiable = priceMatcher.group("negotiable") != null;
|
||||||
|
} else {
|
||||||
|
price = null;
|
||||||
|
negotiable = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ZonedDateTime parseDate(final String text) {
|
private ZonedDateTime parseDate(final String text) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import lombok.Getter;
|
|||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -51,6 +52,11 @@ public class OfferDto {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final String imageURL;
|
private final String imageURL;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final BigDecimal price;
|
||||||
|
|
||||||
|
private final boolean negotiable;
|
||||||
|
|
||||||
public OfferDto(final @NonNull Offer offer) {
|
public OfferDto(final @NonNull Offer offer) {
|
||||||
id = offer.getId();
|
id = offer.getId();
|
||||||
version = offer.getVersion();
|
version = offer.getVersion();
|
||||||
@ -66,8 +72,15 @@ public class OfferDto {
|
|||||||
description = offer.getDescription();
|
description = offer.getDescription();
|
||||||
articleURL = offer.getArticleURL();
|
articleURL = offer.getArticleURL();
|
||||||
imageURL = offer.getImageURL();
|
imageURL = offer.getImageURL();
|
||||||
}
|
price = offer.getPrice();
|
||||||
|
negotiable = offer.isNegotiable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String combinePrice() {
|
||||||
|
if (price == null) {
|
||||||
|
return "Zu Verschenken";
|
||||||
|
}
|
||||||
|
return "%s €%s".formatted(price.intValue(), negotiable ? " VB" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Search {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String query;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int radius;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private Integer priceMin = null;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@Nullable
|
||||||
|
private Integer priceMax = null;
|
||||||
|
|
||||||
|
public Search(final SearchCreate create) {
|
||||||
|
enabled = create.isEnabled();
|
||||||
|
query = create.getQuery();
|
||||||
|
radius = create.getRadius();
|
||||||
|
priceMin = create.getPriceMin();
|
||||||
|
priceMax = create.getPriceMax();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SearchCreate {
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String query;
|
||||||
|
|
||||||
|
private final int radius;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Integer priceMin;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Integer priceMax;
|
||||||
|
|
||||||
|
public SearchCreate(final boolean enabled, @NonNull final String query, final int radius, @Nullable final Integer priceMin, @Nullable final Integer priceMax) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
this.query = query;
|
||||||
|
this.radius = radius;
|
||||||
|
this.priceMin = priceMin;
|
||||||
|
this.priceMax = priceMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SearchDto {
|
||||||
|
|
||||||
|
private final long id;
|
||||||
|
|
||||||
|
private final boolean enabled;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final String query;
|
||||||
|
|
||||||
|
private final int radius;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Integer priceMin;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Integer priceMax;
|
||||||
|
|
||||||
|
public SearchDto(final Search search) {
|
||||||
|
this.id = search.getId();
|
||||||
|
this.enabled = search.isEnabled();
|
||||||
|
this.query = search.getQuery();
|
||||||
|
this.radius = search.getRadius();
|
||||||
|
this.priceMin = search.getPriceMin();
|
||||||
|
this.priceMax = search.getPriceMax();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQueryEscaped() {
|
||||||
|
return query.toLowerCase(Locale.ROOT).replaceAll("^[^a-z0-9]+|[^a-z0-9]+$", "").replaceAll("[^a-z0-9]+", "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriceMinEscaped() {
|
||||||
|
return priceMin == null ? "" : priceMin + "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriceMaxEscaped() {
|
||||||
|
return priceMax == null ? "" : priceMax + "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.ListCrudRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface SearchRepository extends ListCrudRepository<Search, Long> {
|
||||||
|
|
||||||
|
List<Search> findAllByEnabledTrue();
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package de.ph87.kleinanzeigen.kleinanzeigen.search;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SearchService {
|
||||||
|
|
||||||
|
private final SearchRepository searchRepository;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
if (searchRepository.count() == 0) {
|
||||||
|
searchRepository.save(new Search(new SearchCreate(true, "Garten Bank", 15, 0, 30)));
|
||||||
|
searchRepository.save(new Search(new SearchCreate(true, "Teich Pumpe", 15, 0, 30)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SearchDto> findAllEnabledDto() {
|
||||||
|
return searchRepository.findAllByEnabledTrue().stream().map(this::toDto).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchDto toDto(Search search) {
|
||||||
|
return new SearchDto(search);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -186,8 +186,9 @@ public class TelegramService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String createText(final OfferDto offer) {
|
private String createText(final OfferDto offer) {
|
||||||
return "%s\n%s\n%s\n%s\n".formatted(
|
return "%s\n%s\n%s %s (%d km)\n%s\n%s\n".formatted(
|
||||||
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
offer.getTitle().replaceAll("\\[", "(").replaceAll("]", ")"),
|
||||||
|
offer.combinePrice(),
|
||||||
offer.getZipcode(),
|
offer.getZipcode(),
|
||||||
offer.getLocation(),
|
offer.getLocation(),
|
||||||
offer.getDistance(),
|
offer.getDistance(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user