Spaces:
Sleeping
Sleeping
Update index.html to POST to Gradio backend API with progress/error handling
Browse files- 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
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
function
|
| 99 |
-
|
| 100 |
-
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 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
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
pasteBtn?.addEventListener('click', async () => {
|
| 131 |
-
try {
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
});
|
| 134 |
|
| 135 |
form.addEventListener('submit', async (e)=>{
|
| 136 |
-
e.preventDefault();
|
| 137 |
-
|
|
|
|
| 138 |
try {
|
| 139 |
const url = sanitizeUrl(input.value);
|
| 140 |
-
downloadBtn.disabled = true;
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
throw new Error('Failed to fetch media client-side. Use a backend/proxy API.');
|
| 160 |
}
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
const
|
| 177 |
-
|
| 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 |
-
|
| 186 |
setStatus('error', err.message || 'Something went wrong');
|
| 187 |
showWarning(
|
| 188 |
-
`
|
| 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;
|