hltb example: refactor find game progression system, add duckduckgo as fallback game repository

This commit is contained in:
Wouter Groeneveld 2021-08-19 09:46:03 +02:00
parent adde1e8e70
commit 561756ed6f
39 changed files with 1674 additions and 180 deletions

View File

@ -54,7 +54,8 @@ dependencies {
// --- kotlinx extras // --- kotlinx extras
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1")
// --- navigation // --- navigation
// see https://developer.android.com/guide/navigation/navigation-getting-started // see https://developer.android.com/guide/navigation/navigation-getting-started

View File

@ -0,0 +1,35 @@
package be.kuleuven.howlongtobeat.cartridges
import androidx.test.platform.app.InstrumentationRegistry
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
class CartridgeFinderViaDuckDuckGoTest {
private lateinit var repo: CartridgeFinderViaDuckDuckGo
@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
repo = CartridgeFinderViaDuckDuckGo(appContext)
// no need to set the main dispatcher, it's running on the device itself!
}
@Test
fun find_knownCodesForDuckDuckGo_returnsCorrectCartridge() = runBlocking {
launch(Dispatchers.Main) {
val smbDeluxe = repo.find("CGB-AHYE-USA")
assertEquals("super mario bros deluxe", smbDeluxe?.title)
assertEquals("CGB-AHYE-USA", smbDeluxe?.code)
val marioGolf = repo.find("cgb-awxp-eur-1")
assertEquals("mario golf", marioGolf?.title)
assertEquals("cgb-awxp-eur-1", marioGolf?.code)
}
println("launched main dispatcher") // this must be there: Kotlin-to-Java doesn't recognize the retval because of runBlocking.
}
}

View File

@ -0,0 +1,46 @@
package be.kuleuven.howlongtobeat.hltb
import androidx.test.platform.app.InstrumentationRegistry
import be.kuleuven.howlongtobeat.cartridges.Cartridge
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
class HLTBClientTest {
private lateinit var client: HLTBClient
@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
client = HLTBClient(appContext)
}
@Test
fun find_unknownGame_returnsNull() = runBlocking {
launch(Dispatchers.Main) {
val results = client.find(Cartridge("type", "moesjamarramarra tis niet omdat ik wijs dat ge moet kijken he", "invalid"))
assertNull(results)
}
println("Dispatche launched")
}
@Test
fun find_someValidGame_retrievesSomeGamesBasedOnTitle() = runBlocking {
launch(Dispatchers.Main) {
val results = client.find(Cartridge("type", "Gex: Enter the Gecko", "CGB-GEX-666"))
assertEquals(1, results?.size)
val gex = results?.single()!!
assertEquals("Gex Enter the Gecko", gex.title)
assertEquals("CGB-GEX-666", gex.cartCode)
assertEquals(9.0, gex.howlong) // that long, huh.
}
println("Dispatche launched")
}
}

View File

@ -33,20 +33,20 @@ class GamePersistenceTests {
@Test @Test
fun todoItemCanBePersisted() { fun todoItemCanBePersisted() {
val item = Game("brush my little pony", false) val item = Game("brush my little pony","code", 0.0, false)
dao.insert(arrayListOf(item)) dao.insert(arrayListOf(item))
val refreshedItem = dao.query().single() val refreshedItem = dao.query().single()
with(refreshedItem) { with(refreshedItem) {
assertEquals(item.title, title) assertEquals(item.title, title)
assertEquals(item.isDone, isDone) assertEquals(item.finished, finished)
assertEquals(1, id) assertEquals(1, id)
} }
} }
@Test @Test
fun updateUpdatesTodoPropertiesInDb() { fun updateUpdatesTodoPropertiesInDb() {
var todo = Game("git good at Hollow Knight", false) var todo = Game("git good at Hollow Knight", "code", 10.5, false)
dao.insert(arrayListOf(todo)) dao.insert(arrayListOf(todo))
todo = dao.query().single() // refresh to get the ID, otherwise update() will update where ID = 0 todo = dao.query().single() // refresh to get the ID, otherwise update() will update where ID = 0
@ -57,12 +57,12 @@ class GamePersistenceTests {
assertEquals(1, itemsFromDb.size) assertEquals(1, itemsFromDb.size)
with(itemsFromDb.single()) { with(itemsFromDb.single()) {
assertEquals(todo.title, title) assertEquals(todo.title, title)
assertEquals(true, isDone) assertEquals(true, finished)
} }
} }
private fun finallyFinishHollowKnight(item: Game) { private fun finallyFinishHollowKnight(item: Game) {
println("Congrats! On to Demon Souls?") println("Congrats! On to Demon Souls?")
item.check() item.finish()
} }
} }

View File

@ -293,6 +293,8 @@ DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #1,gekkio-1,https://gbhwdb.gekk
DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #2,gekkio-2,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-2.html,gekkio,DMG-ML-ITA,20A,DMG-BEAN-02,I,6,Jun/1996,June/1996,1996,6,LH53517,DMG-MLA-1 S LH531720 JAPAN B1 9627 D,Sharp,Sharp,27/1996,Week 27/1996,1996,,27,MBC1B1,DMG MBC1B1 Nintendo S 9625 3 A,Sharp,Sharp,25/1996,Week 25/1996,1996,,25,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #2,gekkio-2,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-2.html,gekkio,DMG-ML-ITA,20A,DMG-BEAN-02,I,6,Jun/1996,June/1996,1996,6,LH53517,DMG-MLA-1 S LH531720 JAPAN B1 9627 D,Sharp,Sharp,27/1996,Week 27/1996,1996,,27,MBC1B1,DMG MBC1B1 Nintendo S 9625 3 A,Sharp,Sharp,25/1996,Week 25/1996,1996,,25,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #3,gekkio-3,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-3.html,gekkio,DMG-MLA,22A,DMG-BEAN-02,B,5,Jul/1993,July/1993,1993,7,LH53514,DMG-MLA-1 S LH5314B2 JAPAN B1 9339 E,Sharp,Sharp,39/1993,Week 39/1993,1993,,39,MBC1B,DMG MBC1B Nintendo J9330BR,Motorola,Motorola,30/1993,Week 30/1993,1993,,30,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #3,gekkio-3,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-3.html,gekkio,DMG-MLA,22A,DMG-BEAN-02,B,5,Jul/1993,July/1993,1993,7,LH53514,DMG-MLA-1 S LH5314B2 JAPAN B1 9339 E,Sharp,Sharp,39/1993,Week 39/1993,1993,,39,MBC1B,DMG MBC1B Nintendo J9330BR,Motorola,Motorola,30/1993,Week 30/1993,1993,,30,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #1,tobiasvl-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/tobiasvl-1.html,tobiasvl,DMG-ML-USA-1,22A,DMG-BEAN-10,I,5,Jul/1998,July/1998,1998,7,LH53517,DMG-MLA-1 S LH531720 JAPAN B1 9836 D,Sharp,Sharp,36/1998,Week 36/1998,1998,,36,MBC1B,DMG MBC1-B Nintendo P 8'59,,,1998,1998,1998,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MLA-1,Super Mario Land (World) (Rev 1),Entry #1,tobiasvl-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/tobiasvl-1.html,tobiasvl,DMG-ML-USA-1,22A,DMG-BEAN-10,I,5,Jul/1998,July/1998,1998,7,LH53517,DMG-MLA-1 S LH531720 JAPAN B1 9836 D,Sharp,Sharp,36/1998,Week 36/1998,1998,,36,MBC1B,DMG MBC1-B Nintendo P 8'59,,,1998,1998,1998,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (Europe)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MQ-EUR,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MO-USA,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA, Europe)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MQ-NOE,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA, Europe)",Entry #1,max-m-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html,max-m,DMG-MQ-NOE,00,DMG-DECN-02,I,2,Sep/1992,September/1992,1992,9,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9243 D,Sharp,Sharp,43/1992,Week 43/1992,1992,,43,MBC1B,Nintendo DMG MBC1B N 9221BA041,,,21/1992,Week 21/1992,1992,,21,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC,LSI Logic,LSI Logic,42/1992,Week 42/1992,1992,,42,MM1026A,295 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA, Europe)",Entry #1,tobiasvl-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/tobiasvl-1.html,tobiasvl,DMG-MQ-UKV,23,DMG-DECN-02,I,2,Nov/1992,November/1992,1992,11,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9248 D,Sharp,Sharp,48/1992,Week 48/1992,1992,,48,MBC1B,Nintendo DMG MBC1B N 9245BA035,,,45/1992,Week 45/1992,1992,,45,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D244 7 BC,LSI Logic,LSI Logic,44/1992,Week 44/1992,1992,,44,MM1026A,2J5 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MQE-0,"Super Mario Land 2 - 6 Golden Coins (USA, Europe)",Entry #1,tobiasvl-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/tobiasvl-1.html,tobiasvl,DMG-MQ-UKV,23,DMG-DECN-02,I,2,Nov/1992,November/1992,1992,11,LH534M,DMG-MQE-0 S LH534M02 JAPAN E1 9248 D,Sharp,Sharp,48/1992,Week 48/1992,1992,,48,MBC1B,Nintendo DMG MBC1B N 9245BA035,,,45/1992,Week 45/1992,1992,,45,LH5168NFB-10TL,LH5168NFB-10TL LSI LOGIC JAPAN D244 7 BC,LSI Logic,LSI Logic,44/1992,Week 44/1992,1992,,44,MM1026A,2J5 26A,Mitsumi,Mitsumi,1992,1992,1992,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1,20B,DMG-DECN-10,K,6,Mar/1998,March/1998,1998,3,N-4001EJGW,DMG-MQE-2 E1 N-4001EJGW-J08 9822E7026,,,22/1998,Week 22/1998,1998,,22,MBC1B1,DMG MBC1B1 Nintendo S 9816 5 A,Sharp,Sharp,16/1998,Week 16/1998,1998,,16,BR6265BF-10SL,BR6265BF-10SL 814 136N,ROHM,ROHM,14/1998,Week 14/1998,1998,,14,MM1026A,746 26A,Mitsumi,Mitsumi,1997,1997,1997,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1,20B,DMG-DECN-10,K,6,Mar/1998,March/1998,1998,3,N-4001EJGW,DMG-MQE-2 E1 N-4001EJGW-J08 9822E7026,,,22/1998,Week 22/1998,1998,,22,MBC1B1,DMG MBC1B1 Nintendo S 9816 5 A,Sharp,Sharp,16/1998,Week 16/1998,1998,,16,BR6265BF-10SL,BR6265BF-10SL 814 136N,ROHM,ROHM,14/1998,Week 14/1998,1998,,14,MM1026A,746 26A,Mitsumi,Mitsumi,1997,1997,1997,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,

