infra: uploader → post wizard (auto-naming, markdown template, fields output)

This commit is contained in:
Telegram Bot
2026-04-29 13:38:08 +03:00
parent 5f99a0460c
commit 943a111af5
+316 -115
View File
@@ -1,15 +1,14 @@
#!/usr/bin/env python3
import os, tempfile
import os, tempfile, re
from flask import Flask, request, jsonify
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
import pillow_heif
from PIL import Image
pillow_heif.register_heif_opener()
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
S3_CONFIG = dict(
endpoint_url='https://s3.regru.cloud',
@@ -19,148 +18,344 @@ S3_CONFIG = dict(
config=Config(signature_version='s3v4')
)
BUCKET = 'sleeptrip-dev'
PREFIX = 'images/'
ALLOWED = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'mov', 'heic', 'heif'}
MIME = {
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
'gif': 'image/gif', 'webp': 'image/webp',
'mp4': 'video/mp4', 'mov': 'video/quicktime',
'heic': 'image/jpeg', 'heif': 'image/jpeg'
'jpg':'image/jpeg','jpeg':'image/jpeg','png':'image/png',
'gif':'image/gif','webp':'image/webp','mp4':'video/mp4','mov':'video/quicktime',
'heic':'image/jpeg','heif':'image/jpeg'
}
ALLOWED = set(MIME.keys())
PAGE = """<!DOCTYPE html>
PAGE = r"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Upload to S3</title>
<title>PTP Post Wizard</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,monospace;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px}
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;width:100%;max-width:520px}
h1{font-size:16px;font-weight:600;margin-bottom:4px}
.sub{font-size:12px;color:#8b949e;margin-bottom:24px}
.drop{border:2px dashed #30363d;border-radius:8px;padding:48px 24px;text-align:center;cursor:pointer;transition:all .2s}
.drop.over{border-color:#58a6ff;background:rgba(88,166,255,.05)}
.drop input{display:none}
.drop-icon{font-size:36px;margin-bottom:12px}
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:0 0 60px}
.top{background:#161b22;border-bottom:1px solid #30363d;padding:16px 24px;display:flex;align-items:center;gap:12px}
.top h1{font-size:15px;font-weight:600}
.top .sub{font-size:12px;color:#8b949e}
.wrap{max-width:860px;margin:0 auto;padding:24px}
.section{margin-bottom:24px}
.section-title{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#8b949e;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #21262d}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
label{display:block;font-size:11px;color:#8b949e;margin-bottom:4px}
input,textarea{width:100%;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 10px;color:#e6edf3;font-size:13px;font-family:inherit;outline:none;transition:border-color .15s}
input:focus,textarea:focus{border-color:#58a6ff}
.hint{font-size:11px;color:#8b949e;margin-top:4px}
.slug-preview{font-family:monospace;font-size:12px;color:#58a6ff;margin-top:4px}
.drop{border:2px dashed #30363d;border-radius:10px;padding:40px 24px;text-align:center;cursor:pointer;transition:all .2s;position:relative}
.drop.over,.drop.has-files{border-color:#58a6ff;background:rgba(88,166,255,.04)}
.drop-icon{font-size:32px;margin-bottom:10px;color:#8b949e}
.drop-text{font-size:13px;color:#8b949e}
.drop-text b{color:#58a6ff;cursor:pointer}
.progress{margin-top:16px;display:none}
.bar-wrap{background:#21262d;border-radius:4px;height:6px;margin-top:8px}
.bar{background:#58a6ff;height:6px;border-radius:4px;width:0;transition:width .15s}
.status{font-size:12px;color:#8b949e;margin-top:6px}
.result{margin-top:20px;display:none}
.url-box{background:#0d1117;border:1px solid #3fb950;border-radius:6px;padding:12px 14px;font-size:12px;font-family:monospace;word-break:break-all;color:#3fb950;cursor:pointer}
.url-box:hover{background:rgba(63,185,80,.05)}
.copy-hint{font-size:11px;color:#8b949e;margin-top:6px}
.preview{margin-top:12px;max-width:100%;max-height:200px;border-radius:6px;display:none}
.err{color:#f85149;font-size:12px;margin-top:12px;display:none;padding:8px 12px;background:rgba(248,81,73,.08);border-radius:6px}
.hist{margin-top:24px;border-top:1px solid #21262d;padding-top:16px;display:none}
.hist h2{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#8b949e;margin-bottom:10px}
.hi{font-size:11px;font-family:monospace;color:#8b949e;padding:5px 0;border-bottom:1px solid #21262d;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hi:hover{color:#58a6ff}
.drop input{display:none}
.photos{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-top:16px}
.photo-card{background:#161b22;border:1px solid #30363d;border-radius:8px;overflow:hidden;position:relative}
.photo-card img{width:100%;height:110px;object-fit:cover;display:block}
.photo-card .ph-name{font-size:10px;font-family:monospace;padding:6px 8px;color:#58a6ff;word-break:break-all;line-height:1.4}
.photo-card .ph-num{position:absolute;top:6px;left:6px;background:rgba(0,0,0,.7);color:#fff;font-size:11px;font-weight:700;padding:2px 7px;border-radius:4px}
.photo-card .ph-del{position:absolute;top:6px;right:6px;background:rgba(248,81,73,.8);color:#fff;border:none;border-radius:4px;padding:2px 7px;font-size:11px;cursor:pointer}
.photo-card.uploading{opacity:.6}
.photo-card.uploading::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:32px;color:#58a6ff}
.output{background:#161b22;border:1px solid #30363d;border-radius:10px;overflow:hidden}
.out-tabs{display:flex;border-bottom:1px solid #30363d}
.out-tab{padding:10px 18px;font-size:12px;color:#8b949e;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
.out-tab.active{color:#58a6ff;border-bottom-color:#58a6ff}
.out-panel{display:none;padding:16px}
.out-panel.active{display:block}
.out-panel pre{background:#0d1117;border-radius:6px;padding:14px;font-size:12px;font-family:monospace;overflow-x:auto;white-space:pre-wrap;word-break:break-all;color:#e6edf3;max-height:400px;overflow-y:auto}
.copy-btn{background:#21262d;border:1px solid #30363d;border-radius:6px;padding:6px 14px;font-size:12px;color:#8b949e;cursor:pointer;float:right;margin-bottom:8px;transition:all .15s}
.copy-btn:hover{color:#e6edf3;border-color:#8b949e}
.copy-btn.ok{color:#3fb950;border-color:#3fb950}
.err{color:#f85149;font-size:12px;padding:8px 12px;background:rgba(248,81,73,.08);border-radius:6px;margin-top:8px;display:none}
.prog-bar{height:3px;background:#21262d;border-radius:2px;margin-top:4px;display:none}
.prog-bar-fill{height:3px;background:#58a6ff;border-radius:2px;width:0;transition:width .1s}
.notice{background:rgba(88,166,255,.08);border:1px solid rgba(88,166,255,.2);border-radius:6px;padding:10px 14px;font-size:12px;color:#8b949e;margin-bottom:16px}
</style>
</head>
<body>
<div class="card">
<h1>Upload &rarr; S3</h1>
<p class="sub">sleeptrip-dev &middot; s3.regru.cloud/images/</p>
<div class="drop" id="drop">
<input type="file" id="fi" accept="image/*,video/*" multiple>
<div class="drop-icon">&#8679;</div>
<div class="drop-text">Перетащи или <b id="btn">выбери файл</b></div>
</div>
<div class="progress" id="prog">
<div class="bar-wrap"><div class="bar" id="bar"></div></div>
<div class="status" id="stat">Загружаем...</div>
</div>
<div class="err" id="err"></div>
<div class="result" id="res">
<div class="url-box" id="url" onclick="copy(this)"></div>
<div class="copy-hint" id="hint">Нажми чтобы скопировать</div>
<img class="preview" id="prev">
</div>
<div class="hist" id="hist">
<h2>История</h2>
<div id="hl"></div>
<div class="top">
<div>
<h1>PTP Post Wizard</h1>
<div class="sub">Загрузка фото → готовый пост для Sveltia CMS</div>
</div>
</div>
<div class="wrap">
<!-- STEP 1: INFO -->
<div class="section">
<div class="section-title">1. Информация о посте</div>
<div class="grid2" style="margin-bottom:12px">
<div>
<label>Заголовок</label>
<input type="text" id="title" placeholder="Дюкинский заказник" oninput="onTitleInput()">
<div class="hint">Название места — как на сайте</div>
</div>
<div>
<label>Slug (URL страницы и база имён фото)</label>
<input type="text" id="slug" placeholder="Dukyn2504" oninput="onSlugInput()">
<div class="slug-preview" id="slug-preview"></div>
</div>
</div>
<div class="grid3">
<div>
<label>Дата поездки</label>
<input type="date" id="date" oninput="updateOutput()">
<div class="hint" id="date-fmt"></div>
</div>
<div>
<label>Описание (анонс)</label>
<input type="text" id="desc" placeholder="скалы, лес, отличная погода" oninput="updateOutput()">
</div>
<div>
<label>Disqus ID</label>
<input type="number" id="disqus" placeholder="134" min="1" oninput="updateOutput()">
<div class="hint">Следующий свободный: 134</div>
</div>
</div>
</div>
<!-- STEP 2: UPLOAD -->
<div class="section">
<div class="section-title">2. Фотографии</div>
<div class="notice">Фото автоматически называются <b id="naming-hint">slug-YYYYMMDD-N.jpg</b> и загружаются в S3. Первое фото = обложка.</div>
<div class="drop" id="drop">
<input type="file" id="fi" accept="image/*,video/*" multiple>
<div class="drop-icon">⬆</div>
<div class="drop-text">Перетащи фото сюда или <b onclick="document.getElementById('fi').click()">выбери файлы</b></div>
</div>
<div class="prog-bar" id="prog"><div class="prog-bar-fill" id="prog-fill"></div></div>
<div class="err" id="err"></div>
<div class="photos" id="photos"></div>
</div>
<!-- STEP 3: OUTPUT -->
<div class="section" id="output-section" style="display:none">
<div class="section-title">3. Готовый контент — копируй в Sveltia</div>
<div class="output">
<div class="out-tabs">
<div class="out-tab active" onclick="switchTab(0)">Поля формы</div>
<div class="out-tab" onclick="switchTab(1)">Markdown контент</div>
<div class="out-tab" onclick="switchTab(2)">Все URL фото</div>
</div>
<div class="out-panel active" id="tab0">
<button class="copy-btn" onclick="copyPanel('fields-out','this')">Копировать</button>
<pre id="fields-out"></pre>
</div>
<div class="out-panel" id="tab1">
<button class="copy-btn" onclick="copyPanel('md-out','this')">Копировать</button>
<pre id="md-out"></pre>
</div>
<div class="out-panel" id="tab2">
<button class="copy-btn" onclick="copyPanel('urls-out','this')">Копировать</button>
<pre id="urls-out"></pre>
</div>
</div>
</div>
</div>
<script>
var urls = [];
// ---- State ----
var photos = []; // {url, filename, ext}
var counter = 0;
var uploading = 0;
// ---- Transliteration ----
var RU = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z','и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'sch','ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya'};
function translit(s){
return s.toLowerCase().split('').map(function(c){return RU[c]||c;}).join('').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
}
function onTitleInput(){
var slug = document.getElementById('slug');
if(!slug._manual){
var t = translit(document.getElementById('title').value);
slug.value = t;
}
onSlugInput();
}
function onSlugInput(){
var slug = document.getElementById('slug');
slug._manual = slug.value.length > 0;
updateNamingHint();
updateOutput();
}
function getSlug(){ return document.getElementById('slug').value.trim() || 'slug'; }
function getDateStr(){
var d = document.getElementById('date').value;
return d ? d.replace(/-/g,'') : 'YYYYMMDD';
}
function getDateISO(){
var d = document.getElementById('date').value;
return d ? d + 'T00:00:00' : '';
}
function updateNamingHint(){
var s = getSlug(); var d = getDateStr();
document.getElementById('naming-hint').textContent = s+'-'+d+'-N.jpg';
var p = document.getElementById('slug-preview');
p.textContent = s ? 'URL: /post/'+s+'/' : '';
}
// init date = today
(function(){
var d = new Date();
var iso = d.toISOString().slice(0,10);
document.getElementById('date').value = iso;
updateNamingHint();
})();
// ---- Drag & Drop ----
var drop = document.getElementById('drop');
var fi = document.getElementById('fi');
document.getElementById('btn').onclick = function(){ fi.click(); };
drop.addEventListener('dragover', function(e){ e.preventDefault(); drop.classList.add('over'); });
drop.addEventListener('dragleave', function(){ drop.classList.remove('over'); });
drop.addEventListener('drop', function(e){ e.preventDefault(); drop.classList.remove('over'); for(var i=0;i<e.dataTransfer.files.length;i++) doUpload(e.dataTransfer.files[i]); });
fi.addEventListener('change', function(){ for(var i=0;i<fi.files.length;i++) doUpload(fi.files[i]); });
drop.addEventListener('dragover',function(e){e.preventDefault();drop.classList.add('over');});
drop.addEventListener('dragleave',function(){drop.classList.remove('over');});
drop.addEventListener('drop',function(e){
e.preventDefault(); drop.classList.remove('over');
var files = e.dataTransfer.files;
for(var i=0;i<files.length;i++) uploadFile(files[i]);
});
fi.addEventListener('change',function(){
for(var i=0;i<fi.files.length;i++) uploadFile(fi.files[i]);
fi.value='';
});
function uploadFile(file){
var slug = getSlug(); var dateStr = getDateStr();
counter++;
var n = counter;
var ext = file.name.split('.').pop().toLowerCase();
var outExt = (ext==='heic'||ext==='heif') ? 'jpg' : ext;
var filename = slug+'-'+dateStr+'-'+n+'.'+outExt;
// Placeholder card
var card = document.createElement('div');
card.className='photo-card uploading';
card.id='card-'+n;
card.innerHTML='<div style="height:110px;background:#21262d"></div><div class="ph-name">'+filename+'</div><div class="ph-num">#'+n+'</div>';
document.getElementById('photos').appendChild(card);
drop.classList.add('has-files');
function doUpload(file) {
var prog = document.getElementById('prog');
var res = document.getElementById('res');
var err = document.getElementById('err');
prog.style.display = 'block';
res.style.display = 'none';
err.style.display = 'none';
document.getElementById('bar').style.width = '0';
document.getElementById('stat').textContent = file.name + ' ...';
prog.style.display='block';
uploading++;
var fd = new FormData();
fd.append('file', file);
fd.append('filename', filename);
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e){ if(e.lengthComputable) document.getElementById('bar').style.width = (e.loaded/e.total*100)+'%'; };
xhr.upload.onprogress = function(e){
if(e.lengthComputable) document.getElementById('prog-fill').style.width=(e.loaded/e.total*100)+'%';
};
xhr.onload = function(){
prog.style.display = 'none';
if(xhr.status === 200){
uploading--;
if(uploading===0){prog.style.display='none'; document.getElementById('prog-fill').style.width='0';}
if(xhr.status===200){
var r = JSON.parse(xhr.responseText);
showRes(r.url, file.type.indexOf('image')===0);
photos.push({url:r.url, filename:filename, ext:outExt, n:n});
photos.sort(function(a,b){return a.n-b.n;});
// Update card
var c = document.getElementById('card-'+n);
c.className='photo-card';
c.innerHTML='<img src="'+r.url+'" onerror="this.style.display=\'none\'">'
+'<div class="ph-num">#'+n+'</div>'
+'<button class="ph-del" onclick="deletePhoto('+n+')">✕</button>'
+'<div class="ph-name">'+filename+'</div>';
updateOutput();
} else {
var msg = 'Ошибка '+xhr.status;
try{ msg = JSON.parse(xhr.responseText).error; }catch(e){}
showErr(msg);
var msg='Ошибка'; try{msg=JSON.parse(xhr.responseText).error;}catch(e){}
var c=document.getElementById('card-'+n); if(c) c.remove();
showErr(msg); counter--;
}
};
xhr.onerror = function(){ prog.style.display='none'; showErr('Сетевая ошибка'); };
xhr.open('POST', '/upload/api');
xhr.withCredentials = true;
xhr.onerror=function(){uploading--;showErr('Сетевая ошибка');};
xhr.open('POST','/upload/api');
xhr.withCredentials=true;
xhr.send(fd);
}
function showRes(url, isImg) {
var el = document.getElementById('url');
el.textContent = url;
document.getElementById('res').style.display = 'block';
document.getElementById('hint').textContent = 'Нажми чтобы скопировать';
var p = document.getElementById('prev');
if(isImg){ p.src=url; p.style.display='block'; } else { p.style.display='none'; }
urls.unshift(url);
updateHist();
function deletePhoto(n){
photos = photos.filter(function(p){return p.n!==n;});
var c=document.getElementById('card-'+n); if(c) c.remove();
if(photos.length===0) document.getElementById('output-section').style.display='none';
updateOutput();
}
function showErr(msg){ var e=document.getElementById('err'); e.textContent=msg; e.style.display='block'; }
function showErr(msg){var e=document.getElementById('err');e.textContent=msg;e.style.display='block';setTimeout(function(){e.style.display='none';},5000);}
function copy(el){
navigator.clipboard.writeText(el.textContent).then(function(){
document.getElementById('hint').textContent='Скопировано!';
setTimeout(function(){ document.getElementById('hint').textContent='Нажми чтобы скопировать'; }, 2000);
// ---- Output generation ----
function updateOutput(){
updateNamingHint();
if(photos.length===0) return;
document.getElementById('output-section').style.display='block';
var title = document.getElementById('title').value.trim() || '';
var slug = getSlug();
var date = getDateISO();
var desc = document.getElementById('desc').value.trim() || '';
var disqus = document.getElementById('disqus').value.trim() || '';
var cover = photos.length>0 ? photos[0].url : '';
// Tab 0: fields
var fields = 'Заголовок: '+title+'\n'
+'Slug: '+slug+'\n'
+'Дата: '+(document.getElementById('date').value||'')+'\n'
+'Описание: '+desc+'\n'
+'Обложка: '+cover+'\n'
+'Disqus ID: '+disqus;
document.getElementById('fields-out').textContent = fields;
// Tab 1: markdown content
var md = 'Текст поездки — напиши здесь.\n\n';
photos.forEach(function(p){
md += '!['+slug+']('+p.url+')\n\n';
});
md += '{{< youtube id="YOUTUBE_ID" >}}\n\n';
md += '---\n\n';
md += '📍 Локация\n';
md += '{{< rawhtml >}}\n';
md += '<div class="yandex-map-container">\n';
md += '<!-- Вставь embed-код карты из Яндекс Конструктора -->\n';
md += '</div>\n';
md += '{{< /rawhtml >}}\n\n';
md += '{{< rawhtml >}}\n{{< back-to-top >}}\n{{< /rawhtml >}}';
document.getElementById('md-out').textContent = md;
// Tab 2: URLs
var urls = photos.map(function(p,i){return '#'+(i+1)+': '+p.url;}).join('\n');
document.getElementById('urls-out').textContent = urls;
}
function updateHist(){
document.getElementById('hist').style.display='block';
var html = '';
for(var i=0;i<urls.length;i++){
html += '<div class="hi" data-url="'+urls[i]+'">'+urls[i]+'</div>';
}
document.getElementById('hl').innerHTML = html;
var items = document.getElementById('hl').querySelectorAll('.hi');
for(var j=0;j<items.length;j++){
items[j].onclick = (function(u){ return function(){ navigator.clipboard.writeText(u); this.textContent='Скопировано!'; }; })(items[j].getAttribute('data-url'));
}
// ---- Tabs ----
function switchTab(i){
var tabs=document.querySelectorAll('.out-tab');
var panels=document.querySelectorAll('.out-panel');
tabs.forEach(function(t,j){t.className='out-tab'+(j===i?' active':'');});
panels.forEach(function(p,j){p.className='out-panel'+(j===i?' active':'');});
}
// ---- Copy ----
function copyPanel(id, btn){
var text = document.getElementById(id).textContent;
navigator.clipboard.writeText(text).then(function(){
var b = document.querySelector('[onclick*="'+id+'"]');
var orig = b.textContent; b.textContent='✓ Скопировано'; b.className='copy-btn ok';
setTimeout(function(){b.textContent=orig;b.className='copy-btn';},2000);
});
}
</script>
</body>
</html>"""
</html>
"""
@app.route('/upload/')
@@ -174,6 +369,8 @@ def upload_file():
return jsonify({'error': 'Нет файла'}), 400
f = request.files['file']
custom_filename = request.form.get('filename', '').strip()
if not f.filename:
return jsonify({'error': 'Пустое имя файла'}), 400
@@ -181,22 +378,27 @@ def upload_file():
if ext not in ALLOWED:
return jsonify({'error': 'Формат .{} не поддерживается'.format(ext)}), 400
original_name = os.path.splitext(f.filename)[0]
# HEIC/HEIF → конвертируем в JPG
is_heic = ext in ('heic', 'heif')
out_ext = 'jpg' if is_heic else ext
s3_key = '{}{}.{}'.format(PREFIX, original_name, out_ext)
# Use custom filename from wizard if provided
if custom_filename:
out_name = custom_filename
out_ext = out_name.rsplit('.', 1)[-1].lower()
else:
original = os.path.splitext(f.filename)[0]
out_ext = 'jpg' if ext in ('heic', 'heif') else ext
out_name = '{}.{}'.format(original, out_ext)
s3_key = 'images/{}'.format(out_name)
tmp_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.{}'.format(ext)) as tmp:
f.save(tmp.name)
tmp_path = tmp.name
if is_heic:
# HEIC → JPG conversion
if ext in ('heic', 'heif'):
img = Image.open(tmp_path)
converted = tmp_path.replace('.{}'.format(ext), '.jpg')
converted = tmp_path.rsplit('.', 1)[0] + '.jpg'
img.convert('RGB').save(converted, 'JPEG', quality=92)
os.unlink(tmp_path)
tmp_path = converted
@@ -208,14 +410,13 @@ def upload_file():
s3.upload_file(
tmp_path, BUCKET, s3_key,
ExtraArgs={
'ContentType': MIME.get(ext, 'application/octet-stream'),
'ContentType': MIME.get(out_ext, 'application/octet-stream'),
'CacheControl': 'max-age=31536000'
}
)
url = 'https://s3.regru.cloud/{}/{}'.format(BUCKET, s3_key)
# hint: HEIC was converted to JPG
return jsonify({'url': url})
return jsonify({'url': url, 'filename': out_name})
except Exception as e:
return jsonify({'error': str(e)}), 500