diff --git a/requirements.txt b/requirements.txt index f466d71..a68ceb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ aiosqlite>=0.19.0 fastapi>=0.104.0 uvicorn>=0.24.0 python-multipart>=0.0.6 +jinja2>=3.1.0 # Progress bar tqdm>=4.66.0 diff --git a/serp.py b/serp.py index 283a479..be8b496 100644 --- a/serp.py +++ b/serp.py @@ -28,6 +28,7 @@ from typing import List, Dict, Any, Optional from fastapi import FastAPI, Query, HTTPException, BackgroundTasks, Header, Cookie, UploadFile, File, Request from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse, Response, StreamingResponse from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from pydantic import BaseModel from sqlalchemy import text import uvicorn @@ -636,6 +637,191 @@ TRANSLATIONS = { "total_images": "Totalt bilder", "new_session": "Nya denna session", "per_minute": "Per minut", "all_domains": "Alla domäner", "download_phantom": "Ladda ner fantomwebbplats", "phantom_subtitle": "Exportera arkiverade sidor som statisk webbplats med lokal media", "max_pages": "Max sidor", "select_domain": "Välj domän", "sources": "källor", + "source_uri": "Käll-URI", "neopig_uri": "Neopig URI", "source_page": "Källsida", "neopig_page": "Neopig-sida", + "type_label": "Typ", "mime_label": "MIME", "size_label": "Storlek", "alt_label": "Alt", + "keywords_label": "Nyckelord", "description_label": "Beskrivning", "items": "objekt", "bytes": "bytes", + "used_on": "Används på", "page": "Sida", "discovered": "Upptäckt", + "console": "Konsol", "close": "Stäng", "no_logs": "Inga loggar ännu", + }, + "zh-tw": { + "search": "搜尋", "crawl": "爬取", "live": "即時動態", "about": "關於", + "random": "隨機", "phantom": "幻影", "loading": "載入中...", + "start_crawl": "開始爬取", "pause": "暫停", "resume": "繼續", "delete": "刪除", + "url": "網址", "mode": "模式", "depth": "深度", "keywords": "關鍵字", + "screenshots": "螢幕截圖", "fast_mode": "快速模式", "fresh_start": "全新開始", + "pages": "頁面", "found": "發現", "saved": "儲存", "dupes": "重複", + "running": "執行中", "completed": "已完成", "paused": "已暫停", + "no_jobs": "暫無爬取任務", "started": "開始於", "filter": "篩選", + "all_types": "所有類型", "images": "圖片", "videos": "影片", "audio": "音訊", + "everything": "全部(文字+媒體)", "all_media": "所有媒體", + "images_only": "僅圖片", "videos_only": "僅影片", "text_only": "僅文字", + "recent_jobs": "最近任務", "search_placeholder": "搜尋媒體...", + "media": "媒體", "pages_label": "頁面", "download": "下載", + "no_media": "未找到媒體", "no_more": "沒有更多結果", "load_error": "載入失敗", + "copy": "複製", "copied": "已複製!", "starting": "啟動中...", "started_ok": "已啟動!", + "error": "錯誤", "watching": "正在監視新圖片...", "content": "內容", + "previous": "上一頁", "next": "下一頁", "over_9000": "超過9000+!", + "loading_stats": "載入統計中...", "screenshot": "螢幕截圖", + "valid_uri": "請輸入至少一個有效的URI", "load_jobs_error": "載入任務失敗", + "hydrate_subtitle": "從網路取得媒體", "live_subtitle": "觀看正在爬取的圖片", + "total_images": "總圖片數", "new_session": "本次新增", "per_minute": "每分鐘", + "all_domains": "所有網域", "download_phantom": "下載幻影網站", + "phantom_subtitle": "將存檔頁面匯出為帶本地媒體的靜態網站", "max_pages": "最大頁數", "select_domain": "選擇網域", "sources": "來源", + "source_uri": "來源URI", "neopig_uri": "Neopig URI", "source_page": "來源頁面", "neopig_page": "Neopig頁面", + "type_label": "類型", "mime_label": "MIME", "size_label": "大小", "alt_label": "替代文字", + "keywords_label": "關鍵字", "description_label": "描述", "items": "項", "bytes": "位元組", + "used_on": "使用於", "page": "頁面", "discovered": "發現時間", + "console": "主控台", "close": "關閉", "no_logs": "暫無日誌", + }, + "bn": { + "search": "অনুসন্ধান", "crawl": "ক্রল", "live": "লাইভ ফিড", "about": "সম্পর্কে", + "random": "এলোমেলো", "phantom": "ফ্যান্টম", "loading": "লোড হচ্ছে...", + "start_crawl": "ক্রল শুরু করুন", "pause": "বিরতি", "resume": "পুনরায় শুরু", "delete": "মুছুন", + "url": "URL", "mode": "মোড", "depth": "গভীরতা", "keywords": "কীওয়ার্ড", + "screenshots": "স্ক্রিনশট", "fast_mode": "দ্রুত মোড", "fresh_start": "নতুন শুরু", + "pages": "পৃষ্ঠা", "found": "পাওয়া গেছে", "saved": "সংরক্ষিত", "dupes": "ডুপ্লিকেট", + "running": "চলছে", "completed": "সম্পন্ন", "paused": "বিরতি", + "no_jobs": "কোন ক্রল কাজ নেই", "started": "শুরু", "filter": "ফিল্টার", + "all_types": "সব ধরনের", "images": "ছবি", "videos": "ভিডিও", "audio": "অডিও", + "everything": "সবকিছু", "all_media": "সব মিডিয়া", + "images_only": "শুধু ছবি", "videos_only": "শুধু ভিডিও", "text_only": "শুধু টেক্সট", + "recent_jobs": "সাম্প্রতিক কাজ", "search_placeholder": "মিডিয়া অনুসন্ধান...", + "media": "মিডিয়া", "pages_label": "পৃষ্ঠা", "download": "ডাউনলোড", + "no_media": "কোন মিডিয়া পাওয়া যায়নি", "no_more": "আর কোন ফলাফল নেই", "load_error": "লোড ব্যর্থ", + "copy": "কপি", "copied": "কপি হয়েছে!", "starting": "শুরু হচ্ছে...", "started_ok": "শুরু হয়েছে!", + "error": "ত্রুটি", "watching": "নতুন ছবি দেখছি...", "content": "বিষয়বস্তু", + "previous": "পূর্ববর্তী", "next": "পরবর্তী", "over_9000": "৯,০০০+ এর বেশি!", + "loading_stats": "পরিসংখ্যান লোড হচ্ছে...", "screenshot": "স্ক্রিনশট", + "valid_uri": "অন্তত একটি বৈধ URI দিন", "load_jobs_error": "কাজ লোড ব্যর্থ", + "hydrate_subtitle": "ওয়েব থেকে মিডিয়া আনুন", "live_subtitle": "ক্রল হওয়া ছবি দেখুন", + "total_images": "মোট ছবি", "new_session": "এই সেশনে নতুন", "per_minute": "প্রতি মিনিটে", + "all_domains": "সব ডোমেইন", "download_phantom": "ফ্যান্টম সাইট ডাউনলোড", + "phantom_subtitle": "সংরক্ষিত পৃষ্ঠা স্থানীয় মিডিয়া সহ স্ট্যাটিক সাইট হিসেবে রপ্তানি", "max_pages": "সর্বোচ্চ পৃষ্ঠা", "select_domain": "ডোমেইন নির্বাচন", "sources": "সূত্র", + "source_uri": "উৎস URI", "neopig_uri": "Neopig URI", "source_page": "উৎস পৃষ্ঠা", "neopig_page": "Neopig পৃষ্ঠা", + "type_label": "ধরন", "mime_label": "MIME", "size_label": "আকার", "alt_label": "Alt", + "keywords_label": "কীওয়ার্ড", "description_label": "বিবরণ", "items": "আইটেম", "bytes": "বাইট", + "used_on": "ব্যবহৃত", "page": "পৃষ্ঠা", "discovered": "আবিষ্কৃত", + "console": "কনসোল", "close": "বন্ধ", "no_logs": "কোন লগ নেই", + }, + "ur": { + "search": "تلاش", "crawl": "کرال", "live": "لائیو فیڈ", "about": "کے بارے میں", + "random": "بے ترتیب", "phantom": "فینٹم", "loading": "لوڈ ہو رہا ہے...", + "start_crawl": "کرال شروع کریں", "pause": "روکیں", "resume": "جاری رکھیں", "delete": "حذف کریں", + "url": "یو آر ایل", "mode": "موڈ", "depth": "گہرائی", "keywords": "کلیدی الفاظ", + "screenshots": "اسکرین شاٹس", "fast_mode": "تیز موڈ", "fresh_start": "نئی شروعات", + "pages": "صفحات", "found": "ملے", "saved": "محفوظ", "dupes": "ڈپلیکیٹ", + "running": "چل رہا ہے", "completed": "مکمل", "paused": "روکا ہوا", + "no_jobs": "کوئی کرال کام نہیں", "started": "شروع", "filter": "فلٹر", + "all_types": "تمام اقسام", "images": "تصاویر", "videos": "ویڈیوز", "audio": "آڈیو", + "everything": "سب کچھ", "all_media": "تمام میڈیا", + "images_only": "صرف تصاویر", "videos_only": "صرف ویڈیوز", "text_only": "صرف متن", + "recent_jobs": "حالیہ کام", "search_placeholder": "میڈیا تلاش کریں...", + "media": "میڈیا", "pages_label": "صفحات", "download": "ڈاؤن لوڈ", + "no_media": "کوئی میڈیا نہیں ملا", "no_more": "مزید نتائج نہیں", "load_error": "لوڈ ناکام", + "copy": "کاپی", "copied": "کاپی ہو گیا!", "starting": "شروع ہو رہا ہے...", "started_ok": "شروع ہو گیا!", + "error": "خرابی", "watching": "نئی تصاویر دیکھ رہا ہے...", "content": "مواد", + "previous": "پچھلا", "next": "اگلا", "over_9000": "9,000+ سے زیادہ!", + "loading_stats": "اعدادوشمار لوڈ ہو رہے ہیں...", "screenshot": "اسکرین شاٹ", + "valid_uri": "کم از کم ایک درست URI درج کریں", "load_jobs_error": "کام لوڈ ناکام", + "hydrate_subtitle": "ویب سے میڈیا حاصل کریں", "live_subtitle": "کرال ہوتی تصاویر دیکھیں", + "total_images": "کل تصاویر", "new_session": "اس سیشن میں نئی", "per_minute": "فی منٹ", + "all_domains": "تمام ڈومینز", "download_phantom": "فینٹم سائٹ ڈاؤن لوڈ", + "phantom_subtitle": "آرکائیو شدہ صفحات کو مقامی میڈیا کے ساتھ جامد سائٹ کے طور پر برآمد کریں", "max_pages": "زیادہ سے زیادہ صفحات", "select_domain": "ڈومین منتخب کریں", "sources": "ذرائع", + "source_uri": "ماخذ URI", "neopig_uri": "Neopig URI", "source_page": "ماخذ صفحہ", "neopig_page": "Neopig صفحہ", + "type_label": "قسم", "mime_label": "MIME", "size_label": "سائز", "alt_label": "Alt", + "keywords_label": "کلیدی الفاظ", "description_label": "تفصیل", "items": "آئٹمز", "bytes": "بائٹس", + "used_on": "استعمال شدہ", "page": "صفحہ", "discovered": "دریافت", + "console": "کنسول", "close": "بند کریں", "no_logs": "ابھی کوئی لاگ نہیں", + }, + "sw": { + "search": "Tafuta", "crawl": "Tambaa", "live": "Moja kwa Moja", "about": "Kuhusu", + "random": "Nasibu", "phantom": "Phantom", "loading": "Inapakia...", + "start_crawl": "Anza Kutambaa", "pause": "Simamisha", "resume": "Endelea", "delete": "Futa", + "url": "URL", "mode": "Hali", "depth": "Kina", "keywords": "Maneno muhimu", + "screenshots": "Picha za skrini", "fast_mode": "Hali ya Haraka", "fresh_start": "Mwanzo Mpya", + "pages": "kurasa", "found": "zilizopatikana", "saved": "zilizohifadhiwa", "dupes": "nakala", + "running": "inaendelea", "completed": "imekamilika", "paused": "imesimamishwa", + "no_jobs": "Hakuna kazi", "started": "Ilianza", "filter": "Chuja", + "all_types": "Aina zote", "images": "Picha", "videos": "Video", "audio": "Sauti", + "everything": "Kila kitu", "all_media": "Media zote", + "images_only": "Picha tu", "videos_only": "Video tu", "text_only": "Maandishi tu", + "recent_jobs": "Kazi za Hivi Karibuni", "search_placeholder": "Tafuta media...", + "media": "Media", "pages_label": "Kurasa", "download": "Pakua", + "no_media": "Hakuna media iliyopatikana", "no_more": "Hakuna matokeo zaidi", "load_error": "Imeshindwa kupakia", + "copy": "Nakili", "copied": "Imenakiliwa!", "starting": "Inaanza...", "started_ok": "Imeanza!", + "error": "Hitilafu", "watching": "Inatazama picha mpya...", "content": "Maudhui", + "previous": "Iliyotangulia", "next": "Inayofuata", "over_9000": "Zaidi ya 9,000+!", + "loading_stats": "Inapakia takwimu...", "screenshot": "Picha ya skrini", + "valid_uri": "Tafadhali weka URI moja halali", "load_jobs_error": "Imeshindwa kupakia kazi", + "hydrate_subtitle": "Pata media kutoka wavuti", "live_subtitle": "Tazama picha zikitambaazwa", + "total_images": "Jumla ya Picha", "new_session": "Mpya Kipindi Hiki", "per_minute": "Kwa Dakika", + "all_domains": "Vikoa vyote", "download_phantom": "Pakua tovuti ya phantom", + "phantom_subtitle": "Hamisha kurasa zilizohifadhiwa kama tovuti tuli na media za ndani", "max_pages": "Kurasa za juu", "select_domain": "Chagua Kikoa", "sources": "vyanzo", + "source_uri": "URI Chanzo", "neopig_uri": "Neopig URI", "source_page": "Ukurasa Chanzo", "neopig_page": "Ukurasa Neopig", + "type_label": "Aina", "mime_label": "MIME", "size_label": "Ukubwa", "alt_label": "Alt", + "keywords_label": "Maneno muhimu", "description_label": "Maelezo", "items": "vitu", "bytes": "baiti", + "used_on": "Imetumika", "page": "Ukurasa", "discovered": "Iligunduliwa", + "console": "Konsoli", "close": "Funga", "no_logs": "Hakuna kumbukumbu bado", + }, + "mr": { + "search": "शोध", "crawl": "क्रॉल", "live": "थेट फीड", "about": "बद्दल", + "random": "यादृच्छिक", "phantom": "फँटम", "loading": "लोड होत आहे...", + "start_crawl": "क्रॉल सुरू करा", "pause": "थांबवा", "resume": "पुन्हा सुरू करा", "delete": "हटवा", + "url": "URL", "mode": "मोड", "depth": "खोली", "keywords": "कीवर्ड", + "screenshots": "स्क्रीनशॉट", "fast_mode": "जलद मोड", "fresh_start": "नवीन सुरुवात", + "pages": "पृष्ठे", "found": "सापडले", "saved": "जतन केले", "dupes": "डुप्लिकेट", + "running": "चालू आहे", "completed": "पूर्ण", "paused": "थांबवले", + "no_jobs": "कोणतेही क्रॉल काम नाही", "started": "सुरू", "filter": "फिल्टर", + "all_types": "सर्व प्रकार", "images": "प्रतिमा", "videos": "व्हिडिओ", "audio": "ऑडिओ", + "everything": "सर्वकाही", "all_media": "सर्व मीडिया", + "images_only": "फक्त प्रतिमा", "videos_only": "फक्त व्हिडिओ", "text_only": "फक्त मजकूर", + "recent_jobs": "अलीकडील कामे", "search_placeholder": "मीडिया शोधा...", + "media": "मीडिया", "pages_label": "पृष्ठे", "download": "डाउनलोड", + "no_media": "मीडिया सापडला नाही", "no_more": "आणखी परिणाम नाहीत", "load_error": "लोड अयशस्वी", + "copy": "कॉपी", "copied": "कॉपी झाले!", "starting": "सुरू होत आहे...", "started_ok": "सुरू झाले!", + "error": "त्रुटी", "watching": "नवीन प्रतिमा पाहत आहे...", "content": "सामग्री", + "previous": "मागील", "next": "पुढील", "over_9000": "9,000+ पेक्षा जास्त!", + "loading_stats": "आकडेवारी लोड होत आहे...", "screenshot": "स्क्रीनशॉट", + "valid_uri": "कृपया किमान एक वैध URI प्रविष्ट करा", "load_jobs_error": "कामे लोड अयशस्वी", + "hydrate_subtitle": "वेबवरून मीडिया मिळवा", "live_subtitle": "क्रॉल होताना प्रतिमा पहा", + "total_images": "एकूण प्रतिमा", "new_session": "या सत्रात नवीन", "per_minute": "प्रति मिनिट", + "all_domains": "सर्व डोमेन", "download_phantom": "फँटम साइट डाउनलोड करा", + "phantom_subtitle": "संग्रहित पृष्ठे स्थानिक मीडियासह स्थिर साइट म्हणून निर्यात करा", "max_pages": "जास्तीत जास्त पृष्ठे", "select_domain": "डोमेन निवडा", "sources": "स्रोत", + "source_uri": "स्रोत URI", "neopig_uri": "Neopig URI", "source_page": "स्रोत पृष्ठ", "neopig_page": "Neopig पृष्ठ", + "type_label": "प्रकार", "mime_label": "MIME", "size_label": "आकार", "alt_label": "Alt", + "keywords_label": "कीवर्ड", "description_label": "वर्णन", "items": "आयटम", "bytes": "बाइट्स", + "used_on": "वापरलेले", "page": "पृष्ठ", "discovered": "शोधले", + "console": "कन्सोल", "close": "बंद करा", "no_logs": "अद्याप कोणतेही लॉग नाहीत", + }, + "te": { + "search": "శోధన", "crawl": "క్రాల్", "live": "లైవ్ ఫీడ్", "about": "గురించి", + "random": "యాదృచ్ఛిక", "phantom": "ఫాంటమ్", "loading": "లోడ్ అవుతోంది...", + "start_crawl": "క్రాల్ ప్రారంభించు", "pause": "పాజ్", "resume": "పునఃప్రారంభించు", "delete": "తొలగించు", + "url": "URL", "mode": "మోడ్", "depth": "లోతు", "keywords": "కీవర్డ్లు", + "screenshots": "స్క్రీన్షాట్లు", "fast_mode": "వేగవంతమైన మోడ్", "fresh_start": "కొత్త ప్రారంభం", + "pages": "పేజీలు", "found": "కనుగొనబడింది", "saved": "సేవ్ చేయబడింది", "dupes": "నకిలీలు", + "running": "నడుస్తోంది", "completed": "పూర్తయింది", "paused": "పాజ్ చేయబడింది", + "no_jobs": "క్రాల్ జాబ్లు లేవు", "started": "ప్రారంభమైంది", "filter": "ఫిల్టర్", + "all_types": "అన్ని రకాలు", "images": "చిత్రాలు", "videos": "వీడియోలు", "audio": "ఆడియో", + "everything": "అన్నీ", "all_media": "అన్ని మీడియా", + "images_only": "చిత్రాలు మాత్రమే", "videos_only": "వీడియోలు మాత్రమే", "text_only": "వచనం మాత్రమే", + "recent_jobs": "ఇటీవలి జాబ్లు", "search_placeholder": "మీడియా శోధించండి...", + "media": "మీడియా", "pages_label": "పేజీలు", "download": "డౌన్లోడ్", + "no_media": "మీడియా కనుగొనబడలేదు", "no_more": "మరిన్ని ఫలితాలు లేవు", "load_error": "లోడ్ విఫలమైంది", + "copy": "కాపీ", "copied": "కాపీ అయింది!", "starting": "ప్రారంభమవుతోంది...", "started_ok": "ప్రారంభమైంది!", + "error": "లోపం", "watching": "కొత్త చిత్రాల కోసం చూస్తోంది...", "content": "కంటెంట్", + "previous": "మునుపటి", "next": "తదుపరి", "over_9000": "9,000+ కంటే ఎక్కువ!", + "loading_stats": "గణాంకాలు లోడ్ అవుతున్నాయి...", "screenshot": "స్క్రీన్షాట్", + "valid_uri": "దయచేసి కనీసం ఒక చెల్లుబాటు అయ్యే URI నమోదు చేయండి", "load_jobs_error": "జాబ్లు లోడ్ విఫలమైంది", + "hydrate_subtitle": "వెబ్ నుండి మీడియా పొందండి", "live_subtitle": "క్రాల్ అవుతున్న చిత్రాలను చూడండి", + "total_images": "మొత్తం చిత్రాలు", "new_session": "ఈ సెషన్లో కొత్తవి", "per_minute": "నిమిషానికి", + "all_domains": "అన్ని డొమైన్లు", "download_phantom": "ఫాంటమ్ సైట్ డౌన్లోడ్", + "phantom_subtitle": "ఆర్కైవ్ చేసిన పేజీలను స్థానిక మీడియాతో స్టాటిక్ సైట్గా ఎగుమతి చేయండి", "max_pages": "గరిష్ట పేజీలు", "select_domain": "డొమైన్ ఎంచుకోండి", "sources": "మూలాలు", + "source_uri": "మూల URI", "neopig_uri": "Neopig URI", "source_page": "మూల పేజీ", "neopig_page": "Neopig పేజీ", + "type_label": "రకం", "mime_label": "MIME", "size_label": "పరిమాణం", "alt_label": "Alt", + "keywords_label": "కీవర్డ్లు", "description_label": "వివరణ", "items": "అంశాలు", "bytes": "బైట్లు", + "used_on": "ఉపయోగించబడింది", "page": "పేజీ", "discovered": "కనుగొనబడింది", + "console": "కన్సోల్", "close": "మూసివేయి", "no_logs": "ఇంకా లాగ్లు లేవు", }, } @@ -656,10 +842,11 @@ def get_lang(lang_cookie: str = None, accept_language: str = None) -> str: # Language names for the selector dropdown LANG_NAMES = { - "en": "English", "zh": "中文", "es": "Español", "hi": "हिन्दी", "ar": "العربية", + "en": "English", "zh": "中文", "zh-tw": "繁體中文", "es": "Español", "hi": "हिन्दी", "ar": "العربية", "pt": "Português", "ru": "Русский", "ja": "日本語", "fr": "Français", "de": "Deutsch", "ko": "한국어", "it": "Italiano", "nl": "Nederlands", "pl": "Polski", "tr": "Türkçe", "vi": "Tiếng Việt", "th": "ไทย", "id": "Bahasa", "uk": "Українська", "sv": "Svenska", + "bn": "বাংলা", "ur": "اردو", "sw": "Kiswahili", "mr": "मराठी", "te": "తెలుగు", } def t(key: str, lang: str = "en") -> str: @@ -676,6 +863,18 @@ NAV_HTML = '''
''' +# Single source of truth for search box +SEARCH_BOX_HTML = '''''' + def inject_i18n(html: str, lang: str) -> str: """Replace {key} placeholders and inject JS translations + language selector.""" trans = TRANSLATIONS.get(lang, TRANSLATIONS["en"]) @@ -686,6 +885,8 @@ def inject_i18n(html: str, lang: str) -> str: if IMPORT_MODE: nav = nav.replace('', 'Import\n ') html = html.replace('', nav) + # Replace search placeholder with search box + html = html.replace('', SEARCH_BOX_HTML) # Build language selector options lang_options = ''.join(f'' for code, name in LANG_NAMES.items()) @@ -708,6 +909,10 @@ function setLang(code) { app = FastAPI(title="neopig", description="Media crawler + SERP + Screenshot service") +# Set up Jinja2 templates +TEMPLATES_PATH = Path(__file__).parent / "templates" +templates = Jinja2Templates(directory=str(TEMPLATES_PATH)) if TEMPLATES_PATH.exists() else None + # Try to include uri2png screenshot router (optional dependency) try: from uri2png import get_screenshot_router @@ -785,457 +990,6 @@ TEMP_DB_PATH: str = None # Extracted database (SQLite needs real file) # Base CSS shared by all pages -BASE_CSS = """ -* { box-sizing: border-box; } -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - margin: 0; padding: 0; - background: #0a0a0a; color: #e0e0e0; -} -.nav { - background: #1a1a1a; padding: 10px 20px; - display: flex; gap: 20px; align-items: center; - border-bottom: 1px solid #333; - flex-wrap: wrap; -} -.nav a { color: #ff6b6b; text-decoration: none; padding: 5px 0; } -.nav a:hover { text-decoration: underline; } -.nav .brand { font-weight: bold; font-size: 18px; } -@media (max-width: 600px) { - .nav { gap: 12px; padding: 10px 15px; } - .nav a { font-size: 14px; } - .nav .brand { font-size: 16px; width: 100%; margin-bottom: 5px; } -} -.container { padding: 20px; } -a { color: #ff6b6b; } -input[type="text"], select { - padding: 12px 16px; font-size: 16px; - border: 2px solid #333; border-radius: 8px; - background: #1a1a1a; color: #fff; -} -input[type="text"]:focus { outline: none; border-color: #ff6b6b; } -button { - padding: 12px 24px; font-size: 16px; - background: #ff6b6b; color: #fff; - border: none; border-radius: 8px; cursor: pointer; -} -button:hover { background: #ff5252; } -.search-box { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; } -@media (max-width: 600px) { - .search-box input[type="text"] { width: 100%; flex: 1 1 100%; } - .search-box select, .search-box button { flex: 1 1 auto; } -} -""" - -def layout(title: str, content: str, extra_css: str = "", extra_head: str = "") -> str: - """Build a complete HTML page with consistent layout. - - Args: - title: Page title (will be appended with " - neopig") - content: HTML content for the page body - extra_css: Additional CSS to include - extra_head: Additional head elements (scripts, links, etc.) - - Returns: - Complete HTML document string - """ - return f""" - - - -{{hydrate_subtitle}}
- - - -{{live_subtitle}}
- -{md5_hash}'),
- ("{{type_label}}", media['media_type']),
- ("{{mime_label}}", media.get('mime_type') or 'unknown'),
- ("{{size_label}}", f"{media.get('file_size') or 0:,} bytes"),
- ("{{alt_label}}", media.get('alt_text') or '-'),
- ("{{keywords_label}}", keywords_html),
+ ("type_label", media['media_type']),
+ ("mime_label", media.get('mime_type') or 'unknown'),
+ ("size_label", f"{media.get('file_size') or 0:,} bytes"),
+ ("alt_label", media.get('alt_text') or '-'),
+ ("keywords_label", keywords_html),
]
# Download button - prioritize original filename from URL
@@ -2962,6 +1398,7 @@ async def view_media_page(md5_hash: str, noai: bool = Query(False), lang: str =
screenshot_hashes = [h for h in screenshot_hashes if h != md5_hash]
# Build "Used on X pages" section (reverse image search)
+ t = TRANSLATIONS.get(language, TRANSLATIONS["en"])
sources_html = ""
if sources and len(sources) > 0:
import hashlib as hl
@@ -2981,10 +1418,10 @@ async def view_media_page(md5_hash: str, noai: bool = Query(False), lang: str =
'''
total_count = len(unique_pages)
count_display = f"{OVER_9000:,}" if total_count >= OVER_9000 else str(total_count)
- sources_html = '''
+ sources_html = f'''
| {{page}} | {{discovered}} |
|---|---|
| {t["page"]} | {t["discovered"]} |
{{phantom_subtitle}}
- - - -
- Neo Python Image Grabber
-
- A phoenix-like chimera grown from the bones of dead repositories & forums. A pig that devours web domains whole. Where others archive pages, neopig consumes all digital files.
Welcome to An Age of Aquarius, no deceitful secrets.
-
pig.py is a very simple python command line tool to download all the images from a given uri.
- — Russell Ballestrini, August 22, 2011 -- In the beginning, there was pig.py. A simple creature. Innocent. Hungry. -
- -- Russell Ballestrini crafted it in the summer of 2011 — a humble Python script with a singular appetite: images. Point it at a webpage, and it would slurp down every pixel it could find. No configuration. No complexity. Just a hungry little pig gobbling up the visual fabric of the web. -
- -python pig.py https://www.foxhop.net
- - That was it. Entire interface. Pig asked only for a target, and it fed. -
- -
- Russell released it into the public domain — a gift to anyone who needed to harvest images from the wild web. Source lived at bitbucket.org/russellballestrini/pig, nestled safely in a Mercurial repository. Pig slept soundly in its pen, unaware of the extinction event approaching.
-
- Then came the dark times. -
- -- In 2020, Atlassian — the corporate leviathan that had swallowed Bitbucket whole — decreed that Mercurial repositories would be purged. All of them. Every hg repo on Bitbucket Cloud, atomized. Scattered like digital ash across the void. Atlassian never disclosed how many. -
-
-
- - Original pig.py, that innocent image-gobbling creature, was among the casualties. Its home at bitbucket.org/russellballestrini/pig became a tombstone. Click it. We dare you. There's nothing there but the echo of corporate indifference. -
- -They literally killed pig.py & sent him to bitbucket.
- — Cruel irony of naming your graveyard after a slang term for death -- "Sent to the bitbucket" — programmer slang from the age of punch cards. When you discarded bad data, you threw it in the bit bucket. Atlassian named their service after a trash can, then proved the prophecy true by throwing away everyone's code. -
- -- Pig was dead. Its bones scattered across cached search results & archived blog posts. A ghost in the machine, referenced but unreachable. -
- -- But here's where it gets weird. -
-- Russell once wrote that "programming is like alchemy — instead of exchanging matter, we programmers exchange time." -
-
-
- - Programs are golems. Familiar spirits. Magical servants performing repetitive tasks so we don't have to. "It is more accurate to group programs with technology than magic, but less fun." -
- -- Years passed. Web continued its relentless churn — sites going dark, forums shutting down, communities scattering like startled birds. Somewhere, a very angry ex-Ruby developer (Python too) watched the digital decay & remembered the pig. -
- -What if we could transmute that dead code into gold?
- — Thought that started everything -- And speaking of transmutation: Marathon Fusion discovered that tokamak breeding blankets — wrapped in Mercury-Lithium alloy, like pigs in a blanket — can transmute Mercury-198 into Gold-197 through chrysopoeia. Fast neutrons trigger (n, 2n) reactions; unstable mercury decays into stable gold within 64 hours. -
- -- Two metric tons of gold per gigawatt. Alchemists' dream realized, wrapped in radioactive patience (17.7 years of cooling before you can touch your transmuted treasure). -
- -- A golden goose born from the ashes of deprecated version control. Old pig was slaughtered, while a new creature stirred in the digital depths... -
- -- Machine learning uses an equivalent amount of wu wei energy that elites used to kill pig.py to unfold neopig.py. -
-- Like a phoenix rising from dead Bitbucket repos, neopig emerged. -
-
-
- - Not a resurrection. Not a mere fork. Something else. A chimera. A griffin. A creature stitched together from the DNA of the original pig & the fevered dreams of someone who had watched too many sites die. -
- -A pig with an appetite like Gluttony from Fullmetal Alchemist — a homunculus that devours everything in its path, absorbing knowledge, power & form.
-- Where pig.py sipped politely from single pages, neopig devours entire domains. It doesn't just grab images — it consumes HTML, screenshots pages, converts content to markdown, indexes every scrap of text, and stores it all in content-addressed vaults that will outlast the original servers. -
- -- Code lives now at git.unturf.com/engineering/unturf/pig.py. And it keeps evolving. Every crawl adds new capabilities. Every archived site teaches it new tricks. -
- -- We are adding the ability to backup entire codebases. Neopig grows hungrier. -
-- We are about to invert the git tree into itself. -
-
-
- - Like Dr. Strange versus Spider-Man in the mirror dimension — that impossible space where geometry folds back on itself, where buildings become M.C. Escher nightmares & reality reflects endlessly into its own depths. -
- -- neopig doesn't just archive websites. It can archive itself. It can archive the repositories that contain the code that does the archiving. It can crawl git forges & preserve the source code of tools that preserve source code. -
- -Our spider-pig gets out of the danger of the mirror void — where days are years — and exits absorbing everything like a griffin, like a chimera, like a pig that feeds on bones of dead platforms.
-- When a code hosting platform dies (and they all die eventually — Google Code, Gitorious, BitBucket's hg repos), neopig ensures the knowledge survives. We are the ark. We are the vault. We are the pig that remembers. -
- -
- Archives become self-extracting executables. .run files that contain their own server, their own search engine, their own reality. Drop one on a machine with Python, and it wakes up. A sleeping pig, carrying an entire website in its belly, ready to serve it to anyone who asks.
-
Full-domain recursive crawling with configurable depth. Respects robots.txt & crawl delays. Polite but relentless.
-MD5-based deduplication in a 9-deep hex directory structure. Same image from 100 pages? Stored once. Space is memory.
-Every image indexed with its surrounding context — page title, alt text, captions, nearby headings. Find images by meaning, not filename.
-LinkPeek reborn! Full-page captures via uri2png. wkhtmltoimage, cutycapt, or Playwright backends.
-Auto-discover RSS/Atom/Sitemap feeds. Persists feed URLs, checks for new content on every crawl. Self-healing archive maintenance.
-Track first_seen, last_seen, and when each page first linked to media. Watch content spread across the web over time.
-Intelligent HTML-to-Markdown with forum post detection, noise removal & content extraction. Human-readable archives.
-Create .run executables that contain entire websites. Drop on any machine, execute, browse. Pig travels light.
Archive source code, stylesheets, fonts, and scripts alongside media. Complete preservation for developer communities.
-Auto-detect GitHub, GitLab, Bitbucket & more. Clone repos, symlink files to vault. Deduplicated code across forks.
-Watch media appear in real-time as crawls progress. SSE-powered live updates. Pig hunts while you watch.
-FastAPI-powered search engine. Browse by type, search by context, view media details. Your personal media search engine.
-- neopig uses what we call the "skeleton key" approach to media indexing. For every image, we capture all the text that might help you find it later: -
-
-
-
- Page context: Title, description, headings near the image.
- Image attributes: Alt text, title, surrounding captions.
- Link context: Text of links pointing to the image.
- Detail pages: For gallery sites, we follow through to detail pages & harvest their metadata too.
-
- Result? You can search for "sunset over mountains" and find that image even if it was named IMG_4372.jpg with no alt text — because the page title mentioned it, or someone linked to it with descriptive text, or a caption three divs away contained the words.
-
- Every door opens. Every search succeeds. Skeleton key fits every lock because it carries every possible key inside it. -
-- Web is ephemeral. Sites go dark. Forums shut down. Communities scatter. Platforms get acquired & gutted. Executives decide that Mercurial isn't profitable & delete every repository with a single corporate memo. -
- -- We have watched beloved communities vanish. Game modding forums. Technical documentation. Art galleries. Fan wikis. Personal blogs with decades of writing. Gone. Not archived by the Wayback Machine. Not cached by Google. Just gone. -
- -- neopig is built for preservation — capturing not just the media, but the context that gives it meaning. When you archive a site with neopig, you get: -
- -
- • Original HTML with rewritten media links pointing to your local vault
- • Full-text searchable markdown versions of every page
- • Screenshots showing exactly how pages looked
- • A SQLite database you can query, backup, and migrate forever
- • Self-extracting archives that work offline forever
-
-
- This tool was not built for profit. It was forged. -
- -- Forged in the fires of permacomputer.com — the belief that computing should be permanent. That knowledge should not evaporate when a company pivots. That communities should not lose their histories when a platform gets acquired. That the lore doesn't have to die when you get acqui-hired or outright terminated 🟣. A digital commons that belongs to everyone, forever. -
- -
-
- Truth. Harmony. Freedom. Love.
- — The quadrivium of a permacomputer -- Truth — We preserve what actually existed, not sanitized versions. Original HTML. Exact timestamps. Unedited conversations. History as it happened, not as someone wishes it had. -
- -- Harmony — Every piece connects. Media links to pages. Pages link to context. Context links to meaning. Archive is not a graveyard of disconnected files but a living web of relationships, searchable, traversable, whole. -
- -- Freedom — Public domain. No licenses to parse. No corporate terms of service. Take it. Fork it. Run it. Share it. Code is yours. Archives you create are yours. No one can revoke your access to your own preserved knowledge. -
- -- Love — We archive because we care. About the communities that built these spaces. About the people who poured years into forum posts & wiki pages & blog entries. About the future researchers who will want to understand how we lived & thought & created. This is an act of love for human knowledge. -
- -- Pig is imbued with righteous fire. It does not crawl politely because it fears the powerful — it respects robots.txt because it honors the wishes of creators. It does not hoard in darkness — it preserves in light, making archives searchable, shareable, alive. A pig imbued with righteous fire. -
- -- When the platforms fall (and they will fall), when the corporate servers go dark (and they will go dark), when the executives decide your community isn't profitable enough to maintain (and they will decide this) — a permacomputer endures. Archives persist. Knowledge survives. -
- -- This is what we were forged to do. -
-- neopig is built on SQLite — the most deployed database in existence. SQLite adopted the Rule of St. Benedict as their Code of Ethics. 72 principles from a 6th-century monastery, governing software in the 21st. -
- -- We inherit this lineage. All 72 rules, unabridged: -
- -- We archive not to hoard, but to give. We preserve not to control, but to liberate. Database that holds your memories runs on a foundation of ancient wisdom — and so do we. -
-# open http://127.0.0.1:31337 in browser
-python neopig.py --serve
- - neopig = Neo (new) + Python Image Grabber -
- -- A tip of the hat to original pig.py. A nod to Matrix's Neo — seeing through surface of web to underlying content within. And perhaps a warning: this pig has grown teeth. -
-Upload a neopig archive (.tar.gz or .run) to explore it
- -
+ Neo Python Image Grabber
+
+ A phoenix-like chimera grown from the bones of dead repositories & forums. A pig that devours web domains whole. Where others archive pages, neopig consumes all digital files.
Welcome to An Age of Aquarius, no deceitful secrets.
+
pig.py is a very simple python command line tool to download all the images from a given uri.
+ — Russell Ballestrini, August 22, 2011 ++ In the beginning, there was pig.py. A simple creature. Innocent. Hungry. +
+ ++ Russell Ballestrini crafted it in the summer of 2011 — a humble Python script with a singular appetite: images. Point it at a webpage, and it would slurp down every pixel it could find. No configuration. No complexity. Just a hungry little pig gobbling up the visual fabric of the web. +
+ +python pig.py https://www.foxhop.net
+ + That was it. Entire interface. Pig asked only for a target, and it fed. +
+ +
+ Russell released it into the public domain — a gift to anyone who needed to harvest images from the wild web. Source lived at bitbucket.org/russellballestrini/pig, nestled safely in a Mercurial repository. Pig slept soundly in its pen, unaware of the extinction event approaching.
+
+ Then came the dark times. +
+ ++ In 2020, Atlassian — the corporate leviathan that had swallowed Bitbucket whole — decreed that Mercurial repositories would be purged. All of them. Every hg repo on Bitbucket Cloud, atomized. Scattered like digital ash across the void. Atlassian never disclosed how many. +
+
+
+ + Original pig.py, that innocent image-gobbling creature, was among the casualties. Its home at bitbucket.org/russellballestrini/pig became a tombstone. Click it. We dare you. There's nothing there but the echo of corporate indifference. +
+ +They literally killed pig.py & sent him to bitbucket.
+ — Cruel irony of naming your graveyard after a slang term for death ++ "Sent to the bitbucket" — programmer slang from the age of punch cards. When you discarded bad data, you threw it in the bit bucket. Atlassian named their service after a trash can, then proved the prophecy true by throwing away everyone's code. +
+ ++ Pig was dead. Its bones scattered across cached search results & archived blog posts. A ghost in the machine, referenced but unreachable. +
+ ++ But here's where it gets weird. +
++ Russell once wrote that "programming is like alchemy — instead of exchanging matter, we programmers exchange time." +
+
+
+ + Programs are golems. Familiar spirits. Magical servants performing repetitive tasks so we don't have to. "It is more accurate to group programs with technology than magic, but less fun." +
+ ++ Years passed. Web continued its relentless churn — sites going dark, forums shutting down, communities scattering like startled birds. Somewhere, a very angry ex-Ruby developer (Python too) watched the digital decay & remembered the pig. +
+ +What if we could transmute that dead code into gold?
+ — Thought that started everything ++ And speaking of transmutation: Marathon Fusion discovered that tokamak breeding blankets — wrapped in Mercury-Lithium alloy, like pigs in a blanket — can transmute Mercury-198 into Gold-197 through chrysopoeia. Fast neutrons trigger (n, 2n) reactions; unstable mercury decays into stable gold within 64 hours. +
+ ++ Two metric tons of gold per gigawatt. Alchemists' dream realized, wrapped in radioactive patience (17.7 years of cooling before you can touch your transmuted treasure). +
+ ++ A golden goose born from the ashes of deprecated version control. Old pig was slaughtered, while a new creature stirred in the digital depths... +
+ ++ Machine learning uses an equivalent amount of wu wei energy that elites used to kill pig.py to unfold neopig.py. +
++ Like a phoenix rising from dead Bitbucket repos, neopig emerged. +
+
+
+ + Not a resurrection. Not a mere fork. Something else. A chimera. A griffin. A creature stitched together from the DNA of the original pig & the fevered dreams of someone who had watched too many sites die. +
+ +A pig with an appetite like Gluttony from Fullmetal Alchemist — a homunculus that devours everything in its path, absorbing knowledge, power & form.
++ Where pig.py sipped politely from single pages, neopig devours entire domains. It doesn't just grab images — it consumes HTML, screenshots pages, converts content to markdown, indexes every scrap of text, and stores it all in content-addressed vaults that will outlast the original servers. +
+ ++ Code lives now at git.unturf.com/engineering/unturf/pig.py. And it keeps evolving. Every crawl adds new capabilities. Every archived site teaches it new tricks. +
+ ++ We are adding the ability to backup entire codebases. Neopig grows hungrier. +
++ We are about to invert the git tree into itself. +
+
+
+ + Like Dr. Strange versus Spider-Man in the mirror dimension — that impossible space where geometry folds back on itself, where buildings become M.C. Escher nightmares & reality reflects endlessly into its own depths. +
+ ++ neopig doesn't just archive websites. It can archive itself. It can archive the repositories that contain the code that does the archiving. It can crawl git forges & preserve the source code of tools that preserve source code. +
+ +Our spider-pig gets out of the danger of the mirror void — where days are years — and exits absorbing everything like a griffin, like a chimera, like a pig that feeds on bones of dead platforms.
++ When a code hosting platform dies (and they all die eventually — Google Code, Gitorious, BitBucket's hg repos), neopig ensures the knowledge survives. We are the ark. We are the vault. We are the pig that remembers. +
+ +
+ Archives become self-extracting executables. .run files that contain their own server, their own search engine, their own reality. Drop one on a machine with Python, and it wakes up. A sleeping pig, carrying an entire website in its belly, ready to serve it to anyone who asks.
+
Full-domain recursive crawling with configurable depth. Respects robots.txt & crawl delays. Polite but relentless.
+MD5-based deduplication in a 9-deep hex directory structure. Same image from 100 pages? Stored once. Space is memory.
+Every image indexed with its surrounding context — page title, alt text, captions, nearby headings. Find images by meaning, not filename.
+LinkPeek reborn! Full-page captures via uri2png. wkhtmltoimage, cutycapt, or Playwright backends.
+Auto-discover RSS/Atom/Sitemap feeds. Persists feed URLs, checks for new content on every crawl. Self-healing archive maintenance.
+Track first_seen, last_seen, and when each page first linked to media. Watch content spread across the web over time.
+Intelligent HTML-to-Markdown with forum post detection, noise removal & content extraction. Human-readable archives.
+Create .run executables that contain entire websites. Drop on any machine, execute, browse. Pig travels light.
Archive source code, stylesheets, fonts, and scripts alongside media. Complete preservation for developer communities.
+Auto-detect GitHub, GitLab, Bitbucket & more. Clone repos, symlink files to vault. Deduplicated code across forks.
+Watch media appear in real-time as crawls progress. SSE-powered live updates. Pig hunts while you watch.
+FastAPI-powered search engine. Browse by type, search by context, view media details. Your personal media search engine.
++ neopig uses what we call the "skeleton key" approach to media indexing. For every image, we capture all the text that might help you find it later: +
+
+
+
+ Page context: Title, description, headings near the image.
+ Image attributes: Alt text, title, surrounding captions.
+ Link context: Text of links pointing to the image.
+ Detail pages: For gallery sites, we follow through to detail pages & harvest their metadata too.
+
+ Result? You can search for "sunset over mountains" and find that image even if it was named IMG_4372.jpg with no alt text — because the page title mentioned it, or someone linked to it with descriptive text, or a caption three divs away contained the words.
+
+ Every door opens. Every search succeeds. Skeleton key fits every lock because it carries every possible key inside it. +
++ Web is ephemeral. Sites go dark. Forums shut down. Communities scatter. Platforms get acquired & gutted. Executives decide that Mercurial isn't profitable & delete every repository with a single corporate memo. +
+ ++ We have watched beloved communities vanish. Game modding forums. Technical documentation. Art galleries. Fan wikis. Personal blogs with decades of writing. Gone. Not archived by the Wayback Machine. Not cached by Google. Just gone. +
+ ++ neopig is built for preservation — capturing not just the media, but the context that gives it meaning. When you archive a site with neopig, you get: +
+ +
+ • Original HTML with rewritten media links pointing to your local vault
+ • Full-text searchable markdown versions of every page
+ • Screenshots showing exactly how pages looked
+ • A SQLite database you can query, backup, and migrate forever
+ • Self-extracting archives that work offline forever
+
+
+ This tool was not built for profit. It was forged. +
+ ++ Forged in the fires of permacomputer.com — the belief that computing should be permanent. That knowledge should not evaporate when a company pivots. That communities should not lose their histories when a platform gets acquired. That the lore doesn't have to die when you get acqui-hired or outright terminated 🟣. A digital commons that belongs to everyone, forever. +
+ +
+
+ Truth. Harmony. Freedom. Love.
+ — The quadrivium of a permacomputer ++ Truth — We preserve what actually existed, not sanitized versions. Original HTML. Exact timestamps. Unedited conversations. History as it happened, not as someone wishes it had. +
+ ++ Harmony — Every piece connects. Media links to pages. Pages link to context. Context links to meaning. Archive is not a graveyard of disconnected files but a living web of relationships, searchable, traversable, whole. +
+ ++ Freedom — Public domain. No licenses to parse. No corporate terms of service. Take it. Fork it. Run it. Share it. Code is yours. Archives you create are yours. No one can revoke your access to your own preserved knowledge. +
+ ++ Love — We archive because we care. About the communities that built these spaces. About the people who poured years into forum posts & wiki pages & blog entries. About the future researchers who will want to understand how we lived & thought & created. This is an act of love for human knowledge. +
+ ++ Pig is imbued with righteous fire. It does not crawl politely because it fears the powerful — it respects robots.txt because it honors the wishes of creators. It does not hoard in darkness — it preserves in light, making archives searchable, shareable, alive. A pig imbued with righteous fire. +
+ ++ When the platforms fall (and they will fall), when the corporate servers go dark (and they will go dark), when the executives decide your community isn't profitable enough to maintain (and they will decide this) — a permacomputer endures. Archives persist. Knowledge survives. +
+ ++ This is what we were forged to do. +
++ neopig is built on SQLite — the most deployed database in existence. SQLite adopted the Rule of St. Benedict as their Code of Ethics. 72 principles from a 6th-century monastery, governing software in the 21st. +
+ ++ We inherit this lineage. All 72 rules, unabridged: +
+ ++ We archive not to hoard, but to give. We preserve not to control, but to liberate. Database that holds your memories runs on a foundation of ancient wisdom — and so do we. +
+# open http://127.0.0.1:31337 in browser
+python neopig.py --serve
+ + neopig = Neo (new) + Python Image Grabber +
+ ++ A tip of the hat to original pig.py. A nod to Matrix's Neo — seeing through surface of web to underlying content within. And perhaps a warning: this pig has grown teeth. +
+{{ t.hydrate_subtitle }}
+ + + +Upload a neopig archive (.tar.gz or .run) to explore it
+ +{{ t.live_subtitle }}
+ +{{ t.phantom_subtitle }}
+ + + +