File size: 4,659 Bytes
5cc97a0
30a787a
b7d0fd9
30a787a
 
5cc97a0
921564c
 
 
5cc97a0
921564c
 
 
 
 
 
 
5cc97a0
2b76608
 
b7d0fd9
5cc97a0
b7d0fd9
 
921564c
5cc97a0
 
 
 
b7d0fd9
 
5cc97a0
 
2b76608
 
 
b7d0fd9
 
5cc97a0
 
 
 
 
 
b7d0fd9
 
5cc97a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b76608
5cc97a0
 
2b76608
 
b7d0fd9
5cc97a0
 
 
b7d0fd9
2b76608
5cc97a0
 
b7d0fd9
2b76608
 
5cc97a0
 
9243edd
5cc97a0
 
eb8307e
 
 
 
 
5cc97a0
 
eb8307e
5cc97a0
eb8307e
 
 
 
 
 
 
 
5cc97a0
eb8307e
5cc97a0
eb8307e
 
5cc97a0
 
 
 
 
 
 
eb8307e
5cc97a0
 
 
 
 
 
 
 
 
eb8307e
 
5cc97a0
eb8307e
 
5cc97a0
 
eb8307e
5cc97a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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)}