From 06463bb8e6c9ef71aa3713a702429c072a9d6ca5 Mon Sep 17 00:00:00 2001 From: This-Is-Ko <52279273+This-Is-Ko@users.noreply.github.com> Date: Fri, 6 Oct 2023 23:38:27 +1100 Subject: [PATCH] Added image generation and s3 uploads --- README.md | 52 ++++++ build.gradle | 3 + .../configuration/AmazonS3Properties.java | 46 +++++ .../ImageGeneratorProperies.java | 24 +++ .../configuration/InstagramPostProperies.java | 18 ++ .../configuration/MailerProperties.java | 8 + .../controllers/PlayerController.java | 19 +- .../models/ImageStatEntry.java | 13 ++ .../footballupdater/models/InstagramPost.java | 46 +---- .../com/ko/footballupdater/models/Match.java | 5 + .../repositories/PlayerRepository.java | 2 +- .../services/AmazonS3Service.java | 60 +++++++ .../services/EmailService.java | 57 ++++-- .../services/ImageGeneratorService.java | 166 ++++++++++++++++++ .../services/ParsingService.java | 1 + .../services/PlayerService.java | 46 ++++- .../ko/footballupdater/utils/PostHelper.java | 45 ++++- src/main/resources/application.properties | 17 ++ 18 files changed, 562 insertions(+), 66 deletions(-) create mode 100644 src/main/java/com/ko/footballupdater/configuration/AmazonS3Properties.java create mode 100644 src/main/java/com/ko/footballupdater/configuration/ImageGeneratorProperies.java create mode 100644 src/main/java/com/ko/footballupdater/configuration/InstagramPostProperies.java create mode 100644 src/main/java/com/ko/footballupdater/models/ImageStatEntry.java create mode 100644 src/main/java/com/ko/footballupdater/services/AmazonS3Service.java create mode 100644 src/main/java/com/ko/footballupdater/services/ImageGeneratorService.java diff --git a/README.md b/README.md index 1515efc..e73c2f9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Football Updater +Check for player match performance data from their latest game and generate images which contain their statistic for the selected game. + +These are uploaded to a S3 bucket and an email is sent to the configured email containing a generated post caption, S3 file links and Google Image search links for recent photos for the player. + +The players and teams to check are stored in RDS MySQL database and configured with an external cron job to run at scheduled times. ### Build To build jar use @@ -23,6 +28,53 @@ Additional env params can be passed in with --MAILER_TO_ADDRESS=**** --ENDPOINT_SECRET=**** +### Configurable + +The main configurable groupings are: + +Sprint Datasource (Database) + + SPRING_DATASOURCE_HOST + SPRING_DATASOURCE_NAME + SPRING_DATASOURCE_USERNAME + SPRING_DATASOURCE_PASSWORD + +Endpoint secret + + ENDPOINT_SECRET + +Datasource (Stat sources) + + DATASOURCE_PRIORITY + +Post Version + + IG_POST_VERSION + +Stat Image generator + + IMAGE_GENERATOR_ENABLED + IMAGE_GENERATOR_INPUT_PATH + IMAGE_GENERATOR_OUTPUT_PATH + +Mailer + + MAILER_IS_ENABLED + MAILER_SUBJECT + MAILER_FROM_NAME + MAILER_FROM_ADDRESS + MAILER_FROM_PASSWORD + MAILER_TO_NAME + MAILER_TO_ADDRESS + MAILER_IS_ATTACH_IMAGES + +AWS S3 Uploads + + AWS_S3_IS_ENABLED + AWS_ACCESS_KEY + AWS_SECRET_KEY + AWS_S3_BUCKET_NAME + ### Local MYSQL Use to login to local mysql diff --git a/build.gradle b/build.gradle index 7056686..b6f38a2 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,9 @@ dependencies { implementation 'ch.qos.logback:logback-classic:1.4.8' implementation 'org.jsoup:jsoup:1.15.3' implementation 'org.simplejavamail:simple-java-mail:8.1.3' + + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.562' + } springBoot { diff --git a/src/main/java/com/ko/footballupdater/configuration/AmazonS3Properties.java b/src/main/java/com/ko/footballupdater/configuration/AmazonS3Properties.java new file mode 100644 index 0000000..025e56d --- /dev/null +++ b/src/main/java/com/ko/footballupdater/configuration/AmazonS3Properties.java @@ -0,0 +1,46 @@ +package com.ko.footballupdater.configuration; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Slf4j +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "aws.s3") +public class AmazonS3Properties { + + @NotNull + private boolean enabled; + + @NotNull + private String accessKey; + + @NotNull + private String secretKey; + + @NotNull + private String bucketName; + + @Bean + public AmazonS3 s3Client() { + // Set up S3 client + log.info("Initialising S3 client"); + Regions clientRegion = Regions.AP_SOUTHEAST_2; + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return AmazonS3ClientBuilder.standard() + .withRegion(clientRegion) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/com/ko/footballupdater/configuration/ImageGeneratorProperies.java b/src/main/java/com/ko/footballupdater/configuration/ImageGeneratorProperies.java new file mode 100644 index 0000000..ab81e8f --- /dev/null +++ b/src/main/java/com/ko/footballupdater/configuration/ImageGeneratorProperies.java @@ -0,0 +1,24 @@ +package com.ko.footballupdater.configuration; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "image.generator") +public class ImageGeneratorProperies { + + @NotNull + private boolean enabled; + + @NotNull + private String inputPath; + + @NotNull + private String outputPath; + +} diff --git a/src/main/java/com/ko/footballupdater/configuration/InstagramPostProperies.java b/src/main/java/com/ko/footballupdater/configuration/InstagramPostProperies.java new file mode 100644 index 0000000..3de29cc --- /dev/null +++ b/src/main/java/com/ko/footballupdater/configuration/InstagramPostProperies.java @@ -0,0 +1,18 @@ +package com.ko.footballupdater.configuration; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "ig.post") +public class InstagramPostProperies { + + @NotNull + private int version; + +} diff --git a/src/main/java/com/ko/footballupdater/configuration/MailerProperties.java b/src/main/java/com/ko/footballupdater/configuration/MailerProperties.java index 231d1f9..0a62080 100644 --- a/src/main/java/com/ko/footballupdater/configuration/MailerProperties.java +++ b/src/main/java/com/ko/footballupdater/configuration/MailerProperties.java @@ -12,9 +12,17 @@ @ConfigurationProperties(prefix = "mailer") public class MailerProperties { + @NotNull + private boolean enabled; + @NotNull private String subject; + private MailerFromProperties from; + private MailerToProperties to; + @NotNull + private boolean attachImages; + } diff --git a/src/main/java/com/ko/footballupdater/controllers/PlayerController.java b/src/main/java/com/ko/footballupdater/controllers/PlayerController.java index dd9078b..8473d72 100644 --- a/src/main/java/com/ko/footballupdater/controllers/PlayerController.java +++ b/src/main/java/com/ko/footballupdater/controllers/PlayerController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,17 +39,27 @@ public class PlayerController { } @GetMapping(path="/data/update") - public @ResponseBody ResponseEntity dataUpdate() { - UpdatePlayersResponse response = new UpdatePlayersResponse(); + public @ResponseBody ResponseEntity dataUpdateForAllPlayers() { try { - playerService.updateDataForAllPlayers(response); - return ResponseEntity.ok(response); + return ResponseEntity.ok(playerService.updateDataForAllPlayers()); } catch (Exception ex) { throw new ResponseStatusException( HttpStatus.BAD_REQUEST, "Updating players failed", ex); } } + @GetMapping(path="/data/update/{playerId}") + public @ResponseBody ResponseEntity dataUpdateForPlayer( + @PathVariable("playerId") Integer playerId + ) { + try { + return ResponseEntity.ok(playerService.updateDataForPlayer(playerId)); + } catch (Exception ex) { + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, "Updating player failed", ex); + } + } + @PostMapping(path="/data-source/update") public @ResponseBody DataSource updatePlayerDataSource(@RequestBody DataSource dataSource) { try { diff --git a/src/main/java/com/ko/footballupdater/models/ImageStatEntry.java b/src/main/java/com/ko/footballupdater/models/ImageStatEntry.java new file mode 100644 index 0000000..b4107ef --- /dev/null +++ b/src/main/java/com/ko/footballupdater/models/ImageStatEntry.java @@ -0,0 +1,13 @@ +package com.ko.footballupdater.models; + +import lombok.Getter; +import lombok.Setter; +import lombok.AllArgsConstructor; + +@Getter +@Setter +@AllArgsConstructor +public class ImageStatEntry { + private String name; + private String value; +} diff --git a/src/main/java/com/ko/footballupdater/models/InstagramPost.java b/src/main/java/com/ko/footballupdater/models/InstagramPost.java index 344c733..3282367 100644 --- a/src/main/java/com/ko/footballupdater/models/InstagramPost.java +++ b/src/main/java/com/ko/footballupdater/models/InstagramPost.java @@ -1,50 +1,22 @@ package com.ko.footballupdater.models; -import com.ko.footballupdater.utils.PostHelper; +import lombok.Getter; +import lombok.Setter; +import java.util.ArrayList; +import java.util.List; + +@Getter +@Setter public class InstagramPost { private Player player; private PlayerMatchPerformanceStats playerMatchPerformanceStats; - private String caption; - private String imageSearchUrl; + private List imagesFileNames = new ArrayList<>(); + private List imagesS3Urls = new ArrayList<>(); public InstagramPost(Player player, PlayerMatchPerformanceStats playerMatchPerformanceStats) { this.player = player; this.playerMatchPerformanceStats = playerMatchPerformanceStats; - this.caption = PostHelper.generatePostDefaultPlayerCaption(player, playerMatchPerformanceStats); - this.imageSearchUrl = PostHelper.generatePostImageSearchUrl(player, playerMatchPerformanceStats); - } - - public Player getPlayer() { - return player; - } - - public void setPlayer(Player player) { - this.player = player; - } - - public PlayerMatchPerformanceStats getPlayerMatchPerformanceStats() { - return playerMatchPerformanceStats; - } - - public void setPlayerMatchPerformanceStats(PlayerMatchPerformanceStats playerMatchPerformanceStats) { - this.playerMatchPerformanceStats = playerMatchPerformanceStats; - } - - public String getCaption() { - return caption; - } - - public void setCaption(String caption) { - this.caption = caption; - } - - public String getImageSearchUrl() { - return imageSearchUrl; - } - - public void setImageSearchUrl(String imageSearchUrl) { - this.imageSearchUrl = imageSearchUrl; } } diff --git a/src/main/java/com/ko/footballupdater/models/Match.java b/src/main/java/com/ko/footballupdater/models/Match.java index 04eb078..226e137 100644 --- a/src/main/java/com/ko/footballupdater/models/Match.java +++ b/src/main/java/com/ko/footballupdater/models/Match.java @@ -15,6 +15,7 @@ public class Match { private final String relevantTeam; private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd"); + private final SimpleDateFormat formatterFileName = new SimpleDateFormat("yyyy_MM_dd"); public Match(String url, Date date, String homeTeamName, String awayTeamName, String relevantTeam) { this.url = url; @@ -27,4 +28,8 @@ public Match(String url, Date date, String homeTeamName, String awayTeamName, St public String getDateAsFormattedString() { return formatter.format(date); } + + public String getDateAsFormattedStringForFileName() { + return formatterFileName.format(date); + } } diff --git a/src/main/java/com/ko/footballupdater/repositories/PlayerRepository.java b/src/main/java/com/ko/footballupdater/repositories/PlayerRepository.java index 64d2433..496c960 100644 --- a/src/main/java/com/ko/footballupdater/repositories/PlayerRepository.java +++ b/src/main/java/com/ko/footballupdater/repositories/PlayerRepository.java @@ -1,8 +1,8 @@ package com.ko.footballupdater.repositories; +import com.ko.footballupdater.models.Player; import org.springframework.data.repository.CrudRepository; -import com.ko.footballupdater.models.Player; import java.util.List; public interface PlayerRepository extends CrudRepository { diff --git a/src/main/java/com/ko/footballupdater/services/AmazonS3Service.java b/src/main/java/com/ko/footballupdater/services/AmazonS3Service.java new file mode 100644 index 0000000..9f27ee0 --- /dev/null +++ b/src/main/java/com/ko/footballupdater/services/AmazonS3Service.java @@ -0,0 +1,60 @@ +package com.ko.footballupdater.services; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.SdkClientException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.ko.footballupdater.configuration.AmazonS3Properties; +import com.ko.footballupdater.configuration.ImageGeneratorProperies; +import com.ko.footballupdater.models.InstagramPost; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.File; + +@Slf4j +@Service +public class AmazonS3Service { + + @Autowired + private ImageGeneratorProperies imageGeneratorProperies; + + @Autowired + private AmazonS3Properties amazonS3Properties; + + @Autowired + private AmazonS3 s3Client; + + public void uploadtoS3(InstagramPost post) { + if (!amazonS3Properties.isEnabled()) { + return; + } + if (!post.getImagesFileNames().isEmpty()) { + try { + // Upload images and save urls + // Overwrites any file with the same name + for (String imageFileName : post.getImagesFileNames()) { + String filePath = imageGeneratorProperies.getOutputPath() + imageFileName; + PutObjectRequest request = new PutObjectRequest(amazonS3Properties.getBucketName(),imageFileName, new File(filePath)) + .withCannedAcl(CannedAccessControlList.PublicRead); + s3Client.putObject(request); + String imageUrl = s3Client.getUrl(amazonS3Properties.getBucketName(), imageFileName).toString(); + log.atInfo().setMessage(post.getPlayer().getName() + " - Successfully uploaded image " + imageFileName + " to S3 @ " + imageUrl).log(); + post.getImagesS3Urls().add(imageUrl); + } + } catch (AmazonServiceException ex) { + // The call was transmitted successfully, but Amazon S3 couldn't process + // it, so it returned an error response. + log.warn(post.getPlayer().getName() + " - Error attempting to upload", ex); + } catch (SdkClientException ex) { + // Amazon S3 couldn't be contacted for a response, or the client + // couldn't parse the response from Amazon S3. + log.warn(post.getPlayer().getName() + " - Error attempting to upload", ex); + } + } else { + log.atInfo().setMessage(post.getPlayer().getName() + " - No images to upload").log(); + } + } +} diff --git a/src/main/java/com/ko/footballupdater/services/EmailService.java b/src/main/java/com/ko/footballupdater/services/EmailService.java index 933cdf8..de2498c 100644 --- a/src/main/java/com/ko/footballupdater/services/EmailService.java +++ b/src/main/java/com/ko/footballupdater/services/EmailService.java @@ -1,9 +1,15 @@ package com.ko.footballupdater.services; +import com.ko.footballupdater.configuration.ImageGeneratorProperies; +import com.ko.footballupdater.configuration.InstagramPostProperies; import com.ko.footballupdater.configuration.MailerProperties; import com.ko.footballupdater.models.InstagramPost; +import com.ko.footballupdater.utils.PostHelper; +import jakarta.activation.FileDataSource; import lombok.extern.slf4j.Slf4j; +import org.simplejavamail.api.email.AttachmentResource; import org.simplejavamail.api.email.Email; +import org.simplejavamail.api.email.EmailPopulatingBuilder; import org.simplejavamail.api.mailer.Mailer; import org.simplejavamail.api.mailer.config.TransportStrategy; import org.simplejavamail.email.EmailBuilder; @@ -11,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; @Slf4j @@ -20,27 +27,50 @@ public class EmailService { @Autowired private MailerProperties mailerProperties; + @Autowired + private ImageGeneratorProperies imageGeneratorProperies; -// @EventListener(ApplicationReadyEvent.class) -// public void validateMailerOnStartUp() { -// mailer.testConnection(); -// } + @Autowired + InstagramPostProperies instagramPostProperies; public boolean sendEmailUpdate(List posts){ + // Check config for email enabled status + if (!mailerProperties.isEnabled()) { + return false; + } + + // Initialise email builder + EmailPopulatingBuilder emailBuilder = EmailBuilder.startingBlank() + .from(mailerProperties.getFrom().getName(), mailerProperties.getFrom().getAddress()) + .to(mailerProperties.getTo().getName(), mailerProperties.getTo().getAddress()) + .withSubject(mailerProperties.getSubject()); + + List attachments = new ArrayList<>(); // Create email body StringBuilder emailContent = new StringBuilder(); for (InstagramPost post : posts) { - emailContent.append("############").append(post.getPlayer().getName()).append("############\n\n"); - emailContent.append(post.getCaption()).append("\n\n"); - emailContent.append(post.getImageSearchUrl()).append("\n\n\n"); + emailContent.append("############").append(post.getPlayer().getName()).append(" - ").append(post.getPlayerMatchPerformanceStats().getMatch().getDateAsFormattedString()).append("############\n\n"); + emailContent.append(PostHelper.generatePostCaption(instagramPostProperies.getVersion(), post)).append("\n\n"); + emailContent.append("Stat image(s)\n").append(PostHelper.generateS3UrlList(post)).append("\n"); + emailContent.append("Google image search links\n").append(PostHelper.generatePostImageSearchUrl(post)).append("\n\n\n"); + + // Add images to attachment - config driven + if (mailerProperties.isAttachImages()) { + if (!post.getImagesFileNames().isEmpty()) { + for (String fileName : post.getImagesFileNames()) { + attachments.add(new AttachmentResource(fileName, new FileDataSource(imageGeneratorProperies.getOutputPath() + fileName))); + } + } + } } + // Add caption, attachments + emailBuilder = emailBuilder.withPlainText(emailContent.toString()); + emailBuilder = emailBuilder.withAttachments(attachments); + + // Send email try { - Email email = EmailBuilder.startingBlank() - .from(mailerProperties.getFrom().getName(), mailerProperties.getFrom().getAddress()) - .to(mailerProperties.getTo().getName(), mailerProperties.getTo().getAddress()) - .withSubject(mailerProperties.getSubject()) - .withPlainText(emailContent.toString()) - .buildEmail(); + log.info("Attempting to send email"); + Email email = emailBuilder.buildEmail(); Mailer mailer = MailerBuilder .withSMTPServer("smtp.gmail.com", 587, mailerProperties.getFrom().getAddress(), mailerProperties.getFrom().getPassword()) @@ -50,7 +80,6 @@ public boolean sendEmailUpdate(List posts){ .withDebugLogging(true) .buildMailer(); -// mailer.testConnection(); mailer.validate(email); mailer.sendMail(email); } catch (Exception ex) { diff --git a/src/main/java/com/ko/footballupdater/services/ImageGeneratorService.java b/src/main/java/com/ko/footballupdater/services/ImageGeneratorService.java new file mode 100644 index 0000000..25bdab0 --- /dev/null +++ b/src/main/java/com/ko/footballupdater/services/ImageGeneratorService.java @@ -0,0 +1,166 @@ +package com.ko.footballupdater.services; + +import com.ko.footballupdater.configuration.ImageGeneratorProperies; +import com.ko.footballupdater.models.ImageStatEntry; +import com.ko.footballupdater.models.InstagramPost; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class ImageGeneratorService { + + @Autowired + private ImageGeneratorProperies imageGeneratorProperies; + + private final int STAT_Y_COORDINATE = 350; + private final String BASE_IMAGE_FILE_NAME = "_base_player_stat_image.jpg"; + + public void generatePlayerStatImage(InstagramPost post) { + if (!imageGeneratorProperies.isEnabled()) { + return; + } + + try { + // Load the base image + String playerImageBaseFilePath = imageGeneratorProperies.getInputPath() + post.getPlayer().getName().replaceAll(" ", "") + BASE_IMAGE_FILE_NAME; + BufferedImage image = setUpBaseImage(playerImageBaseFilePath, post); + + // Match stats to image + Graphics2D graphics = setUpStatsGraphicsDefaults(image); + // Starting coordinate for first row + int statNameX = 79; + int statValueX = 450; + int statY = STAT_Y_COORDINATE; + + int attributeCounter = 0; + int createdImageCounter = 0; +// Map attributeValueMap = new HashMap<>(); + for (Field field : post.getPlayerMatchPerformanceStats().getClass().getDeclaredFields()) { + field.setAccessible(true); // Make the private field accessible + try { + Object value = field.get(post.getPlayerMatchPerformanceStats()); // Get the field's value + if (value != null && !field.getName().equals("match")) { + List zeroValueFilter = getZeroValueFilter(); + if (zeroValueFilter.contains(field.getName()) && value.equals(0)) { + continue; + } + // Generate proper stat name - capitalise words and spacing + ImageStatEntry imageStatEntry = generateDisplayedName(field.getName(), value); + +// attributeValueMap.put(field.getName(), value); + graphics.drawString(imageStatEntry.getName(), statNameX, statY); + graphics.drawString(imageStatEntry.getValue(), statValueX, statY); + // Shift y coordinate down to next position + statY += 51; + attributeCounter++; + // 12 Rows on one image + // Once max stats for one image is added, generate new image + if (attributeCounter % 12 == 0) { + createdImageCounter++; + saveImage(post, image, createdImageCounter); + image = setUpBaseImage(playerImageBaseFilePath, post); + graphics = setUpStatsGraphicsDefaults(image); + statY = STAT_Y_COORDINATE; + } + } + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + // Dispose of the graphics object to release resources + graphics.dispose(); + + // Save the modified image + createdImageCounter++; + saveImage(post, image, createdImageCounter); + } catch (Exception ex) { + log.warn(post.getPlayer().getName() + " - Error while generating stat image ", ex); + } + } + + private BufferedImage setUpBaseImage(String baseImagePath, InstagramPost post) throws IOException { + BufferedImage baseImage = ImageIO.read(new File(baseImagePath)); + Graphics2D graphics = baseImage.createGraphics(); + + // Add player name to the image + graphics.setFont(new Font("Nike Ithaca", Font.PLAIN, 47)); + graphics.setColor(Color.BLACK); + int playerNameX = 77; + int playerNameY = 116; + graphics.drawString(post.getPlayer().getName().toUpperCase(), playerNameX, playerNameY); + graphics.dispose(); + return baseImage; + } + + private Graphics2D setUpStatsGraphicsDefaults(BufferedImage image) { + Graphics2D graphics = image.createGraphics(); + Font font = new Font("Chakra Petch", Font.BOLD, 30); + Color textColor = Color.BLACK; + graphics.setFont(font); + graphics.setColor(textColor); + return graphics; + } + + private ImageStatEntry generateDisplayedName(String displayStatName, Object value) { + if (displayStatName.contains("All")) { + displayStatName = displayStatName.replace("All", ""); + } + if (displayStatName.contains("Percentage")) { + displayStatName = displayStatName.replace("Percentage", ""); + value = value + "%"; + } + if (displayStatName.contains("gk")) { + displayStatName = displayStatName.replace("gk", ""); + if (displayStatName.contains("Penalties")) { + displayStatName = displayStatName.replace("Penalties", "Pens"); + } + } + displayStatName = displayStatName.replaceAll("(.)([A-Z])", "$1 $2"); + displayStatName = displayStatName.substring(0, 1).toUpperCase() + displayStatName.substring(1); + return new ImageStatEntry(displayStatName, String.valueOf(value)); + } + + private void saveImage(InstagramPost post, BufferedImage image, int createdImageCounter) throws IOException { + String fileName = post.getPlayer().getName().replaceAll(" ", "") + "_" + post.getPlayerMatchPerformanceStats().getMatch().getDateAsFormattedStringForFileName() + "_stat_image_" + createdImageCounter + ".jpg"; + String outputImageFilePath = imageGeneratorProperies.getOutputPath() + fileName; + ImageIO.write(image, "jpg", new File(outputImageFilePath)); + post.getImagesFileNames().add(fileName); + log.atInfo().setMessage(post.getPlayer().getName() + " - Generated stat image " + createdImageCounter).addKeyValue("player", post.getPlayer().getName()).log(); + } + + private static List getZeroValueFilter() { + List zeroValueFilter = new ArrayList<>(); + zeroValueFilter.add("penaltiesScored"); + zeroValueFilter.add("penaltiesWon"); + zeroValueFilter.add("shotsOnTarget"); + zeroValueFilter.add("shotsBlocked"); + zeroValueFilter.add("yellowCards"); + zeroValueFilter.add("redCards"); + zeroValueFilter.add("fouled"); + zeroValueFilter.add("offsides"); + zeroValueFilter.add("crosses"); + zeroValueFilter.add("crossingAccuracyAll"); + zeroValueFilter.add("groundDuelsWon"); + zeroValueFilter.add("aerialDuelsWon"); + zeroValueFilter.add("xg"); + zeroValueFilter.add("xg_assist"); + zeroValueFilter.add("chancesCreatedAll"); + zeroValueFilter.add("gkPenaltiesAttemptedAgainst"); + zeroValueFilter.add("gkPenaltiesScoredAgainst"); + zeroValueFilter.add("gkPenaltiesSaved"); + return zeroValueFilter; + } + +} diff --git a/src/main/java/com/ko/footballupdater/services/ParsingService.java b/src/main/java/com/ko/footballupdater/services/ParsingService.java index b4bc0bf..5aea3ac 100644 --- a/src/main/java/com/ko/footballupdater/services/ParsingService.java +++ b/src/main/java/com/ko/footballupdater/services/ParsingService.java @@ -117,6 +117,7 @@ public PlayerMatchPerformanceStats parsePlayerMatchData(Player player) { Document doc = Jsoup.connect(dataSource.getUrl()).get(); PlayerMatchPerformanceStats playerMatchPerformanceStats = dataSourceParser.parsePlayerMatchData(player, doc); if (playerMatchPerformanceStats != null) { + log.atInfo().setMessage(player.getName() + " - " + dataSource.getSiteName() + " - Successfully parse player data").addKeyValue("player", player.getName()).log(); player.getCheckedStatus().setSiteName(dataSource.getSiteName()); return playerMatchPerformanceStats; } diff --git a/src/main/java/com/ko/footballupdater/services/PlayerService.java b/src/main/java/com/ko/footballupdater/services/PlayerService.java index f1ea6e2..6142bb3 100644 --- a/src/main/java/com/ko/footballupdater/services/PlayerService.java +++ b/src/main/java/com/ko/footballupdater/services/PlayerService.java @@ -16,6 +16,7 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.Optional; @Service public class PlayerService { @@ -32,6 +33,12 @@ public class PlayerService { @Autowired private EmailService emailService; + @Autowired + private AmazonS3Service amazonS3Service; + + @Autowired + private ImageGeneratorService imageGeneratorService; + public Player addPlayer(Player newPlayer, DataSourceSiteName dataSourceSiteName) throws Exception { if (!playerRepository.findByNameEquals(newPlayer.getName()).isEmpty()) { throw new Exception("Player already exists"); @@ -50,29 +57,56 @@ public Iterable getPlayers() { return playerRepository.findAll(); } - public void updateDataForAllPlayers(UpdatePlayersResponse response) { + public UpdatePlayersResponse updateDataForAllPlayers() { // Find latest match data for each player Iterator playerIterator = playerRepository.findAll().iterator(); - List posts = new ArrayList<>(); + List requestPlayersToUpdate = new ArrayList<>(); while(playerIterator.hasNext()){ Player player = playerIterator.next(); + requestPlayersToUpdate.add(player); + } + + return updateDataForPlayers(requestPlayersToUpdate); + } + + public UpdatePlayersResponse updateDataForPlayer(Integer playerId) throws Exception { + // Find latest match data for each player + Optional requestPlayersToUpdate = playerRepository.findById(playerId); + if (requestPlayersToUpdate.isEmpty()) { + throw new Exception("Player name not found"); + } + return updateDataForPlayers(requestPlayersToUpdate.stream().toList()); + } + + public UpdatePlayersResponse updateDataForPlayers(List requestPlayersToUpdate) { + UpdatePlayersResponse response = new UpdatePlayersResponse(); + + // Find latest match data for each player + List posts = new ArrayList<>(); + + for (Player player : requestPlayersToUpdate) { PlayerMatchPerformanceStats playerMatchPerformanceStats = parsingService.parsePlayerMatchData(player); if (playerMatchPerformanceStats == null) { // No new updates continue; } - // Generate caption + // Generate post and caption InstagramPost post = new InstagramPost(player, playerMatchPerformanceStats); + // Generate stat images + imageGeneratorService.generatePlayerStatImage(post); + // Upload stat images to s3 + amazonS3Service.uploadtoS3(post); posts.add(post); } // No updates if (posts.isEmpty()) { - return; + return response; } boolean isEmailSent = emailService.sendEmailUpdate(posts); + response.setEmailSent(isEmailSent); if (isEmailSent) { List playersToUpdate = new ArrayList<>(); @@ -89,9 +123,11 @@ public void updateDataForAllPlayers(UpdatePlayersResponse response) { response.setPlayersUpdated(playersToUpdate); response.setNumPlayersUpdated(playersToUpdate.size()); } + return response; } - public DataSource updatePlayerDataSource(DataSource dataSource) { + + public DataSource updatePlayerDataSource(DataSource dataSource) { // playerRepository.findByNameEquals(); return dataSource; } diff --git a/src/main/java/com/ko/footballupdater/utils/PostHelper.java b/src/main/java/com/ko/footballupdater/utils/PostHelper.java index 1140fd8..f81426f 100644 --- a/src/main/java/com/ko/footballupdater/utils/PostHelper.java +++ b/src/main/java/com/ko/footballupdater/utils/PostHelper.java @@ -1,10 +1,23 @@ package com.ko.footballupdater.utils; +import com.ko.footballupdater.models.InstagramPost; import com.ko.footballupdater.models.Player; import com.ko.footballupdater.models.PlayerMatchPerformanceStats; public class PostHelper { + // Generate caption based on post version + // v1 All stats in caption + // v2 Only name, match, date, hashtags in caption + public static String generatePostCaption(int version, InstagramPost post) { + if (version == 2) { + return generatePostDefaultPlayerCaptionV2(post.getPlayer(), post.getPlayerMatchPerformanceStats()); + } else { + return generatePostDefaultPlayerCaption(post.getPlayer(), post.getPlayerMatchPerformanceStats()); + } + } + + // V1 public static String generatePostDefaultPlayerCaption(Player player, PlayerMatchPerformanceStats playerMatchPerformanceStats) { return String.format("%s stats in %s vs %s on %s\n", player.getName(), @@ -14,9 +27,20 @@ public static String generatePostDefaultPlayerCaption(Player player, PlayerMatch ) + playerMatchPerformanceStats.toFormattedString() + generatePlayerHashtags(player, playerMatchPerformanceStats); } - public static String generatePostImageSearchUrl(Player player, PlayerMatchPerformanceStats playerMatchPerformanceStats) { - String searchPhrase = player.getName() + " " + playerMatchPerformanceStats.getMatch().getRelevantTeam(); - return String.format("https://www.google.com/search?q=%s&tbm=isch&hl=en&tbs=qdr:d\n\n", searchPhrase.replaceAll(" ", "%20")) + String.format("https://www.google.com/search?q=%s&tbm=isch&hl=en&tbs=qdr:w", searchPhrase.replaceAll(" ", "%20")); + + // V2 + public static String generatePostDefaultPlayerCaptionV2(Player player, PlayerMatchPerformanceStats playerMatchPerformanceStats) { + return String.format("%s stats in %s vs %s on %s\n", + player.getName(), + playerMatchPerformanceStats.getMatch().getHomeTeamName(), + playerMatchPerformanceStats.getMatch().getAwayTeamName(), + playerMatchPerformanceStats.getMatch().getDateAsFormattedString() + ) + generatePlayerHashtags(player, playerMatchPerformanceStats); + } + + public static String generatePostImageSearchUrl(InstagramPost post) { + String searchPhrase = post.getPlayer().getName() + " " + post.getPlayerMatchPerformanceStats().getMatch().getRelevantTeam(); + return String.format("https://www.google.com/search?q=%s&tbm=isch&hl=en&tbs=qdr:d\n", searchPhrase.replaceAll(" ", "%20")) + String.format("https://www.google.com/search?q=%s&tbm=isch&hl=en&tbs=qdr:w", searchPhrase.replaceAll(" ", "%20")); } public static String generatePlayerHashtags(Player player, PlayerMatchPerformanceStats playerMatchPerformanceStats) { @@ -25,9 +49,20 @@ public static String generatePlayerHashtags(Player player, PlayerMatchPerformanc teamNameHashtag = "#" + playerMatchPerformanceStats.getMatch().getRelevantTeam().replaceAll(" ", ""); } return "#" + player.getName().replaceAll(" ", "") + " " + - teamNameHashtag + " " + "#upthetillies" + " " + "#womensfootball" + " " + - "#womenssoccer"; + "#womenssoccer" + " " + + "#woso" + " " + + teamNameHashtag; + } + + public static String generateS3UrlList(InstagramPost post) { + StringBuilder builder = new StringBuilder(); + if (!post.getImagesS3Urls().isEmpty()) { + for (String url : post.getImagesS3Urls()) { + builder.append(url).append("\n"); + } + } + return builder.toString(); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bf1da35..5a7665a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,12 +10,29 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect endpoint.secret=${ENDPOINT_SECRET} +# Data source configs datasource.sitename=FBREF datasource.priority=${DATASOURCE_PRIORITY} +# Post configs +ig.post.version=${IG_POST_VERSION:2} + +# Image generator configs +image.generator.enabled=${IMAGE_GENERATOR_ENABLED:true} +image.generator.inputPath=${IMAGE_GENERATOR_INPUT_PATH:./src/main/resources/images/} +image.generator.outputPath=${IMAGE_GENERATOR_OUTPUT_PATH:./src/main/resources/images/output/} + +mailer.enabled=${MAILER_IS_ENABLED:true} mailer.subject=${MAILER_SUBJECT:Latest Football Updates} mailer.from.name=${MAILER_FROM_NAME:Football Updater} mailer.from.address=${MAILER_FROM_ADDRESS} mailer.from.password=${MAILER_FROM_PASSWORD} mailer.to.name=${MAILER_TO_NAME:You} mailer.to.address=${MAILER_TO_ADDRESS} +mailer.attachImages=${MAILER_IS_ATTACH_IMAGES:false} + +# AWS credential configs +aws.s3.enabled=${AWS_S3_IS_ENABLED:true} +aws.s3.accessKey=${AWS_ACCESS_KEY:access key} +aws.s3.secretKey=${AWS_SECRET_KEY:secret key} +aws.s3.bucketName=${AWS_S3_BUCKET_NAME:football-updater-bucket} \ No newline at end of file