File size: 4,755 Bytes
a3a5495 4fc602f a3a5495 3ce94a3 a3a5495 3ce94a3 4e555a8 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f a3a5495 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f a3a5495 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b a3a5495 f0efb1b 4fc602f a3a5495 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4fc602f f0efb1b 4e555a8 f0efb1b a3a5495 f0efb1b 4fc602f 4e555a8 4fc602f f0efb1b 4fc602f f0efb1b a3a5495 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 |
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import httpx
import json
import logging
app = FastAPI()
logging.basicConfig(level=logging.INFO)
# Allow all CORS so browser apps can call this API
def configure_cors(app: FastAPI):
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
configure_cors(app)
# Precompute Base62 index mapping for O(1) lookup
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=10.0)
return app.state.client
# Convert base62 string to integer
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/"
# Common headers and payload for iCloud requests
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:
body = resp.json()
host = body.get("X-Apple-MMe-Host")
return f"https://{host}/{token}/sharedstreams/"
return base_url
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":
logging.info(f"Photo {photo.get('photoGuid')} is not a 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:
logging.info(f"No video derivative for photo {photo.get('photoGuid')}")
continue
checksum = best.get("checksum")
info = asset_map.get(checksum)
if not info:
logging.info(f"Missing asset for checksum {checksum}")
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
})
return {"videos": videos}
except Exception as e:
logging.error(f"Error in get_album: {e}")
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.error(f"Error in get_album_raw: {e}")
return {"error": str(e)}
|