🚧 Add filter for free games from the Epic store
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 4 Apr 2024 17:03:35 +0000 (19:03 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 4 Apr 2024 17:03:35 +0000 (19:03 +0200)
src/main/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilter.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/rhynodge/filters/webpages/epicgames/EpicGamesFilterTest.kt [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/filters/webpages/epicgames/epic.json [new file with mode: 0644]

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 (file)
index 0000000..31bb11f
--- /dev/null
@@ -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<FreeGame>) : 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 (file)
index 0000000..2d44796
--- /dev/null
@@ -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 (file)
index 0000000..592f8ee
--- /dev/null
@@ -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