SalexAI commited on
Commit
30a787a
·
verified ·
1 Parent(s): 75a3f76

Update app.py

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