File size: 5,225 Bytes
a3a5495 3ce94a3 a3a5495 c4819ba 3ce94a3 4e555a8 a3a5495 4e555a8 a3a5495 4e555a8 a3a5495 c4819ba 4e555a8 a3a5495 c4819ba 4e555a8 a3a5495 c4819ba 4e555a8 a3a5495 c4819ba 4e555a8 a3a5495 c4819ba a3a5495 4e555a8 a3a5495 c4819ba a3a5495 c4819ba 4e555a8 3ce94a3 4e555a8 c4819ba 4e555a8 c4819ba 3ce94a3 c4819ba 3ce94a3 c4819ba 3ce94a3 a3a5495 4e555a8 a3a5495 c4819ba 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 |
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import requests
import json
import logging
app = FastAPI()
# Set up logging for debugging
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 from the token
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 from iCloud
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 (the "webstream" data)
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 mapping for a list of photoGuids
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 video URLs
@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 = []
# Process each photo in the album
for photo in metadata:
# We check if the overall photo is a video
if photo.get("mediaAssetType", "").lower() == "video":
best_video = None
best_size = 0
# Look at each derivative
derivatives = photo.get("derivatives", {})
for key, derivative in derivatives.items():
# Skip the poster frame (which is clearly an image)
if key.lower() == "posterframe":
continue
# Try to read the fileSize as integer
try:
file_size = int(derivative.get("fileSize", 0))
except (ValueError, TypeError):
file_size = 0
# Choose the derivative with the highest file size
if file_size > best_size:
best_size = file_size
best_video = derivative
# If we found a candidate and its checksum exists in asset_urls, build the video URL
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')}")
else:
logging.info(f"Photo {photo.get('photoGuid')} is not a video. mediaAssetType: {photo.get('mediaAssetType')}")
return {"videos": video_list}
except Exception as e:
return {"error": str(e)}
# Debug endpoint to return raw metadata for further inspection
@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)}
|