Skip to content

Commit

Permalink
Add support for iOS games (#63)
Browse files Browse the repository at this point in the history
Signed-off-by: Eiko Wagenknecht <[email protected]>
  • Loading branch information
eikowagenknecht authored Jun 19, 2022
1 parent 84b1258 commit 851be16
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 11 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ You can either run this script locally on your computer or in any environment ca

Just want the feeds? Sure. You can use the links below. They are updated every 20 minutes.

If you prefer Telegram, you can instead subscribe to the [Telegram LootScraperBot](https://t.me/LootScraperBot) to get push notifications for new offers.
If you prefer Telegram, you can instead subscribe to the [Telegram LootScraperBot](https://t.me/LootScraperBot) to get push notifications for new offers. You can choose which categories to subscribe there.

- Amazon Prime ([games](https://feed.phenx.de/lootscraper_amazon_game.xml) and [ingame loot](https://feed.phenx.de/lootscraper_amazon_loot.xml))
- [Epic Games (games only)](https://feed.phenx.de/lootscraper_epic_game.xml)
- [Gog (games only)](https://feed.phenx.de/lootscraper_gog_game.xml)
- [Humble (games only)](https://feed.phenx.de/lootscraper_humble_game.xml)
- [Epic Games](https://feed.phenx.de/lootscraper_epic_game.xml)
- [Gog games](https://feed.phenx.de/lootscraper_gog_game.xml)
- [Humble games](https://feed.phenx.de/lootscraper_humble_game.xml)
- [itch.io games](https://feed.phenx.de/lootscraper_itch_game.xml)
- Steam ([games](https://feed.phenx.de/lootscraper_steam_game.xml) and [ingame loot](https://feed.phenx.de/lootscraper_steam_loot.xml))

If you want everything in one feed, use [this link](https://feed.phenx.de/lootscraper.xml). If you want to get the offers by email instead, you can use free services like <https://blogtrottr.com/> or <https://feedsub.com/> to convert from RSS to email.
For our mobile gamers:

- [Apple iPhone games](https://feed.phenx.de/lootscraper_apple_game.xml)

If you want *everything* in one feed, use [this link](https://feed.phenx.de/lootscraper.xml). If you want to get the offers by email instead, you can use free services like <https://blogtrottr.com/> or <https://feedsub.com/> to convert from RSS to email.

This is what it currently looks like in Feedly:

Expand Down
1 change: 1 addition & 0 deletions app/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class OfferType(Enum):


class Source(Enum):
APPLE = "Apple App Store"
AMAZON = "Amazon Prime"
EPIC = "Epic Games"
GOG = "GOG"
Expand Down
2 changes: 2 additions & 0 deletions app/configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ def get() -> ParsedConfig:

if config.getboolean("offer_sources", "Amazon"):
parsed_config.enabled_offer_sources.append(Source.AMAZON)
if config.getboolean("offer_sources", "Apple"):
parsed_config.enabled_offer_sources.append(Source.APPLE)
if config.getboolean("offer_sources", "Epic"):
parsed_config.enabled_offer_sources.append(Source.EPIC)
if config.getboolean("offer_sources", "GOG"):
Expand Down
149 changes: 149 additions & 0 deletions app/scraper/loot/ios_games.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import logging
from dataclasses import dataclass
from datetime import datetime, timezone

from selenium.common.exceptions import WebDriverException
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

from app.common import OfferDuration, OfferType, Source
from app.scraper.loot.scraper import Scraper
from app.sqlalchemy import Offer

logger = logging.getLogger(__name__)

ROOT_URL = (
"https://appsliced.co/apps/iphone?sort=latest&price=free&cat%5B0%5D=6014&page=1"
)

XPATH_SEARCH_RESULTS = (
"""//article[contains(concat(" ", normalize-space(@class), " "), " app ")]"""
)
SUBPATH_TITLE = """.//div[contains(concat(" ", normalize-space(@class), " "), " title ")]//a""" # /text()
SUBPATH_IMAGE = """.//div[contains(concat(" ", normalize-space(@class), " "), " icon ")]//img""" # Attr "src"


@dataclass
class RawOffer:
title: str | None
url: str | None
img_url: str | None


class AppStoreScraper(Scraper):
@staticmethod
def get_source() -> Source:
return Source.APPLE

@staticmethod
def get_type() -> OfferType:
return OfferType.GAME

@staticmethod
def get_duration() -> OfferDuration:
return OfferDuration.CLAIMABLE

@staticmethod
def scrape(driver: WebDriver) -> list[Offer]:
return AppStoreScraper.read_offers_from_page(driver)

@staticmethod
def read_offers_from_page(driver: WebDriver) -> list[Offer]:
driver.get(ROOT_URL)
raw_offers: list[RawOffer] = []

try:
# Wait until the page loaded
WebDriverWait(driver, Scraper.get_max_wait_seconds()).until(
EC.presence_of_element_located((By.XPATH, XPATH_SEARCH_RESULTS))
)

offer_elements = driver.find_elements(By.XPATH, XPATH_SEARCH_RESULTS)
for offer_element in offer_elements:
raw_offers.append(AppStoreScraper.read_raw_offer(offer_element))

except WebDriverException:
logger.info(
f"Free search results took longer than {Scraper.get_max_wait_seconds()} to load, probably there are none"
)

normalized_offers = AppStoreScraper.normalize_offers(raw_offers)

return normalized_offers

@staticmethod
def read_raw_offer(element: WebElement) -> RawOffer:
title_str = None
url_str = None
img_url_str = None

try:
title_str = str(
element.find_element(By.XPATH, SUBPATH_TITLE).get_attribute("title")
)
except WebDriverException:
# Nothing to do here, string stays empty
pass

try:
url_str = str(element.find_element(By.XPATH, SUBPATH_TITLE).get_attribute("href")) # type: ignore
except WebDriverException:
# Nothing to do here, string stays empty
pass

try:
img_url_str = str(
element.find_element(By.XPATH, SUBPATH_IMAGE).get_attribute("src")
)
except WebDriverException:
# Nothing to do here, string stays empty
pass

return RawOffer(
title=title_str,
url=url_str,
img_url=img_url_str,
)

@staticmethod
def normalize_offers(raw_offers: list[RawOffer]) -> list[Offer]:
normalized_offers: list[Offer] = []

for raw_offer in raw_offers:
# Skip not recognized offers
if not raw_offer.title:
logger.error(f"Offer not recognized, skipping: {raw_offer}")
continue

# Skip some Demo spam in Humble store
if raw_offer.title.endswith("Demo"):
continue

# Raw text
rawtext = ""
if raw_offer.title:
rawtext += f"<title>{raw_offer.title}</title>"

# Title
title = raw_offer.title

nearest_url = raw_offer.url if raw_offer.url else ROOT_URL
offer = Offer(
source=AppStoreScraper.get_source(),
duration=AppStoreScraper.get_duration(),
type=AppStoreScraper.get_type(),
title=title,
probable_game_name=title,
seen_last=datetime.now(timezone.utc),
rawtext=rawtext,
url=nearest_url,
img_url=raw_offer.img_url,
)

if title is not None and len(title) > 0:
normalized_offers.append(offer)

return normalized_offers
4 changes: 3 additions & 1 deletion app/scraper/loot/scraperhelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from app.scraper.loot.gog_games import GogGamesScraper
from app.scraper.loot.gog_games_alwaysfree import GogGamesAlwaysFreeScraper
from app.scraper.loot.humble_games import HumbleGamesScraper
from app.scraper.loot.ios_games import AppStoreScraper
from app.scraper.loot.itch_games import ItchGamesScraper
from app.scraper.loot.scraper import Scraper
from app.scraper.loot.steam_games import SteamGamesScraper
Expand All @@ -16,11 +17,12 @@ def get_all_scrapers() -> list[Type[Scraper]]:
return [
AmazonGamesScraper,
AmazonLootScraper,
AppStoreScraper,
EpicGamesScraper,
GogGamesScraper,
GogGamesAlwaysFreeScraper,
HumbleGamesScraper,
ItchGamesScraper,
SteamGamesScraper,
SteamLootScraper,
ItchGamesScraper,
]
12 changes: 7 additions & 5 deletions app/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,13 +857,15 @@ def offer_message(self, offer: Offer) -> str:
content += f"Offer expired {markdown_escape(time_to_end)} ago"
else:
content += f"Offer expires in {markdown_escape(time_to_end)}"
content += f" \\({markdown_escape(offer.valid_to.strftime(TIMESTAMP_READABLE_WITH_HOUR))}\\)\\."
content += markdown_escape(
" (" + offer.valid_to.strftime(TIMESTAMP_READABLE_WITH_HOUR) + ")."
)
elif offer.duration == OfferDuration.ALWAYS:
content += "Offer will stay free, no need to hurry\\!"
content += markdown_escape("Offer will stay free, no need to hurry.")
else:
content += "Offer is valid forever\\.\\. just kidding, I just don't know when it will end, so grab it now\\!"

content += "\n"
content += markdown_escape(
"Offer is valid forever.. just kidding, I just don't know when it will end."
)

if offer.url:
content += " " + markdown_url(
Expand Down
1 change: 1 addition & 0 deletions config.default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ HeadlessChrome = yes
VirtualLinuxDisplay = no

[offer_sources]
Apple = no
Amazon = no
Epic = no
Gog = no
Expand Down

0 comments on commit 851be16

Please sign in to comment.