File size: 6,304 Bytes
a3a5495
 
 
 
3ce94a3
a3a5495
 
 
3ce94a3
 
 
4e555a8
a3a5495
 
 
 
 
 
 
4e555a8
a3a5495
 
 
4e555a8
 
a3a5495
 
4e555a8
 
 
 
 
a3a5495
 
4e555a8
 
a3a5495
 
 
 
 
 
 
 
 
 
 
 
 
4e555a8
 
a3a5495
 
 
 
 
 
 
 
 
4e555a8
 
a3a5495
 
 
 
 
 
 
 
 
3ce94a3
a3a5495
 
 
 
 
 
4e555a8
 
a3a5495
 
4e555a8
a3a5495
4e555a8
 
3ce94a3
 
 
 
 
 
 
 
 
6c3bd2c
3ce94a3
4e555a8
 
3ce94a3
4e555a8
 
 
 
3ce94a3
 
 
 
 
 
 
 
 
 
 
 
 
6c3bd2c
 
3ce94a3
 
6c3bd2c
 
3ce94a3
6c3bd2c
 
 
 
4b36c6f
3ce94a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3a5495
4e555a8
 
a3a5495
4e555a8
 
 
 
 
 
 
 
 
 
a3a5495
 
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 requests
import json
import logging

app = FastAPI()

# Set up basic logging
logging.basicConfig(level=logging.INFO)

# Allow all CORS so browser apps can call this API
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Base62 conversion helper
BASE_62_CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def base62_to_int(e):
    t = 0
    for char in e:
        t = t * 62 + BASE_62_CHAR_SET.index(char)
    return t

# Construct the base URL for the shared album
def get_base_url(token: str) -> str:
    first_char = token[0]
    n = base62_to_int(token[1]) if first_char == "A" else base62_to_int(token[1:3])
    base_url = f"https://p{n:02d}-sharedstreams.icloud.com/{token}/sharedstreams/"
    return base_url

# Handle 330 redirect by calling the webstream endpoint with allow_redirects=False
def get_redirected_base_url(base_url: str, token: str) -> str:
    url = base_url + "webstream"
    headers = {
        'Origin': 'https://www.icloud.com',
        'Content-Type': 'text/plain',
    }
    data = '{"streamCtag":null}'
    response = requests.post(url, headers=headers, data=data, allow_redirects=False)
    if response.status_code == 330:
        body = response.json()
        new_base_url = f"https://{body['X-Apple-MMe-Host']}/{token}/sharedstreams/"
        return new_base_url
    return base_url

# Fetch metadata from the webstream endpoint
def get_metadata(base_url: str):
    url = base_url + "webstream"
    headers = {
        'Origin': 'https://www.icloud.com',
        'Content-Type': 'text/plain',
    }
    data = '{"streamCtag":null}'
    response = requests.post(url, headers=headers, data=data)
    return response.json().get("photos", [])

# Fetch asset URLs for a list of photoGuids from the webasseturls endpoint
def get_asset_urls(base_url: str, guids: list):
    url = base_url + "webasseturls"
    headers = {
        'Origin': 'https://www.icloud.com',
        'Content-Type': 'text/plain',
    }
    data = json.dumps({"photoGuids": guids})
    response = requests.post(url, headers=headers, data=data)
    return response.json().get("items", {})

# Main endpoint to extract videos with extensive fallback checks
@app.get("/album/{token}")
def get_album(token: str):
    try:
        base_url = get_base_url(token)
        base_url = get_redirected_base_url(base_url, token)
        metadata = get_metadata(base_url)
        guids = [photo["photoGuid"] for photo in metadata]
        asset_urls = get_asset_urls(base_url, guids)

        video_list = []
        # Loop over each photo in the album
        for photo in metadata:
            best_video = None
            best_size = 0

            # Check derivatives first
            derivatives = photo.get("derivatives", {})
            for key, derivative in derivatives.items():
                # Log the derivative keys for debugging
                logging.info(f"Photo {photo.get('photoGuid')} derivative key: {key} | content: {list(derivative.keys())}")

                # Standard checks
                if (derivative.get("mediaAssetType") == "video" or
                    derivative.get("type", "").startswith("video") or
                    derivative.get("filename", "").lower().endswith(".mp4")):
                    try:
                        file_size = int(derivative.get("fileSize", 0))
                    except (ValueError, TypeError):
                        file_size = 0
                    if file_size > best_size:
                        best_size = file_size
                        best_video = derivative
                else:
                    # Fallback: Check if any value in the derivative JSON contains "mp4"
                    derivative_str = json.dumps(derivative).lower()
                    if "mp4" in derivative_str:
                        try:
                            file_size = int(derivative.get("fileSize", 0))
                        except (ValueError, TypeError):
                            file_size = 0
                        if file_size > best_size:
                            best_size = file_size
                            best_video = derivative

            # Fallback to 'movies' field if present
            if not best_video and "movies" in photo:
                for movie in photo["movies"]:
                    logging.info(f"Photo {photo.get('photoGuid')} movie entry keys: {list(movie.keys())}")
                    if movie.get("filename", "").lower().endswith(".mp4"):
                        try:
                            file_size = int(movie.get("fileSize", 0))
                        except (ValueError, TypeError):
                            file_size = 0
                        if file_size > best_size:
                            best_size = file_size
                            best_video = movie

            # Build the video URL if found
            if best_video:
                checksum = best_video.get("checksum")
                if checksum in asset_urls:
                    url_info = asset_urls[checksum]
                    video_url = f"https://{url_info['url_location']}{url_info['url_path']}"
                    video_list.append({
                        "caption": photo.get("caption", ""),
                        "url": video_url
                    })
                else:
                    logging.info(f"Checksum {checksum} not found in asset URLs for photo {photo.get('photoGuid')}")
            else:
                logging.info(f"No video derivative found for photo {photo.get('photoGuid')}")
        
        return {"videos": video_list}
    except Exception as e:
        return {"error": str(e)}

# Debug endpoint to return raw metadata from iCloud
@app.get("/album/{token}/raw")
def get_album_raw(token: str):
    try:
        base_url = get_base_url(token)
        base_url = get_redirected_base_url(base_url, token)
        metadata = get_metadata(base_url)
        guids = [photo["photoGuid"] for photo in metadata]
        asset_urls = get_asset_urls(base_url, guids)
        return {"metadata": metadata, "asset_urls": asset_urls}
    except Exception as e:
        return {"error": str(e)}