Download app.py from kphfrepo/gridview: direct link, hf CLI and curl.
- Browser
- Download file 22 kB
-
https://huggingface.co/spaces/kphfrepo/gridview/resolve/main/app.py
- Command line
-
hf download hf://spaces/kphfrepo/gridview/app.py
-
curl -L -o app.py https://huggingface.co/spaces/kphfrepo/gridview/resolve/main/app.py
22 kB
| import os | |
| import glob | |
| import uvicorn | |
| import urllib.parse | |
| from urllib.parse import unquote, quote | |
| import gradio as gr | |
| DEFAULT_DIR = "/bucket_data" | |
| # Authenticated WebDAV Base configuration | |
| WEBDAV_USER = os.getenv("WEBDAV_USER") | |
| WEBDAV_PASS = os.getenv("WEBDAV_PASS") | |
| WEBDAV_BASE_URL = f"https://{WEBDAV_USER}:{WEBDAV_PASS}@kphfrepo-webdav.hf.space" | |
| def load_live_cctv_dashboard(): | |
| try: | |
| print("\nโก Rocket-Loading local filesystem assets ...") | |
| image_exts = ('.png', '.jpg', '.jpeg', '.webp', '.gif') | |
| video_exts = ('.mp4', '.mkv', '.avi', '.mov', '.dav', '.ts') | |
| grouped_assets = {} | |
| video_count = 0 | |
| image_count = 0 | |
| for root, dirs, files in os.walk(DEFAULT_DIR): | |
| has_video = any(f.lower().endswith(video_exts) for f in files) | |
| if has_video: | |
| print(f"๐ฅ Video found in '{root}'. Skipping all its subfolders.") | |
| dirs.clear() | |
| for filename in files: | |
| if filename.lower().endswith('.7z') or filename == ".gitattributes": | |
| continue | |
| f = os.path.join(root, filename) | |
| relative_path = os.path.relpath(f, DEFAULT_DIR).replace("\\", "/") | |
| if "/" in relative_path: | |
| folder_name = relative_path.split("/")[0] | |
| else: | |
| folder_name = "Root Directory" | |
| if folder_name not in grouped_assets: | |
| grouped_assets[folder_name] = {"images": [], "videos": []} | |
| abs_path = os.path.abspath(f) | |
| encoded_path = urllib.parse.quote(abs_path) | |
| web_url = f"{encoded_path}" | |
| if filename.lower().endswith(video_exts): | |
| video_count += 1 | |
| grouped_assets[folder_name]["videos"].append({ | |
| "name": filename, | |
| "video_url": web_url | |
| }) | |
| elif filename.lower().endswith(image_exts): | |
| image_count += 1 | |
| grouped_assets[folder_name]["images"].append({ | |
| "name": filename, | |
| "image_url": web_url | |
| }) | |
| print(f"โ Chunk Load Finished. Mapped {video_count} videos with URLs natively.\n") | |
| return grouped_assets | |
| except Exception as e: | |
| print(f"โ Chunk Scan Error: {str(e)}") | |
| return {} | |
| # ๐ ๏ธ JavaScript State Engine + Dynamic Multi-Frame Thumbnail Handler (FIXED URL JOIN) | |
| custom_js = """ | |
| function() { | |
| window.videoTimeRegistry = window.videoTimeRegistry || {}; | |
| // 2. Thumbnail Engine utilizing URL path structures safely | |
| function initThumbStrip(placeholder) { | |
| // 1. Guard Clauses & State Locks | |
| if (placeholder.dataset.thumbInit === "1") return; | |
| let base = placeholder.getAttribute('data-thumb-base'); | |
| if (!base) return; | |
| // Ensure the base path ends with a clean slash for asset indexing | |
| if (!base.endsWith('/')) { | |
| base += '/'; | |
| } | |
| placeholder.dataset.thumbInit = "1"; | |
| placeholder.style.position = "relative"; | |
| placeholder.style.overflow = "hidden"; | |
| // 2. DOM Setup for Poster Image | |
| let thumbImg = placeholder.querySelector('.native-thumb-poster'); | |
| if (!thumbImg) { | |
| thumbImg = document.createElement('img'); | |
| thumbImg.className = 'native-thumb-poster'; | |
| thumbImg.style.cssText = "position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:1; pointer-events:none;"; | |
| // Handle broken image paths gracefully without breaking layout | |
| thumbImg.onerror = () => { | |
| console.warn(`โ ๏ธ Thumbnail failed to load at path: ${base}thumbnail1.webp`); | |
| thumbImg.style.display = 'none'; | |
| placeholder.style.backgroundColor = '#1e293b'; | |
| }; | |
| placeholder.appendChild(thumbImg); | |
| } | |
| // Keep play buttons and titles on top of the thumbnail image | |
| placeholder.querySelectorAll('span, div').forEach((el) => { | |
| if (el !== thumbImg) { | |
| el.style.position = "relative"; | |
| el.style.zIndex = "2"; | |
| } | |
| }); | |
| // 3. Asset Ingestion & Image Preloading | |
| thumbImg.src = base + "thumbnail1.webp"; | |
| let currentFrame = 1; | |
| let loadedFramesCount = 0; | |
| const preloadedImages = []; | |
| for (let i = 1; i <= 5; i++) { | |
| const img = new Image(); | |
| img.onload = () => { loadedFramesCount++; }; // Track successfully loaded frames | |
| img.src = base + "thumbnail" + i + ".webp"; | |
| preloadedImages.push(img); | |
| console.log(`Loaded Thumbnail Path: ${img.src}`); | |
| } | |
| // 4. Safe Rotator Engine | |
| if (placeholder.cycleInterval) clearInterval(placeholder.cycleInterval); | |
| placeholder.cycleInterval = setInterval(() => { | |
| // Only cycle frames if assets are completing network transit | |
| if (loadedFramesCount >= 2) { | |
| currentFrame = (currentFrame % 5) + 1; | |
| thumbImg.src = base + "thumbnail" + currentFrame + ".webp"; | |
| } | |
| }, 2000); | |
| } | |
| // VIEWPORT OBSERVER TO CONSERVE NETWORK TRAFFIC | |
| const thumbObserver = new IntersectionObserver((entries) => { | |
| entries.forEach((entry) => { | |
| const placeholder = entry.target; | |
| if (entry.isIntersecting) { | |
| initThumbStrip(placeholder); | |
| } else { | |
| if (placeholder.cycleInterval) { | |
| clearInterval(placeholder.cycleInterval); | |
| placeholder.cycleInterval = null; | |
| } | |
| placeholder.dataset.thumbInit = "0"; | |
| } | |
| }); | |
| }, { | |
| root: null, | |
| threshold: 0.1, | |
| rootMargin: "200px 0px" | |
| }); | |
| function observeThumbPlaceholders() { | |
| document.querySelectorAll('.video-placeholder').forEach((el) => { | |
| thumbObserver.observe(el); | |
| }); | |
| } | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", observeThumbPlaceholders); | |
| } else { | |
| observeThumbPlaceholders(); | |
| } | |
| const domObserver = new MutationObserver(() => { | |
| observeThumbPlaceholders(); | |
| }); | |
| domObserver.observe(document.body, { childList: true, subtree: true }); | |
| // Video Player Stream Mounting & State Management Toggle | |
| document.body.addEventListener('click', function(e) { | |
| const placeholder = e.target.closest('.video-placeholder'); | |
| if (!placeholder) return; | |
| const container = placeholder.parentElement; | |
| const videoUrl = placeholder.getAttribute('data-video-url'); | |
| if (placeholder.cycleInterval) { | |
| clearInterval(placeholder.cycleInterval); | |
| } | |
| const currentActiveVideo = document.querySelector('.active-dashboard-player'); | |
| if (currentActiveVideo) { | |
| const activeUrl = currentActiveVideo.getAttribute('data-video-url'); | |
| window.videoTimeRegistry[activeUrl] = currentActiveVideo.currentTime; | |
| currentActiveVideo.pause(); | |
| const activeContainer = currentActiveVideo.parentElement; | |
| try { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = currentActiveVideo.videoWidth || currentActiveVideo.clientWidth || 420; | |
| canvas.height = currentActiveVideo.videoHeight || currentActiveVideo.clientHeight || 250; | |
| canvas.style.cssText = "width: 100%; height: 100%; object-fit: contain; position: absolute; top: 0; left: 0; background: #000000;"; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(currentActiveVideo, 0, 0, canvas.width, canvas.height); | |
| activeContainer.innerHTML = ''; | |
| activeContainer.style.position = 'relative'; | |
| activeContainer.appendChild(canvas); | |
| } catch(err) { | |
| activeContainer.innerHTML = `<div style="width: 100%; height: 100%; background: #1e293b;"></div>`; | |
| } | |
| currentActiveVideo.removeAttribute('src'); | |
| currentActiveVideo.load(); | |
| const fakePlaceholder = document.createElement('div'); | |
| fakePlaceholder.className = 'video-placeholder'; | |
| fakePlaceholder.setAttribute('data-video-url', activeUrl); | |
| fakePlaceholder.setAttribute('data-thumb-base', placeholder.getAttribute('data-thumb-base')); | |
| fakePlaceholder.style.cssText = "position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: pointer; z-index: 10; display: flex; align-items: center; justify-content: center; color: white;"; | |
| fakePlaceholder.innerHTML = `<span style="font-size: 2rem; opacity: 0.4; pointer-events: none;">โถ๏ธ</span>`; | |
| activeContainer.appendChild(fakePlaceholder); | |
| } | |
| const savedTime = window.videoTimeRegistry[videoUrl] || 0; | |
| container.style.position = 'relative'; | |
| container.innerHTML = ` | |
| <video class="active-dashboard-player" src="${videoUrl}" data-video-url="${videoUrl}" controls autoplay style="width: 100%; height: 100%; max-height: 250px; object-fit: contain; display: block; z-index: 5; position: relative;"></video> | |
| `; | |
| const newVideoPlayer = container.querySelector('video'); | |
| newVideoPlayer.addEventListener('loadedmetadata', function() { | |
| if (savedTime > 0) { | |
| newVideoPlayer.currentTime = savedTime; | |
| } | |
| }); | |
| }); | |
| } | |
| """ | |
| custom_img_js = """<style>.zoomable-img { cursor: zoom-in; transition: transform 0.2s ease; }.zoomable-img:hover { transform: scale(1.03); }.img-modal-overlay { display: none; position: fixed !important; z-index: 2147483647 !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; width: 100vw !important; height: 100vh !important; background-color: rgba(15, 23, 42, 0.95) !important; align-items: center !important; justify-content: center !important;}.img-modal-wrapper { position: relative; width: 100%; height: 100%; display: flex !important; align-items: center !important; justify-content: center !important; }.img-modal-content { max-width: 90% !important; max-height: 90vh !important; object-fit: contain !important; border-radius: 8px; box-shadow: 0 30px 60px -15px rgba(0, 0, 0, 0.85); animation: galleryZoom 0.15s cubic-bezier(0.16, 1, 0.3, 1); }.gallery-nav-btn { position: absolute !important; top: 50% !important; transform: translateY(-50%) !important; background: rgba(15, 23, 42, 0.85) !important; color: #ffffff !important; border: 1px solid rgba(255, 255, 255, 0.2) !important; font-size: 2rem !important; padding: 10px 18px !important; cursor: pointer !important; border-radius: 50% !important; z-index: 2147483647 !important; user-select: none !important; }.gallery-prev { left: 40px !important; }.gallery-next { right: 40px !important; }@keyframes galleryZoom { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }</style><div id="imageModalContainer" class="img-modal-overlay" onclick="if(event.target === this) window.closeImageModal()"> <div class="img-modal-wrapper"> <button class="gallery-nav-btn gallery-prev" onclick="window.changeGalleryImage(-1, event)">โ</button> <img id="modalTargetImage" class="img-modal-content" src="" alt="Enlarged view" onclick="window.closeImageModal()" style="cursor: zoom-out;" /> <button class="gallery-nav-btn gallery-next" onclick="window.changeGalleryImage(1, event)">โถ</button> </div></div><script>(function() { window.galleryImages = []; window.currentGalleryIndex = -1; function relocateModalToBody() { const modal = document.getElementById('imageModalContainer'); if (modal && modal.parentElement && modal.parentElement !== document.body) { document.body.appendChild(modal); } } setTimeout(relocateModalToBody, 100); setTimeout(relocateModalToBody, 500); | |
| window.openImageModal = function(clickedElement) { relocateModalToBody(); const modal = document.getElementById('imageModalContainer'); const modalImg = document.getElementById('modalTargetImage'); if (!modal || !modalImg) return; const rawElements = document.querySelectorAll('.zoomable-img'); window.galleryImages = Array.from(rawElements).map(img => img.src); window.currentGalleryIndex = Array.from(rawElements).indexOf(clickedElement); modalImg.src = window.galleryImages[window.currentGalleryIndex]; modal.style.setProperty('display', 'flex', 'important'); }; window.changeGalleryImage = function(direction, event) { if (event) event.stopPropagation(); if (window.galleryImages.length <= 1) return; window.currentGalleryIndex = (window.currentGalleryIndex + direction + window.galleryImages.length) % window.galleryImages.length; document.getElementById('modalTargetImage').src = window.galleryImages[window.currentGalleryIndex]; }; window.closeImageModal = function() { const modal = document.getElementById('imageModalContainer'); if (modal) modal.style.setProperty('display', 'none', 'important'); }; document.addEventListener('keydown', function(e) { const modal = document.getElementById('imageModalContainer'); if (!modal || modal.style.display === 'none') return; if (e.key === 'ArrowRight' || e.key.toLowerCase() === 'd') window.changeGalleryImage(1); else if (e.key === 'ArrowLeft' || e.key.toLowerCase() === 'a') window.changeGalleryImage(-1); else if (e.key === 'Escape') window.closeImageModal(); });})();</script>""" | |
| gr.set_static_paths(paths=[DEFAULT_DIR]) | |
| with gr.Blocks(title="CCTV Ultimate Explorer", theme=gr.themes.Soft(), js=custom_js) as demo: | |
| init_trigger = gr.HTML(custom_img_js, visible=True) | |
| gr.Markdown("# ๐น CCTV Structured Media Monitor (Instant Chunk Thumbnail View)") | |
| refresh_btn = gr.Button("๐ Sync Latest Camera Footage", variant="primary") | |
| def render_ui(*args): | |
| print("\n--- [RENDER START] ---") | |
| assets = load_live_cctv_dashboard() | |
| if not assets: | |
| gr.Markdown("### โ No media data discovered inside the local folders.") | |
| return | |
| html_buffer = [] | |
| for folder, media in assets.items(): | |
| html_buffer.append(f""" | |
| <div class="folder-block" style="margin-top: 20px; margin-bottom: 40px; font-family: system-ui, sans-serif; clear: both; display: block;"> | |
| <hr style='border: 0; border-top: 2px solid #4ade80; margin: 20px 0 30px 0;'/> | |
| <div style="margin-bottom: 20px;"> | |
| <h2 style="margin: 0; font-size: 1.4rem; color: #1f2937; display: flex; align-items: center; gap: 8px; font-weight: 700;"> | |
| <span>๐</span> Camera Channel / Directory: | |
| <code style="background: #eef2f6; color: #1e293b; padding: 4px 10px; border-radius: 6px; font-size: 1.1rem; border: 1px solid #cbd5e1; font-family: monospace;">{folder}</code> | |
| </h2> | |
| </div> | |
| """) | |
| if media["videos"]: | |
| html_buffer.append('<h3 style="color: #4b5563; font-size: 1.05rem; font-weight: 600; margin: 15px 0 12px 0;">๐ฌ Playable Video Feeds</h3>') | |
| html_buffer.append('<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 20px; margin-bottom: 25px; width: 100%;">') | |
| for video_item in media["videos"]: | |
| video_path = video_item['video_url'].lstrip('/') | |
| public_video_url = f"{WEBDAV_BASE_URL}/{video_path}" | |
| # print(f"{public_video_url}") | |
| decoded_path = unquote(video_path) | |
| # ๐ ๏ธ STRIP EXTENSION FOR BASE URL LOOKUP: | |
| # Trims path from "folder/clip.mp4" down to "folder/clip" | |
| path_without_ext, _ = os.path.splitext(decoded_path) | |
| thumb_folder_encoded = quote(path_without_ext, safe="/") | |
| thumb_base_url = f"{WEBDAV_BASE_URL}/{thumb_folder_encoded}" | |
| # print(f"{thumb_base_url}") | |
| html_buffer.append(f""" | |
| <div style="background: #ffffff; border: 1px solid #e2e8f0; padding: 14px; border-radius: 10px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); display: flex; flex-direction: column; justify-content: space-between; max-width: 100%; box-sizing: border-box;"> | |
| <p style="margin: 0 0 10px 0; font-size: 13px; color: #334155; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;" title="{video_item['name']}"> | |
| <strong>Clip ID:</strong> <code style="background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 12px; color: #0f172a;">{video_item['name']}</code> | |
| </p> | |
| <div class="video-wrapper" style="background: #000000; border-radius: 6px; overflow: hidden; width: 100%; height: 250px; position: relative; display: flex; align-items: center; justify-content: center;"> | |
| <div class="video-placeholder" data-video-url="{public_video_url}" data-thumb-base="{thumb_base_url}" style="width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #1e293b; cursor: pointer; color: white; position: relative; z-index: 2;"> | |
| <span style="font-size: 3rem; z-index: 3;">โถ๏ธ</span> | |
| <span style="font-size: 12px; margin-top: 8px; opacity: 0.9; z-index: 3; text-shadow: 0px 2px 4px rgba(0,0,0,0.8);">Click to Stream Feed</span> | |
| </div> | |
| </div> | |
| </div> | |
| """) | |
| html_buffer.append('</div>') | |
| else: | |
| html_buffer.append('<p style="font-style: italic; color: #94a3b8; margin: 10px 0 25px 10px; font-size: 14px;">*No video tracks detected.*</p>') | |
| if media["images"]: | |
| html_buffer.append('<h3 style="color: #4b5563; font-size: 1.05rem; font-weight: 600; margin: 15px 0 12px 0;">๐ผ Camera Snapshots Gallery</h3>') | |
| html_buffer.append('<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; margin-bottom: 25px; width: 100%;">') | |
| for img_item in media["images"]: | |
| img_path = img_item['image_url'].lstrip('/') | |
| public_image_url = f"{WEBDAV_BASE_URL}/{img_path}" | |
| html_buffer.append(f""" | |
| <div style="border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; background: #0f172a; height: 160px; display: flex; align-items: center; justify-content: center;"> | |
| <img class="zoomable-img" src="{public_image_url}" loading="lazy" onclick="window.openImageModal(this)" style="max-width: 100%; max-height: 100%; object-fit: contain; display: block;" /> | |
| </div> | |
| """) | |
| html_buffer.append('</div>') | |
| html_buffer.append('</div>') | |
| gr.HTML("".join(html_buffer)) | |
| print("--- [RENDER END] ---\n") | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False, allowed_paths=["/bucket_data"]) |