SalexAI commited on
Commit
4a51820
·
verified ·
1 Parent(s): ef08f0b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +251 -140
app.py CHANGED
@@ -1,140 +1,251 @@
1
- from fastapi import FastAPI
2
- from fastapi.middleware.cors import CORSMiddleware
3
- import httpx
4
- import json
5
- import logging
6
-
7
- app = FastAPI()
8
- logging.basicConfig(level=logging.INFO)
9
-
10
- # Allow all CORS so browser apps can call this API
11
- def configure_cors(app: FastAPI):
12
- app.add_middleware(
13
- CORSMiddleware,
14
- allow_origins=["*"],
15
- allow_methods=["*"],
16
- allow_headers=["*"],
17
- )
18
-
19
- configure_cors(app)
20
-
21
- # Precompute Base62 index mapping for O(1) lookup
22
- BASE_62_MAP = {c: i for i, c in enumerate(
23
- "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
24
- )}
25
-
26
- async def get_client() -> httpx.AsyncClient:
27
- if not hasattr(app.state, "client"):
28
- app.state.client = httpx.AsyncClient(timeout=10.0)
29
- return app.state.client
30
-
31
- # Convert base62 string to integer
32
- def base62_to_int(token: str) -> int:
33
- result = 0
34
- for ch in token:
35
- result = result * 62 + BASE_62_MAP[ch]
36
- return result
37
-
38
- async def get_base_url(token: str) -> str:
39
- first = token[0]
40
- if first == "A":
41
- n = base62_to_int(token[1])
42
- else:
43
- n = base62_to_int(token[1:3])
44
- return f"https://p{n:02d}-sharedstreams.icloud.com/{token}/sharedstreams/"
45
-
46
- # Common headers and payload for iCloud requests
47
- ICLOUD_HEADERS = {"Origin": "https://www.icloud.com", "Content-Type": "text/plain"}
48
- ICLOUD_PAYLOAD = '{"streamCtag":null}'
49
-
50
- async def get_redirected_base_url(base_url: str, token: str) -> str:
51
- client = await get_client()
52
- resp = await client.post(
53
- f"{base_url}webstream", headers=ICLOUD_HEADERS, data=ICLOUD_PAYLOAD, follow_redirects=False
54
- )
55
- if resp.status_code == 330:
56
- body = resp.json()
57
- host = body.get("X-Apple-MMe-Host")
58
- return f"https://{host}/{token}/sharedstreams/"
59
- return base_url
60
-
61
- async def post_json(path: str, base_url: str, payload: str) -> dict:
62
- client = await get_client()
63
- resp = await client.post(
64
- f"{base_url}{path}", headers=ICLOUD_HEADERS, data=payload
65
- )
66
- resp.raise_for_status()
67
- return resp.json()
68
-
69
- async def get_metadata(base_url: str) -> list:
70
- data = await post_json("webstream", base_url, ICLOUD_PAYLOAD)
71
- return data.get("photos", [])
72
-
73
- async def get_asset_urls(base_url: str, guids: list) -> dict:
74
- payload = json.dumps({"photoGuids": guids})
75
- data = await post_json("webasseturls", base_url, payload)
76
- return data.get("items", {})
77
-
78
- @app.get("/album/{token}")
79
- async def get_album(token: str):
80
- try:
81
- base_url = await get_base_url(token)
82
- base_url = await get_redirected_base_url(base_url, token)
83
-
84
- metadata = await get_metadata(base_url)
85
- guids = [photo["photoGuid"] for photo in metadata]
86
- asset_map = await get_asset_urls(base_url, guids)
87
-
88
- videos = []
89
- for photo in metadata:
90
- if photo.get("mediaAssetType", "").lower() != "video":
91
- logging.info(f"Photo {photo.get('photoGuid')} is not a video.")
92
- continue
93
-
94
- derivatives = photo.get("derivatives", {})
95
- best = max(
96
- (d for k, d in derivatives.items() if k.lower() != "posterframe"),
97
- key=lambda d: int(d.get("fileSize") or 0),
98
- default=None
99
- )
100
- if not best:
101
- logging.info(f"No video derivative for photo {photo.get('photoGuid')}")
102
- continue
103
-
104
- checksum = best.get("checksum")
105
- info = asset_map.get(checksum)
106
- if not info:
107
- logging.info(f"Missing asset for checksum {checksum}")
108
- continue
109
- video_url = f"https://{info['url_location']}{info['url_path']}"
110
-
111
- poster = None
112
- pf = derivatives.get("PosterFrame")
113
- if pf:
114
- pf_info = asset_map.get(pf.get("checksum"))
115
- if pf_info:
116
- poster = f"https://{pf_info['url_location']}{pf_info['url_path']}"
117
-
118
- videos.append({
119
- "caption": photo.get("caption", ""),
120
- "url": video_url,
121
- "poster": poster
122
- })
123
-
124
- return {"videos": videos}
125
- except Exception as e:
126
- logging.error(f"Error in get_album: {e}")
127
- return {"error": str(e)}
128
-
129
- @app.get("/album/{token}/raw")
130
- async def get_album_raw(token: str):
131
- try:
132
- base_url = await get_base_url(token)
133
- base_url = await get_redirected_base_url(base_url, token)
134
- metadata = await get_metadata(base_url)
135
- guids = [photo["photoGuid"] for photo in metadata]
136
- asset_map = await get_asset_urls(base_url, guids)
137
- return {"metadata": metadata, "asset_urls": asset_map}
138
- except Exception as e:
139
- logging.error(f"Error in get_album_raw: {e}")
140
- return {"error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>CCP - Central Content Pool</title>
6
+ <style>
7
+ body {
8
+ background-color: #ffcc00;
9
+ font-family: "Comic Sans MS", cursive, sans-serif;
10
+ margin: 0;
11
+ padding: 0;
12
+ text-align: center;
13
+ }
14
+ header {
15
+ background-color: #003399;
16
+ color: white;
17
+ padding: 15px;
18
+ font-size: 2em;
19
+ border-bottom: 4px double #fff;
20
+ }
21
+ .logo {
22
+ font-size: 50px;
23
+ font-weight: bold;
24
+ color: #ff0000;
25
+ text-shadow: 3px 3px 0 #000;
26
+ margin-top: 20px;
27
+ }
28
+ .subtext {
29
+ font-size: 24px;
30
+ color: black;
31
+ margin-top: -10px;
32
+ }
33
+ .fineprint {
34
+ font-size: 12px;
35
+ color: gray;
36
+ margin-top: -5px;
37
+ font-style: italic;
38
+ }
39
+ .container {
40
+ margin: 20px auto;
41
+ width: 90%;
42
+ max-width: 1200px;
43
+ }
44
+ .search-container {
45
+ margin: 20px auto;
46
+ width: 100%;
47
+ display: flex;
48
+ justify-content: center;
49
+ }
50
+ #search-input {
51
+ width: 80%;
52
+ padding: 10px;
53
+ font-size: 18px;
54
+ border: 3px solid #000;
55
+ outline: none;
56
+ background: #ffffff;
57
+ transition: 0.3s ease-in-out;
58
+ }
59
+ #search-input:focus {
60
+ background: #ffff99;
61
+ }
62
+ .videos {
63
+ display: flex;
64
+ flex-wrap: wrap;
65
+ gap: 15px;
66
+ justify-content: center;
67
+ }
68
+ .video-card {
69
+ background-color: #fff;
70
+ border: 3px solid black;
71
+ padding: 10px;
72
+ width: 320px;
73
+ cursor: pointer;
74
+ transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
75
+ animation: fadeIn 0.5s ease-in;
76
+ }
77
+ .video-card:hover {
78
+ transform: scale(1.05);
79
+ box-shadow: 5px 5px 0 #000;
80
+ }
81
+ .video-card img {
82
+ width: 100%;
83
+ height: auto;
84
+ aspect-ratio: 16/9;
85
+ object-fit: cover;
86
+ border: 4px solid black;
87
+ image-rendering: pixelated;
88
+ }
89
+ .video-card h2 {
90
+ margin: 10px 0 0 0;
91
+ font-size: 20px;
92
+ color: #003399;
93
+ }
94
+ .modal-overlay {
95
+ position: fixed;
96
+ top: 0;
97
+ left: 0;
98
+ right: 0;
99
+ bottom: 0;
100
+ background: rgba(0,0,0,0.5);
101
+ display: none;
102
+ align-items: center;
103
+ justify-content: center;
104
+ animation: fadeIn 0.5s;
105
+ }
106
+ .modal-window {
107
+ background-color: #c0c0c0;
108
+ border: 2px solid #000;
109
+ width: 80%;
110
+ max-width: 800px;
111
+ box-shadow: 5px 5px 0 #000;
112
+ animation: slideDown 0.5s ease-out;
113
+ }
114
+ .modal-header {
115
+ background: #000080;
116
+ color: white;
117
+ padding: 5px;
118
+ display: flex;
119
+ justify-content: space-between;
120
+ align-items: center;
121
+ }
122
+ .modal-header button {
123
+ background: #c0c0c0;
124
+ border: 2px outset #fff;
125
+ cursor: pointer;
126
+ margin-left: 5px;
127
+ }
128
+ .modal-content {
129
+ padding: 10px;
130
+ background: #fff;
131
+ }
132
+ .modal-content video {
133
+ width: 100%;
134
+ height: auto;
135
+ max-height: 80vh;
136
+ object-fit: contain;
137
+ display: block;
138
+ }
139
+ @keyframes fadeIn {
140
+ from { opacity: 0; }
141
+ to { opacity: 1; }
142
+ }
143
+ @keyframes slideDown {
144
+ from { transform: translateY(-20px); opacity: 0; }
145
+ to { transform: translateY(0); opacity: 1; }
146
+ }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <header>Welcome to CCP</header>
151
+ <div class="logo">CCP</div>
152
+ <div class="subtext">(Central Content Pool)</div>
153
+ <div class="fineprint">(not the China guys, they copied us...)</div>
154
+
155
+ <div class="container">
156
+ <div class="search-container">
157
+ <input type="text" id="search-input" placeholder="Search for videos...">
158
+ </div>
159
+
160
+ <div class="videos" id="video-list"></div>
161
+ </div>
162
+
163
+ <div class="modal-overlay" id="modal-overlay">
164
+ <div class="modal-window">
165
+ <div class="modal-header">
166
+ <span id="modal-title">Video Player</span>
167
+ <div>
168
+ <button id="fullscreen-btn">Full Screen</button>
169
+ <button id="close-btn">Close</button>
170
+ </div>
171
+ </div>
172
+ <div class="modal-content">
173
+ <video id="modal-video" controls>
174
+ <source id="video-source" src="" type="video/mp4">
175
+ Your browser does not support the video tag.
176
+ </video>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <script>
182
+ let allVideos = [];
183
+ const videoList = document.getElementById('video-list');
184
+ const searchInput = document.getElementById('search-input');
185
+
186
+ async function fetchVideos() {
187
+ try {
188
+ const response = await fetch('https://salexai-funserver.hf.space/album/B0p532ODWH4KDMb');
189
+ const data = await response.json();
190
+ allVideos = data.videos;
191
+ renderVideos(allVideos);
192
+ } catch (error) {
193
+ console.error('Failed to fetch videos:', error);
194
+ }
195
+ }
196
+
197
+ async function renderVideos(videoArray) {
198
+ videoList.innerHTML = '';
199
+ for (const vid of videoArray) {
200
+ const title = vid.caption && vid.caption.trim() ? vid.caption.trim() : 'Unnamed Video';
201
+ const thumb = vid.poster && vid.poster.trim() ? vid.poster : 'https://via.placeholder.com/320x180.png?text=No+Preview';
202
+
203
+ const card = document.createElement('div');
204
+ card.className = 'video-card';
205
+ card.innerHTML = `
206
+ <img src="${thumb}" alt="Thumbnail">
207
+ <h2>${title}</h2>
208
+ `;
209
+ card.addEventListener('click', () => openModal(title, vid.url));
210
+ videoList.appendChild(card);
211
+ }
212
+ }
213
+
214
+ searchInput.addEventListener('input', (e) => {
215
+ const searchVal = e.target.value.toLowerCase();
216
+ const filtered = allVideos.filter(v => {
217
+ const cap = v.caption && v.caption.trim() ? v.caption.trim().toLowerCase() : 'unnamed video';
218
+ return cap.includes(searchVal);
219
+ });
220
+ renderVideos(filtered);
221
+ });
222
+
223
+ function openModal(title, url) {
224
+ document.getElementById('modal-title').textContent = title;
225
+ document.getElementById('video-source').src = url;
226
+ const modalVideo = document.getElementById('modal-video');
227
+ modalVideo.load();
228
+ document.getElementById('modal-overlay').style.display = 'flex';
229
+ }
230
+
231
+ document.getElementById('close-btn').addEventListener('click', () => {
232
+ const modalVideo = document.getElementById('modal-video');
233
+ modalVideo.pause();
234
+ document.getElementById('modal-overlay').style.display = 'none';
235
+ });
236
+
237
+ document.getElementById('fullscreen-btn').addEventListener('click', () => {
238
+ const modalVideo = document.getElementById('modal-video');
239
+ if (modalVideo.requestFullscreen) {
240
+ modalVideo.requestFullscreen();
241
+ } else if (modalVideo.webkitRequestFullscreen) {
242
+ modalVideo.webkitRequestFullscreen();
243
+ } else if (modalVideo.msRequestFullscreen) {
244
+ modalVideo.msRequestFullscreen();
245
+ }
246
+ });
247
+
248
+ fetchVideos();
249
+ </script>
250
+ </body>
251
+ </html>