Skip to content

Commit bbc840d

Browse files
authored
Merge pull request #47 from ScrappyCocco/auto_filtering
Add auto filter code based on flags #46
2 parents 1279872 + 9050bdb commit bbc840d

11 files changed

+171
-43
lines changed

README.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@ It is inspired by [ckatzorke - howlongtobeat](https://github.com/ckatzorke/howlo
1212

1313
## Content
1414

15-
- [Usage](#usage)
16-
- [Installation](#installation)
17-
- [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release)
18-
- [Installing the package from the source code](#installing-the-package-from-the-source-code)
19-
- [Usage in code](#usage-in-code)
20-
- [Start including it in your file](#start-including-it-in-your-file)
21-
- [Now call search()](#now-call-search)
22-
- [Alternative search (by ID)](#alternative-search-by-id)
23-
- [DLC search](#dlc-search)
24-
- [Results auto-filter](#results-auto-filter)
25-
- [Reading an entry](#reading-an-entry)
26-
- [Issues, Questions & Discussions](#issues-questions--discussions)
27-
- [Authors](#authors)
28-
- [License](#license)
15+
- [HowLongToBeat Python API](#howlongtobeat-python-api)
16+
- [Content](#content)
17+
- [Usage](#usage)
18+
- [Installation](#installation)
19+
- [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release)
20+
- [Installing the package from the source code](#installing-the-package-from-the-source-code)
21+
- [Usage in code](#usage-in-code)
22+
- [Start including it in your file](#start-including-it-in-your-file)
23+
- [Now call search()](#now-call-search)
24+
- [Alternative search (by ID)](#alternative-search-by-id)
25+
- [DLC search](#dlc-search)
26+
- [Results auto-filters](#results-auto-filters)
27+
- [Reading an entry](#reading-an-entry)
28+
- [Issues, Questions \& Discussions](#issues-questions--discussions)
29+
- [Authors](#authors)
30+
- [License](#license)
2931

3032
## Usage
3133

@@ -114,7 +116,7 @@ SearchModifiers.HIDE_DLC
114116

115117
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).
116118

117-
### Results auto-filter
119+
### Results auto-filters
118120

119121
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.
120122
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
133135

134136
**Remember** that, when searching by ID, the similarity value and the case-sensitive bool are **ignored**.
135137

138+
An auto-filter for game-types has been added, it is not active by default (False) but can be used as:
139+
140+
```python
141+
results = HowLongToBeat(input_auto_filter_times = True).search("The Witcher 3")
142+
```
143+
144+
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.
145+
136146
### Reading an entry
137147

138148
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
145155

146156
## Authors
147157

148-
* **ScrappyCocco** - Thank you for using my API
158+
- **ScrappyCocco** - Thank you for using my API
149159

150160
## License
151161

howlongtobeatpy/README.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,22 @@ It is inspired by [ckatzorke - howlongtobeat](https://github.com/ckatzorke/howlo
1212

1313
## Content
1414

15-
- [Usage](#usage)
16-
- [Installation](#installation)
17-
- [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release)
18-
- [Installing the package from the source code](#installing-the-package-from-the-source-code)
19-
- [Usage in code](#usage-in-code)
20-
- [Start including it in your file](#start-including-it-in-your-file)
21-
- [Now call search()](#now-call-search)
22-
- [Alternative search (by ID)](#alternative-search-by-id)
23-
- [DLC search](#dlc-search)
24-
- [Results auto-filter](#results-auto-filter)
25-
- [Reading an entry](#reading-an-entry)
26-
- [Issues, Questions & Discussions](#issues-questions--discussions)
27-
- [Authors](#authors)
28-
- [License](#license)
15+
- [HowLongToBeat Python API](#howlongtobeat-python-api)
16+
- [Content](#content)
17+
- [Usage](#usage)
18+
- [Installation](#installation)
19+
- [Installing the package downloading the last release](#installing-the-package-downloading-the-last-release)
20+
- [Installing the package from the source code](#installing-the-package-from-the-source-code)
21+
- [Usage in code](#usage-in-code)
22+
- [Start including it in your file](#start-including-it-in-your-file)
23+
- [Now call search()](#now-call-search)
24+
- [Alternative search (by ID)](#alternative-search-by-id)
25+
- [DLC search](#dlc-search)
26+
- [Results auto-filters](#results-auto-filters)
27+
- [Reading an entry](#reading-an-entry)
28+
- [Issues, Questions \& Discussions](#issues-questions--discussions)
29+
- [Authors](#authors)
30+
- [License](#license)
2931

3032
## Usage
3133

@@ -114,7 +116,7 @@ SearchModifiers.HIDE_DLC
114116

115117
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).
116118

117-
### Results auto-filter
119+
### Results auto-filters
118120

119121
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.
120122
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
133135

134136
**Remember** that, when searching by ID, the similarity value and the case-sensitive bool are **ignored**.
135137

138+
An auto-filter for game-types has been added, it is not active by default (False) but can be used as:
139+
140+
```python
141+
results = HowLongToBeat(input_auto_filter_times = True).search("The Witcher 3")
142+
```
143+
144+
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.
145+
136146
### Reading an entry
137147

138148
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
145155

146156
## Authors
147157

148-
* **ScrappyCocco** - Thank you for using my API
158+
- **ScrappyCocco** - Thank you for using my API
149159

150160
## License
151161

howlongtobeatpy/howlongtobeatpy/HowLongToBeat.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ class HowLongToBeat:
2020
# Constructor with optional parameters
2121
# ------------------------------------------
2222

23-
def __init__(self, input_minimum_similarity: float = 0.4):
23+
def __init__(self, input_minimum_similarity: float = 0.4, input_auto_filter_times: bool = False):
2424
"""
2525
@param input_minimum_similarity: Minimum similarity to use to filter the results with the found name,
26+
@param input_auto_filter_times: If the json parser should automatically filter times based on the game types (online/sp)
2627
0 will return all the results; 1 means perfectly equal and should not be used; default is 0.4;
2728
"""
2829
self.minimum_similarity = input_minimum_similarity
30+
self.auto_filter_times = input_auto_filter_times
2931

3032
if platform.system() == 'Windows':
3133
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
@@ -47,7 +49,7 @@ async def async_search(self, game_name: str, search_modifiers: SearchModifiers =
4749
return None
4850
html_result = await HTMLRequests.send_async_web_request(game_name, search_modifiers)
4951
if html_result is not None:
50-
return self.__parse_web_result(game_name, html_result, None, similarity_case_sensitive)
52+
return self.__parse_web_result(game_name, html_result, input_similarity_case_sensitive = similarity_case_sensitive)
5153
return None
5254

5355
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
6365
return None
6466
html_result = HTMLRequests.send_web_request(game_name, search_modifiers)
6567
if html_result is not None:
66-
return self.__parse_web_result(game_name, html_result, None, similarity_case_sensitive)
68+
return self.__parse_web_result(game_name, html_result, input_similarity_case_sensitive = similarity_case_sensitive)
6769
return None
6870

6971
# ------------------------------------------
@@ -116,7 +118,7 @@ def search_from_id(self, game_id: int):
116118
# ------------------------------------------
117119

118120
def __parse_web_result(self, game_name: str, html_result, game_id=None,
119-
similarity_case_sensitive: bool = True):
121+
input_similarity_case_sensitive: bool = True):
120122
"""
121123
Function that call the HTML parser to get the data
122124
@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,
126128
"""
127129
if game_id is None:
128130
parser = JSONResultParser(game_name, HTMLRequests.GAME_URL, self.minimum_similarity, game_id,
129-
similarity_case_sensitive)
131+
input_similarity_case_sensitive, self.auto_filter_times)
130132
else:
131-
# If the search is by id, ignore class minimum_similarity and set it to 0.0
133+
# If the search is by id, minimum_similarity and similarity_case_sensitive are reset inside
132134
# The result is filtered by ID anyway, so the similarity shouldn't count too much
133-
# Also ignore similarity_case_sensitive and leave default value
134-
parser = JSONResultParser(game_name, HTMLRequests.GAME_URL, 0.0, game_id)
135+
parser = JSONResultParser(game_name, HTMLRequests.GAME_URL, self.minimum_similarity,
136+
input_game_id = game_id, input_auto_filter_times = self.auto_filter_times)
135137
parser.parse_json_result(html_result)
136138
return parser.results

howlongtobeatpy/howlongtobeatpy/HowLongToBeatEntry.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,13 @@ def __init__(self):
4343
self.completionist = None
4444
# All styles
4545
self.all_styles = None
46+
# invested_co value
47+
self.coop_time = None
48+
# invested_mp value
49+
self.mp_time = None
50+
# These are used to identify if the game has singpe, coop and/or multiplayer
51+
# So you can filter data based on those
52+
self.complexity_lvl_combine = False
53+
self.complexity_lvl_sp = False
54+
self.complexity_lvl_co = False
55+
self.complexity_lvl_mp = False

howlongtobeatpy/howlongtobeatpy/JSONResultParser.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ class JSONResultParser:
2020

2121
def __init__(self, input_game_name: str, input_game_url: str,
2222
input_minimum_similarity: float, input_game_id: int = None,
23-
input_similarity_case_sensitive: bool = True):
23+
input_similarity_case_sensitive: bool = True,
24+
input_auto_filter_times: bool = False):
2425
# Init instance variables
2526
self.results = []
2627
self.minimum_similarity = input_minimum_similarity
2728
self.similarity_case_sensitive = input_similarity_case_sensitive
29+
self.auto_filter_times = input_auto_filter_times
2830
self.game_id = input_game_id
31+
if self.game_id is not None:
32+
self.minimum_similarity = 0
33+
self.similarity_case_sensitive = False
2934
self.base_game_url = input_game_url
3035
# Init object
3136
self.game_name = input_game_name
@@ -75,6 +80,26 @@ def parse_json_element(self, input_game_element):
7580
current_entry.completionist = round(input_game_element.get("comp_100") / 3600, 2)
7681
if "comp_all" in input_game_element:
7782
current_entry.all_styles = round(input_game_element.get("comp_all") / 3600, 2)
83+
if "invested_co" in input_game_element:
84+
current_entry.coop_time = round(input_game_element.get("invested_co") / 3600, 2)
85+
if "invested_mp" in input_game_element:
86+
current_entry.mp_time = round(input_game_element.get("invested_mp") / 3600, 2)
87+
# Add complexity booleans
88+
current_entry.complexity_lvl_combine = bool(input_game_element.get("comp_lvl_combine", 0))
89+
current_entry.complexity_lvl_sp = bool(input_game_element.get("comp_lvl_sp", 0))
90+
current_entry.complexity_lvl_co = bool(input_game_element.get("comp_lvl_co", 0))
91+
current_entry.complexity_lvl_mp = bool(input_game_element.get("comp_lvl_mp", 0))
92+
# Auto-Nullify values based on the flags
93+
if self.auto_filter_times:
94+
if current_entry.complexity_lvl_sp is False:
95+
current_entry.main_story = None
96+
current_entry.main_extra = None
97+
current_entry.completionist = None
98+
current_entry.all_styles = None
99+
if current_entry.complexity_lvl_co is False:
100+
current_entry.coop_time = None
101+
if current_entry.complexity_lvl_mp is False:
102+
current_entry.mp_time = None
78103
# Compute Similarity
79104
game_name_similarity = self.similar(self.game_name, current_entry.game_name,
80105
self.game_name_numbers, self.similarity_case_sensitive)

howlongtobeatpy/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
long_description = fh.read()
55

66
setup(name='howlongtobeatpy',
7-
version='1.0.17',
7+
version='1.0.18',
88
packages=find_packages(exclude=['tests']),
99
description='A Python API for How Long to Beat',
1010
long_description=long_description,

howlongtobeatpy/tests/test_async_request.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,35 @@ async def test_game_name_with_numbers(self):
4242
self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name)
4343
self.assertAlmostEqual(50, TestNormalRequest.getSimpleNumber(best_result.main_story), delta=25)
4444

45+
@async_test
46+
async def test_game_with_auto_filter(self):
47+
results = await HowLongToBeat(input_auto_filter_times = True).async_search("The Witcher 3")
48+
self.assertNotEqual(None, results, "Search Results are None")
49+
best_result = TestNormalRequest.getMaxSimilarityElement(results)
50+
self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name)
51+
self.assertEqual(None, best_result.coop_time)
52+
self.assertEqual(None, best_result.mp_time)
53+
54+
@async_test
55+
async def test_multiplayer_game_with_auto_filter(self):
56+
results = await HowLongToBeat(input_auto_filter_times = True).async_search("Overwatch")
57+
self.assertNotEqual(None, results, "Search Results are None")
58+
best_result = TestNormalRequest.getMaxSimilarityElement(results)
59+
self.assertEqual("Overwatch", best_result.game_name)
60+
self.assertEqual(None, best_result.main_story)
61+
self.assertEqual(None, best_result.main_extra)
62+
self.assertEqual(None, best_result.completionist)
63+
64+
@async_test
65+
async def test_multiplayer_game_with_no_auto_filter(self):
66+
results = await HowLongToBeat(input_auto_filter_times = False).async_search("Overwatch")
67+
self.assertNotEqual(None, results, "Search Results are None")
68+
best_result = TestNormalRequest.getMaxSimilarityElement(results)
69+
self.assertEqual("Overwatch", best_result.game_name)
70+
self.assertNotEqual(None, best_result.main_story)
71+
self.assertNotEqual(None, best_result.main_extra)
72+
self.assertNotEqual(None, best_result.completionist)
73+
4574
@async_test
4675
async def test_game_with_values(self):
4776
results = await HowLongToBeat().async_search("Crysis 3")

howlongtobeatpy/tests/test_async_request_by_id.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ async def test_game_name_with_numbers(self):
3636
self.assertEqual("The Witcher 3: Wild Hunt", result.game_name)
3737
self.assertAlmostEqual(50, TestNormalRequest.getSimpleNumber(result.main_story), delta=25)
3838

39+
@async_test
40+
async def test_game_with_auto_filter(self):
41+
result = await HowLongToBeat(input_auto_filter_times = True).async_search_from_id(10270)
42+
self.assertNotEqual(None, result, "Search Result is None")
43+
self.assertEqual("The Witcher 3: Wild Hunt", result.game_name)
44+
self.assertEqual(None, result.coop_time)
45+
self.assertEqual(None, result.mp_time)
46+
3947
@async_test
4048
async def test_game_with_values(self):
4149
result = await HowLongToBeat().async_search_from_id(2070)

howlongtobeatpy/tests/test_normal_request.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,33 @@ def test_game_name_with_numbers(self):
5858
self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name)
5959
self.assertAlmostEqual(50, self.getSimpleNumber(best_result.main_story), delta=5)
6060

61+
def test_game_with_auto_filter(self):
62+
results = HowLongToBeat(input_auto_filter_times = True).search("The Witcher 3")
63+
self.assertNotEqual(None, results, "Search Results are None")
64+
best_result = self.getMaxSimilarityElement(results)
65+
self.assertEqual("The Witcher 3: Wild Hunt", best_result.game_name)
66+
self.assertAlmostEqual(50, self.getSimpleNumber(best_result.main_story), delta=5)
67+
self.assertEqual(None, best_result.coop_time)
68+
self.assertEqual(None, best_result.mp_time)
69+
70+
def test_multiplayer_game_with_auto_filter(self):
71+
results = HowLongToBeat(input_auto_filter_times = True).search("Overwatch")
72+
self.assertNotEqual(None, results, "Search Results are None")
73+
best_result = self.getMaxSimilarityElement(results)
74+
self.assertEqual("Overwatch", best_result.game_name)
75+
self.assertEqual(None, best_result.main_story)
76+
self.assertEqual(None, best_result.main_extra)
77+
self.assertEqual(None, best_result.completionist)
78+
79+
def test_multiplayer_game_with_no_auto_filter(self):
80+
results = HowLongToBeat(input_auto_filter_times = False).search("Overwatch")
81+
self.assertNotEqual(None, results, "Search Results are None")
82+
best_result = self.getMaxSimilarityElement(results)
83+
self.assertEqual("Overwatch", best_result.game_name)
84+
self.assertNotEqual(None, best_result.main_story)
85+
self.assertNotEqual(None, best_result.main_extra)
86+
self.assertNotEqual(None, best_result.completionist)
87+
6188
def test_game_with_values(self):
6289
results = HowLongToBeat().search("Battlefield 2142")
6390
self.assertNotEqual(None, results, "Search Results are None")

howlongtobeatpy/tests/test_normal_request_by_id.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ def test_game_name_with_numbers(self):
3131
self.assertEqual("The Witcher 3: Wild Hunt", result.game_name)
3232
self.assertAlmostEqual(50, TestNormalRequest.getSimpleNumber(result.main_story), delta=5)
3333

34+
def test_game_with_auto_filter(self):
35+
result = HowLongToBeat(input_auto_filter_times = True).search_from_id(10270)
36+
self.assertNotEqual(None, result, "Search Result is None")
37+
self.assertEqual("The Witcher 3: Wild Hunt", result.game_name)
38+
self.assertEqual(None, result.coop_time)
39+
self.assertEqual(None, result.mp_time)
40+
3441
def test_game_with_values(self):
3542
result = HowLongToBeat().search_from_id(936)
3643
self.assertNotEqual(None, result, "Search Result is None")

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ sonar.organization=scrappycocco-github
22
sonar.projectKey=ScrappyCocco_HowLongToBeat-PythonAPI
33

44
sonar.projectName=HowLongToBeat-PythonAPI
5-
sonar.projectVersion=1.0.17
5+
sonar.projectVersion=1.0.18
66
sonar.python.version=3.9
77

88
# Define separate root directories for sources and tests

0 commit comments

Comments
 (0)