infra/stacks/servarr/mam-farming/files/bp-spender.py

164 lines
5.1 KiB
Python
Raw Normal View History

"""
MAM bonus-point spender tier-aware, pay-what-we-owe.
MAM's bonusBuy.php API enforces a hard 50 GiB minimum per purchase
("Automated spenders are limited to buying at least 50 GB... due to log
spam"). Valid API tiers are 50, 100, 200, 500 GiB (@ 500 BP/GiB). That
means the "pay exactly what we owe" approach from the recovery plan
rounds UP to 50 GiB for the first purchase small buys can only be done
via the web UI, not the API.
Logic: pick the smallest valid tier that both (a) satisfies the ratio
deficit and (b) we can afford without burning the BP reserve. Skip if
nothing fits; the cron will retry in 6 h once BP grows.
"""
import math
import os
import sys
import tempfile
import time
import requests
PUSHGW = "http://prometheus-prometheus-pushgateway.monitoring:9091"
COOKIE_FILE = "/data/mam_id"
TARGET_RATIO = float(os.environ.get("TARGET_RATIO", "2.0"))
RESERVE_BP = int(os.environ.get("RESERVE_BP", "500"))
BP_PER_GB = int(os.environ.get("BP_PER_GB", "500"))
# MAM-enforced minimum purchase for API callers: 50 GiB.
API_TIERS_GIB = (50, 100, 200, 500)
CLASS_CODES = {
"Mouse": 0,
"Vole": 1,
"User": 2,
"Power User": 3,
"Elite": 4,
"Torrent Master": 5,
"Power TM": 6,
"Elite TM": 7,
"VIP": 8,
}
def save_cookie(resp):
for c in resp.cookies:
if c.name == "mam_id":
fd, tmp = tempfile.mkstemp(dir="/data")
os.write(fd, c.value.encode())
os.close(fd)
os.rename(tmp, COOKIE_FILE)
return
def push(metrics):
try:
requests.post(
f"{PUSHGW}/metrics/job/mam-bp-spender", data=metrics, timeout=10
)
except Exception as e:
print(f"pushgateway error: {e}", file=sys.stderr)
def load_cookie():
if os.path.exists(COOKIE_FILE):
return open(COOKIE_FILE).read().strip()
return os.environ.get("MAM_ID", "")
def main():
mam_id = load_cookie()
if not mam_id:
print("No mam_id available", file=sys.stderr)
sys.exit(1)
s = requests.Session()
s.cookies.set("mam_id", mam_id, domain=".myanonamouse.net")
r = s.get("https://www.myanonamouse.net/jsonLoad.php", timeout=15)
if r.status_code != 200:
push("mam_farming_cookie_expired 1\n")
print(f"Cookie expired: {r.status_code}", file=sys.stderr)
sys.exit(1)
save_cookie(r)
profile = r.json()
ratio = float(profile.get("ratio", 0) or 0)
classname = profile.get("classname", "Mouse")
class_code = CLASS_CODES.get(classname, 0)
# MAM returns `downloaded`/`uploaded` as pretty strings ("715.55 MiB");
# `*_bytes` are the authoritative integer fields.
downloaded = int(profile.get("downloaded_bytes", 0) or 0)
uploaded = int(profile.get("uploaded_bytes", 0) or 0)
bp = int(float(profile.get("seedbonus", 0) or 0))
deficit_bytes = max(0, int(downloaded * TARGET_RATIO) - uploaded)
needed_gib = math.ceil(deficit_bytes / (1024**3)) + 1 if deficit_bytes > 0 else 0
affordable_gib = max(0, (bp - RESERVE_BP) // BP_PER_GB)
# Pick the smallest API tier that satisfies the deficit AND fits the
# budget. If even the smallest tier is too expensive, skip — the cron
# will retry in 6 h once BP has grown.
buy_gib = 0
for tier in API_TIERS_GIB:
if tier >= needed_gib and tier <= affordable_gib:
buy_gib = tier
break
if buy_gib == 0 and needed_gib > 0 and affordable_gib >= API_TIERS_GIB[0]:
# Deficit exceeds all tiers we can afford — buy the largest
# tier that fits to make progress.
for tier in reversed(API_TIERS_GIB):
if tier <= affordable_gib:
buy_gib = tier
break
print(
f"Profile: ratio={ratio} class={classname} "
f"DL={downloaded / 1024**3:.2f} GiB UL={uploaded / 1024**3:.2f} GiB "
f"BP={bp} | deficit={deficit_bytes / 1024**3:.2f} GiB "
f"needed={needed_gib} affordable={affordable_gib} buy={buy_gib}"
)
spent_gib = 0
if buy_gib >= API_TIERS_GIB[0]:
time.sleep(3)
url = (
"https://www.myanonamouse.net/json/bonusBuy.php"
f"?spendtype=upload&amount={buy_gib}"
)
r2 = s.get(url, timeout=15)
save_cookie(r2)
try:
body = r2.json()
except ValueError:
body = {}
ok = r2.status_code == 200 and body.get("success") is True
print(
f"Buy {buy_gib} GiB -> {r2.status_code} "
f"success={body.get('success')} {r2.text[:160]}"
)
if ok:
spent_gib = buy_gib
metrics = (
"mam_farming_cookie_expired 0\n"
f"mam_ratio {ratio}\n"
f'mam_class_code{{classname="{classname}"}} {class_code}\n'
f"mam_downloaded_bytes {downloaded}\n"
f"mam_uploaded_bytes {uploaded}\n"
f"mam_bp_balance {bp}\n"
f"mam_bp_spent_gb {spent_gib}\n"
f"mam_bp_needed_gib {needed_gib}\n"
f"mam_bp_affordable_gib {affordable_gib}\n"
)
push(metrics)
print(
f"Done: BP={bp}, spent={spent_gib} GiB (needed={needed_gib}, "
f"affordable={affordable_gib})"
)
if __name__ == "__main__":
main()