mirror of
https://github.com/Emersont1/itchio.git
synced 2026-03-11 08:54:39 +00:00
First Commit, added everything for a version 1.0
This commit is contained in:
commit
0f645f1923
12 changed files with 409 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
venv
|
||||
__pycache__
|
||||
|
||||
src/*
|
||||
!src/itchio
|
||||
21
Readme.md
Normal file
21
Readme.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Itchio Downloader Tool
|
||||
|
||||
## Download All Games in library from account
|
||||
|
||||
```bash
|
||||
python -m itchio.downloader
|
||||
```
|
||||
|
||||
This uses the same API the itchio app uses to download the files. If you have 2FA enabled, generate an API key [here](https://itch.io/user/settings/api-keys) and run the following instead
|
||||
|
||||
```bash
|
||||
python -m itchio.downloader --api-key=KEYHERE
|
||||
```
|
||||
|
||||
## Add All Games in a bundle to your library
|
||||
|
||||
```bash
|
||||
python -m itchio.downloader
|
||||
```
|
||||
|
||||
This is a bit of a bodge, but it works. It essentially goes through and clicks the "Download" link on every item on the bundle's page, which adds it to your itchio library. It does not download any files. You will need the download page's URL (this will be in the bundle's email, and possibly your purchase history). It will not work with 2FA, and I'm unlikely to be able to fix it without making it far more complicated
|
||||
147
poetry.lock
generated
Normal file
147
poetry.lock
generated
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.10.0"
|
||||
description = "Screen-scraping library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">3.0.0"
|
||||
|
||||
[package.dependencies]
|
||||
soupsieve = ">1.2"
|
||||
|
||||
[package.extras]
|
||||
html5lib = ["html5lib"]
|
||||
lxml = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "2.0.9"
|
||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.0"
|
||||
|
||||
[package.extras]
|
||||
unicode_backport = ["unicodedata2"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.3"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.26.0"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.3.1"
|
||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.62.3"
|
||||
description = "Fast, Extensible Progress Meter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["py-make (>=0.1.0)", "twine", "wheel"]
|
||||
notebook = ["ipywidgets (>=6)"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.7"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "c4579afeecd957dc271da7282c523c619640819471bde1ca4c41228d0521932c"
|
||||
|
||||
[metadata.files]
|
||||
beautifulsoup4 = [
|
||||
{file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"},
|
||||
{file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"},
|
||||
{file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
]
|
||||
soupsieve = [
|
||||
{file = "soupsieve-2.3.1-py3-none-any.whl", hash = "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb"},
|
||||
{file = "soupsieve-2.3.1.tar.gz", hash = "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"},
|
||||
]
|
||||
tqdm = [
|
||||
{file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"},
|
||||
{file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
|
||||
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
|
||||
]
|
||||
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[tool.poetry]
|
||||
name = "itchio"
|
||||
version = "1.0.0"
|
||||
description = "Python Scripts for downloading / archiving your itchio library"
|
||||
authors = ["Peter Taylor <me@et1.uk>"]
|
||||
license = "MIT"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
requests = "beautifulsoup4"
|
||||
beautifulsoup4 = "^4.10.0"
|
||||
tqdm = "^4.62.3"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
4
src/itchio/__init__.py
Normal file
4
src/itchio/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .login import LoginWeb, LoginAPI
|
||||
from .bundle import Bundle
|
||||
from .library import Library
|
||||
from .game import Game
|
||||
39
src/itchio/bundle.py
Normal file
39
src/itchio/bundle.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import requests
|
||||
from bs4 import BeautifulSoup as soup
|
||||
|
||||
class Bundle:
|
||||
def __init__(self, login, url):
|
||||
self.login = login
|
||||
self.url = url
|
||||
|
||||
def load_games(self):
|
||||
i = 1
|
||||
|
||||
r = self.login.get(self.url)
|
||||
s = soup(r.text, "html.parser")
|
||||
pages = int(s.find("span", {"class": "pager_label"}).find_all("a")[-1].text)
|
||||
while i < pages:
|
||||
if self.load_game(i):
|
||||
i += 1
|
||||
print(f"Processing Page {i} of {pages}")
|
||||
|
||||
def load_game(self, i):
|
||||
# Load 1 game. This will refresh the game afterwards, as the csrf token will update
|
||||
r = self.login.get(f"{self.url}?page={i}")
|
||||
s = soup(r.text, "html.parser")
|
||||
for g in s.find_all("div", {"class": "game_row"}):
|
||||
name = g.find("h2").find("a").text
|
||||
if f := g.find("form"):
|
||||
print(f"Processing {name}")
|
||||
|
||||
game_id = f.find("input", {"name":"game_id"})["value"]
|
||||
csrf_token = f.find("input", {"name":"csrf_token"})["value"]
|
||||
|
||||
data = {"action":"claim", "game_id":game_id, "csrf_token":csrf_token}
|
||||
|
||||
r = self.login.post(f"{self.url}?page={i}", data=data)
|
||||
return False
|
||||
#else:
|
||||
# print(f"Skipping {name} - Already in Library")
|
||||
return True
|
||||
|
||||
11
src/itchio/bundle/__main__.py
Normal file
11
src/itchio/bundle/__main__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from getpass import getpass
|
||||
import itchio
|
||||
|
||||
user = input("Username: ")
|
||||
password = getpass("Password: ")
|
||||
|
||||
l = itchio.LoginWeb(user, password)
|
||||
|
||||
url = input("Bundle URL: ")
|
||||
b = itchio.Bundle(l, url)
|
||||
b.load_games()
|
||||
26
src/itchio/downloader/__main__.py
Normal file
26
src/itchio/downloader/__main__.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import argparse
|
||||
from getpass import getpass
|
||||
|
||||
import itchio
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='python -m hstp',
|
||||
description='Build an '
|
||||
)
|
||||
|
||||
parser.add_argument("-k", "--api-key", help="Use API key instead of username/password")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
l = ""
|
||||
|
||||
if not args.api_key:
|
||||
user = input("Username: ")
|
||||
password = getpass("Password: ")
|
||||
l = itchio.LoginWeb(user, password)
|
||||
else:
|
||||
l = args.api_key
|
||||
|
||||
lib = itchio.Library(l)
|
||||
lib.load_games()
|
||||
lib.download_library()
|
||||
58
src/itchio/game.py
Normal file
58
src/itchio/game.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import re
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
import itchio.utils
|
||||
|
||||
class Game:
|
||||
def __init__(self, data):
|
||||
self.data = data["game"]
|
||||
self.name = self.data["title"]
|
||||
self.publisher = self.data["user"]["username"]
|
||||
self.link = self.data["url"]
|
||||
|
||||
self.id = data["id"]
|
||||
self.game_id = data["game_id"]
|
||||
|
||||
matches = re.match(r"https://(.+)\.itch\.io/(.+)", self.link)
|
||||
self.game_slug = matches.group(2)
|
||||
self.publisher_slug = matches.group(1)
|
||||
|
||||
self.files = []
|
||||
|
||||
def load_downloads(self, token):
|
||||
self.downloads = []
|
||||
r = requests.get(f"https://api.itch.io/games/{self.game_id}/uploads?download_key_id={self.id}", headers={"Authorization": token})
|
||||
j = r.json()
|
||||
for d in j["uploads"]:
|
||||
self.downloads.append(d)
|
||||
|
||||
def download(self, token):
|
||||
self.load_downloads(token)
|
||||
|
||||
if not os.path.exists(self.publisher_slug):
|
||||
os.mkdir(self.publisher_slug)
|
||||
|
||||
if not os.path.exists(f"{self.publisher_slug}/{self.game_slug}"):
|
||||
os.mkdir(f"{self.publisher_slug}/{self.game_slug}")
|
||||
|
||||
with open(f"{self.publisher_slug}/{self.game_slug}.json", "w") as f:
|
||||
json.dump({
|
||||
"name": self.name,
|
||||
"publisher": self.publisher,
|
||||
"link": self.link,
|
||||
"itch_id": self.id,
|
||||
"game_id": self.game_id,
|
||||
"itch_data": self.data,
|
||||
}, f)
|
||||
|
||||
for d in self.downloads:
|
||||
# Get UUID
|
||||
r = requests.post(f"https://api.itch.io/games/{self.game_id}/download-sessions", headers={"Authorization": token})
|
||||
j = r.json()
|
||||
print(j)
|
||||
|
||||
# Download
|
||||
url = f"https://api.itch.io/uploads/{d['id']}/download?api_key={token}&download_key_id={self.id}&uuid={j['uuid']}"
|
||||
itchio.utils.download_url(url, f"{self.publisher_slug}/{self.game_slug}/{d['filename']}", self.name +" - "+d["filename"])
|
||||
32
src/itchio/library.py
Normal file
32
src/itchio/library.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import requests
|
||||
import json
|
||||
|
||||
from itchio.game import Game
|
||||
|
||||
class Library:
|
||||
def __init__(self, login):
|
||||
self.login = login
|
||||
self.games = []
|
||||
|
||||
def load_game_page(self, page):
|
||||
print("Loading page", page)
|
||||
r = requests.get(f"https://api.itch.io/profile/owned-keys?page={page}", headers={"Authorization": self.login})
|
||||
j = json.loads(r.text)
|
||||
|
||||
|
||||
for s in j["owned_keys"]:
|
||||
self.games.append(Game(s))
|
||||
|
||||
return len(j["owned_keys"])
|
||||
|
||||
def load_games(self):
|
||||
page = 1
|
||||
while True:
|
||||
n = self.load_game_page(page)
|
||||
if n == 0:
|
||||
break
|
||||
page += 1
|
||||
|
||||
def download_library(self):
|
||||
for game in self.games:
|
||||
game.download(self.login)
|
||||
32
src/itchio/login.py
Normal file
32
src/itchio/login.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import requests
|
||||
import json
|
||||
from bs4 import BeautifulSoup as soup
|
||||
import requests
|
||||
|
||||
def LoginWeb(user, password):
|
||||
session = requests.Session()
|
||||
|
||||
# GET the page first so we have a valid CSRF token value
|
||||
login1 = session.get("https://itch.io/login")
|
||||
s = soup(login1.text, "html.parser")
|
||||
csrf_token = s.find("input", {"name": "csrf_token"})["value"]
|
||||
|
||||
# Now POST the login
|
||||
r = session.post("https://itch.io/login", {"username": user, "password":password, "csrf_token": csrf_token})
|
||||
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError
|
||||
|
||||
return session
|
||||
|
||||
def LoginAPI(user, password):
|
||||
r = requests.post("https://api.itch.io/login", {"username": user, "password":password, "source": "desktop"})
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError
|
||||
t = json.loads(r.text)
|
||||
|
||||
if not t["success"]:
|
||||
raise RuntimeError
|
||||
|
||||
return t["key"]["key"]
|
||||
|
||||
16
src/itchio/utils.py
Normal file
16
src/itchio/utils.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import urllib.request
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
class DownloadProgressBar(tqdm):
|
||||
def update_to(self, b=1, bsize=1, tsize=None):
|
||||
if tsize is not None:
|
||||
self.total = tsize
|
||||
self.update(b * bsize - self.n)
|
||||
|
||||
|
||||
def download_url(url, output_path, desc):
|
||||
with DownloadProgressBar(unit='B', unit_scale=True,
|
||||
miniters=1, desc=desc) as t:
|
||||
urllib.request.urlretrieve(url, filename=output_path, reporthook=t.update_to)
|
||||
Loading…
Reference in a new issue