Skip to content

Commit

Permalink
Merge pull request #47 from ScrappyCocco/auto_filtering
Browse files Browse the repository at this point in the history
Add auto filter code based on flags #46
  • Loading branch information
ScrappyCocco authored Feb 18, 2025
2 parents 1279872 + 9050bdb commit bbc840d
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 43 deletions.
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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

Expand Down
42 changes: 26 additions & 16 deletions howlongtobeatpy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand All @@ -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

Expand Down
18 changes: 10 additions & 8 deletions howlongtobeatpy/howlongtobeatpy/HowLongToBeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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,
Expand All @@ -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

# ------------------------------------------
Expand Down Expand Up @@ -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
Expand All @@ -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
10 changes: 10 additions & 0 deletions howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 26 additions & 1 deletion howlongtobeatpy/howlongtobeatpy/JSONResultParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion howlongtobeatpy/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions howlongtobeatpy/tests/test_async_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions howlongtobeatpy/tests/test_async_request_by_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions howlongtobeatpy/tests/test_normal_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 7 additions & 0 deletions howlongtobeatpy/tests/test_normal_request_by_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit bbc840d

Please sign in to comment.