Skip to content

Commit

Permalink
Fix to show choices before paused months (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleGoogle authored Feb 3, 2021
1 parent 89d7b65 commit 7d36ecc
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 55 deletions.
36 changes: 21 additions & 15 deletions src/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@
logger = logging.getLogger()
logger.addFilter(SensitiveFilter())

sentry_logging = LoggingIntegration(
level=logging.INFO,
event_level=logging.ERROR
)
sentry_sdk.init(
dsn="https://76abb44bffbe45998dd304898327b718@sentry.io/1764525",
integrations=[sentry_logging],
release=f"hb-galaxy@{__version__}"
)

def setup_sentry():
sentry_logging = LoggingIntegration(
level=logging.INFO,
event_level=logging.ERROR
)
sentry_sdk.init(
dsn="https://76abb44bffbe45998dd304898327b718@sentry.io/1764525",
integrations=[sentry_logging],
release=f"hb-galaxy@{__version__}"
)


class HumbleBundlePlugin(Plugin):
Expand All @@ -60,7 +62,7 @@ def __init__(self, reader, writer, token):
self._app_finder = AppFinder()
self._settings = Settings()
self._library_resolver = None
self._subscription_months: List[ChoiceMonth] = []
self._subscription_months: t.List[ChoiceMonth] = []

self._owned_games: t.Dict[str, HumbleGame] = {}
self._trove_games: t.Dict[str, TroveGame] = {}
Expand Down Expand Up @@ -192,17 +194,20 @@ async def _get_subscription_plan(self, month_path: str) -> t.Optional[UserSubscr
return month_content.user_subscription_plan

async def get_subscriptions(self):
subscriptions: List[Subscription] = []
subscriptions: t.List[Subscription] = []
historical_subscriber = await self._api.had_subscription()
active_content_unlocked = False

if historical_subscriber:
async for product in self._api.get_subscription_products_with_gamekeys():
if 'contentChoiceData' not in product:
break # all Humble Choice months already yielded

subscriptions.append(Subscription(
self._normalize_subscription_name(product.product_machine_name),
owned=True
self._normalize_subscription_name(product['productMachineName']),
owned='gamekey' in product
))
if product.is_active_content: # assuming there is only one "active" month at a time
if product.get('isActiveContent'): # assuming there is only one "active" month at a time
active_content_unlocked = True

if not active_content_unlocked:
Expand Down Expand Up @@ -230,7 +235,7 @@ async def get_subscriptions(self):

async def _get_trove_games(self):
def parse_and_cache(troves):
games: List[SubscriptionGame] = []
games: t.List[SubscriptionGame] = []
for trove in troves:
try:
trove_game = TroveGame(trove)
Expand Down Expand Up @@ -469,6 +474,7 @@ async def shutdown(self):


def main():
setup_sentry()
create_and_run_plugin(HumbleBundlePlugin, sys.argv)

if __name__ == "__main__":
Expand Down
48 changes: 24 additions & 24 deletions src/webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from galaxy.api.errors import UnknownBackendResponse

from model.download import TroveDownload, DownloadStructItem
from model.subscription import MontlyContentData, ChoiceContentData, ContentChoiceOptions, ChoiceMarketingData, ChoiceMonth
from model.subscription import MontlyContentData, ChoiceContentData, ChoiceMarketingData, ChoiceMonth


class AuthorizedHumbleAPI:
Expand Down Expand Up @@ -99,51 +99,51 @@ async def _get_trove_details(self, chunk_index) -> list:
res = await self._request('get', self._TROVE_CHUNK_URL.format(chunk_index))
return await res.json()

async def get_subscription_products_with_gamekeys(self):
async def get_subscription_products_with_gamekeys(self) -> t.AsyncGenerator[dict, None]:
"""
Yields list of products - historically backward subscriptions info.
Every product includes few representative games from given subscription and other data as:
`ContentChoiceOptions` (with gamekey if unlocked and made choices)
or `MontlyContentData` (with `download_url` if was subscribed this month)
Used in `https://www.humblebundle.com/subscription/home`
Yields list of subscription products (json) - historically backward subscriptions info
for Humble Choice proceeded by Humble Monthly. Used by HumbleBundle in
`https://www.humblebundle.com/subscription/home`
Every product includes only A FEW representative games from given subscription and other data.
For Choice: `gamekey` field presence means user has unlocked that month and made choices.
For Monhly: `download_url` field presence means user has subscribed this month.
"""
cursor = ''
while True:
res = await self._request('GET', self._SUBSCRIPTION_PRODUCTS + f"/{cursor}")
if res.status == 404: # Ends in November 2015
res = await self._request('GET', self._SUBSCRIPTION_PRODUCTS + f"/{cursor}", raise_for_status=False)
if res.status == 404: # Ends with "Humble Monthly" in November 2015
return
with handle_exception():
res.raise_for_status()
res_json = await res.json()
for product in res_json['products']:
if 'isChoiceTier' in product:
try:
yield ContentChoiceOptions(product)
except KeyError as e:
logging.warning(repr(e))
continue # ignore unexpected response without exiting generator
else: # no more choice content, now humble montly goes
# yield MontlyContentData(product)
return
yield product
cursor = res_json['cursor']

async def get_subscription_history(self, from_product: str):
async def get_subscription_history(self, from_product: str) -> aiohttp.ClientResponse:
"""
Marketing data of previous subscription months.
:param from_product: machine_name of subscription following requested months
for example 'february_2020_choice' to got a few month data items including
'january_2020_choice', 'december_2019_choice', 'december_2020_monthly'
"""
res = await self._request('GET', self._SUBSCRIPTION_HISTORY.format(from_product))
return await res.json()
return await self._request('GET', self._SUBSCRIPTION_HISTORY.format(from_product), raise_for_status=False)

async def get_previous_subscription_months(self, from_product: str):
"""Generator wrapper for get_subscription_history previous months"""
while True:
res = await self.get_subscription_history(from_product)
if res.status == 404:
return
for month in res['previous_months']:
yield ChoiceMonth(month)
from_product = month['machine_name']
with handle_exception():
res.raise_for_status()
data = await res.json()
if not data['previous_months']:
return
for prev_month in data['previous_months']:
yield ChoiceMonth(prev_month)
from_product = prev_month['machine_name']

async def had_subscription(self) -> t.Optional[bool]:
"""Based on current behavior of `humblebundle.com/subscription/home`
Expand Down
53 changes: 37 additions & 16 deletions tests/common/test_subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ async def test_get_subscriptions_never_subscribed(api_mock, plugin_with_sub):


@pytest.mark.asyncio
async def test_get_subscriptions_subscriber_all_from_api(api_mock, plugin_with_sub):
async def test_get_subscriptions_multiple_where_one_paused(api_mock, plugin_with_sub):
api_mock.had_subscription.return_value = True
content_choice_options = [
Mock(**{'product_machine_name': 'may_2020_choice', 'is_active_content': True}),
Mock(**{'product_machine_name': 'april_2020_choice', 'is_active_content': False}),
Mock(**{'product_machine_name': 'march_2020_choice', 'is_active_content': False}),
Mock(**{'product_machine_name': 'february_2020_choice', 'is_active_content': False}),
Mock(**{'product_machine_name': 'january_2020_choice', 'is_active_content': False}),
Mock(**{'product_machine_name': 'december_2019_choice', 'is_active_content': False}),
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'may_2020_choice', 'isActiveContent': True},
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'april_2020_choice', 'isActiveContent': False},
{'contentChoiceData': Mock(dict), 'productMachineName': 'march_2020_choice', 'isActiveContent': False}, # paused month
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'february_2020_choice', 'isActiveContent': False},
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'january_2020_choice', 'isActiveContent': False},
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'december_2019_choice', 'isActiveContent': False},
]
api_mock.get_subscription_products_with_gamekeys = MagicMock(return_value=aiter(content_choice_options))

Expand All @@ -64,24 +64,47 @@ async def test_get_subscriptions_subscriber_all_from_api(api_mock, plugin_with_s
Subscription("Humble Choice 2019-12", owned=True),
Subscription("Humble Choice 2020-01", owned=True),
Subscription("Humble Choice 2020-02", owned=True),
Subscription("Humble Choice 2020-03", owned=True),
Subscription("Humble Choice 2020-03", owned=False), # paused month
Subscription("Humble Choice 2020-04", owned=True),
Subscription("Humble Choice 2020-05", owned=True),
Subscription("Humble Trove", owned=True),
]


@pytest.mark.asyncio
async def test_get_subscriptions_humble_choice_and_humble_monthly(api_mock, plugin_with_sub):
"""
The subscription_products_with_gamekeys API returns firstly Choice months data, then old Humble Monthly subscription data.
Expected: Plugin should ignore Humble Montly subscription months.
"""
api_mock.had_subscription.return_value = True
content_choice_options = [
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'january_2020_choice', 'isActiveContent': True},
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'december_2019_choice', 'isActiveContent': False},
{'machine_name': 'december_2019_monthly', 'order_url': '/downloads?key=b6BVmZ4AuvPwfa3S', 'short_human_name': 'December 2019'}, # subscribed
{'machine_name': 'november_2019_monthly', 'order_url': None, 'short_human_name': 'November 2019'}, # not subscribed
]
api_mock.get_subscription_products_with_gamekeys = MagicMock(return_value=aiter(content_choice_options))

res = await plugin_with_sub.get_subscriptions()
assert sorted(res, key=lambda x: x.subscription_name) == [
Subscription("Humble Choice 2019-12", owned=True),
Subscription("Humble Choice 2020-01", owned=True),
Subscription("Humble Trove", owned=True),
]


@pytest.mark.asyncio
async def test_get_subscriptions_past_subscriber(api_mock, plugin_with_sub):
"""
Testcase: Currently no subscribtion but user was subscriber in the past
Testcase: Currently no subscriptiion but user was subscriber in the past
Expected: Active subscription months + not owned Trove & and owned active month
"""
api_mock.had_subscription.return_value = True
api_mock.get_choice_content_data.return_value = Mock(**{'user_subscription_plan': None})
content_choice_options = [
Mock(**{'product_machine_name': 'march_2020_choice', 'is_active_content': False}),
Mock(**{'product_machine_name': 'february_2020_choice', 'is_active_content': False}),
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'march_2020_choice', 'isActiveContent': False},
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'february_2020_choice', 'isActiveContent': False},
]
api_mock.get_subscription_products_with_gamekeys = MagicMock(return_value=aiter(content_choice_options))

Expand All @@ -107,19 +130,17 @@ async def test_get_subscriptions_current_month_not_unlocked_yet(
api_mock, plugin_with_sub
):
"""
Technically only unlocked choice months are owned (locked are already payed and can be canceled).
But for user convenience plugin marks month as owned if it *is going to* be unloacked if not cancelled untill last Friday.
Technically only unlocked choice months are owned (locked are not already payed and can be canceled).
But for user convenience plugin marks month as owned if it *is going to* be unloacked (if not cancelled untill last Friday).
Without this, Galaxy won't display games until user manualy select current month as owned.
This would be annoying as, as new subscription month happen... well every month.
---
Test rely not only on API thus subscription status must be verified.
---
Test checks also logic for Trove ownership base on subscription status.
"""
api_mock.had_subscription.return_value = True
api_mock.get_choice_content_data.return_value = Mock(user_subscription_plan=current_subscription_plan)
content_choice_options = [
Mock(**{'product_machine_name': 'april_2020_choice', 'is_active_content': False}),
{'contentChoiceData': Mock(dict), 'gamekey': Mock(str), 'productMachineName': 'april_2020_choice', 'isActiveContent': False}
]
api_mock.get_subscription_products_with_gamekeys = MagicMock(return_value=aiter(content_choice_options))
res = await plugin_with_sub.get_subscriptions()
Expand Down

0 comments on commit 7d36ecc

Please sign in to comment.