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)}
|