From 3fa39437688d88f56f9bdeea01f29fc8b9c3cb6c Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Thu, 4 Apr 2024 19:03:35 +0200 Subject: [PATCH 1/1] =?utf8?q?=F0=9F=9A=A7=20Add=20filter=20for=20free=20g?= =?utf8?q?ames=20from=20the=20Epic=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../filters/webpages/epicgames/EpicGamesFilter.kt | 49 ++++++++++++++++++++++ .../webpages/epicgames/EpicGamesFilterTest.kt | 43 +++++++++++++++++++ .../rhynodge/filters/webpages/epicgames/epic.json | 1 + 3 files changed, 93 insertions(+) create mode 100644 src/main/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilter.kt create mode 100644 src/test/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilterTest.kt create mode 100644 src/test/resources/net/pterodactylus/rhynodge/filters/webpages/epicgames/epic.json diff --git a/src/main/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilter.kt b/src/main/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilter.kt new file mode 100644 index 0000000..31bb11f --- /dev/null +++ b/src/main/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilter.kt @@ -0,0 +1,49 @@ +package net.pterodactylus.rhynodge.filters.webpages.epicgames + +import com.fasterxml.jackson.databind.JsonNode +import net.pterodactylus.rhynodge.Filter +import net.pterodactylus.rhynodge.State +import net.pterodactylus.rhynodge.states.AbstractState +import net.pterodactylus.rhynodge.states.JsonState +import java.time.Instant + +class EpicGamesFilter : Filter { + + override fun filter(state: State): State { + state as? JsonState ?: throw IllegalArgumentException("state must be a JSON state") + return state.jsonNode.at("/data/Catalog/searchStore/elements").map { gameJson: JsonNode -> + val title = gameJson.get("title").asText() + val imageUrl = gameJson.at("/keyImages/1/url").asText() + val startDate = getPromotionalOfferStartDate(gameJson) + val endDate = getPromotionalOfferEndDate(gameJson) + FreeGame(title, imageUrl, startDate, endDate) + }.let { FreeGamesState(it.toSet()) } + } + + private fun getPromotionalOfferEndDate(gameJson: JsonNode) = getPromotionalOfferDate(gameJson, "endDate") + private fun getPromotionalOfferStartDate(gameJson: JsonNode) = getPromotionalOfferDate(gameJson, "startDate") + + private fun getPromotionalOfferDate(gameJson: JsonNode, date: String) = listOf("promotionalOffers", "upcomingPromotionalOffers") + .map { "/promotions/$it/0/promotionalOffers/0/$date" } + .map { gameJson.at(it) } + .filter { !it.isMissingNode } + .map { it.asText() } + .map(Instant::parse) + .first() + +} + +data class FreeGame( + val title: String, + val imageUrl: String, + val startDate: Instant, + val endDate: Instant +) + +class FreeGamesState(val games: Set) : AbstractState(true) { + + override fun plainText(): String { + TODO("Not yet implemented") + } + +} diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilterTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilterTest.kt new file mode 100644 index 0000000..2d44796 --- /dev/null +++ b/src/test/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilterTest.kt @@ -0,0 +1,43 @@ +package net.pterodactylus.rhynodge.filters.webpages.epicgames + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import net.pterodactylus.rhynodge.Filter +import net.pterodactylus.rhynodge.states.HttpState +import net.pterodactylus.rhynodge.states.JsonState +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.instanceOf +import org.junit.Assert.assertThrows +import org.junit.Test +import java.time.Instant.parse + +class EpicGamesFilterTest { + + @Test + fun `epic games filter is a filter`() { + assertThat(filter, instanceOf(Filter::class.java)) + } + + @Test + fun `filter throws exception when given html state`() { + assertThrows(IllegalArgumentException::class.java) { filter.filter(HttpState("", 200, "", byteArrayOf())) } + } + + @Test + fun `filter finds correct games`() { + val gameState = filter.filter(JsonState(objectMapper.readTree(EpicGamesFilterTest::class.java.getResourceAsStream("epic.json")))) as FreeGamesState + assertThat( + gameState.games, contains( + FreeGame("Islets", "https://cdn1.epicgames.com/spt-assets/f991a978e0ce4156a52f951e96e388e7/download-islets-offer-rtq8h.png", parse("2024-03-28T15:00:00.000Z"), parse("2024-04-04T15:00:00.000Z")), + FreeGame("The Outer Worlds: Spacer's Choice Edition", "https://cdn1.epicgames.com/offer/dc61166eea95474e912953b163791d42/EGS_TheOuterWorldsSpacersChoiceEdition_ObsidianEntertainment_S2_1200x1600-24b156886564b75bf9aa823a0a0eb18e", parse("2024-04-04T15:00:00.000Z"), parse("2024-04-11T15:00:00.000Z")), + FreeGame("Thief", "https://cdn1.epicgames.com/spt-assets/44b12bc6a7f045a3bf313574c344dfd7/thief-1hsod.png", parse("2024-04-04T15:00:00.000Z"), parse("2024-04-11T15:00:00.000Z")), + FreeGame("Lost Castle: The Old Ones Awaken", "https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-1qvy6.jpg", parse("2024-04-11T15:00:00.000Z"), parse("2024-04-25T15:00:00.000Z")), + ) + ) + } + + private val filter = EpicGamesFilter() + +} + +private val objectMapper = jacksonObjectMapper() diff --git a/src/test/resources/net/pterodactylus/rhynodge/filters/webpages/epicgames/epic.json b/src/test/resources/net/pterodactylus/rhynodge/filters/webpages/epicgames/epic.json new file mode 100644 index 0000000..592f8ee --- /dev/null +++ b/src/test/resources/net/pterodactylus/rhynodge/filters/webpages/epicgames/epic.json @@ -0,0 +1 @@ +{"data":{"Catalog":{"searchStore":{"elements":[{"title":"Islets","id":"4862aa73b8b048c782958b8f22118d28","namespace":"cecc5a53aa534e15881fdbd67a1e83b7","description":"Take to the sky and reunite a fragmented world in this surprisingly wholesome metroidvania! Help Iko adventure across beautiful hand-painted islands, receive letters from a quirky cast of characters, and face powerful monstrous adversaries.","effectiveDate":"2022-08-24T13:00:00.000Z","offerType":"BASE_GAME","expiryDate":null,"viewableDate":"2022-08-15T13:00:00.000Z","status":"ACTIVE","isCodeRedemptionOnly":false,"keyImages":[{"type":"OfferImageWide","url":"https://cdn1.epicgames.com/spt-assets/f991a978e0ce4156a52f951e96e388e7/islets-offer-1ok6p.png"},{"type":"OfferImageTall","url":"https://cdn1.epicgames.com/spt-assets/f991a978e0ce4156a52f951e96e388e7/download-islets-offer-rtq8h.png"},{"type":"Thumbnail","url":"https://cdn1.epicgames.com/spt-assets/f991a978e0ce4156a52f951e96e388e7/download-islets-offer-rtq8h.png"}],"seller":{"id":"o-ttudgfyffrswppq6pflnkwfzdm6qc3","name":"Armor Games Studios"},"productSlug":null,"urlSlug":"99c1d8e3306e4b6081f12eb58cfd2c09","url":null,"items":[{"id":"e6fa7f09955b400ba1392a82bf397b11","namespace":"cecc5a53aa534e15881fdbd67a1e83b7"}],"customAttributes":[{"key":"autoGeneratedPrice","value":"false"},{"key":"isManuallySetPCReleaseDate","value":"false"},{"key":"isBlockchainUsed","value":"false"}],"categories":[{"path":"freegames"},{"path":"games"},{"path":"games/edition"},{"path":"games/edition/base"}],"tags":[{"id":"1381"},{"id":"1336"},{"id":"1370"},{"id":"9547"},{"id":"9549"},{"id":"1151"}],"catalogNs":{"mappings":[{"pageSlug":"islets-5f2670","pageType":"productHome"}]},"offerMappings":[{"pageSlug":"islets-5f2670","pageType":"productHome"}],"price":{"totalPrice":{"discountPrice":0,"originalPrice":1950,"voucherDiscount":0,"discount":1950,"currencyCode":"EUR","currencyInfo":{"decimals":2},"fmtPrice":{"originalPrice":"€19.50","discountPrice":"0","intermediatePrice":"0"}},"lineOffers":[{"appliedRules":[{"id":"28ce846017a24861a1ea9a47f231b685","endDate":"2024-04-04T15:00:00.000Z","discountSetting":{"discountType":"PERCENTAGE"}}]}]},"promotions":{"promotionalOffers":[{"promotionalOffers":[{"startDate":"2024-03-28T15:00:00.000Z","endDate":"2024-04-04T15:00:00.000Z","discountSetting":{"discountType":"PERCENTAGE","discountPercentage":0}}]}],"upcomingPromotionalOffers":[]}},{"title":"The Outer Worlds: Spacer's Choice Edition","id":"0769596f15a445b7a5ad3f8d7c7730e2","namespace":"dc61166eea95474e912953b163791d42","description":"The Outer Worlds: Spacer’s Choice Edition is the ultimate way to play the award-winning RPG from Obsidian Entertainment and Private Division. Including the base game and all DLC, this remastered masterpiece is the absolute best version of The Outer Worlds.","effectiveDate":"2023-03-07T16:00:00.000Z","offerType":"BASE_GAME","expiryDate":null,"viewableDate":"2023-02-27T14:00:00.000Z","status":"ACTIVE","isCodeRedemptionOnly":false,"keyImages":[{"type":"Thumbnail","url":"https://cdn1.epicgames.com/offer/dc61166eea95474e912953b163791d42/EGS_TheOuterWorldsSpacersChoiceEdition_ObsidianEntertainment_S2_1200x1600-24b156886564b75bf9aa823a0a0eb18e"},{"type":"OfferImageTall","url":"https://cdn1.epicgames.com/offer/dc61166eea95474e912953b163791d42/EGS_TheOuterWorldsSpacersChoiceEdition_ObsidianEntertainment_S2_1200x1600-24b156886564b75bf9aa823a0a0eb18e"},{"type":"OfferImageWide","url":"https://cdn1.epicgames.com/offer/dc61166eea95474e912953b163791d42/EGS_TheOuterWorldsSpacersChoiceEdition_ObsidianEntertainment_S1_2560x1440-dd9211a8277a2392a9dd5b108858ba33"},{"type":"CodeRedemption_340x440","url":"https://cdn1.epicgames.com/offer/dc61166eea95474e912953b163791d42/EGS_TheOuterWorldsSpacersChoiceEdition_ObsidianEntertainment_S2_1200x1600-24b156886564b75bf9aa823a0a0eb18e"}],"seller":{"id":"o-6emrb2lpzacm9ued3z37u42x56wkaz","name":"Private Division"},"productSlug":"the-outer-worlds-spacers-choice-edition","urlSlug":"the-outer-worlds-spacers-choice-edition","url":null,"items":[{"id":"63e19dde752b4306aa0c2afeddfc93aa","namespace":"dc61166eea95474e912953b163791d42"}],"customAttributes":[{"key":"com.epicgames.app.blacklist","value":"[]"},{"key":"com.epicgames.app.productSlug","value":"the-outer-worlds-spacers-choice-edition"}],"categories":[{"path":"freegames"},{"path":"games"},{"path":"games/edition"},{"path":"games/edition/base"},{"path":"applications"}],"tags":[{"id":"1216"},{"id":"21122"},{"id":"1188"},{"id":"21894"},{"id":"21127"},{"id":"9547"},{"id":"9549"},{"id":"15375"},{"id":"21138"},{"id":"21139"},{"id":"21140"},{"id":"21141"},{"id":"1367"},{"id":"1210"},{"id":"1370"},{"id":"21147"},{"id":"21149"},{"id":"21119"}],"catalogNs":{"mappings":[{"pageSlug":"the-outer-worlds-spacers-choice-edition","pageType":"productHome"}]},"offerMappings":[],"price":{"totalPrice":{"discountPrice":5999,"originalPrice":5999,"voucherDiscount":0,"discount":0,"currencyCode":"EUR","currencyInfo":{"decimals":2},"fmtPrice":{"originalPrice":"€59.99","discountPrice":"€59.99","intermediatePrice":"€59.99"}},"lineOffers":[{"appliedRules":[]}]},"promotions":{"promotionalOffers":[],"upcomingPromotionalOffers":[{"promotionalOffers":[{"startDate":"2024-04-04T15:00:00.000Z","endDate":"2024-04-11T15:00:00.000Z","discountSetting":{"discountType":"PERCENTAGE","discountPercentage":0}}]}]}},{"title":"Thief","id":"23e4018b5fea4fa0bc1337da2b286154","namespace":"3319fe5042ab4392a2b11c6938c0cda1","description":"Thief is a 1st person stealth-action game by Eidos-Montréal. In this reimagination of the cult classic Thief franchise, steal, stealth and infiltrate your way through the treacherous City as Garrett, the Master Thief.","effectiveDate":"2023-12-13T16:00:00.000Z","offerType":"BASE_GAME","expiryDate":null,"viewableDate":"2023-12-13T16:00:00.000Z","status":"ACTIVE","isCodeRedemptionOnly":false,"keyImages":[{"type":"OfferImageWide","url":"https://cdn1.epicgames.com/spt-assets/44b12bc6a7f045a3bf313574c344dfd7/thief-ms1j4.png"},{"type":"OfferImageTall","url":"https://cdn1.epicgames.com/spt-assets/44b12bc6a7f045a3bf313574c344dfd7/thief-1hsod.png"},{"type":"Thumbnail","url":"https://cdn1.epicgames.com/spt-assets/44b12bc6a7f045a3bf313574c344dfd7/thief-1hsod.png"}],"seller":{"id":"o-6e9jt6yt8fym4t52q6pcfeke4rxv5l","name":"Eidos Interactive Corporation"},"productSlug":null,"urlSlug":"89f4a7b9f3df4f2aa468a2ee2fe38154","url":null,"items":[{"id":"a8d4d0ed64d743c8a8e1adb0e18dccbc","namespace":"3319fe5042ab4392a2b11c6938c0cda1"}],"customAttributes":[{"key":"autoGeneratedPrice","value":"false"},{"key":"isManuallySetViewableDate","value":"false"},{"key":"isManuallySetPCReleaseDate","value":"true"},{"key":"isBlockchainUsed","value":"false"}],"categories":[{"path":"freegames"},{"path":"games"},{"path":"games/edition"},{"path":"games/edition/base"}],"tags":[{"id":"21894"},{"id":"1336"},{"id":"1370"},{"id":"9547"},{"id":"1084"},{"id":"15375"}],"catalogNs":{"mappings":[{"pageSlug":"thief-5bb95f","pageType":"productHome"}]},"offerMappings":[{"pageSlug":"thief-5bb95f","pageType":"productHome"}],"price":{"totalPrice":{"discountPrice":1999,"originalPrice":1999,"voucherDiscount":0,"discount":0,"currencyCode":"EUR","currencyInfo":{"decimals":2},"fmtPrice":{"originalPrice":"€19.99","discountPrice":"€19.99","intermediatePrice":"€19.99"}},"lineOffers":[{"appliedRules":[]}]},"promotions":{"promotionalOffers":[],"upcomingPromotionalOffers":[{"promotionalOffers":[{"startDate":"2024-04-04T15:00:00.000Z","endDate":"2024-04-11T15:00:00.000Z","discountSetting":{"discountType":"PERCENTAGE","discountPercentage":0}}]}]}},{"title":"Lost Castle: The Old Ones Awaken","id":"4a88d0dc64114b20b67339c74543f859","namespace":"ab29925a0a9a49598adba45d108ceb3e","description":"The Old Ones Awaken adds new levels to explore, new enemies to defeat, and new bosses to face along with a host exciting new weapons and equipment to wield and wear.","effectiveDate":"2024-02-08T16:00:00.000Z","offerType":"ADD_ON","expiryDate":null,"viewableDate":"2024-02-01T16:00:00.000Z","status":"ACTIVE","isCodeRedemptionOnly":false,"keyImages":[{"type":"OfferImageWide","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-r390n.png"},{"type":"OfferImageTall","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-1qvy6.jpg"},{"type":"Thumbnail","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-1qvy6.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-5fr2h.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-tl3jh.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-ooqww.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-y89ep.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-sagu3.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-1309n.jpg"},{"type":"featuredMedia","url":"https://cdn1.epicgames.com/spt-assets/a6d76157ad884f2c9aa470b30da9e2ff/lost-castle-1mwvz.jpg"}],"seller":{"id":"o-ze7grkplqlrzc92lepkjv4xpaj7gn8","name":"Another Indie Studio Limited"},"productSlug":null,"urlSlug":"lost-castle-the-old-ones-awaken","url":null,"items":[{"id":"30f2fedfe5af4e9d96e151696f372a70","namespace":"ab29925a0a9a49598adba45d108ceb3e"}],"customAttributes":[{"key":"isManuallySetRefundableType","value":"true"},{"key":"autoGeneratedPrice","value":"false"},{"key":"isManuallySetViewableDate","value":"true"},{"key":"isManuallySetPCReleaseDate","value":"false"},{"key":"isBlockchainUsed","value":"false"}],"categories":[{"path":"addons"},{"path":"freegames"},{"path":"addons/durable"}],"tags":[{"id":"1264"},{"id":"1265"},{"id":"1367"},{"id":"1370"},{"id":"1083"},{"id":"9547"},{"id":"9549"}],"catalogNs":{"mappings":[{"pageSlug":"lost-castle-abb2e2","pageType":"productHome"}]},"offerMappings":[{"pageSlug":"lost-castle-lost-castle-the-old-ones-awaken-db1545","pageType":"offer"}],"price":{"totalPrice":{"discountPrice":359,"originalPrice":359,"voucherDiscount":0,"discount":0,"currencyCode":"EUR","currencyInfo":{"decimals":2},"fmtPrice":{"originalPrice":"€3.59","discountPrice":"€3.59","intermediatePrice":"€3.59"}},"lineOffers":[{"appliedRules":[]}]},"promotions":{"promotionalOffers":[],"upcomingPromotionalOffers":[{"promotionalOffers":[{"startDate":"2024-04-11T15:00:00.000Z","endDate":"2024-04-25T15:00:00.000Z","discountSetting":{"discountType":"PERCENTAGE","discountPercentage":50}}]}]}}],"paging":{"count":1000,"total":4}}}},"extensions":{}} \ No newline at end of file -- 2.7.4