1 type name title slug url contributor code stamp board_type board_circled_letters board_extra_label board_calendar_short board_calendar board_year board_month rom_kind rom_label rom_manufacturer rom_manufacturer_name rom_calendar_short rom_calendar rom_year rom_month rom_week mapper_kind mapper_label mapper_manufacturer mapper_manufacturer_name mapper_calendar_short mapper_calendar mapper_year mapper_month mapper_week ram_kind ram_label ram_manufacturer ram_manufacturer_name ram_calendar_short ram_calendar ram_year ram_month ram_week ram_protector_kind ram_protector_label ram_protector_manufacturer ram_protector_manufacturer_name ram_protector_calendar_short ram_protector_calendar ram_protector_year ram_protector_month ram_protector_week crystal_kind crystal_label crystal_manufacturer crystal_manufacturer_name crystal_calendar_short crystal_calendar crystal_year crystal_month crystal_week rom2_kind rom2_label rom2_manufacturer rom2_manufacturer_name rom2_calendar_short rom2_calendar rom2_year rom2_month rom2_week flash_kind flash_label flash_manufacturer flash_manufacturer_name flash_calendar_short flash_calendar flash_year flash_month flash_week line_decoder_kind line_decoder_label line_decoder_manufacturer line_decoder_manufacturer_name line_decoder_calendar_short line_decoder_calendar line_decoder_year line_decoder_month line_decoder_week eeprom_kind eeprom_label eeprom_manufacturer eeprom_manufacturer_name eeprom_calendar_short eeprom_calendar eeprom_year eeprom_month eeprom_week accelerometer_kind accelerometer_label accelerometer_manufacturer accelerometer_manufacturer_name accelerometer_calendar_short accelerometer_calendar accelerometer_year accelerometer_month accelerometer_week u4_kind u4_label u4_manufacturer u4_manufacturer_name u4_calendar_short u4_calendar u4_year u4_month u4_week u5_kind u5_label u5_manufacturer u5_manufacturer_name u5_calendar_short u5_calendar u5_year u5_month u5_week crystal
293 DMG-MLA-1 Super Mario Land (World) (Rev 1) Entry #2 gekkio-2 https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-2.html gekkio DMG-ML-ITA 20A DMG-BEAN-02 I 6 Jun/1996 June/1996 1996 6 LH53517 DMG-MLA-1 S LH531720 JAPAN B1 9627 D Sharp Sharp 27/1996 Week 27/1996 1996 27 MBC1B1 DMG MBC1B1 Nintendo S 9625 3 A Sharp Sharp 25/1996 Week 25/1996 1996 25
294 DMG-MLA-1 Super Mario Land (World) (Rev 1) Entry #3 gekkio-3 https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/gekkio-3.html gekkio DMG-MLA 22A DMG-BEAN-02 B 5 Jul/1993 July/1993 1993 7 LH53514 DMG-MLA-1 S LH5314B2 JAPAN B1 9339 E Sharp Sharp 39/1993 Week 39/1993 1993 39 MBC1B DMG MBC1B Nintendo J9330BR Motorola Motorola 30/1993 Week 30/1993 1993 30
295 DMG-MLA-1 Super Mario Land (World) (Rev 1) Entry #1 tobiasvl-1 https://gbhwdb.gekkio.fi/cartridges/DMG-MLA-1/tobiasvl-1.html tobiasvl DMG-ML-USA-1 22A DMG-BEAN-10 I 5 Jul/1998 July/1998 1998 7 LH53517 DMG-MLA-1 S LH531720 JAPAN B1 9836 D Sharp Sharp 36/1998 Week 36/1998 1998 36 MBC1B DMG MBC1-B Nintendo P 8'59 1998 1998 1998
296 DMG-MQE-0 Super Mario Land 2 - 6 Golden Coins (Europe) Entry #1 max-m-1 https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html max-m DMG-MQ-EUR 00 DMG-DECN-02 I 2 Sep/1992 September/1992 1992 9 LH534M DMG-MQE-0 S LH534M02 JAPAN E1 9243 D Sharp Sharp 43/1992 Week 43/1992 1992 43 MBC1B Nintendo DMG MBC1B N 9221BA041 21/1992 Week 21/1992 1992 21 LH5168NFB-10TL LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC LSI Logic LSI Logic 42/1992 Week 42/1992 1992 42 MM1026A 295 26A Mitsumi Mitsumi 1992 1992 1992
297 DMG-MQE-0 Super Mario Land 2 - 6 Golden Coins (USA) Entry #1 max-m-1 https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html max-m DMG-MO-USA 00 DMG-DECN-02 I 2 Sep/1992 September/1992 1992 9 LH534M DMG-MQE-0 S LH534M02 JAPAN E1 9243 D Sharp Sharp 43/1992 Week 43/1992 1992 43 MBC1B Nintendo DMG MBC1B N 9221BA041 21/1992 Week 21/1992 1992 21 LH5168NFB-10TL LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC LSI Logic LSI Logic 42/1992 Week 42/1992 1992 42 MM1026A 295 26A Mitsumi Mitsumi 1992 1992 1992
298 DMG-MQE-0 Super Mario Land 2 - 6 Golden Coins (USA, Europe) Entry #1 max-m-1 https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/max-m-1.html max-m DMG-MQ-NOE 00 DMG-DECN-02 I 2 Sep/1992 September/1992 1992 9 LH534M DMG-MQE-0 S LH534M02 JAPAN E1 9243 D Sharp Sharp 43/1992 Week 43/1992 1992 43 MBC1B Nintendo DMG MBC1B N 9221BA041 21/1992 Week 21/1992 1992 21 LH5168NFB-10TL LH5168NFB-10TL LSI LOGIC JAPAN D242 7 BC LSI Logic LSI Logic 42/1992 Week 42/1992 1992 42 MM1026A 295 26A Mitsumi Mitsumi 1992 1992 1992
299 DMG-MQE-0 Super Mario Land 2 - 6 Golden Coins (USA, Europe) Entry #1 tobiasvl-1 https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-0/tobiasvl-1.html tobiasvl DMG-MQ-UKV 23 DMG-DECN-02 I 2 Nov/1992 November/1992 1992 11 LH534M DMG-MQE-0 S LH534M02 JAPAN E1 9248 D Sharp Sharp 48/1992 Week 48/1992 1992 48 MBC1B Nintendo DMG MBC1B N 9245BA035 45/1992 Week 45/1992 1992 45 LH5168NFB-10TL LH5168NFB-10TL LSI LOGIC JAPAN D244 7 BC LSI Logic LSI Logic 44/1992 Week 44/1992 1992 44 MM1026A 2J5 26A Mitsumi Mitsumi 1992 1992 1992
300 DMG-MQE-2 Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2) Entry #1 creeps-1 https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html creeps DMG-MQ-USA-1 20B DMG-DECN-10 K 6 Mar/1998 March/1998 1998 3 N-4001EJGW DMG-MQE-2 E1 N-4001EJGW-J08 9822E7026 22/1998 Week 22/1998 1998 22 MBC1B1 DMG MBC1B1 Nintendo S 9816 5 A Sharp Sharp 16/1998 Week 16/1998 1998 16 BR6265BF-10SL BR6265BF-10SL 814 136N ROHM ROHM 14/1998 Week 14/1998 1998 14 MM1026A 746 26A Mitsumi Mitsumi 1997 1997 1997

View File

@ -1,20 +1,30 @@
package be.kuleuven.howlongtobeat package be.kuleuven.howlongtobeat
import android.app.Activity import android.content.ContentResolver
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import java.net.URL import java.net.URL
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun Uri.toBitmap(activity: Activity): Bitmap fun Fragment.requireContentResolver(): ContentResolver = requireContext().contentResolver
= BitmapFactory.decodeStream(activity.contentResolver.openInputStream(this))
fun Uri.toBitmap(contentResolver: ContentResolver): Bitmap
= BitmapFactory.decodeStream(contentResolver.openInputStream(this))
fun URL.downloadAsImage(): Bitmap = fun URL.downloadAsImage(): Bitmap =
BitmapFactory.decodeStream(this.openConnection().getInputStream()) BitmapFactory.decodeStream(this.openConnection().getInputStream())
fun Bitmap.save(location: String, context: Context) {
context.openFileOutput(location, Context.MODE_PRIVATE).use {
compress(Bitmap.CompressFormat.JPEG, 85, it)
}
}
fun Bitmap.scaleToWidth(width: Int): Bitmap { fun Bitmap.scaleToWidth(width: Int): Bitmap {
val aspectRatio = this.width.toFloat() / this.height.toFloat() val aspectRatio = this.width.toFloat() / this.height.toFloat()
val height = (width / aspectRatio).roundToInt() val height = (width / aspectRatio).roundToInt()

View File

@ -0,0 +1,49 @@
package be.kuleuven.howlongtobeat
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import be.kuleuven.howlongtobeat.databinding.FragmentGamedetailBinding
import be.kuleuven.howlongtobeat.model.Game
import be.kuleuven.howlongtobeat.model.GameRepository
import com.google.android.material.snackbar.Snackbar
class GameDetailFragment : Fragment(R.layout.fragment_gamedetail) {
private lateinit var binding: FragmentGamedetailBinding
private lateinit var gameRepo: GameRepository
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentGamedetailBinding.inflate(layoutInflater)
gameRepo = GameRepository.defaultImpl(requireContext())
val gameId = arguments?.getSerializable(Game.GAME_ID).toString().toInt()
val game = gameRepo.find(gameId)
binding.txtDetailTitle.text = "${game.title}\n${game.cartCode}"
binding.txtDetailGameStats.text = "${game.howLongToBeat} hr(s) to beat"
binding.chkDetailFinished.isChecked = game.finished
binding.chkDetailFinished.setOnClickListener {
game.finished = binding.chkDetailFinished.isChecked
gameRepo.update(game)
Snackbar.make(binding.root, "Saved!", Snackbar.LENGTH_SHORT).show()
}
requireContext().openFileInput(game.boxartFileName).use {
binding.imgDetailBoxArt.setImageBitmap(BitmapFactory.decodeStream(it))
}
requireContext().openFileInput(game.snapshotFileName).use {
binding.imgDetailSnapshot.setImageBitmap(BitmapFactory.decodeStream(it))
}
return binding.root
}
}

View File

@ -5,27 +5,36 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.findFragment
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import be.kuleuven.howlongtobeat.model.Game import be.kuleuven.howlongtobeat.model.Game
class GameListAdapter(val items: List<Game>) : RecyclerView.Adapter<GameListAdapter.GameListViewHolder>() { class GameListAdapter(val items: List<Game>) : RecyclerView.Adapter<GameListAdapter.GameListViewHolder>() {
private lateinit var parentFragment: GameListFragment
inner class GameListViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView) inner class GameListViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameListViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameListViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_game, parent, false) val view = LayoutInflater.from(parent.context).inflate(R.layout.item_game, parent, false)
parentFragment = parent.findFragment()
return GameListViewHolder(view) return GameListViewHolder(view)
} }
override fun onBindViewHolder(holder: GameListViewHolder, position: Int) { override fun onBindViewHolder(holder: GameListViewHolder, position: Int) {
val currentTodoItem = items[position] val game = items[position]
holder.itemView.apply { holder.itemView.apply {
val checkBoxTodo = findViewById<CheckBox>(R.id.chkTodoDone) setOnLongClickListener {
findViewById<TextView>(R.id.txtTodoTitle).text = currentTodoItem.title parentFragment.selectGame(game)
true
}
checkBoxTodo.isChecked = currentTodoItem.isDone val chkFinished = findViewById<CheckBox>(R.id.chkGameFinished)
checkBoxTodo.setOnClickListener { findViewById<TextView>(R.id.txtTodoTitle).text = game.title
currentTodoItem.isDone = checkBoxTodo.isChecked
chkFinished.isChecked = game.finished
chkFinished.setOnClickListener {
game.finished = chkFinished.isChecked
} }
} }
} }

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -41,32 +42,22 @@ class GameListFragment : Fragment(R.layout.fragment_gamelist) {
} }
private fun loadGames() { private fun loadGames() {
gameList.clear()
gameList.addAll(gameRepository.load()) gameList.addAll(gameRepository.load())
if(!gameList.any()) { if(!gameList.any()) {
gameList.add(Game.NONE_YET) gameList.add(Game.NONE_YET)
} }
} }
/* fun selectGame(game: Game) {
fun onHltbGamesRetrieved(games: List<HowLongToBeatResult>) { findNavController().navigate(R.id.action_gameListFragment_to_gameDetailFragment, bundleOf(Game.GAME_ID to game.id.toString()))
gameList.clear()
gameList.addAll(games.map { Game("${it.title} (${it.howlong})", false) })
adapter.notifyDataSetChanged()
} }
fun clearAllItems() { fun clearAllItems() {
gameList.clear() gameList.clear()
gameList.add(Game.NONE_YET)
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
fun clearLatestItem() {
if(gameList.size >= 1) {
gameList.removeAt(gameList.size - 1)
adapter.notifyItemRemoved(gameList.size - 1)
}
}
*/
} }

