File size: 4,640 Bytes
a3a5495 4fc602f a3a5495 3ce94a3 a3a5495 3ce94a3 47ed581 4fc602f 47ed581 4fc602f f0efb1b 47ed581 4fc602f a3a5495 4fc602f f0efb1b 4fc602f 47ed581 4fc602f f0efb1b 4fc602f 47ed581 a3a5495 4fc602f 47ed581 4fc602f f0efb1b 4fc602f f0efb1b a3a5495 f0efb1b 4fc602f a3a5495 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 47ed581 4fc602f f0efb1b 47ed581 4e555a8 47ed581 f0efb1b a3a5495 f0efb1b 4fc602f 4e555a8 4fc602f f0efb1b 4fc602f f0efb1b a3a5495 47ed581 f0efb1b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import httpx
import json
import logging
app = FastAPI()
logging.basicConfig(level=logging.INFO)
# Enable CORS for all origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
BASE_62_MAP = {c: i for i, c in enumerate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")}
async def get_client() -> httpx.AsyncClient:
if not hasattr(app.state, "client"):
app.state.client = httpx.AsyncClient(timeout=15.0)
return app.state.client
def base62_to_int(token: str) -> int:
result = 0
for ch in token:
result = result * 62 + BASE_62_MAP[ch]
return result
async def get_base_url(token: str) -> str:
first = token[0]
if first == "A":
n = base62_to_int(token[1])
else:
n = base62_to_int(token[1:3])
return f"https://p{n:02d}-sharedstreams.icloud.com/{token}/sharedstreams/"
ICLOUD_HEADERS = {
"Origin": "https://www.icloud.com",
"Content-Type": "text/plain"
}
ICLOUD_PAYLOAD = '{"streamCtag":null}'
async def get_redirected_base_url(base_url: str, token: str) -> str:
client = await get_client()
resp = await client.post(
f"{base_url}webstream", headers=ICLOUD_HEADERS, data=ICLOUD_PAYLOAD, follow_redirects=False
)
if resp.status_code == 330:
try:
body = resp.json()
host = body.get("X-Apple-MMe-Host")
if not host:
raise ValueError("Missing X-Apple-MMe-Host in 330 response")
logging.info(f"Redirected to {host}")
return f"https://{host}/{token}/sharedstreams/"
except Exception as e:
logging.error(f"Redirect parsing failed: {e}")
raise
elif resp.status_code == 200:
return base_url
else:
resp.raise_for_status()
async def post_json(path: str, base_url: str, payload: str) -> dict:
client = await get_client()
resp = await client.post(f"{base_url}{path}", headers=ICLOUD_HEADERS, data=payload)
resp.raise_for_status()
return resp.json()
async def get_metadata(base_url: str) -> list:
data = await post_json("webstream", base_url, ICLOUD_PAYLOAD)
return data.get("photos", [])
async def get_asset_urls(base_url: str, guids: list) -> dict:
payload = json.dumps({"photoGuids": guids})
data = await post_json("webasseturls", base_url, payload)
return data.get("items", {})
@app.get("/album/{token}")
async def get_album(token: str):
try:
base_url = await get_base_url(token)
base_url = await get_redirected_base_url(base_url, token)
metadata = await get_metadata(base_url)
guids = [photo["photoGuid"] for photo in metadata]
asset_map = await get_asset_urls(base_url, guids)
videos = []
for photo in metadata:
if photo.get("mediaAssetType", "").lower() != "video":
continue
derivatives = photo.get("derivatives", {})
best = max(
(d for k, d in derivatives.items() if k.lower() != "posterframe"),
key=lambda d: int(d.get("fileSize") or 0),
default=None
)
if not best:
continue
checksum = best.get("checksum")
info = asset_map.get(checksum)
if not info:
continue
video_url = f"https://{info['url_location']}{info['url_path']}"
poster = None
pf = derivatives.get("PosterFrame")
if pf:
pf_info = asset_map.get(pf.get("checksum"))
if pf_info:
poster = f"https://{pf_info['url_location']}{pf_info['url_path']}"
videos.append({
"caption": photo.get("caption", ""),
"url": video_url,
"poster": poster or ""
})
return {"videos": videos}
except Exception as e:
logging.exception("Error in get_album")
return {"error": str(e)}
@app.get("/album/{token}/raw")
async def get_album_raw(token: str):
try:
base_url = await get_base_url(token)
base_url = await get_redirected_base_url(base_url, token)
metadata = await get_metadata(base_url)
guids = [photo["photoGuid"] for photo in metadata]
asset_map = await get_asset_urls(base_url, guids)
return {"metadata": metadata, "asset_urls": asset_map}
except Exception as e:
logging.exception("Error in get_album_raw")
return {"error": str(e)}
|