amalsp commited on
Commit
f323a9e
·
verified ·
1 Parent(s): 82f5e79

Update index.html to POST to Gradio backend API with progress/error handling

Browse files
Files changed (1) hide show
  1. index.html +104 -83
index.html CHANGED
@@ -24,6 +24,7 @@
24
  .copy:hover{ background:#e5e7eb; }
25
  .download-btn{ width:100%; padding:14px; background:linear-gradient(135deg,#667eea 0%, #764ba2 100%); color:#fff; border:none; border-radius:10px; font-size:18px; font-weight:600; cursor:pointer; transition: all .25s ease; box-shadow:0 4px 15px rgba(102,126,234,.4); display:inline-flex; align-items:center; justify-content:center; gap:10px; }
26
  .download-btn:hover{ transform: translateY(-2px); box-shadow:0 6px 20px rgba(102,126,234,.6); }
 
27
  .status { margin-top:12px; min-height:22px; font-size:14px; }
28
  .status .info{ color:#555; }
29
  .status .error{ color:#b00020; }
@@ -55,7 +56,6 @@
55
  </div>
56
  <h1><span class="gradient-text">Instagram Downloader</span></h1>
57
  <p class="instructions">Paste any public Instagram image or video link below and click Download.</p>
58
-
59
  <form id="downloadForm">
60
  <div class="input-group">
61
  <label for="linkInput">Instagram Link</label>
@@ -67,10 +67,8 @@
67
  <button type="submit" class="download-btn" id="downloadBtn">⬇️ Download</button>
68
  <div class="progress" id="progress"><span id="bar"></span></div>
69
  <div class="status" id="status" aria-live="polite"></div>
70
- <div class="note" id="warning" style="display:none;"></div>
71
- <div class="preview" id="preview"></div>
72
  </form>
73
-
74
  <div class="features">
75
  <ul class="feature-list">
76
  <li>Download Instagram photos in high quality</li>
@@ -80,114 +78,137 @@
80
  </ul>
81
  </div>
82
  </div>
83
-
84
  <script>
85
  const form = document.getElementById('downloadForm');
86
  const input = document.getElementById('linkInput');
87
  const statusEl = document.getElementById('status');
88
  const progress = document.getElementById('progress');
89
  const bar = document.getElementById('bar');
90
- const preview = document.getElementById('preview');
91
  const downloadBtn = document.getElementById('downloadBtn');
92
  const pasteBtn = document.getElementById('pasteBtn');
93
  const warning = document.getElementById('warning');
94
 
95
- const sleep = (ms) => new Promise(r => setTimeout(r, ms));
96
- function setStatus(type, msg){ statusEl.innerHTML = msg ? `<span class="${type}">${msg}</span>` : ''; }
97
- function showWarning(msg){ warning.style.display = 'block'; warning.innerHTML = msg; }
98
- function resetUI(){ setStatus('info',''); bar.style.width = '0%'; progress.style.display = 'none'; preview.style.display = 'none'; preview.innerHTML = ''; warning.style.display = 'none'; warning.innerHTML = ''; }
99
- function startLoading(){ progress.style.display = 'block'; setStatus('info','Resolving media...'); let w = 10; bar.style.width = w + '%'; const id = setInterval(()=>{ w = Math.min(90, w + 5); bar.style.width = w + '%'; }, 400); return () => clearInterval(id); }
100
- function sanitizeUrl(u){ try{ const url = new URL(u.trim()); if(!url.hostname.replace(/^www\./,'').endsWith('instagram.com')) throw new Error('Not an Instagram link'); return url.toString(); } catch{ throw new Error('Please enter a valid Instagram URL'); } }
101
 
102
- // NOTE: Direct client-side fetch to Instagram is blocked by CORS and auth.
103
- // This demo previously attempted to use noembed.com which is unreliable for Instagram.
104
- // We now short-circuit and show clear guidance instead of failing with "Failed to fetch".
 
105
 
106
- const BACKEND_DOC_URL = 'https://github.com/huggingface/hub-examples/tree/main/spaces-examples/backend-proxy';
107
- const SAMPLE_PROXY = 'https://r.jina.ai/http://example.com'; // placeholder, explain below
 
 
 
 
 
108
 
109
- async function tryLegacyNoEmbed(instaUrl){
110
- const endpoint = `https://noembed.com/embed?url=${encodeURIComponent(instaUrl)}`;
111
- const res = await fetch(endpoint, {mode:'cors', credentials:'omit'});
112
- if(!res.ok) throw new Error(`Network error: ${res.status}`);
113
- const data = await res.json();
114
- if(data.url) return { direct: data.url, type: 'image', title: data.title || 'instagram-media' };
115
- if(data.thumbnail_url) return { direct: data.thumbnail_url, type: 'image', title: data.title || 'instagram-image' };
116
- if(data.html){
117
- const m = data.html.match(/<video[^>]+src="([^"]+)"/i);
118
- if(m) return { direct: m[1], type: 'video', title: data.title || 'instagram-video' };
119
- }
120
- throw new Error('Unable to resolve media from the link');
121
  }
122
 
123
- function showPreview(res){
124
- const html = res.type === 'video'
125
- ? `<video controls src="${res.direct}" style="max-width:100%; border-radius:8px;"></video>`
126
- : `<img alt="preview" src="${res.direct}" style="max-width:100%; border-radius:8px;"/>`;
127
- preview.innerHTML = html; preview.style.display = 'block';
 
 
 
 
128
  }
129
 
130
  pasteBtn?.addEventListener('click', async () => {
131
- try { const text = await navigator.clipboard.readText(); if(text) input.value = text.trim(); }
132
- catch { setStatus('error','Clipboard blocked by browser'); }
 
 
 
 
133
  });
134
 
135
  form.addEventListener('submit', async (e)=>{
136
- e.preventDefault(); resetUI();
137
- let stopTicker;
 
138
  try {
139
  const url = sanitizeUrl(input.value);
140
- downloadBtn.disabled = true; stopTicker = startLoading();
141
-
142
- // Always inform about limitations first
143
- showWarning(
144
- `This Space cannot directly download from Instagram. Due to CORS and Instagram's `+
145
- `security/auth protections, client-side fetches will fail. `+
146
- `For a production downloader, add a backend/proxy that fetches media server-side, `+
147
- `then returns a direct URL/blob to the browser. <br/><br/>`+
148
- `Suggested approach: build a tiny proxy API (Cloudflare Worker, Vercel/Netlify `+
149
- `serverless, or Spaces CPU/GPU with FastAPI/Node) that accepts an Instagram URL, `+
150
- `retrieves the media using authenticated or headless scraping as needed, and sends `+
151
- `back a downloadable link with proper CORS headers. <br/><br/>`+
152
- `See example backend-proxy patterns: <a href="${BACKEND_DOC_URL}" target="_blank" rel="noopener">Backend proxy examples</a>.`
153
- );
154
-
155
- // Try legacy fallback (noembed) only to provide a preview when it works.
156
- // This may fail frequently; we catch and show a friendly message.
157
- let resolved;
158
- try { resolved = await tryLegacyNoEmbed(url); } catch (err) {
159
- throw new Error('Failed to fetch media client-side. Use a backend/proxy API.');
160
  }
161
-
162
- stopTicker(); bar.style.width = '92%';
163
- showPreview(resolved);
164
-
165
- // Attempt to download; if CORS blocks, provide guidance instead of a hard failure.
166
- try {
167
- const head = await fetch(resolved.direct, { method: 'HEAD' });
168
- const disp = head.headers.get('content-disposition');
169
- let name = 'instagram-media';
170
- if (disp) {
171
- const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(disp);
172
- name = decodeURIComponent(m?.[1] || m?.[2] || name);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
- const res = await fetch(resolved.direct);
175
- if (!res.ok) throw new Error('Download blocked');
176
- const blob = await res.blob();
177
- const a = document.createElement('a');
178
- a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); a.remove();
179
- await sleep(200); URL.revokeObjectURL(a.href);
180
- bar.style.width = '100%'; setStatus('success','Download complete.');
181
- } catch {
182
- setStatus('error','Browser blocked direct download due to CORS. Use a backend/proxy.');
183
  }
184
  } catch (err){
185
- stopTicker && stopTicker();
186
  setStatus('error', err.message || 'Something went wrong');
187
  showWarning(
188
- `We cannot fetch private posts or any media directly from Instagram in the browser. `+
189
- `Please add a backend or proxy API to handle fetching with proper headers and cookies. `+
190
- `Example patterns: <a href="${BACKEND_DOC_URL}" target="_blank" rel="noopener">backend proxy</a>.`
191
  );
192
  } finally {
193
  downloadBtn.disabled = false;
 
24
  .copy:hover{ background:#e5e7eb; }
25
  .download-btn{ width:100%; padding:14px; background:linear-gradient(135deg,#667eea 0%, #764ba2 100%); color:#fff; border:none; border-radius:10px; font-size:18px; font-weight:600; cursor:pointer; transition: all .25s ease; box-shadow:0 4px 15px rgba(102,126,234,.4); display:inline-flex; align-items:center; justify-content:center; gap:10px; }
26
  .download-btn:hover{ transform: translateY(-2px); box-shadow:0 6px 20px rgba(102,126,234,.6); }
27
+ .download-btn:disabled{ opacity:0.6; cursor:not-allowed; }
28
  .status { margin-top:12px; min-height:22px; font-size:14px; }
29
  .status .info{ color:#555; }
30
  .status .error{ color:#b00020; }
 
56
  </div>
57
  <h1><span class="gradient-text">Instagram Downloader</span></h1>
58
  <p class="instructions">Paste any public Instagram image or video link below and click Download.</p>
 
59
  <form id="downloadForm">
60
  <div class="input-group">
61
  <label for="linkInput">Instagram Link</label>
 
67
  <button type="submit" class="download-btn" id="downloadBtn">⬇️ Download</button>
68
  <div class="progress" id="progress"><span id="bar"></span></div>
69
  <div class="status" id="status" aria-live="polite"></div>
70
+ <div class="note" id="warning" style="display:none"></div>
 
71
  </form>
 
72
  <div class="features">
73
  <ul class="feature-list">
74
  <li>Download Instagram photos in high quality</li>
 
78
  </ul>
79
  </div>
80
  </div>
 
81
  <script>
82
  const form = document.getElementById('downloadForm');
83
  const input = document.getElementById('linkInput');
84
  const statusEl = document.getElementById('status');
85
  const progress = document.getElementById('progress');
86
  const bar = document.getElementById('bar');
 
87
  const downloadBtn = document.getElementById('downloadBtn');
88
  const pasteBtn = document.getElementById('pasteBtn');
89
  const warning = document.getElementById('warning');
90
 
91
+ // Backend API endpoint (Gradio)
92
+ const API_ENDPOINT = '/api/predict';
93
+
94
+ function setStatus(type, msg){
95
+ statusEl.innerHTML = msg ? `<span class="${type}">${msg}</span>` : '';
96
+ }
97
 
98
+ function showWarning(msg){
99
+ warning.style.display = 'block';
100
+ warning.innerHTML = msg;
101
+ }
102
 
103
+ function resetUI(){
104
+ setStatus('info','');
105
+ bar.style.width = '0%';
106
+ progress.style.display = 'none';
107
+ warning.style.display = 'none';
108
+ warning.innerHTML = '';
109
+ }
110
 
111
+ function startLoading(){
112
+ progress.style.display = 'block';
113
+ setStatus('info','Processing...');
114
+ bar.style.width = '20%';
 
 
 
 
 
 
 
 
115
  }
116
 
117
+ function sanitizeUrl(u){
118
+ try{
119
+ const url = new URL(u.trim());
120
+ if(!url.hostname.replace(/^www\./,'').endsWith('instagram.com'))
121
+ throw new Error('Not an Instagram link');
122
+ return url.toString();
123
+ } catch{
124
+ throw new Error('Please enter a valid Instagram URL');
125
+ }
126
  }
127
 
128
  pasteBtn?.addEventListener('click', async () => {
129
+ try {
130
+ const text = await navigator.clipboard.readText();
131
+ if(text) input.value = text.trim();
132
+ } catch {
133
+ setStatus('error','Clipboard blocked by browser');
134
+ }
135
  });
136
 
137
  form.addEventListener('submit', async (e)=>{
138
+ e.preventDefault();
139
+ resetUI();
140
+
141
  try {
142
  const url = sanitizeUrl(input.value);
143
+ downloadBtn.disabled = true;
144
+ startLoading();
145
+
146
+ setStatus('info','Sending request to backend...');
147
+ bar.style.width = '30%';
148
+
149
+ // POST to Gradio API endpoint
150
+ const response = await fetch(API_ENDPOINT, {
151
+ method: 'POST',
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ },
155
+ body: JSON.stringify({
156
+ data: [url]
157
+ })
158
+ });
159
+
160
+ if (!response.ok) {
161
+ throw new Error(`Server error: ${response.status}`);
 
162
  }
163
+
164
+ bar.style.width = '60%';
165
+ setStatus('info','Receiving response...');
166
+
167
+ const result = await response.json();
168
+
169
+ bar.style.width = '80%';
170
+ setStatus('info','Processing download...');
171
+
172
+ // Gradio returns data in result.data array
173
+ // data[0] is the file path/blob, data[1] is the status message
174
+ if (result.data && result.data[0]) {
175
+ const fileData = result.data[0];
176
+ const statusMessage = result.data[1] || '';
177
+
178
+ // If we have a file URL or blob
179
+ if (fileData && fileData.name) {
180
+ // Create download link
181
+ const fileUrl = fileData.url || `/file=${fileData.path}`;
182
+ const fileName = fileData.orig_name || fileData.name || 'instagram-media';
183
+
184
+ // Fetch the file and trigger download
185
+ const fileResponse = await fetch(fileUrl);
186
+ const blob = await fileResponse.blob();
187
+
188
+ const a = document.createElement('a');
189
+ a.href = URL.createObjectURL(blob);
190
+ a.download = fileName;
191
+ document.body.appendChild(a);
192
+ a.click();
193
+ a.remove();
194
+
195
+ setTimeout(() => URL.revokeObjectURL(a.href), 100);
196
+
197
+ bar.style.width = '100%';
198
+ setStatus('success', statusMessage || '✅ Download complete!');
199
+ } else {
200
+ throw new Error(statusMessage || 'No file returned from backend');
201
  }
202
+ } else {
203
+ // Check if there's an error message
204
+ const errorMsg = result.data && result.data[1] ? result.data[1] : 'Failed to download media';
205
+ throw new Error(errorMsg);
 
 
 
 
 
206
  }
207
  } catch (err){
208
+ bar.style.width = '0%';
209
  setStatus('error', err.message || 'Something went wrong');
210
  showWarning(
211
+ ` Error: ${err.message}. Make sure the Instagram URL is valid and the post is public.`
 
 
212
  );
213
  } finally {
214
  downloadBtn.disabled = false;