|
from fastapi import FastAPI |
|
from fastapi.middleware.cors import CORSMiddleware |
|
import requests |
|
import json |
|
import logging |
|
|
|
app = FastAPI() |
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
|
|
|
app.add_middleware( |
|
CORSMiddleware, |
|
allow_origins=["*"], |
|
allow_methods=["*"], |
|
allow_headers=["*"], |
|
) |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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", []) |
|
|
|
|
|
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", {}) |
|
|
|
|
|
@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 = [] |
|
|
|
for photo in metadata: |
|
|
|
if photo.get("mediaAssetType", "").lower() == "video": |
|
best_video = None |
|
best_size = 0 |
|
|
|
derivatives = photo.get("derivatives", {}) |
|
for key, derivative in derivatives.items(): |
|
|
|
if key.lower() == "posterframe": |
|
continue |
|
|
|
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 |
|
|
|
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)} |
|
|
|
|
|
@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)} |
|
|