Skip to content

Commit

Permalink
Merge pull request #15 from garamb1/football-data-org
Browse files Browse the repository at this point in the history
Add football data support
  • Loading branch information
garamb1 authored Mar 31, 2024
2 parents a65dc69 + 974c519 commit 3129678
Show file tree
Hide file tree
Showing 24 changed files with 2,889 additions and 16 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ RetroSearch is a Spring Web Application that presents very simple HTML pages whi
It provides the ability to search the Web using DuckDuckGo with a custom scraper that loads the first page of results and allows you to browse pages in plain text.
You can deploy it on your local network and access it from your old computer!

## News and sports APIs support

### Enabling the News API

RetroSearch can fetch news articles by using the GNews API, to allow this, add the environment variables as follows when running the Docker image:
Expand All @@ -25,8 +27,23 @@ RetroSearch can fetch news articles by using the GNews API, to allow this, add t
docker run -e NEWS_ACTIVE=true -e NEWS_API_KEY={your GNews API Key} -d -p80:8080 garambo/retrosearch:{Retro Search Version} --restart unless-stopped
```

### Enabling the football-data.org API

RetroSearch can fetch the latest football scores using the football-data.org API, to allow this, add the environment variables as follows when running the Docker image:

```
docker run -e FOOTBALL_API_ACTIVE=true -e FOOTBALL_API_KEY={your football-data.org API Key} -d -p80:8080 garambo/retrosearch:{Retro Search Version} --restart unless-stopped
```

### Enabling both

```
docker run -e NEWS_ACTIVE=true -e NEWS_API_KEY={your GNews API Key} FOOTBALL_API_ACTIVE=true -e FOOTBALL_API_KEY={your football-data.org API Key} -d -p80:8080 garambo/retrosearch:{Retro Search Version} --restart unless-stopped
```

If running locally, just replace the property values in `application.properties` or create a new Spring run configuration.


### WIP
Currently in progress:
- Improve the parsing abilities for the browsing functionality
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.2-testing
0.6
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package it.garambo.retrosearch.controller;

import it.garambo.retrosearch.sports.football.repository.FootballRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@ConditionalOnBean(FootballRepository.class)
public class FootballController {

@Autowired private FootballRepository footballRepository;

@GetMapping(path = {"/football", "/sports/football"})
public String football(Model model) {
model.addAttribute("updatedAt", footballRepository.getUpdatedAt());
model.addAttribute("results", footballRepository.getAllMatches());
return "football";
}
}
7 changes: 7 additions & 0 deletions src/main/java/it/garambo/retrosearch/http/HttpService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import org.apache.http.Header;

public interface HttpService {

String get(URI uri) throws IOException, URISyntaxException;

String get(URI uri, Map<String, String> params) throws IOException, URISyntaxException;

String get(URI uri, List<Header> additionalHeaders) throws IOException, URISyntaxException;

String get(URI uri, Map<String, String> params, List<Header> additionalHeaders)
throws IOException, URISyntaxException;

String post(URI uri, String body) throws IOException;

String post(URI uri, Map<String, String> formData) throws IOException, URISyntaxException;
Expand Down
37 changes: 29 additions & 8 deletions src/main/java/it/garambo/retrosearch/http/HttpServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -27,12 +28,12 @@ public class HttpServiceImpl implements HttpService {

private final ResponseHandler<String> responseHandler;

private final Header[] defaultClientHeaders = {
new BasicHeader("charset", "UTF-8"),
new BasicHeader(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
};
private final List<BasicHeader> defaultClientHeaders =
List.of(
new BasicHeader("charset", "UTF-8"),
new BasicHeader(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"));

public HttpServiceImpl(
@Autowired HttpClientFactory clientFactory,
Expand All @@ -48,18 +49,38 @@ public String get(URI uri) throws IOException, URISyntaxException {

@Override
public String get(URI uri, Map<String, String> params) throws IOException, URISyntaxException {
return get(uri, params, Collections.emptyList());
}

@Override
public String get(URI uri, List<Header> additionalHeaders)
throws IOException, URISyntaxException {
return get(uri, Collections.emptyMap(), additionalHeaders);
}

@Override
public String get(URI uri, Map<String, String> params, List<Header> additionalHeaders)
throws IOException, URISyntaxException {
URIBuilder newUri = new URIBuilder(uri).setParameters(mapToNameValuePair(params));
final HttpGet get = new HttpGet(newUri.build());

get.setHeaders(defaultClientHeaders);
Header[] requestHeaders = defaultClientHeaders.toArray(new Header[0]);

if (!CollectionUtils.isEmpty(additionalHeaders)) {
List<Header> newHeaders = new ArrayList<>(defaultClientHeaders);
newHeaders.addAll(additionalHeaders);
requestHeaders = newHeaders.toArray(new Header[0]);
}

get.setHeaders(requestHeaders);
return clientFactory.createHttpClient().execute(get, responseHandler);
}

@Override
public String post(URI uri, String body) throws IOException {
final HttpPost post = new HttpPost(uri);
post.setEntity(new StringEntity(body));
post.setHeaders(defaultClientHeaders);
post.setHeaders(defaultClientHeaders.toArray(new Header[0]));
return clientFactory.createHttpClient().execute(post, responseHandler);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package it.garambo.retrosearch.sports.football.client;

import com.fasterxml.jackson.databind.ObjectMapper;
import it.garambo.retrosearch.http.HttpService;
import it.garambo.retrosearch.sports.football.model.FootballDataResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.apache.http.message.BasicHeader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnProperty(value = "retrosearch.sports.football.enable", havingValue = "true")
public class FootballDataOrgClient {

private final String API_URL = "https://api.football-data.org/v4/matches/";

@Value("${retrosearch.sports.football.api.key:}")
private String apiKey;

@Autowired HttpService httpService;

public FootballDataResponse fetchFootballData() throws IOException, URISyntaxException {
URI apiUri = new URI(API_URL);
BasicHeader apiKeyHeader = new BasicHeader("X-Auth-Token", apiKey);

String response = httpService.get(apiUri, List.of(apiKeyHeader));
return new ObjectMapper().readValue(response, FootballDataResponse.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package it.garambo.retrosearch.sports.football.model;

import it.garambo.retrosearch.sports.football.model.match.Match;
import java.util.List;
import java.util.Map;

public record FootballDataResponse(
Map<String, String> filters, Map<String, String> resultSet, List<Match> matches) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package it.garambo.retrosearch.sports.football.model.match;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record Area(int id, String name, String code) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package it.garambo.retrosearch.sports.football.model.match;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record Competition(int id, String name, String code) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package it.garambo.retrosearch.sports.football.model.match;

public record HomeAwayScore(int home, int away) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package it.garambo.retrosearch.sports.football.model.match;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import it.garambo.retrosearch.sports.football.model.match.enums.Status;
import java.util.Date;
import org.jetbrains.annotations.NotNull;

@JsonIgnoreProperties(ignoreUnknown = true)
public record Match(
int id,
Date utcDate,
Date lastUpdated,
Area area,
Status status,
Competition competition,
Team homeTeam,
Team awayTeam,
Score score)
implements Comparable<Match> {

@Override
public int compareTo(@NotNull Match o) {
return this.area.id()
- o.area.id()
+ this.competition.id()
- o.competition.id()
+ this.id
- o.id
+ this.status.ordinal()
- this.status.ordinal();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package it.garambo.retrosearch.sports.football.model.match;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record Score(HomeAwayScore halfTime, HomeAwayScore fullTime) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package it.garambo.retrosearch.sports.football.model.match;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record Team(int id, String name, String shortName) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package it.garambo.retrosearch.sports.football.model.match.enums;

public enum Status {
SCHEDULED("Scheduled"),
TIMED("Timed"),
IN_PLAY("In Play"),
PAUSED("Paused"),
FINISHED("Finished"),
SUSPENDED("Suspended"),
POSTPONED("Postponed"),
CANCELLED("Canceled"),
AWARDED("Awarded");

final String description;

private Status(String description) {
this.description = description;
}

public String getDescription() {
return description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package it.garambo.retrosearch.sports.football.repository;

import it.garambo.retrosearch.sports.football.model.match.Match;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

public interface FootballRepository {

Map<String, Set<Match>> getAllMatches();

Set<Match> getAllMatchesByArea(String areaName);

void updateAll(List<Match> newMatches);

Date getUpdatedAt();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package it.garambo.retrosearch.sports.football.repository;

import it.garambo.retrosearch.sports.football.model.match.Match;
import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@ConditionalOnProperty(value = "retrosearch.sports.football.enable", havingValue = "true")
public class InMemoryFootballRepository implements FootballRepository {

private Map<String, Set<Match>> matchesByArea;
private Date updatedAt;

@Override
public Map<String, Set<Match>> getAllMatches() {
return matchesByArea;
}

@Override
public Set<Match> getAllMatchesByArea(String areaName) {
return matchesByArea.get(areaName);
}

@Override
public void updateAll(List<Match> newMatches) {
Map<String, Set<Match>> updatedMatches = new HashMap<>();
newMatches.forEach(
match -> {
String areaName = match.area().name();
updatedMatches.putIfAbsent(areaName, new HashSet<>());
updatedMatches.get(areaName).add(match);
});
log.info("Football scores updated");
matchesByArea = updatedMatches;
updatedAt = new Date();
}

@Override
public Date getUpdatedAt() {
return updatedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package it.garambo.retrosearch.sports.football.scheduled;

import it.garambo.retrosearch.sports.football.client.FootballDataOrgClient;
import it.garambo.retrosearch.sports.football.model.FootballDataResponse;
import it.garambo.retrosearch.sports.football.repository.FootballRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@ConditionalOnProperty(value = "retrosearch.sports.football.enable", havingValue = "true")
public class FootballDataScheduledTask {

@Autowired private FootballDataOrgClient apiClient;

@Autowired private FootballRepository repository;

@Scheduled(fixedRate = 30 * 60 * 1000)
private void updateFootballData() {
try {
log.info("Updating football result list...");
FootballDataResponse footballData = apiClient.fetchFootballData();
repository.updateAll(footballData.matches());
} catch (Exception e) {
log.error("Football result list update failed:", e);
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/templates/error.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<body>
<h1>Error - RetroSearch</h1>
<h2>Sorry, something went wrong :(</h2>
<img th:src="@{img/error.gif}">
<img th:src="@{/img/error.gif}">
<br>
<a href="/">Go Back</a>
<br>
Expand Down
Loading

0 comments on commit 3129678

Please sign in to comment.