Spaces:
No application file
No application file
| #!/usr/bin/env python3 | |
| """ | |
| Video Player Web App - Read-only video streaming server. | |
| Loads videos from the 'playlist' folder and serves them via a web interface. | |
| """ | |
| import os | |
| import json | |
| import mimetypes | |
| from http.server import HTTPServer, SimpleHTTPRequestHandler | |
| from urllib.parse import unquote, urlparse | |
| import re | |
| PLAYLIST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "playlist") | |
| PORT = 8080 | |
| VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov", ".m4v", ".avi", ".mkv", ".ogg", ".ogv"} | |
| def get_video_files(): | |
| """Get list of video files from playlist directory.""" | |
| videos = [] | |
| if not os.path.isdir(PLAYLIST_DIR): | |
| return videos | |
| for filename in sorted(os.listdir(PLAYLIST_DIR)): | |
| ext = os.path.splitext(filename)[1].lower() | |
| if ext in VIDEO_EXTENSIONS: | |
| filepath = os.path.join(PLAYLIST_DIR, filename) | |
| size = os.path.getsize(filepath) | |
| videos.append({ | |
| "name": filename, | |
| "title": os.path.splitext(filename)[0].replace("_", " ").replace("-", " ").title(), | |
| "path": f"/playlist/{filename}", | |
| "size": size, | |
| "size_human": f"{size / (1024*1024):.1f} MB" | |
| }) | |
| return videos | |
| class VideoPlayerHandler(SimpleHTTPRequestHandler): | |
| """Custom handler for video player - read-only, no modifications allowed.""" | |
| def do_GET(self): | |
| path = urlparse(self.path).path | |
| if path == "/" or path == "/index.html": | |
| self.serve_index() | |
| elif path == "/api/playlist": | |
| self.serve_playlist_api() | |
| elif path.startswith("/playlist/"): | |
| self.serve_video(path) | |
| elif path == "/style.css": | |
| self.serve_static("style.css", "text/css") | |
| elif path == "/script.js": | |
| self.serve_static("script.js", "application/javascript") | |
| else: | |
| self.send_error(404, "Not Found") | |
| def do_POST(self): | |
| self.send_error(405, "Method Not Allowed - Read Only") | |
| def do_PUT(self): | |
| self.send_error(405, "Method Not Allowed - Read Only") | |
| def do_DELETE(self): | |
| self.send_error(405, "Method Not Allowed - Read Only") | |
| def serve_index(self): | |
| """Serve the main HTML page.""" | |
| html = self.get_index_html() | |
| self.send_response(200) | |
| self.send_header("Content-Type", "text/html; charset=utf-8") | |
| self.send_header("Content-Length", len(html.encode())) | |
| self.end_headers() | |
| self.wfile.write(html.encode()) | |
| def serve_playlist_api(self): | |
| """Serve playlist as JSON.""" | |
| videos = get_video_files() | |
| data = json.dumps({"videos": videos}) | |
| self.send_response(200) | |
| self.send_header("Content-Type", "application/json") | |
| self.send_header("Content-Length", len(data.encode())) | |
| self.end_headers() | |
| self.wfile.write(data.encode()) | |
| def serve_video(self, path): | |
| """Serve video file with range support for seeking.""" | |
| filename = unquote(path.replace("/playlist/", "")) | |
| if "/" in filename or ".." in filename: | |
| self.send_error(403, "Forbidden") | |
| return | |
| filepath = os.path.join(PLAYLIST_DIR, filename) | |
| if not os.path.isfile(filepath): | |
| self.send_error(404, "Video Not Found") | |
| return | |
| file_size = os.path.getsize(filepath) | |
| content_type, _ = mimetypes.guess_type(filepath) | |
| if not content_type: | |
| content_type = "video/mp4" | |
| range_header = self.headers.get("Range") | |
| if range_header: | |
| range_match = re.match(r"bytes=(\d+)-(\d*)", range_header) | |
| if range_match: | |
| start = int(range_match.group(1)) | |
| end = int(range_match.group(2)) if range_match.group(2) else file_size - 1 | |
| end = min(end, file_size - 1) | |
| length = end - start + 1 | |
| self.send_response(206) | |
| self.send_header("Content-Type", content_type) | |
| self.send_header("Content-Length", length) | |
| self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}") | |
| self.send_header("Accept-Ranges", "bytes") | |
| self.end_headers() | |
| with open(filepath, "rb") as f: | |
| f.seek(start) | |
| remaining = length | |
| chunk_size = 64 * 1024 | |
| while remaining > 0: | |
| chunk = f.read(min(chunk_size, remaining)) | |
| if not chunk: | |
| break | |
| self.wfile.write(chunk) | |
| remaining -= len(chunk) | |
| return | |
| self.send_response(200) | |
| self.send_header("Content-Type", content_type) | |
| self.send_header("Content-Length", file_size) | |
| self.send_header("Accept-Ranges", "bytes") | |
| self.end_headers() | |
| with open(filepath, "rb") as f: | |
| chunk_size = 64 * 1024 | |
| while True: | |
| chunk = f.read(chunk_size) | |
| if not chunk: | |
| break | |
| self.wfile.write(chunk) | |
| def serve_static(self, filename, content_type): | |
| """Serve static files (CSS, JS).""" | |
| filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) | |
| if not os.path.isfile(filepath): | |
| self.send_error(404, "Not Found") | |
| return | |
| with open(filepath, "r", encoding="utf-8") as f: | |
| content = f.read() | |
| self.send_response(200) | |
| self.send_header("Content-Type", f"{content_type}; charset=utf-8") | |
| self.send_header("Content-Length", len(content.encode())) | |
| self.end_headers() | |
| self.wfile.write(content.encode()) | |
| def get_index_html(self): | |
| return '''<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Video demos of apps</title> | |
| <link rel="stylesheet" href="/style.css"> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>๐ฌ Video demos of apps</h1> | |
| </header> | |
| <main> | |
| <div class="player-section"> | |
| <div class="video-container"> | |
| <video id="videoPlayer" controls> | |
| <source src="" type="video/mp4"> | |
| Your browser does not support the video tag. | |
| </video> | |
| <div class="video-overlay" id="videoOverlay"> | |
| <span>Select a video from the playlist</span> | |
| </div> | |
| </div> | |
| <div class="controls-bar"> | |
| <div class="now-playing" id="nowPlaying"> | |
| <span class="label">Now Playing:</span> | |
| <span class="title" id="currentTitle">-</span> | |
| </div> | |
| <div class="playback-controls"> | |
| <button id="prevBtn" title="Previous">โฎ</button> | |
| <button id="playPauseBtn" title="Play/Pause">โถ</button> | |
| <button id="nextBtn" title="Next">โญ</button> | |
| <button id="muteBtn" title="Mute">๐</button> | |
| <input type="range" id="volumeSlider" min="0" max="1" step="0.1" value="1" title="Volume"> | |
| <button id="fullscreenBtn" title="Fullscreen">โถ</button> | |
| </div> | |
| </div> | |
| </div> | |
| <aside class="playlist-section"> | |
| <h2>๐ Playlist</h2> | |
| <div class="playlist" id="playlist"> | |
| <div class="loading">Loading playlist...</div> | |
| </div> | |
| </aside> | |
| </main> | |
| <footer> | |
| <p>Read-only Video Player โข Videos cannot be modified or deleted</p> | |
| </footer> | |
| </div> | |
| <script src="/script.js"></script> | |
| </body> | |
| </html>''' | |
| def log_message(self, format, *args): | |
| print(f"[{self.log_date_time_string()}] {args[0]}") | |
| def main(): | |
| os.chdir(os.path.dirname(os.path.abspath(__file__))) | |
| videos = get_video_files() | |
| print(f"\n{'='*50}") | |
| print("๐ฌ Video Player Server") | |
| print(f"{'='*50}") | |
| print(f"๐ Playlist folder: {PLAYLIST_DIR}") | |
| print(f"๐ฅ Videos found: {len(videos)}") | |
| for v in videos: | |
| print(f" โข {v['name']} ({v['size_human']})") | |
| print(f"\n๐ Server starting at: http://localhost:{PORT}") | |
| print(f"{'='*50}\n") | |
| server = HTTPServer(("", PORT), VideoPlayerHandler) | |
| try: | |
| server.serve_forever() | |
| except KeyboardInterrupt: | |
| print("\n\nServer stopped.") | |
| server.shutdown() | |
| if __name__ == "__main__": | |
| main() | |