diff --git a/README.md b/README.md index 619c82f..7e445d6 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,22 @@ It is inspired by [ckatzorke - howlongtobeat](https://github.com/ckatzorke/howlo ## Content -- [Usage](#usage) -- [Installation](#installation) - - [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release) - - [Installing the package from the source code](#installing-the-package-from-the-source-code) -- [Usage in code](#usage-in-code) - - [Start including it in your file](#start-including-it-in-your-file) - - [Now call search()](#now-call-search) - - [Alternative search (by ID)](#alternative-search-by-id) - - [DLC search](#dlc-search) - - [Results auto-filter](#results-auto-filter) - - [Reading an entry](#reading-an-entry) -- [Issues, Questions & Discussions](#issues-questions--discussions) -- [Authors](#authors) -- [License](#license) +- [HowLongToBeat Python API](#howlongtobeat-python-api) + - [Content](#content) + - [Usage](#usage) + - [Installation](#installation) + - [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release) + - [Installing the package from the source code](#installing-the-package-from-the-source-code) + - [Usage in code](#usage-in-code) + - [Start including it in your file](#start-including-it-in-your-file) + - [Now call search()](#now-call-search) + - [Alternative search (by ID)](#alternative-search-by-id) + - [DLC search](#dlc-search) + - [Results auto-filters](#results-auto-filters) + - [Reading an entry](#reading-an-entry) + - [Issues, Questions \& Discussions](#issues-questions--discussions) + - [Authors](#authors) + - [License](#license) ## Usage @@ -114,7 +116,7 @@ SearchModifiers.HIDE_DLC This optional parameter allow you to specify in the search if you want the default search (with DLCs), to HIDE DLCs and only show games, or to ISOLATE DLCs (show only DLCs). -### Results auto-filter +### Results auto-filters To ignore games with a very different name, the standard search automatically filter results with a game name that has a similarity with the given name > than `0.4`, not adding the others to the result list. If you want all the results, or you want to change this value, you can put a parameter in the constructor: @@ -133,6 +135,14 @@ results = HowLongToBeat(0.0).search("Awesome Game", similarity_case_sensitive=Fa **Remember** that, when searching by ID, the similarity value and the case-sensitive bool are **ignored**. +An auto-filter for game-types has been added, it is not active by default (False) but can be used as: + +```python +results = HowLongToBeat(input_auto_filter_times = True).search("The Witcher 3") +``` + +That auto-filter "nullify" values based on the game-type, if it is a singleplayer game then the coop/multiplayer values are overridden to Null; on the other side if it is a Multiplayer game the singleplayer values such as "main story" could be overridden to Null if that game doesn't have a story. Use with caution, it is probably better if you decide what fits best for you. + ### Reading an entry An entry is made of few values, you can check them [in the Entry class file](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI/blob/master/howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py). It also include the full JSON of values (already converted to Python dict) received from HLTB. @@ -145,7 +155,7 @@ If you need any new feature, or want to discuss the current implementation/featu ## Authors -* **ScrappyCocco** - Thank you for using my API +- **ScrappyCocco** - Thank you for using my API ## License diff --git a/howlongtobeatpy/README.md b/howlongtobeatpy/README.md index 619c82f..7e445d6 100644 --- a/howlongtobeatpy/README.md +++ b/howlongtobeatpy/README.md @@ -12,20 +12,22 @@ It is inspired by [ckatzorke - howlongtobeat](https://github.com/ckatzorke/howlo ## Content -- [Usage](#usage) -- [Installation](#installation) - - [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release) - - [Installing the package from the source code](#installing-the-package-from-the-source-code) -- [Usage in code](#usage-in-code) - - [Start including it in your file](#start-including-it-in-your-file) - - [Now call search()](#now-call-search) - - [Alternative search (by ID)](#alternative-search-by-id) - - [DLC search](#dlc-search) - - [Results auto-filter](#results-auto-filter) - - [Reading an entry](#reading-an-entry) -- [Issues, Questions & Discussions](#issues-questions--discussions) -- [Authors](#authors) -- [License](#license) +- [HowLongToBeat Python API](#howlongtobeat-python-api) + - [Content](#content) + - [Usage](#usage) + - [Installation](#installation) + - [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release) + - [Installing the package from the source code](#installing-the-package-from-the-source-code) + - [Usage in code](#usage-in-code) + - [Start including it in your file](#start-including-it-in-your-file) + - [Now call search()](#now-call-search) + - [Alternative search (by ID)](#alternative-search-by-id) + - [DLC search](#dlc-search) + - [Results auto-filters](#results-auto-filters) + - [Reading an entry](#reading-an-entry) + - [Issues, Questions \& Discussions](#issues-questions--discussions) + - [Authors](#authors) + - [License](#license) ## Usage @@ -114,7 +116,7 @@ SearchModifiers.HIDE_DLC This optional parameter allow you to specify in the search if you want the default search (with DLCs), to HIDE DLCs and only show games, or to ISOLATE DLCs (show only DLCs). -### Results auto-filter +### Results auto-filters To ignore games with a very different name, the standard search automatically filter results with a game name that has a similarity with the given name > than `0.4`, not adding the others to the result list. If you want all the results, or you want to change this value, you can put a parameter in the constructor: @@ -133,6 +135,14 @@ results = HowLongToBeat(0.0).search("Awesome Game", similarity_case_sensitive=Fa **Remember** that, when searching by ID, the similarity value and the case-sensitive bool are **ignored**. +An auto-filter for game-types has been added, it is not active by default (False) but can be used as: + +```python +results = HowLongToBeat(input_auto_filter_times = True).search("The Witcher 3") +``` + +That auto-filter "nullify" values based on the game-type, if it is a singleplayer game then the coop/multiplayer values are overridden to Null; on the other side if it is a Multiplayer game the singleplayer values such as "main story" could be overridden to Null if that game doesn't have a story. Use with caution, it is probably better if you decide what fits best for you. + ### Reading an entry An entry is made of few values, you can check them [in the Entry class file](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI/blob/master/howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py). It also include the full JSON of values (already converted to Python dict) received from HLTB. @@ -145,7 +155,7 @@ If you need any new feature, or want to discuss the current implementation/featu ## Authors -* **ScrappyCocco** - Thank you for using my API +- **ScrappyCocco** - Thank you for using my API ## License diff --git a/howlongtobeatpy/howlongtobeatpy/HowLongToBeat.py b/howlongtobeatpy/howlongtobeatpy/HowLongToBeat.py index 8cb9dcc..d5ae9ea 100644 --- a/howlongtobeatpy/howlongtobeatpy/HowLongToBeat.py +++ b/howlongtobeatpy/howlongtobeatpy/HowLongToBeat.py @@ -20,12 +20,14 @@ class HowLongToBeat: # Constructor with optional parameters # ------------------------------------------ - def __init__(self, input_minimum_similarity: float = 0.4): + def __init__(self, input_minimum_similarity: float = 0.4, input_auto_filter_times: bool = False): """ @param input_minimum_similarity: Minimum similarity to use to filter the results with the found name, + @param input_auto_filter_times: If the json parser should automatically filter times based on the game types (online/sp) 0 will return all the results; 1 means perfectly equal and should not be used; default is 0.4; """ self.minimum_similarity = input_minimum_similarity + self.auto_filter_times = input_auto_filter_times if platform.system() == 'Windows': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -47,7 +49,7 @@ async def async_search(self, game_name: str, search_modifiers: SearchModifiers = return None html_result = await HTMLRequests.send_async_web_request(game_name, search_modifiers) if html_result is not None: - return self.__parse_web_result(game_name, html_result, None, similarity_case_sensitive) + return self.__parse_web_result(game_name, html_result, input_similarity_case_sensitive = similarity_case_sensitive) return None def search(self, game_name: str, search_modifiers: SearchModifiers = SearchModifiers.NONE, @@ -63,7 +65,7 @@ def search(self, game_name: str, search_modifiers: SearchModifiers = SearchModif return None html_result = HTMLRequests.send_web_request(game_name, search_modifiers) if html_result is not None: - return self.__parse_web_result(game_name, html_result, None, similarity_case_sensitive) + return self.__parse_web_result(game_name, html_result, input_similarity_case_sensitive = similarity_case_sensitive) return None # ------------------------------------------ @@ -116,7 +118,7 @@ def search_from_id(self, game_id: int): # ------------------------------------------ def __parse_web_result(self, game_name: str, html_result, game_id=None, - similarity_case_sensitive: bool = True): + input_similarity_case_sensitive: bool = True): """ Function that call the HTML parser to get the data @param game_name: The original game name received as input @@ -126,11 +128,11 @@ def __parse_web_result(self, game_name: str, html_result, game_id=None, """ if game_id is None: parser = JSONResultParser(game_name, HTMLRequests.GAME_URL, self.minimum_similarity, game_id, - similarity_case_sensitive) + input_similarity_case_sensitive, self.auto_filter_times) else: - # If the search is by id, ignore class minimum_similarity and set it to 0.0 + # If the search is by id, minimum_similarity and similarity_case_sensitive are reset inside # The result is filtered by ID anyway, so the similarity shouldn't count too much - # Also ignore similarity_case_sensitive and leave default value - parser = JSONResultParser(game_name, HTMLRequests.GAME_URL, 0.0, game_id) + parser = JSONResultParser(game_name, HTMLRequests.GAME_URL, self.minimum_similarity, + input_game_id = game_id, input_auto_filter_times = self.auto_filter_times) parser.parse_json_result(html_result) return parser.results diff --git a/howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py b/howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py index c03514d..7ddd18c 100644 --- a/howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py +++ b/howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py @@ -43,3 +43,13 @@ def __init__(self): self.completionist = None # All styles self.all_styles = None + # invested_co value + self.coop_time = None + # invested_mp value + self.mp_time = None + # These are used to identify if the game has singpe, coop and/or multiplayer + # So you can filter data based on those + self.complexity_lvl_combine = False + self.complexity_lvl_sp = False + self.complexity_lvl_co = False + self.complexity_lvl_mp = False diff --git a/howlongtobeatpy/howlongtobeatpy/JSONResultParser.py b/howlongtobeatpy/howlongtobeatpy/JSONResultParser.py index e1a5a47..6ed8ea9 100644 --- a/howlongtobeatpy/howlongtobeatpy/JSONResultParser.py +++ b/howlongtobeatpy/howlongtobeatpy/JSONResultParser.py @@ -20,12 +20,17 @@ class JSONResultParser: def __init__(self, input_game_name: str, input_game_url: str, input_minimum_similarity: float, input_game_id: int = None, - input_similarity_case_sensitive: bool = True): + input_similarity_case_sensitive: bool = True, + input_auto_filter_times: bool = False): # Init instance variables self.results = [] self.minimum_similarity = input_minimum_similarity self.similarity_case_sensitive = input_similarity_case_sensitive + self.auto_filter_times = input_auto_filter_times self.game_id = input_game_id + if self.game_id is not None: + self.minimum_similarity = 0 + self.similarity_case_sensitive = False self.base_game_url = input_game_url # Init object self.game_name = input_game_name @@ -75,6 +80,26 @@ def parse_json_element(self, input_game_element): current_entry.completionist = round(input_game_element.get("comp_100") / 3600, 2) if "comp_all" in input_game_element: current_entry.all_styles = round(input_game_element.get("comp_all") / 3600, 2) + if "invested_co" in input_game_element: + current_entry.coop_time = round(input_game_element.get("invested_co") / 3600, 2) + if "invested_mp" in input_game_element: + current_entry.mp_time = round(input_game_element.get("invested_mp") / 3600, 2) + # Add complexity booleans + current_entry.complexity_lvl_combine = bool(input_game_element.get("comp_lvl_combine", 0)) + current_entry.complexity_lvl_sp = bool(input_game_element.get("comp_lvl_sp", 0)) + current_entry.complexity_lvl_co = bool(input_game_element.get("comp_lvl_co", 0)) + current_entry.complexity_lvl_mp = bool(input_game_element.get("comp_lvl_mp", 0)) + # Auto-Nullify values based on the flags + if self.auto_filter_times: + if current_entry.complexity_lvl_sp is False: + current_entry.main_story = None + current_entry.main_extra = None + current_entry.completionist = None + current_entry.all_styles = None + if current_entry.complexity_lvl_co is False: + current_entry.coop_time = None + if current_entry.complexity_lvl_mp is False: + current_entry.mp_time = None # Compute Similarity game_name_similarity = self.similar(self.game_name, current_entry.game_name, self.game_name_numbers, self.similarity_case_sensitive) diff --git a/howlongtobeatpy/setup.py b/howlongtobeatpy/setup.py index 4878f49..304d0cc 100644 --- a/howlongtobeatpy/setup.py +++ b/howlongtobeatpy/setup.py @@ -4,7 +4,7 @@ long_description = fh.read() setup(name='howlongtobeatpy', - version='1.0.17', + version='1.0.18', packages=find_packages(exclude=['tests']), description='A Python API for How Long to Beat', long_description=long_description, diff --git a/howlongtobeatpy/tests/test_async_request.py b/howlongtobeatpy/tests/test_async_request.py index 6e1ed47..b4781fc 100644 --- a/howlongtobeatpy/tests/test_async_request.py +++ b/howlongtobeatpy/tests/test_async_request.py @@ -42,6 +42,35 @@ async def test_game_name_with_numbers(self): self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name) self.assertAlmostEqual(50, TestNormalRequest.getSimpleNumber(best_result.main_story), delta=25) + @async_test + async def test_game_with_auto_filter(self): + results = await HowLongToBeat(input_auto_filter_times = True).async_search("The Witcher 3") + self.assertNotEqual(None, results, "Search Results are None") + best_result = TestNormalRequest.getMaxSimilarityElement(results) + self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name) + self.assertEqual(None, best_result.coop_time) + self.assertEqual(None, best_result.mp_time) + + @async_test + async def test_multiplayer_game_with_auto_filter(self): + results = await HowLongToBeat(input_auto_filter_times = True).async_search("Overwatch") + self.assertNotEqual(None, results, "Search Results are None") + best_result = TestNormalRequest.getMaxSimilarityElement(results) + self.assertEqual("Overwatch", best_result.game_name) + self.assertEqual(None, best_result.main_story) + self.assertEqual(None, best_result.main_extra) + self.assertEqual(None, best_result.completionist) + + @async_test + async def test_multiplayer_game_with_no_auto_filter(self): + results = await HowLongToBeat(input_auto_filter_times = False).async_search("Overwatch") + self.assertNotEqual(None, results, "Search Results are None") + best_result = TestNormalRequest.getMaxSimilarityElement(results) + self.assertEqual("Overwatch", best_result.game_name) + self.assertNotEqual(None, best_result.main_story) + self.assertNotEqual(None, best_result.main_extra) + self.assertNotEqual(None, best_result.completionist) + @async_test async def test_game_with_values(self): results = await HowLongToBeat().async_search("Crysis 3") diff --git a/howlongtobeatpy/tests/test_async_request_by_id.py b/howlongtobeatpy/tests/test_async_request_by_id.py index 058db64..2af4a45 100644 --- a/howlongtobeatpy/tests/test_async_request_by_id.py +++ b/howlongtobeatpy/tests/test_async_request_by_id.py @@ -36,6 +36,14 @@ async def test_game_name_with_numbers(self): self.assertEqual("The Witcher 3: Wild Hunt", result.game_name) self.assertAlmostEqual(50, TestNormalRequest.getSimpleNumber(result.main_story), delta=25) + @async_test + async def test_game_with_auto_filter(self): + result = await HowLongToBeat(input_auto_filter_times = True).async_search_from_id(10270) + self.assertNotEqual(None, result, "Search Result is None") + self.assertEqual("The Witcher 3: Wild Hunt", result.game_name) + self.assertEqual(None, result.coop_time) + self.assertEqual(None, result.mp_time) + @async_test async def test_game_with_values(self): result = await HowLongToBeat().async_search_from_id(2070) diff --git a/howlongtobeatpy/tests/test_normal_request.py b/howlongtobeatpy/tests/test_normal_request.py index d4b7519..2d4358b 100644 --- a/howlongtobeatpy/tests/test_normal_request.py +++ b/howlongtobeatpy/tests/test_normal_request.py @@ -58,6 +58,33 @@ def test_game_name_with_numbers(self): self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name) self.assertAlmostEqual(50, self.getSimpleNumber(best_result.main_story), delta=5) + def test_game_with_auto_filter(self): + results = HowLongToBeat(input_auto_filter_times = True).search("The Witcher 3") + self.assertNotEqual(None, results, "Search Results are None") + best_result = self.getMaxSimilarityElement(results) + self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name) + self.assertAlmostEqual(50, self.getSimpleNumber(best_result.main_story), delta=5) + self.assertEqual(None, best_result.coop_time) + self.assertEqual(None, best_result.mp_time) + + def test_multiplayer_game_with_auto_filter(self): + results = HowLongToBeat(input_auto_filter_times = True).search("Overwatch") + self.assertNotEqual(None, results, "Search Results are None") + best_result = self.getMaxSimilarityElement(results) + self.assertEqual("Overwatch", best_result.game_name) + self.assertEqual(None, best_result.main_story) + self.assertEqual(None, best_result.main_extra) + self.assertEqual(None, best_result.completionist) + + def test_multiplayer_game_with_no_auto_filter(self): + results = HowLongToBeat(input_auto_filter_times = False).search("Overwatch") + self.assertNotEqual(None, results, "Search Results are None") + best_result = self.getMaxSimilarityElement(results) + self.assertEqual("Overwatch", best_result.game_name) + self.assertNotEqual(None, best_result.main_story) + self.assertNotEqual(None, best_result.main_extra) + self.assertNotEqual(None, best_result.completionist) + def test_game_with_values(self): results = HowLongToBeat().search("Battlefield 2142") self.assertNotEqual(None, results, "Search Results are None") diff --git a/howlongtobeatpy/tests/test_normal_request_by_id.py b/howlongtobeatpy/tests/test_normal_request_by_id.py index f6ab7f9..2a435d8 100644 --- a/howlongtobeatpy/tests/test_normal_request_by_id.py +++ b/howlongtobeatpy/tests/test_normal_request_by_id.py @@ -31,6 +31,13 @@ def test_game_name_with_numbers(self): self.assertEqual("The Witcher 3: Wild Hunt", result.game_name) self.assertAlmostEqual(50, TestNormalRequest.getSimpleNumber(result.main_story), delta=5) + def test_game_with_auto_filter(self): + result = HowLongToBeat(input_auto_filter_times = True).search_from_id(10270) + self.assertNotEqual(None, result, "Search Result is None") + self.assertEqual("The Witcher 3: Wild Hunt", result.game_name) + self.assertEqual(None, result.coop_time) + self.assertEqual(None, result.mp_time) + def test_game_with_values(self): result = HowLongToBeat().search_from_id(936) self.assertNotEqual(None, result, "Search Result is None") diff --git a/sonar-project.properties b/sonar-project.properties index 52a5543..573d788 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,7 +2,7 @@ sonar.organization=scrappycocco-github sonar.projectKey=ScrappyCocco_HowLongToBeat-PythonAPI sonar.projectName=HowLongToBeat-PythonAPI -sonar.projectVersion=1.0.17 +sonar.projectVersion=1.0.18 sonar.python.version=3.9 # Define separate root directories for sources and tests