File size: 4,616 Bytes
a3a5495
 
4fc602f
a3a5495
3ce94a3
a3a5495
 
3ce94a3
 
4e555a8
4fc602f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3a5495
4fc602f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3a5495
 
4fc602f
 
 
 
 
 
 
 
 
 
 
 
 
a3a5495
4fc602f
a3a5495
4fc602f
 
a3a5495
4fc602f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e555a8
4fc602f
 
a3a5495
4fc602f
 
 
4e555a8
4fc602f
 
 
 
 
 
a3a5495
4fc602f
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
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 to int using dict lookup
def base62_to_int(token: str) -> int:
    result = 0
    for ch in token:
        result = result * 62 + BASE_62_MAP[ch]
    return result

# Get base URL from token
async def get_base_url(token: str) -> str:
    first = token[0]
    n = base62_to_int(token[1]) if first == 'A' else base62_to_int(token[1:3])
    return f"https://p{n:02d}-sharedstreams.icloud.com/{token}/sharedstreams/"

# Static request details
ICLOUD_HEADERS = {'Origin': 'https://www.icloud.com', 'Content-Type': 'text/plain'}
ICLOUD_PAYLOAD = '{"streamCtag":null}'

# Handle possible 330 redirect
async def get_redirected_base_url(base_url: str, token: str) -> str:
    client = await get_client()
    resp = await client.post(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

# Generic function to POST and parse JSON
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()

# Fetch metadata
async def get_metadata(base_url: str) -> list:
    return (await post_json('webstream', base_url, ICLOUD_PAYLOAD)).get('photos', [])

# Fetch asset URL mappings
async def get_asset_urls(base_url: str, guids: list) -> dict:
    data = json.dumps({"photoGuids": guids})
    return (await post_json('webasseturls', base_url, data)).get('items', {})

# Main endpoint\@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 = [p['photoGuid'] for p in metadata]
        asset_map = await get_asset_urls(base_url, guids)

        videos = []
        for p in metadata:
            if p.get('mediaAssetType', '').lower() != 'video':
                continue

            # select the largest derivative (exclude poster)
            derivatives = p.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')
            if checksum not in asset_map:
                logging.info(f"Missing asset for checksum {checksum}")
                continue

            info = asset_map[checksum]
            video_url = f"https://{info['url_location']}{info['url_path']}"

            poster = None
            pf = derivatives.get('PosterFrame')
            if pf and pf.get('checksum') in asset_map:
                pi = asset_map[pf['checksum']]
                poster = f"https://{pi['url_location']}{pi['url_path']}"

            videos.append({
                'caption': p.get('caption', ''),
                'url': video_url,
                'poster': poster
            })

        return {'videos': videos}

    except Exception as e:
        logging.error(f"Error fetching album {token}: {e}")
        return {'error': str(e)}

# Debug endpoint
@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 = [p['photoGuid'] for p in metadata]
        asset_map = await get_asset_urls(base_url, guids)
        return {'metadata': metadata, 'asset_urls': asset_map}
    except Exception as e:
        return {'error': str(e)}