View File

@ -1,6 +1,6 @@
package be.kuleuven.howlongtobeat package be.kuleuven.howlongtobeat
import android.graphics.Bitmap import android.graphics.BitmapFactory
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -31,8 +31,11 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
val itm = items[position] val itm = items[position]
holder.itemView.apply { holder.itemView.apply {
setOnClickListener { var art = BitmapFactory.decodeResource(resources, R.drawable.emptygb)
parentFragment.addResultToGameLibrary(itm)
setOnLongClickListener {
parentFragment.addResultToGameLibrary(itm, art)
true
} }
findViewById<TextView>(R.id.txtHltbItemResult).text = itm.toString() findViewById<TextView>(R.id.txtHltbItemResult).text = itm.toString()
@ -40,7 +43,6 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
if(itm.hasBoxart()) { if(itm.hasBoxart()) {
MainScope().launch{ MainScope().launch{
var art: Bitmap? = null
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
art = itm.boxartUrl().downloadAsImage() art = itm.boxartUrl().downloadAsImage()
} }
@ -49,7 +51,7 @@ class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : Recycle
} }
} }
} else { } else {
boxArtView.visibility = View.INVISIBLE boxArtView.setImageBitmap(art)
} }
} }
} }

View File

@ -1,5 +1,8 @@
package be.kuleuven.howlongtobeat package be.kuleuven.howlongtobeat
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -12,8 +15,10 @@ import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
import be.kuleuven.howlongtobeat.model.Game import be.kuleuven.howlongtobeat.model.Game
import be.kuleuven.howlongtobeat.model.GameRepository import be.kuleuven.howlongtobeat.model.GameRepository
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.io.FileInputStream
class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) { class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
private lateinit var cachedSnapshotPath: Uri
private lateinit var binding: FragmentHltbresultsBinding private lateinit var binding: FragmentHltbresultsBinding
private lateinit var adapter: HltbResultsAdapter private lateinit var adapter: HltbResultsAdapter
private lateinit var gameRepository: GameRepository private lateinit var gameRepository: GameRepository
@ -26,6 +31,7 @@ class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
binding = FragmentHltbresultsBinding.inflate(layoutInflater) binding = FragmentHltbresultsBinding.inflate(layoutInflater)
val resultFromLoadingFragment = arguments?.getSerializable(HowLongToBeatResult.RESULT) as List<HowLongToBeatResult> val resultFromLoadingFragment = arguments?.getSerializable(HowLongToBeatResult.RESULT) as List<HowLongToBeatResult>
cachedSnapshotPath = Uri.parse(arguments?.getSerializable(HowLongToBeatResult.SNAPSHOT_URI) as String)
gameRepository = GameRepository.defaultImpl(requireContext()) gameRepository = GameRepository.defaultImpl(requireContext())
@ -36,9 +42,22 @@ class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
return binding.root return binding.root
} }
fun addResultToGameLibrary(hltbResult: HowLongToBeatResult) { fun addResultToGameLibrary(hltbResult: HowLongToBeatResult, downloadedBoxart: Bitmap) {
gameRepository.save(Game(hltbResult)) val game = Game(hltbResult)
downloadedBoxart.save(game.boxartFileName, requireContext())
copyCachedSnapshotTo(game.snapshotFileName)
gameRepository.save(game)
Snackbar.make(requireView(), "Added ${hltbResult.title} to library!", Snackbar.LENGTH_LONG).show() Snackbar.make(requireView(), "Added ${hltbResult.title} to library!", Snackbar.LENGTH_LONG).show()
findNavController().navigate(R.id.action_hltbResultsFragment_to_gameListFragment) findNavController().navigate(R.id.action_hltbResultsFragment_to_gameListFragment)
} }
private fun copyCachedSnapshotTo(destination: String) {
val inStream = requireContext().contentResolver.openInputStream(cachedSnapshotPath) as FileInputStream
inStream.use {
requireContext().openFileOutput(destination, Context.MODE_PRIVATE).use { outStream ->
inStream.channel.transferTo(0, inStream.channel.size(), outStream.channel)
}
}
}
} }

View File

@ -14,8 +14,10 @@ import androidx.core.content.PermissionChecker
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import be.kuleuven.howlongtobeat.cartridges.Cartridge import be.kuleuven.howlongtobeat.cartridges.CartridgeFinderViaDuckDuckGo
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepositoryGekkioFi
import be.kuleuven.howlongtobeat.cartridges.findFirstCartridgeForRepos
import be.kuleuven.howlongtobeat.databinding.FragmentLoadingBinding import be.kuleuven.howlongtobeat.databinding.FragmentLoadingBinding
import be.kuleuven.howlongtobeat.google.GoogleVisionClient import be.kuleuven.howlongtobeat.google.GoogleVisionClient
import be.kuleuven.howlongtobeat.hltb.HLTBClient import be.kuleuven.howlongtobeat.hltb.HLTBClient
@ -28,7 +30,7 @@ import java.io.File
class LoadingFragment : Fragment(R.layout.fragment_loading) { class LoadingFragment : Fragment(R.layout.fragment_loading) {
private lateinit var hltbClient: HLTBClient private lateinit var hltbClient: HLTBClient
private lateinit var cartRepo: CartridgesRepository private lateinit var cartRepos: List<CartridgesRepository>
private lateinit var imageRecognizer: ImageRecognizer private lateinit var imageRecognizer: ImageRecognizer
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String> private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
@ -46,7 +48,11 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
binding = FragmentLoadingBinding.inflate(layoutInflater) binding = FragmentLoadingBinding.inflate(layoutInflater)
main = activity as MainActivity main = activity as MainActivity
cartRepo = CartridgesRepository.fromAsset(main.applicationContext) // If we fail to find info in the first repo, it falls back to the second one: a (scraped) DuckDuckGo search.
cartRepos = listOf(
CartridgesRepositoryGekkioFi.fromAsset(main.applicationContext),
CartridgeFinderViaDuckDuckGo(main.applicationContext)
)
imageRecognizer = GoogleVisionClient() imageRecognizer = GoogleVisionClient()
hltbClient = HLTBClient(main.applicationContext) hltbClient = HLTBClient(main.applicationContext)
@ -55,11 +61,27 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
binding.btnRetryAfterLoading.setOnClickListener { binding.btnRetryAfterLoading.setOnClickListener {
tryToMakeCameraSnap() tryToMakeCameraSnap()
} }
tryToMakeCameraSnap()
return binding.root return binding.root
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) {
val inProgress = savedInstanceState?.getBoolean("inprogress") ?: false
if(!inProgress) {
// Don't do this in onCreateView, things go awry if you rotate the smartphone!
tryToMakeCameraSnap()
}
super.onViewStateRestored(savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.run {
putBoolean("inprogress", snapshot != null)
}
super.onSaveInstanceState(outState)
}
private fun cameraSnapTaken(succeeded: Boolean) { private fun cameraSnapTaken(succeeded: Boolean) {
if(!succeeded || snapshot == null) { if(!succeeded || snapshot == null) {
errorInProgress("Photo could not be saved, try again?") errorInProgress("Photo could not be saved, try again?")
@ -67,42 +89,40 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
} }
progress("Scaling image for upload...") progress("Scaling image for upload...")
val bitmap = snapshot!!.toBitmap(main).scaleToWidth(1600) val bitmap = snapshot!!.toBitmap(requireContentResolver()).scaleToWidth(1600)
MainScope().launch{ MainScope().launch{
findGameBasedOnCameraSnap(bitmap) try {
findGameBasedOnCameraSnap(bitmap)
} catch (errorDuringFind: UnableToFindGameException) {
errorInProgress("${errorDuringFind.message}\nRetry?")
}
} }
} }
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) { private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
/* var picToAnalyze = pic
// Uncomment this line if you want to stub out camera pictures
// picToAnalyze = BitmapFactory.decodeResource(resources, R.drawable.sml2)
progress("Recognizing game cart from picture...") progress("Recognizing game cart from picture...")
val cartCode = imageRecognizer.recognizeCartCode(pic) val cartCode = imageRecognizer.recognizeCartCode(picToAnalyze)
?: throw UnableToFindGameException("No cart code in your pic found")
if (cartCode == null) { progress("Found cart code $cartCode\nLooking in DBs for matching game...")
errorInProgress("No cart code in your pic found. Retry?") val foundCart = findFirstCartridgeForRepos(cartCode, cartRepos)
return ?: throw UnableToFindGameException("$cartCode is an unknown game cart.")
}
progress("Found cart code $cartCode, looking in DB...") progress("Valid cart code: $cartCode\n Looking in HLTB for ${foundCart.title}...")
val foundCart = cartRepo.find(cartCode) val hltbResults = hltbClient.find(foundCart)
?: throw UnableToFindGameException("HLTB does not know ${foundCart.title}")
if (foundCart == Cartridge.UNKNOWN_CART) { Snackbar.make(requireView(), "Found ${hltbResults.size} game(s) for cart ${foundCart.code}", Snackbar.LENGTH_LONG).show()
errorInProgress("$cartCode is an unknown game cart. Retry?") val bundle = bundleOf(
return HowLongToBeatResult.RESULT to hltbResults,
} HowLongToBeatResult.SNAPSHOT_URI to snapshot.toString()
)
progress("Valid cart code $cartCode, looking in HLTB...") findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle)
*/
val foundCart = Cartridge("DMG", "Super Mario Land", "DMG-something")
hltbClient.find(foundCart.title) {
Snackbar.make(requireView(), "Found ${it.size} game(s) for cart ${foundCart.code}", Snackbar.LENGTH_LONG).show()
// TODO wat als geen hltb results gevonden?
val bundle = bundleOf(HowLongToBeatResult.RESULT to it)
findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle)
}
} }
private fun cameraPermissionAcceptedOrDenied(succeeded: Boolean) { private fun cameraPermissionAcceptedOrDenied(succeeded: Boolean) {
@ -130,6 +150,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
createNewFile() createNewFile()
deleteOnExit() deleteOnExit()
} }
snapshot = FileProvider.getUriForFile(main.applicationContext, "${BuildConfig.APPLICATION_ID}.provider", tempFile) snapshot = FileProvider.getUriForFile(main.applicationContext, "${BuildConfig.APPLICATION_ID}.provider", tempFile)
} }
@ -144,6 +165,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
} }
private fun errorInProgress(msg: String) { private fun errorInProgress(msg: String) {
snapshot = null
progress(msg) progress(msg)
binding.indeterminateBar.visibility = View.GONE binding.indeterminateBar.visibility = View.GONE
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show() Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()

View File

@ -1,21 +1,32 @@
package be.kuleuven.howlongtobeat package be.kuleuven.howlongtobeat
import android.app.AlertDialog
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.navigation.fragment.NavHostFragment
import be.kuleuven.howlongtobeat.databinding.ActivityMainBinding import be.kuleuven.howlongtobeat.databinding.ActivityMainBinding
import be.kuleuven.howlongtobeat.model.GameRepository
import kotlin.math.roundToInt
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var menuBarToggle: ActionBarDrawerToggle private lateinit var menuBarToggle: ActionBarDrawerToggle
private lateinit var gameRepository: GameRepository
private val navHostFragment: NavHostFragment
get() = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
gameRepository = GameRepository.defaultImpl(applicationContext)
setupMenuDrawer() setupMenuDrawer()
setContentView(binding.root) setContentView(binding.root)
@ -30,24 +41,56 @@ class MainActivity : AppCompatActivity() {
binding.navView.setNavigationItemSelectedListener { binding.navView.setNavigationItemSelectedListener {
when (it.itemId) { when (it.itemId) {
R.id.mnuClear -> clearAllItems() R.id.mnuClear -> tryToClearAllItems()
R.id.mnuClearLatest -> clearLatestItem() R.id.mnuStats -> showStats()
R.id.mnuReset -> resetItems()
} }
true true
} }
} }
private fun clearAllItems() { private fun clearAllItems() {
//todoFragment.clearAllItems() gameRepository.overwrite(listOf())
val currentActiveFragment = navHostFragment.childFragmentManager.fragments[0] as GameListFragment
currentActiveFragment.clearAllItems()
binding.drawerLayout.closeDrawer(GravityCompat.START)
} }
private fun clearLatestItem() { private fun tryToClearAllItems() {
//todoFragment.clearLatestItem() AlertDialog.Builder(this)
.setTitle("Delete all games from the DB")
.setMessage("Are you sure?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("Yup") { dialog, _ ->
clearAllItems()
dialog.dismiss()
}
.setNegativeButton("Nah") { dialog, _ ->
close(dialog)
}
.create()
.show()
} }
private fun resetItems() { private fun close(dialog: DialogInterface) {
//todoFragment.resetItems() dialog.dismiss()
binding.drawerLayout.closeDrawer(GravityCompat.START)
}
private fun showStats() {
val allGames = gameRepository.load()
val hoursToBeat = allGames.filter { !it.finished }.map { it.howLongToBeat }.sum()
val hoursAlreadyBeat = allGames.filter { it.finished }.map { it.howLongToBeat }.sum()
val percCompleted = if(hoursAlreadyBeat > 0) ((hoursToBeat / hoursAlreadyBeat) * 100).roundToInt() else 0
AlertDialog.Builder(this)
.setTitle("Game library stats")
.setMessage("Total games: ${allGames.size}\nHours still to beat: ${hoursToBeat}\nHours already beat: ${hoursAlreadyBeat}\n\n$percCompleted% total completed.")
.setIcon(android.R.drawable.ic_dialog_info)
.setNeutralButton("Nice!") { dialog, _ ->
close(dialog)
}
.create()
.show()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@ -0,0 +1,3 @@
package be.kuleuven.howlongtobeat
class UnableToFindGameException(message: String?) : Throwable(message)

View File

@ -5,8 +5,11 @@ data class Cartridge(val type: String, val name: String, val code: String) {
companion object { companion object {
val bracketRe = """\(.+\)""".toRegex() val bracketRe = """\(.+\)""".toRegex()
val UNKNOWN_CART = Cartridge("", "UNKNOWN CART", "DMG-???") val UNKNOWN_CART = Cartridge("", "UNKNOWN CART", "DMG-???")
val KNOWN_CART_PREFIXES = listOf("DMG", "CGB")
fun isValid(code: String): Boolean = code.startsWith("DMG-") || code.startsWith("CGB-") fun isValid(code: String): Boolean =
code != "" && !code.contains("\n") &&
KNOWN_CART_PREFIXES.any { code.startsWith("$it-") }
} }
val title = bracketRe.replace(name, "").replace("-", "").trim() val title = bracketRe.replace(name, "").replace("-", "").trim()

View File

@ -0,0 +1,33 @@
package be.kuleuven.howlongtobeat.cartridges
import android.content.Context
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
import kotlin.coroutines.suspendCoroutine
class CartridgeFinderViaDuckDuckGo(private val context: Context) : CartridgesRepository {
class CartridgeFinderViaDuckDuckGoRequest(query: String, responseListener: Response.Listener<String>) :
StringRequest(
Method.GET, "https://html.duckduckgo.com/html/?q=${query}", responseListener,
Response.ErrorListener {
println("Something went wrong: ${it.message}")
}) {
override fun getHeaders(): MutableMap<String, String> {
return hashMapOf("User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:90.0) Gecko/20100101 Firefox/90.0")
}
}
override suspend fun find(code: String?): Cartridge? = suspendCoroutine { cont ->
if(code == null) {
cont.resumeWith(Result.success(null))
} else {
val queue = Volley.newRequestQueue(context)
queue.add(CartridgeFinderViaDuckDuckGoRequest(code) { html ->
cont.resumeWith(Result.success(DuckDuckGoResultParser.parse(html, code)))
})
}
}
}

View File

@ -1,38 +1,13 @@
package be.kuleuven.howlongtobeat.cartridges package be.kuleuven.howlongtobeat.cartridges
import android.content.Context interface CartridgesRepository {
import java.io.InputStream suspend fun find(code: String?): Cartridge?
/**
* Reads cartridges.csv export from https://gbhwdb.gekkio.fi/cartridges/
* Headers: type,name,title,slug,url,contributor,code,...
* E.g.: DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1,
*/
class CartridgesRepository(csvStream: InputStream) {
companion object {
fun fromAsset(context: Context): CartridgesRepository =
CartridgesRepository(context.assets.open("cartridges.csv"))
}
val cartridges: List<Cartridge> =
csvStream.bufferedReader().use {
it.readLines()
}.filter {
!it.startsWith("type,name,title,")
}.map {
val data = it.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)".toRegex())
Cartridge(data[0].replace("\"", "").trim(), data[1].replace("\"", "").trim(), data[6].replace("\"", "").trim())
}.filter { it.code != "" }
.toList()
fun find(code: String?): Cartridge {
if(code == null) return Cartridge.UNKNOWN_CART
val possiblyFound = cartridges.filter {
it.code == code || it.code.contains(code) || code.contains(it.code)
}.firstOrNull()
return possiblyFound ?: Cartridge.UNKNOWN_CART
}
} }
suspend fun findFirstCartridgeForRepos(code: String?, repos: List<CartridgesRepository>): Cartridge? {
for(repo in repos) {
val result = repo.find(code)
if(result != null) return result
}
return null
}

View File

@ -0,0 +1,41 @@
package be.kuleuven.howlongtobeat.cartridges
import android.content.Context
import java.io.InputStream
import kotlin.coroutines.suspendCoroutine
/**
* Reads cartridges.csv export from https://gbhwdb.gekkio.fi/cartridges/
* Headers: type,name,title,slug,url,contributor,code,...
* E.g.: DMG-MQE-2,"Super Mario Land 2 - 6 Golden Coins (USA, Europe) (Rev 2)",Entry #1,creeps-1,https://gbhwdb.gekkio.fi/cartridges/DMG-MQE-2/creeps-1.html,creeps,DMG-MQ-USA-1,
*/
class CartridgesRepositoryGekkioFi(csvStream: InputStream) : CartridgesRepository {
companion object {
fun fromAsset(context: Context): CartridgesRepositoryGekkioFi =
CartridgesRepositoryGekkioFi(context.assets.open("cartridges.csv"))
}
val cartridges: List<Cartridge> =
csvStream.bufferedReader().use {
it.readLines()
}.filter {
!it.startsWith("type,name,title,")
}.map {
val data = it.split(",(?=([^\"]*\"[^\"]*\")*[^\"]*$)".toRegex())
Cartridge(data[0].replace("\"", "").trim(), data[1].replace("\"", "").trim(), data[6].replace("\"", "").trim())
}.filter { it.code != "" }
.toList()
override suspend fun find(code: String?): Cartridge? = suspendCoroutine { cont ->
if(code == null) {
cont.resumeWith(Result.success(null))
} else {
val possiblyFound = cartridges.filter {
it.code == code || it.code.contains(code) || code.contains(it.code)
}.firstOrNull()
cont.resumeWith(Result.success(possiblyFound))
}
}
}

View File

@ -0,0 +1,41 @@
package be.kuleuven.howlongtobeat.cartridges
object DuckDuckGoResultParser {
private val resultMatcher = """<a rel=".+" class="result__a" href=".+">(.+)</a>""".toRegex()
private val specialCharsToRemove = listOf(
"|",
".",
"-",
"/",
",",
"!",
"Get information and compare prices of",
"for Game Boy",
"Release Information",
"Nintendo",
"Game Boy Advance",
"Game Boy color",
"Game Boy",
"GameBoy",
"Game ",
"GBC",
"GBA",
"VGDb",
"ebay"
)
fun parse(html: String, fromCode: String): Cartridge? {
// There are bound to be multiple results. Just fetch the first one as an educated guess
val matched = resultMatcher.find(html) ?: return null
var match = matched.groupValues[1]
.lowercase()
.replace(fromCode.lowercase(), "")
specialCharsToRemove.forEach {
match = match.replace(it.lowercase(), "")
}
return Cartridge("Unknown", match.trim(), fromCode)
}
}

View File

@ -1,9 +1,11 @@
package be.kuleuven.howlongtobeat.hltb package be.kuleuven.howlongtobeat.hltb
import android.content.Context import android.content.Context
import be.kuleuven.howlongtobeat.cartridges.Cartridge
import com.android.volley.Response import com.android.volley.Response
import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley import com.android.volley.toolbox.Volley
import kotlin.coroutines.suspendCoroutine
class HLTBClient(val context: Context) { class HLTBClient(val context: Context) {
@ -41,10 +43,11 @@ class HLTBClient(val context: Context) {
} }
} }
fun find(query: String, onResponseFetched: (List<HowLongToBeatResult>) -> Unit) { suspend fun find(cart: Cartridge): List<HowLongToBeatResult>? = suspendCoroutine { cont ->
val queue = Volley.newRequestQueue(context) val queue = Volley.newRequestQueue(context)
val req = HLTBRequest(query) { val req = HLTBRequest(cart.title) {
onResponseFetched(HowLongToBeatResultParser.parse(it)) val hltbResults = HowLongToBeatResultParser.parse(it, cart)
cont.resumeWith(Result.success(hltbResults))
} }
queue.add(req) queue.add(req)
} }

View File

@ -4,12 +4,13 @@ import kotlinx.serialization.Serializable
import java.net.URL import java.net.URL
@Serializable @Serializable
data class HowLongToBeatResult(val title: String, val howlong: Double, val boxart: String = "") : java.io.Serializable { data class HowLongToBeatResult(val title: String, val cartCode: String, val howlong: Double, val boxartUrl: String = "") : java.io.Serializable {
companion object { companion object {
const val RESULT = "HowLongToBeatResult" const val RESULT = "HowLongToBeatResult"
const val SNAPSHOT_URI = "SnapshotUri"
} }
fun hasBoxart(): Boolean = boxart.startsWith(HLTBClient.DOMAIN) fun hasBoxart(): Boolean = boxartUrl.startsWith(HLTBClient.DOMAIN)
fun boxartUrl(): URL = URL(boxart) fun boxartUrl(): URL = URL(boxartUrl)
override fun toString(): String = "$title ($howlong hrs)" override fun toString(): String = "$title ($howlong hrs)"
} }

View File

@ -1,12 +1,14 @@
package be.kuleuven.howlongtobeat.hltb package be.kuleuven.howlongtobeat.hltb
import be.kuleuven.howlongtobeat.cartridges.Cartridge
object HowLongToBeatResultParser { object HowLongToBeatResultParser {
private val titleMatcher = """<a class=".+" title="(.+)" href=""".toRegex() private val titleMatcher = """<a class=".+" title="(.+)" href=""".toRegex()
private val hourMatcher = """<div class=".+">(.+) Hours""".toRegex() private val hourMatcher = """<div class=".+">(.+) Hours""".toRegex()
private val boxArtMatcher = """<img alt=".+" src="(.+)"""".toRegex() private val boxArtMatcher = """<img alt=".+" src="(.+)"""".toRegex()
fun parse(html: String): List<HowLongToBeatResult> { fun parse(html: String, sourceCart: Cartridge): List<HowLongToBeatResult>? {
val result = arrayListOf<HowLongToBeatResult>() val result = arrayListOf<HowLongToBeatResult>()
val rows = html.split("\n") val rows = html.split("\n")
for(i in 0..rows.size - 1) { for(i in 0..rows.size - 1) {
@ -16,11 +18,11 @@ object HowLongToBeatResultParser {
val hour = parseHoursFromRow(i, rows) val hour = parseHoursFromRow(i, rows)
val boxart = parseBoxArtFromRow(i, rows) val boxart = parseBoxArtFromRow(i, rows)
result.add(HowLongToBeatResult(title, hour, boxart)) result.add(HowLongToBeatResult(title, sourceCart.code, hour, boxart))
} }
} }
return result return if(result.any()) result else null
} }
private fun parseBoxArtFromRow(row: Int, rows: List<String>): String { private fun parseBoxArtFromRow(row: Int, rows: List<String>): String {

View File

@ -8,19 +8,25 @@ import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
@Entity @Entity
data class Game( data class Game(
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "is_done") var isDone: Boolean = false, @ColumnInfo(name = "code") val cartCode: String,
@ColumnInfo(name = "hltb") val howLongToBeat: Double,
@ColumnInfo(name = "finished") var finished: Boolean = false,
@PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable { @PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable {
constructor(result: HowLongToBeatResult) : this(result.title) constructor(result: HowLongToBeatResult) : this(result.title, result.cartCode, result.howlong)
// TODO more columns (platform? hours, paths of images?) val boxartFileName
get() = "box-${cartCode}.jpg"
val snapshotFileName
get() = "snap-${cartCode}.jpg"
fun check() { fun finish() {
isDone = true finished = true
} }
companion object { companion object {
val NONE_YET = Game("No entries yet, add one!") val NONE_YET = Game("No entries yet, add one!", "", 0.0)
val GAME_ID = "GameId"
} }
} }

View File

@ -15,6 +15,10 @@ interface GameRepository {
fun load(): List<Game> fun load(): List<Game>
fun update(game: Game)
fun find(id: Int): Game
fun save(game: Game) fun save(game: Game)
fun overwrite(items: List<Game>) fun overwrite(items: List<Game>)

View File

@ -19,6 +19,9 @@ class GameRepositoryRoomImpl(appContext: Context) :
} }
override fun load(): List<Game> = dao.query() override fun load(): List<Game> = dao.query()
override fun update(game: Game) = dao.update(listOf(game))
override fun find(id: Int): Game = load().single { it.id == id }
override fun save(game: Game) { override fun save(game: Game) {
db.runInTransaction { db.runInTransaction {

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imgDetailBoxArt"
android:layout_width="279dp"
android:layout_height="224dp"
android:layout_marginTop="36dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/txtDetailTitle"
app:srcCompat="@drawable/emptygb" />
<ImageView
android:id="@+id/imgDetailSnapshot"
android:layout_width="213dp"
android:layout_height="194dp"
app:layout_constraintBottom_toTopOf="@+id/txtDetailGameStats"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgDetailBoxArt"
app:layout_constraintVertical_bias="0.413"
app:srcCompat="@drawable/emptygb" />
<TextView
android:id="@+id/txtDetailTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Game Title"
android:textAlignment="center"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/txtDetailGameStats"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="28dp"
android:text="stats"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/txtDetailFinishedLabel"
app:layout_constraintHorizontal_bias="0.057"
app:layout_constraintStart_toStartOf="parent" />
<CheckBox
android:id="@+id/chkDetailFinished"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/txtDetailFinishedLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="Finished:"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/chkDetailFinished"
app:layout_constraintHorizontal_bias="0.879"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -13,12 +13,12 @@
android:text="Some Title" android:text="Some Title"
android:textSize="24sp" android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/chkTodoDone" app:layout_constraintEnd_toStartOf="@+id/chkGameFinished"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<CheckBox <CheckBox
android:id="@+id/chkTodoDone" android:id="@+id/chkGameFinished"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="100dp"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:longClickable="true"
android:padding="16dp"> android:padding="16dp">
<TextView <TextView

View File

@ -1,7 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="150dp" android:layout_height="150dp"
android:background="@color/design_default_color_primary"> android:background="@color/design_default_color_primary">
<ImageView
android:id="@+id/imageView"
android:layout_width="179dp"
android:layout_height="152dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:translationX="10dp"
android:translationY="-20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/sml2" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="HLTB App"
android:textColor="@color/white"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,12 +2,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:id="@+id/mnuClear" android:id="@+id/mnuClear"
android:title="Clear All Items" /> android:title="Remove All Games" />
<item <item
android:id="@+id/mnuClearLatest" android:id="@+id/mnuStats"
android:title="Clear Latest Item" /> android:title="Statistics" />
<item
android:id="@+id/mnuReset"
android:title="Reset to Default Items" />
</menu> </menu>

View File

@ -11,6 +11,9 @@
<action <action
android:id="@+id/action_gameListFragment_to_loadingFragment" android:id="@+id/action_gameListFragment_to_loadingFragment"
app:destination="@id/loadingFragment" /> app:destination="@id/loadingFragment" />
<action
android:id="@+id/action_gameListFragment_to_gameDetailFragment"
app:destination="@id/gameDetailFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/loadingFragment" android:id="@+id/loadingFragment"
@ -28,4 +31,8 @@
android:id="@+id/action_hltbResultsFragment_to_gameListFragment" android:id="@+id/action_hltbResultsFragment_to_gameListFragment"
app:destination="@id/gameListFragment" /> app:destination="@id/gameListFragment" />
</fragment> </fragment>
<fragment
android:id="@+id/gameDetailFragment"
android:name="be.kuleuven.howlongtobeat.GameDetailFragment"
android:label="GameDetailFragment" />
</navigation> </navigation>

View File

@ -1,10 +1,28 @@
package be.kuleuven.howlongtobeat.cartridges package be.kuleuven.howlongtobeat.cartridges
import junit.framework.Assert.assertEquals
import junit.framework.TestCase.*
import org.junit.Test import org.junit.Test
class CartridgeTest { class CartridgeTest {
@Test
fun isValid_OnlyCartridgeCodeItself_IsValid() {
assertTrue(Cartridge.isValid("DMG-MQ-EUR"))
assertTrue(Cartridge.isValid("DMG-MQ-USA-1"))
assertTrue(Cartridge.isValid("CGB-ABC-DEF-WHATEVER"))
}
@Test
fun isValid_Empty_IsNotValid() {
assertFalse(Cartridge.isValid(""))
}
@Test
fun isValid_CatridgeCodeWithOtherJunkOnNewLine_IsNotValid() {
assertFalse(Cartridge.isValid("DMG-MQ-EUR\nMADE IN JAPAN"))
}
@Test @Test
fun titleReplacesIrrelevantDetailsFromName() { fun titleReplacesIrrelevantDetailsFromName() {
val cart = Cartridge("type", "name (irrelevant details please remove thxxx)", "DMG-whatever") val cart = Cartridge("type", "name (irrelevant details please remove thxxx)", "DMG-whatever")

View File

@ -0,0 +1,64 @@
package be.kuleuven.howlongtobeat.car
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepositoryGekkioFi
import junit.framework.Assert.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.concurrent.Executors
class CartridgesRepositoryGekkioFiTest {
private lateinit var repo: CartridgesRepositoryGekkioFi
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
@Before
fun setUp() {
repo = javaClass.getResource("/cartridges.csv").openStream().use {
CartridgesRepositoryGekkioFi(it)
}
Dispatchers.setMain(dispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
dispatcher.close()
}
@Test
fun noSingleCodeShouldBeEmpty() {
repo.cartridges.forEach {
assertTrue("Code of ${it.title} should be filled in but is '${it.code}'", it.code.length > 4)
}
}
@Test
fun findReturnsCartridgeObjectOfCode() = runBlocking {
launch(Dispatchers.Main) {
val smbDeluxe = repo.find("CGB-AHYE-USA")
assertTrue(smbDeluxe!!.name.contains("Super Mario Bros. Deluxe"))
}
println("done")
}
@Test
fun readsWholeCsvFileAsListOfCartridges() {
assertFalse(repo.cartridges.isEmpty())
val smbDeluxe = repo.cartridges.find {
it.code == "CGB-AHYE-USA"
}
assertTrue(smbDeluxe != null)
assertTrue(smbDeluxe!!.name.contains("Super Mario Bros. Deluxe"))
assertEquals("Super Mario Bros. Deluxe", smbDeluxe.title)
}
}

View File

@ -1,39 +0,0 @@
package be.kuleuven.howlongtobeat.car
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository
import junit.framework.Assert.*
import org.junit.Before
import org.junit.Test
class CartridgesRepositoryTest {
private lateinit var repo: CartridgesRepository
@Before
fun setUp() {
repo = javaClass.getResource("/cartridges.csv").openStream().use {
CartridgesRepository(it)
}
}
@Test
fun noSingleCodeShouldBeEmpty() {
repo.cartridges.forEach {
assertTrue("Code of ${it.title} should be filled in but is '${it.code}'", it.code.length > 4)
}
}
@Test
fun readsWholeCsvFileAsListOfCartridges() {
assertFalse(repo.cartridges.isEmpty())
val smbDeluxe = repo.cartridges.find {
it.code == "CGB-AHYE-USA"
}
assertTrue(smbDeluxe != null)
assertTrue(smbDeluxe!!.name.contains("Super Mario Bros. Deluxe"))
assertEquals("Super Mario Bros. Deluxe", smbDeluxe.title)
}
}

View File

@ -0,0 +1,994 @@
package be.kuleuven.howlongtobeat.cartridges
import junit.framework.TestCase.assertEquals
import org.junit.Test
import junit.framework.TestCase.assertNull as assertNull1
class DuckDuckGoResultParserTest {
@Test
fun parse_marioGolfCodeSample() {
val html = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!--[if IE 6]><html class="ie6" xmlns="http://www.w3.org/1999/xhtml"><![endif]-->
<!--[if IE 7]><html class="lt-ie8 lt-ie9" xmlns="http://www.w3.org/1999/xhtml"><![endif]-->
<!--[if IE 8]><html class="lt-ie9" xmlns="http://www.w3.org/1999/xhtml"><![endif]-->
<!--[if gt IE 8]><!--><html xmlns="http://www.w3.org/1999/xhtml"><!--<![endif]-->
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=3.0, user-scalable=1" />
<meta name="referrer" content="origin" />
<meta name="HandheldFriendly" content="true" />
<meta name="robots" content="noindex, nofollow" />
<title>cgb-awxp-eur-1 at DuckDuckGo</title>
<link title="DuckDuckGo (HTML)" type="application/opensearchdescription+xml" rel="search" href="//duckduckgo.com/opensearch_html_v2.xml" />
<link href="//duckduckgo.com/favicon.ico" rel="shortcut icon" />
<link rel="icon" href="//duckduckgo.com/favicon.ico" type="image/x-icon" />
<link rel="apple-touch-icon" href="//duckduckgo.com/assets/logo_icon128.v101.png"/>
<link rel="image_src" href="//duckduckgo.com/assets/logo_homepage.normal.v101.png"/>
<link type="text/css" media="handheld, all" href="//duckduckgo.com/h1997.css" rel="stylesheet" />
</head>
<body class="body--html">
<a name="top" id="top"></a>
<form action="/html/" method="post">
<input type="text" name="state_hidden" id="state_hidden" />
</form>
<div>
<div class="site-wrapper-border"></div>
<div id="header" class="header cw header--html">
<a title="DuckDuckGo" href="/html/" class="header__logo-wrap"></a>
<form name="x" class="header__form" action="/html/" method="post">
<div class="search search--header">
<input name="q" autocomplete="off" class="search__input" id="search_form_input_homepage" type="text" value="cgb-awxp-eur-1" />
<input name="b" id="search_button_homepage" class="search__button search__button--html" value="" title="Search" alt="Search" type="submit" />
</div>
<div class="frm__select">
<select name="kl">
<option value="" >All Regions</option>
<option value="ar-es" >Argentina</option>
<option value="au-en" >Australia</option>
<option value="at-de" >Austria</option>
<option value="be-fr" >Belgium (fr)</option>
<option value="be-nl" >Belgium (nl)</option>
<option value="br-pt" >Brazil</option>
<option value="bg-bg" >Bulgaria</option>
<option value="ca-en" >Canada (en)</option>
<option value="ca-fr" >Canada (fr)</option>
<option value="ct-ca" >Catalonia</option>
<option value="cl-es" >Chile</option>
<option value="cn-zh" >China</option>
<option value="co-es" >Colombia</option>
<option value="hr-hr" >Croatia</option>
<option value="cz-cs" >Czech Republic</option>
<option value="dk-da" >Denmark</option>
<option value="ee-et" >Estonia</option>
<option value="fi-fi" >Finland</option>
<option value="fr-fr" >France</option>
<option value="de-de" >Germany</option>
<option value="gr-el" >Greece</option>
<option value="hk-tzh" >Hong Kong</option>
<option value="hu-hu" >Hungary</option>
<option value="in-en" >India (en)</option>
<option value="id-en" >Indonesia (en)</option>
<option value="ie-en" >Ireland</option>
<option value="il-en" >Israel (en)</option>
<option value="it-it" >Italy</option>
<option value="jp-jp" >Japan</option>
<option value="kr-kr" >Korea</option>
<option value="lv-lv" >Latvia</option>
<option value="lt-lt" >Lithuania</option>
<option value="my-en" >Malaysia (en)</option>
<option value="mx-es" >Mexico</option>
<option value="nl-nl" >Netherlands</option>
<option value="nz-en" >New Zealand</option>
<option value="no-no" >Norway</option>
<option value="pk-en" >Pakistan (en)</option>
<option value="pe-es" >Peru</option>
<option value="ph-en" >Philippines (en)</option>
<option value="pl-pl" >Poland</option>
<option value="pt-pt" >Portugal</option>
<option value="ro-ro" >Romania</option>
<option value="ru-ru" >Russia</option>
<option value="xa-ar" >Saudi Arabia</option>
<option value="sg-en" >Singapore</option>
<option value="sk-sk" >Slovakia</option>
<option value="sl-sl" >Slovenia</option>
<option value="za-en" >South Africa</option>
<option value="es-ca" >Spain (ca)</option>
<option value="es-es" >Spain (es)</option>
<option value="se-sv" >Sweden</option>
<option value="ch-de" >Switzerland (de)</option>
<option value="ch-fr" >Switzerland (fr)</option>
<option value="tw-tzh" >Taiwan</option>
<option value="th-en" >Thailand (en)</option>
<option value="tr-tr" >Turkey</option>
<option value="us-en" >US (English)</option>
<option value="us-es" >US (Spanish)</option>
<option value="ua-uk" >Ukraine</option>
<option value="uk-en" >United Kingdom</option>
<option value="vn-en" >Vietnam (en)</option>
</select>
</div>
<div class="frm__select frm__select--last">
<select class="" name="df">
<option value="" selected>Any Time</option>
<option value="d" >Past Day</option>
<option value="w" >Past Week</option>
<option value="m" >Past Month</option>
<option value="y" >Past Year</option>
</select>
</div>
</form>
</div>
<!-- Web results are present -->
<div>
<div class="serp__results">
<div id="links" class="results">
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fvgdb.uk%2Fmario%2Dgolf%2Fgame%2F48484&amp;rut=b6b8b1ab81b51d84905332762ecdb6db58a60d37b266e4b6fdb2cfd7005feea0">Get information and compare prices of Mario Golf for Game Boy | VGDb</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fvgdb.uk%2Fmario%2Dgolf%2Fgame%2F48484&amp;rut=b6b8b1ab81b51d84905332762ecdb6db58a60d37b266e4b6fdb2cfd7005feea0">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/vgdb.uk.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fvgdb.uk%2Fmario%2Dgolf%2Fgame%2F48484&amp;rut=b6b8b1ab81b51d84905332762ecdb6db58a60d37b266e4b6fdb2cfd7005feea0">
vgdb.uk/mario-golf/game/48484
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fvgdb.uk%2Fmario%2Dgolf%2Fgame%2F48484&amp;rut=b6b8b1ab81b51d84905332762ecdb6db58a60d37b266e4b6fdb2cfd7005feea0">045496730963. MPN. <b>CGB-AWXP-EUR-1</b>. Release Date.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.ebay.de%2Fitm%2FMario%2DGolf%2DNintendo%2DGame%2DBoy%2Dcolor%2DAdvance%2DGBC%2DGBA%2DCGB%2DAWXP%2DEUR%2F264853273128%3Fepid%3D167015543%26hash%3Ditem3daa7c3a28%3Ag%3A0U0AAOSwC%2D5fUT36&amp;rut=5d364b7b299d63d00736bfd7fd0835ac27551cd84b2424019d36a682ad982aea">Mario Golf - Nintendo Game Boy color / Advance GBC GBA... | eBay</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.ebay.de%2Fitm%2FMario%2DGolf%2DNintendo%2DGame%2DBoy%2Dcolor%2DAdvance%2DGBC%2DGBA%2DCGB%2DAWXP%2DEUR%2F264853273128%3Fepid%3D167015543%26hash%3Ditem3daa7c3a28%3Ag%3A0U0AAOSwC%2D5fUT36&amp;rut=5d364b7b299d63d00736bfd7fd0835ac27551cd84b2424019d36a682ad982aea">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.ebay.de.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.ebay.de%2Fitm%2FMario%2DGolf%2DNintendo%2DGame%2DBoy%2Dcolor%2DAdvance%2DGBC%2DGBA%2DCGB%2DAWXP%2DEUR%2F264853273128%3Fepid%3D167015543%26hash%3Ditem3daa7c3a28%3Ag%3A0U0AAOSwC%2D5fUT36&amp;rut=5d364b7b299d63d00736bfd7fd0835ac27551cd84b2424019d36a682ad982aea">
www.ebay.de/itm/Mario-Golf-Nintendo-Game-Boy-color-Advance-GBC-GBA-CGB-AWXP-EUR/264853273128?epid=167015543&amp;hash=item3daa7c3a28:g:0U0AAOSwC-5fUT36
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.ebay.de%2Fitm%2FMario%2DGolf%2DNintendo%2DGame%2DBoy%2Dcolor%2DAdvance%2DGBC%2DGBA%2DCGB%2DAWXP%2DEUR%2F264853273128%3Fepid%3D167015543%26hash%3Ditem3daa7c3a28%3Ag%3A0U0AAOSwC%2D5fUT36&amp;rut=5d364b7b299d63d00736bfd7fd0835ac27551cd84b2424019d36a682ad982aea">Super mario bros deluxe game boy color game nintendo original gbc <b>EUR-1</b> version. Golf. Herstellernummer: <b>CGB-AWXP-EUR</b>. Herausgeber: Nintendo.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=http%3A%2F%2Fpassat.neostrada.pl%2Fzabikt%2FGB.txt&amp;rut=df8a2bed4111ed3dc8458c505782b07b995d248bdb2adf11a264f0c8e216b6ee">passat.neostrada.pl/zabikt/GB.txt</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=http%3A%2F%2Fpassat.neostrada.pl%2Fzabikt%2FGB.txt&amp;rut=df8a2bed4111ed3dc8458c505782b07b995d248bdb2adf11a264f0c8e216b6ee">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/passat.neostrada.pl.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=http%3A%2F%2Fpassat.neostrada.pl%2Fzabikt%2FGB.txt&amp;rut=df8a2bed4111ed3dc8458c505782b07b995d248bdb2adf11a264f0c8e216b6ee">
passat.neostrada.pl/zabikt/GB.txt
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=http%3A%2F%2Fpassat.neostrada.pl%2Fzabikt%2FGB.txt&amp;rut=df8a2bed4111ed3dc8458c505782b07b995d248bdb2adf11a264f0c8e216b6ee">and Jerry <b>CGB</b>-AW8A-<b>EUR</b> DMG-A03-10 <b>CGB</b>-AW8A- 11&#x27;00 Nintendo Wario Land 3 <b>CGB-AWXP-EUR-1</b> DMG-A08-01 <b>CGB</b>-<b>AWXP</b>- 07&#x27;99 <b>CGB</b>-BBZD- 05&#x27;02 Banpresto DragonBall Z - Legendare Superkampfer <b>CGB</b>-BCKP-<b>EUR</b> DMG-A07-01 <b>CGB</b>-BCKP- 10&#x27;00 THQ Chicken Run...</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.xe.com%2Fcurrencyconverter%2Fconvert%2F%3FAmount%3D1%26From%3DEUR%26To%3DXPF&amp;rut=786e13090f4b7968730f2b4700edf1855318e23a3e904b10640c077e450018b5">Convert 1 Euro to CFP Franc - EUR to XPF Exchange Rates | Xe</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.xe.com%2Fcurrencyconverter%2Fconvert%2F%3FAmount%3D1%26From%3DEUR%26To%3DXPF&amp;rut=786e13090f4b7968730f2b4700edf1855318e23a3e904b10640c077e450018b5">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.xe.com.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.xe.com%2Fcurrencyconverter%2Fconvert%2F%3FAmount%3D1%26From%3DEUR%26To%3DXPF&amp;rut=786e13090f4b7968730f2b4700edf1855318e23a3e904b10640c077e450018b5">
www.xe.com/currencyconverter/convert/?Amount=1&amp;From=EUR&amp;To=XPF
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.xe.com%2Fcurrencyconverter%2Fconvert%2F%3FAmount%3D1%26From%3DEUR%26To%3DXPF&amp;rut=786e13090f4b7968730f2b4700edf1855318e23a3e904b10640c077e450018b5">Get live mid-market exchange rates, historical rates and data &amp; currency charts for <b>EUR</b> to XPF with Xe&#x27;s free Currency Converter.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2F1prime.ru%2FFinancial_market%2F20210818%2F834477683.html&amp;rut=98ca5523ff9959af22ddde1d49a41c29cec74571bf11f14960fb6e2753d47349">Евро сдувается: когда пора бежать в обменник - ПРАЙМ, 18.08.2021</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2F1prime.ru%2FFinancial_market%2F20210818%2F834477683.html&amp;rut=98ca5523ff9959af22ddde1d49a41c29cec74571bf11f14960fb6e2753d47349">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/1prime.ru.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2F1prime.ru%2FFinancial_market%2F20210818%2F834477683.html&amp;rut=98ca5523ff9959af22ddde1d49a41c29cec74571bf11f14960fb6e2753d47349">
1prime.ru/Financial_market/20210818/834477683.html
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2F1prime.ru%2FFinancial_market%2F20210818%2F834477683.html&amp;rut=98ca5523ff9959af22ddde1d49a41c29cec74571bf11f14960fb6e2753d47349"><b>EUR</b>/RUB. <b>EUR</b>/CHF.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.cgb.fr%2Fespagne%2Dserie%2Deuro%2Dbrillant%2Duniversel%2D2002%2Dbu%2CEA_102_101_117_95_54_57_48_52_52_51%2Ca.html&amp;rut=4e2756edf796854d544428c15a6044ce2bba3dc48072b3b8ea2b6c465b3cd73d">ESPAGNE SÉRIE Euro BRILLANT UNIVERSEL 2002 Madrid...</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.cgb.fr%2Fespagne%2Dserie%2Deuro%2Dbrillant%2Duniversel%2D2002%2Dbu%2CEA_102_101_117_95_54_57_48_52_52_51%2Ca.html&amp;rut=4e2756edf796854d544428c15a6044ce2bba3dc48072b3b8ea2b6c465b3cd73d">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.cgb.fr.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.cgb.fr%2Fespagne%2Dserie%2Deuro%2Dbrillant%2Duniversel%2D2002%2Dbu%2CEA_102_101_117_95_54_57_48_52_52_51%2Ca.html&amp;rut=4e2756edf796854d544428c15a6044ce2bba3dc48072b3b8ea2b6c465b3cd73d">
www.cgb.fr/espagne-serie-euro-brillant-universel-2002-bu,EA_102_101_117_95_54_57_48_52_52_51,a.html
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.cgb.fr%2Fespagne%2Dserie%2Deuro%2Dbrillant%2Duniversel%2D2002%2Dbu%2CEA_102_101_117_95_54_57_48_52_52_51%2Ca.html&amp;rut=4e2756edf796854d544428c15a6044ce2bba3dc48072b3b8ea2b6c465b3cd73d"><b>cgb</b>.fr. La vente sera clôturée à l&#x27;heure indiquée sur la fiche descriptive, toute offre reçue après l&#x27;heure de clôture ne sera pas validée. <b>cgb</b>.fr utilise des cookies pour vous garantir une meilleure expérience utilisateur et réaliser des statistiques de visites.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.chamber%2Dinternational.com%2Fexporting%2Dchamber%2Dinternational%2Fdocumentation%2Dfor%2Dexport%2Dand%2Dimport%2Feur%2D1%2Dcertificates%2F&amp;rut=faece7b1a1828d2ac5bee57a7c3582afbbd2defc0f12b9cfdabcce29c7728229">EUR1 certificates</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.chamber%2Dinternational.com%2Fexporting%2Dchamber%2Dinternational%2Fdocumentation%2Dfor%2Dexport%2Dand%2Dimport%2Feur%2D1%2Dcertificates%2F&amp;rut=faece7b1a1828d2ac5bee57a7c3582afbbd2defc0f12b9cfdabcce29c7728229">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.chamber-international.com.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.chamber%2Dinternational.com%2Fexporting%2Dchamber%2Dinternational%2Fdocumentation%2Dfor%2Dexport%2Dand%2Dimport%2Feur%2D1%2Dcertificates%2F&amp;rut=faece7b1a1828d2ac5bee57a7c3582afbbd2defc0f12b9cfdabcce29c7728229">
www.chamber-international.com/exporting-chamber-international/documentation-for-export-and-import/eur-1-certificates/
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.chamber%2Dinternational.com%2Fexporting%2Dchamber%2Dinternational%2Fdocumentation%2Dfor%2Dexport%2Dand%2Dimport%2Feur%2D1%2Dcertificates%2F&amp;rut=faece7b1a1828d2ac5bee57a7c3582afbbd2defc0f12b9cfdabcce29c7728229"><b>EUR</b> <b>1</b> certificates are issued by Chamber International under authority from HM Customs and Excise. No matter where you are in the UK we have a dedicated service that can walk you through the <b>EUR1</b> process, to take away the confusion so you can get on with what matters to you.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEUR.1_movement_certificate&amp;rut=24216430601fd3be32ae5f33be1579dd66e4e2a2b52d4e8edf1d1df49da81a76">EUR.1 movement certificate - Wikipedia</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEUR.1_movement_certificate&amp;rut=24216430601fd3be32ae5f33be1579dd66e4e2a2b52d4e8edf1d1df49da81a76">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/en.wikipedia.org.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEUR.1_movement_certificate&amp;rut=24216430601fd3be32ae5f33be1579dd66e4e2a2b52d4e8edf1d1df49da81a76">
en.wikipedia.org/wiki/EUR.1_movement_certificate
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FEUR.1_movement_certificate&amp;rut=24216430601fd3be32ae5f33be1579dd66e4e2a2b52d4e8edf1d1df49da81a76">The <b>EUR.1</b> movement certificate (also known as <b>EUR.1</b> certificate, or <b>EUR.1</b>) is a form used in international commodity traffic. The <b>EUR.1</b> is most importantly recognized as a certificate of origin in...</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.tinko.ru%2Fcatalog%2Fproduct%2F245755%2F&amp;rut=6690f23522657070be76b9ad00b6ba1084ebed21008130b7a37e8c3fa25874ac">CGB-1U-19 (7113c): Медная шина заземления, 19&quot;</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.tinko.ru%2Fcatalog%2Fproduct%2F245755%2F&amp;rut=6690f23522657070be76b9ad00b6ba1084ebed21008130b7a37e8c3fa25874ac">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.tinko.ru.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.tinko.ru%2Fcatalog%2Fproduct%2F245755%2F&amp;rut=6690f23522657070be76b9ad00b6ba1084ebed21008130b7a37e8c3fa25874ac">
www.tinko.ru/catalog/product/245755/
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.tinko.ru%2Fcatalog%2Fproduct%2F245755%2F&amp;rut=6690f23522657070be76b9ad00b6ba1084ebed21008130b7a37e8c3fa25874ac"><b>CGB</b>-1U-19 (7113c). Избранное. Сравнить.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.justetf.com%2Fde%2Fetf%2Dprofile.html%3Fisin%3DIE00B3RBWM25&amp;rut=6ef75eb1798a163400e483634db05a9bb8eb3b05d1b4b99c19cd2b62b91313c8">Vanguard FTSE All-World UCITS ETF Distributing | A1JX52</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.justetf.com%2Fde%2Fetf%2Dprofile.html%3Fisin%3DIE00B3RBWM25&amp;rut=6ef75eb1798a163400e483634db05a9bb8eb3b05d1b4b99c19cd2b62b91313c8">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.justetf.com.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.justetf.com%2Fde%2Fetf%2Dprofile.html%3Fisin%3DIE00B3RBWM25&amp;rut=6ef75eb1798a163400e483634db05a9bb8eb3b05d1b4b99c19cd2b62b91313c8">
www.justetf.com/de/etf-profile.html?isin=IE00B3RBWM25
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.justetf.com%2Fde%2Fetf%2Dprofile.html%3Fisin%3DIE00B3RBWM25&amp;rut=6ef75eb1798a163400e483634db05a9bb8eb3b05d1b4b99c19cd2b62b91313c8">Ausschüttungen der letzten 12 Monate. <b>EUR</b> 1,42. <b>EUR</b> 1,42. Premium-Funktion. 2020.</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.shiphub.co%2Feur%2D1%2Ddocument%2F&amp;rut=a7ea72c573daebf0f67f613309117c6ec11844bff0cd1b2afbb2aa37948f3186">EUR 1 document - Information about EUR 1 | ShipHub</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.shiphub.co%2Feur%2D1%2Ddocument%2F&amp;rut=a7ea72c573daebf0f67f613309117c6ec11844bff0cd1b2afbb2aa37948f3186">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.shiphub.co.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.shiphub.co%2Feur%2D1%2Ddocument%2F&amp;rut=a7ea72c573daebf0f67f613309117c6ec11844bff0cd1b2afbb2aa37948f3186">
www.shiphub.co/eur-1-document/
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.shiphub.co%2Feur%2D1%2Ddocument%2F&amp;rut=a7ea72c573daebf0f67f613309117c6ec11844bff0cd1b2afbb2aa37948f3186">The <b>EUR</b> <b>1</b> document also helps to set the rates of customs duties. This is related to the fact that goods from one region of the world in different countries The <b>EUR</b> <b>1</b> document is one of the most common certificates of origin on the international market. Well, it is issued when there is a preferential bilateral...</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Feur1.ch%2Feur1&amp;rut=9d288ecd9c501724cd409741f2f94aa7ae11ea635feb3abba833b9ebc779b1fa">EUR1 - eur1.ch Informationen zu EUR1 bestellen</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Feur1.ch%2Feur1&amp;rut=9d288ecd9c501724cd409741f2f94aa7ae11ea635feb3abba833b9ebc779b1fa">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/eur1.ch.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Feur1.ch%2Feur1&amp;rut=9d288ecd9c501724cd409741f2f94aa7ae11ea635feb3abba833b9ebc779b1fa">
eur1.ch/eur1
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Feur1.ch%2Feur1&amp;rut=9d288ecd9c501724cd409741f2f94aa7ae11ea635feb3abba833b9ebc779b1fa"><b>EUR1</b> Formular online ausfüllen, <b>EUR1</b> Download, <b>EUR1</b> Fahrzeug, <b>EUR1</b> PKW, <b>EUR1</b> Export, Präferenz, <b>EUR1</b> Formular online, Ursprungserklärung Ein <b>EUR.1</b> ist ein Formular, welches im internationalen Handelswarenverkehr angewendet wird. Die Anwendung basiert auf diversen bi- und...</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fec.europa.eu%2Finfo%2Fstrategy%2Frecovery%2Dplan%2Deurope_en&amp;rut=4e8c965208bfb5af73f9a91301e671a1f719f15a7cb4e4f00761367ceadfcbe2">Recovery plan for Europe | European Commission</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fec.europa.eu%2Finfo%2Fstrategy%2Frecovery%2Dplan%2Deurope_en&amp;rut=4e8c965208bfb5af73f9a91301e671a1f719f15a7cb4e4f00761367ceadfcbe2">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/ec.europa.eu.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fec.europa.eu%2Finfo%2Fstrategy%2Frecovery%2Dplan%2Deurope_en&amp;rut=4e8c965208bfb5af73f9a91301e671a1f719f15a7cb4e4f00761367ceadfcbe2">
ec.europa.eu/info/strategy/recovery-plan-europe_en
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fec.europa.eu%2Finfo%2Fstrategy%2Frecovery%2Dplan%2Deurope_en&amp;rut=4e8c965208bfb5af73f9a91301e671a1f719f15a7cb4e4f00761367ceadfcbe2"><b>EUR</b>, der skal bidrage til opbygningen af et grønnere, mere digitalt og mere modstandsdygtigt Europa). (Bugetul UE: Comisia Europeană salută acordul privind pachetul în valoare de 1,8 mii de miliarde <b>EUR</b> care va contribui la construirea unei Europe mai verzi, mai digitale și mai reziliente).</a>
<div class="clear"></div>
</div>
</div>
<div class="result results_links results_links_deep web-result ">
<div class="links_main links_deep result__body"> <!-- This is the visible part -->
<h2 class="result__title">
<a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D685J1hos%2Dzw&amp;rut=0de6e73357766837f1535544102aba3a21dfeb0ebe95183417e313e54b315bf1">EUR.1 - einfach erklärt (2020) - YouTube</a>
</h2>
<div class="result__extras">
<div class="result__extras__url">
<span class="result__icon">
<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D685J1hos%2Dzw&amp;rut=0de6e73357766837f1535544102aba3a21dfeb0ebe95183417e313e54b315bf1">
<img class="result__icon__img" width="16" height="16" alt=""
src="//external-content.duckduckgo.com/ip3/www.youtube.com.ico" name="i15" />
</a>
</span>
<a class="result__url" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D685J1hos%2Dzw&amp;rut=0de6e73357766837f1535544102aba3a21dfeb0ebe95183417e313e54b315bf1">
www.youtube.com/watch?v=685J1hos-zw
</a>
</div>
</div>
<a class="result__snippet" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D685J1hos%2Dzw&amp;rut=0de6e73357766837f1535544102aba3a21dfeb0ebe95183417e313e54b315bf1">Die <b>EUR.1</b> ermöglicht Importeuren, Waren zu sogenannten Präferenzzollsätzen einzuführen. Sie gilt für fast alle Länder mit denen die EU Freihandelsabkommen abgeschlossen hat. Notwendig für eine rechtmäßigen Ausstellung ist...</a>
<div class="clear"></div>
</div>
</div>
<div class="nav-link">
<form action="/html/" method="post">
<input type="submit" class='btn btn--alt' value="Next" />
<input type="hidden" name="q" value="cgb-awxp-eur-1" />
<input type="hidden" name="s" value="30" />
<input type="hidden" name="nextParams" value="" />
<input type="hidden" name="v" value="l" />
<input type="hidden" name="o" value="json" />
<input type="hidden" name="dc" value="15" />
<input type="hidden" name="api" value="d.js" />
<input type="hidden" name="vqd" value="3-170112077246644806044058132965997084141-24182048638364464238089820915205130627" />
<input name="kl" value="wt-wt" type="hidden" />
</form>
</div>
<div class=" feedback-btn">
<a rel="nofollow" href="//duckduckgo.com/feedback.html" target="_new">Feedback</a>
</div>
<div class="clear"></div>
</div>
</div> <!-- links wrapper //-->
</div>
</div>
<div id="bottom_spacing2"></div>
<img src="//duckduckgo.com/t/sl_h"/>
</body>
</html>
""".trimIndent()
val cart = DuckDuckGoResultParser.parse(html, "cgb-awxp-eur-1")
assertEquals("mario golf", cart?.title)
}
@Test
fun parse_NoResultsFound_returnsNull() {
val result = DuckDuckGoResultParser.parse("whooptie-doo", "some-code")
assertNull1(result)
}
@Test
fun parse_ResultWithStuffBetweenBrackets_removesThose() {
val html = """
<a rel="nofollow" class="result__a" href="url">My Little Pony (USA Release)!</a>
""".trimIndent()
val cart = DuckDuckGoResultParser.parse(html, "some-code")
assertEquals("my little pony", cart?.title)
}
@Test
fun parse_ResultWithCodeAgainInResult_removesRedundantCode() {
val html = """
<a rel="nofollow" class="result__a" href="url">My Little Pony (USA Release) DMG-PONY-007 ebay</a>
""".trimIndent()
val cart = DuckDuckGoResultParser.parse(html, "DMG-PONY-007")
assertEquals("my little pony", cart?.title)
assertEquals("DMG-PONY-007", cart?.code)
}
}

View File

@ -1,14 +1,16 @@
package be.kuleuven.howlongtobeat.hltb package be.kuleuven.howlongtobeat.hltb
import be.kuleuven.howlongtobeat.cartridges.Cartridge
import junit.framework.TestCase.assertNull
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class HowLongToBeatResultParserTest { class HowLongToBeatResultParserTest {
@Test @Test
fun parseWithEmptyStringReturnsEmptyList() { fun parseWithEmptyStringReturnsNull() {
val result = HowLongToBeatResultParser.parse("") val result = HowLongToBeatResultParser.parse("", Cartridge.UNKNOWN_CART)
assertEquals(0, result.size) assertNull(result)
} }
@Test @Test
@ -66,16 +68,22 @@ I/System.out: <div class="search_list_tidbit text_white shadow_text">Ma
<img alt="Box Art" src="/games/250px-Super_Mario_Land_2_box_art.jpg" /> <img alt="Box Art" src="/games/250px-Super_Mario_Land_2_box_art.jpg" />
</a> </a>
""".trimIndent() """.trimIndent()
val result = HowLongToBeatResultParser.parse(html) val result = HowLongToBeatResultParser.parse(html, Cartridge("DMG", "name", "DMG-CODE1"))!!
val smland = result[0] val smland = result[0]
val sm3dland = result[1] val sm3dland = result[1]
assertEquals(3, result.size) assertEquals(3, result.size)
assertEquals("Super Mario Land", smland.title) assertEquals("Super Mario Land", smland.title)
assertEquals("Super Mario 3D Land", sm3dland.title) assertEquals("Super Mario 3D Land", sm3dland.title)
assertEquals("https://howlongtobeat.com/games/250px-Supermariolandboxart.jpg", smland.boxart)
assertEquals("https://howlongtobeat.com/games/250px-Super-Mario-3D-Land-Logo.jpg", sm3dland.boxart) assertEquals("https://howlongtobeat.com/games/250px-Supermariolandboxart.jpg", smland.boxartUrl)
assertEquals("https://howlongtobeat.com/games/250px-Super-Mario-3D-Land-Logo.jpg", sm3dland.boxartUrl)
assertEquals(1.0, smland.howlong, 0.0) assertEquals(1.0, smland.howlong, 0.0)
assertEquals(6.5, sm3dland.howlong, 0.0) assertEquals(6.5, sm3dland.howlong, 0.0)
assertEquals("DMG-CODE1", smland.cartCode)
assertEquals("DMG-CODE1", sm3dland.cartCode)
} }
} }