# huggingvideo.py - Полный видеохостинг на Hugging Face Spaces import os, json, uuid, hashlib, sqlite3, subprocess, shutil, time, threading, re from datetime import datetime from pathlib import Path from functools import wraps from flask import Flask, request, render_template_string, jsonify, send_file, abort, url_for, send_from_directory, stream_with_context, Response, redirect from werkzeug.utils import secure_filename import cv2 import numpy as np BASE_DIR = "/data" VIDEO_DIR = os.path.join(BASE_DIR, "videos") THUMBNAIL_DIR = os.path.join(BASE_DIR, "thumbnails") CACHE_DIR = os.path.join(BASE_DIR, "cache") METADATA_DIR = os.path.join(BASE_DIR, "metadata") DB_PATH = os.path.join(BASE_DIR, "huggingvideo.db") for dir_path in [VIDEO_DIR, THUMBNAIL_DIR, CACHE_DIR, METADATA_DIR]: os.makedirs(dir_path, exist_ok=True) MAX_FILE_SIZE = 500 * 1024 * 1024 ALLOWED_EXTENSIONS = {'mp4', 'webm', 'avi', 'mov', 'mkv', 'flv', 'wmv', 'm4v', '3gp'} CHUNK_SIZE = 1024 * 1024 MAX_VIDEO_DURATION = 3600 VERSION = "1.0.0" app = Flask(__name__) app.config['SECRET_KEY'] = os.urandom(24) app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE def init_db(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS videos ( id TEXT PRIMARY KEY, title TEXT, description TEXT, filename TEXT, size INTEGER, duration INTEGER, width INTEGER, height INTEGER, views INTEGER DEFAULT 0, likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, upload_date TIMESTAMP, last_viewed TIMESTAMP, status TEXT DEFAULT 'processing', format TEXT, video_url TEXT, thumbnail_url TEXT, hls_url TEXT, category TEXT, tags TEXT, is_public INTEGER DEFAULT 1 )''') c.execute('''CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT UNIQUE, password_hash TEXT, created_at TIMESTAMP )''') c.execute('''CREATE TABLE IF NOT EXISTS playlists ( id TEXT PRIMARY KEY, name TEXT, user_id TEXT, created_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) )''') c.execute('''CREATE TABLE IF NOT EXISTS playlist_videos ( playlist_id TEXT, video_id TEXT, position INTEGER, FOREIGN KEY (playlist_id) REFERENCES playlists (id), FOREIGN KEY (video_id) REFERENCES videos (id) )''') c.execute('''CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, video_id TEXT, user_id TEXT, text TEXT, created_at TIMESTAMP, likes INTEGER DEFAULT 0, FOREIGN KEY (video_id) REFERENCES videos (id) )''') conn.commit() conn.close() init_db() def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def get_video_info(filepath): try: cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', filepath] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) data = json.loads(result.stdout) info = {'duration': 0, 'width': 0, 'height': 0, 'format': '', 'size': os.path.getsize(filepath)} if 'format' in data: info['duration'] = int(float(data['format'].get('duration', 0))) info['format'] = data['format'].get('format_name', '') for stream in data.get('streams', []): if stream.get('codec_type') == 'video': info['width'] = int(stream.get('width', 0)) info['height'] = int(stream.get('height', 0)) break return info except: return {'duration': 0, 'width': 0, 'height': 0, 'format': 'unknown', 'size': 0} def generate_thumbnail(video_path, thumbnail_path, time_sec=5): try: cap = cv2.VideoCapture(video_path) fps = cap.get(cv2.CAP_PROP_FPS) frame_number = int(time_sec * fps) cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number) ret, frame = cap.read() cap.release() if ret: height, width = frame.shape[:2] scale = min(640 / width, 360 / height) new_width = int(width * scale) new_height = int(height * scale) frame = cv2.resize(frame, (new_width, new_height)) cv2.imwrite(thumbnail_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) return True except: pass return False def create_hls(video_path, output_dir): try: os.makedirs(output_dir, exist_ok=True) playlist_path = os.path.join(output_dir, 'playlist.m3u8') cmd = ['ffmpeg', '-i', video_path, '-c:v', 'libx264', '-crf', '23', '-preset', 'medium', '-c:a', 'aac', '-b:a', '128k', '-f', 'hls', '-hls_time', '10', '-hls_list_size', '0', '-hls_segment_filename', os.path.join(output_dir, 'segment_%03d.ts'), playlist_path] subprocess.run(cmd, check=True, capture_output=True, timeout=300) return playlist_path except: return None def compress_video(input_path, output_path, quality='medium'): qualities = { 'low': ['-c:v', 'libx264', '-crf', '28', '-preset', 'fast'], 'medium': ['-c:v', 'libx264', '-crf', '23', '-preset', 'medium'], 'high': ['-c:v', 'libx264', '-crf', '18', '-preset', 'slow'] } try: cmd = ['ffmpeg', '-i', input_path, *qualities[quality], '-c:a', 'aac', '-b:a', '128k', output_path] subprocess.run(cmd, check=True, capture_output=True, timeout=600) return True except: return False def get_dir_size(path): total = 0 for entry in os.scandir(path): if entry.is_file(): total += entry.stat().st_size elif entry.is_dir(): total += get_dir_size(entry.path) return total @app.route('/') def index(): return render_template_string(HTML_TEMPLATE) @app.route('/api/videos', methods=['GET']) def get_videos(): page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 20)) offset = (page - 1) * per_page category = request.args.get('category', '') conn = sqlite3.connect(DB_PATH) c = conn.cursor() query = 'SELECT COUNT(*) FROM videos WHERE status = "ready"' params = [] if category: query += ' AND category = ?' params.append(category) c.execute(query, params) total = c.fetchone()[0] query = '''SELECT id, title, description, filename, size, duration, width, height, views, likes, dislikes, upload_date, format, thumbnail_url, video_url, hls_url, category, tags FROM videos WHERE status = "ready"''' if category: query += ' AND category = ?' query += ' ORDER BY upload_date DESC LIMIT ? OFFSET ?' params.extend([per_page, offset]) c.execute(query, params) videos = [] for row in c.fetchall(): videos.append({ 'id': row[0], 'title': row[1], 'description': row[2], 'filename': row[3], 'size': row[4], 'duration': row[5], 'width': row[6], 'height': row[7], 'views': row[8], 'likes': row[9], 'dislikes': row[10], 'upload_date': row[11], 'format': row[12], 'thumbnail_url': row[13] or f"/api/thumbnail/{row[0]}", 'video_url': row[14] or f"/api/video/{row[0]}", 'hls_url': row[15], 'category': row[16] or 'other', 'tags': row[17] or '' }) conn.close() return jsonify({'videos': videos, 'total': total, 'page': page, 'per_page': per_page, 'total_pages': (total + per_page - 1) // per_page}) @app.route('/api/video/', methods=['GET']) def get_video(video_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''SELECT id, title, description, filename, size, duration, width, height, views, likes, dislikes, upload_date, format, thumbnail_url, video_url, hls_url, status, category, tags FROM videos WHERE id = ?''', (video_id,)) row = c.fetchone() if not row: conn.close() return jsonify({'error': 'Video not found'}), 404 c.execute('UPDATE videos SET views = views + 1, last_viewed = CURRENT_TIMESTAMP WHERE id = ?', (video_id,)) conn.commit() conn.close() return jsonify({ 'id': row[0], 'title': row[1], 'description': row[2], 'filename': row[3], 'size': row[4], 'duration': row[5], 'width': row[6], 'height': row[7], 'views': row[8], 'likes': row[9], 'dislikes': row[10], 'upload_date': row[11], 'format': row[12], 'thumbnail_url': row[13] or f"/api/thumbnail/{row[0]}", 'video_url': row[14] or f"/api/video/{row[0]}", 'hls_url': row[15], 'status': row[16], 'category': row[17] or 'other', 'tags': row[18] or '' }) @app.route('/api/video//stream') def stream_video(video_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('SELECT filename, video_url FROM videos WHERE id = ?', (video_id,)) row = c.fetchone() conn.close() if not row: return jsonify({'error': 'Video not found'}), 404 video_path = os.path.join(VIDEO_DIR, row[0]) if not os.path.exists(video_path): if row[1]: return redirect(row[1]) return jsonify({'error': 'Video unavailable'}), 404 size = os.path.getsize(video_path) range_header = request.headers.get('Range', None) if not range_header: return send_file(video_path, mimetype='video/mp4') try: byte_range = range_header.replace('bytes=', '').split('-') byte_start = int(byte_range[0]) if byte_range[0] else 0 byte_end = int(byte_range[1]) if len(byte_range) > 1 and byte_range[1] else size - 1 byte_end = min(byte_end, size - 1) except: byte_start = 0 byte_end = size - 1 length = byte_end - byte_start + 1 def generate(): with open(video_path, 'rb') as f: f.seek(byte_start) remaining = length while remaining > 0: chunk = f.read(min(CHUNK_SIZE, remaining)) if not chunk: break remaining -= len(chunk) yield chunk response = Response(generate(), 206, mimetype='video/mp4') response.headers.add('Content-Range', f'bytes {byte_start}-{byte_end}/{size}') response.headers.add('Accept-Ranges', 'bytes') response.headers.add('Content-Length', str(length)) response.headers.add('Cache-Control', 'public, max-age=86400') return response @app.route('/api/upload', methods=['POST']) def upload_video(): if 'video' not in request.files: return jsonify({'error': 'No file'}), 400 file = request.files['video'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 if not allowed_file(file.filename): return jsonify({'error': 'Unsupported format'}), 400 filename = secure_filename(file.filename) video_id = str(uuid.uuid4()) safe_filename = f"{video_id}_{filename}" filepath = os.path.join(VIDEO_DIR, safe_filename) file.save(filepath) info = get_video_info(filepath) duration = info['duration'] if duration > MAX_VIDEO_DURATION: os.remove(filepath) return jsonify({'error': f'Video too long (max {MAX_VIDEO_DURATION}s)'}), 400 thumbnail_filename = f"{video_id}.jpg" thumbnail_path = os.path.join(THUMBNAIL_DIR, thumbnail_filename) generate_thumbnail(filepath, thumbnail_path, min(5, duration // 2)) title = request.form.get('title', filename) description = request.form.get('description', '') category = request.form.get('category', 'other') tags = request.form.get('tags', '') conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''INSERT INTO videos (id, title, description, filename, size, duration, width, height, upload_date, format, status, thumbnail_url, category, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?)''', (video_id, title, description, safe_filename, info['size'], duration, info['width'], info['height'], info['format'], 'processing', f"/api/thumbnail/{video_id}", category, tags)) conn.commit() conn.close() thread = threading.Thread(target=process_video, args=(video_id, filepath)) thread.start() return jsonify({'id': video_id, 'message': 'Video uploaded, processing started', 'status': 'processing'}) def process_video(video_id, filepath): try: compressed_filename = f"{video_id}_compressed.mp4" compressed_path = os.path.join(VIDEO_DIR, compressed_filename) compress_video(filepath, compressed_path, 'medium') hls_dir = os.path.join(CACHE_DIR, video_id) hls_path = create_hls(compressed_path, hls_dir) conn = sqlite3.connect(DB_PATH) c = conn.cursor() if hls_path: c.execute('''UPDATE videos SET status = 'ready', video_url = ?, hls_url = ? WHERE id = ?''', (f"/api/video/{video_id}/stream", f"/api/hls/{video_id}/playlist.m3u8", video_id)) else: c.execute('''UPDATE videos SET status = 'ready', video_url = ? WHERE id = ?''', (f"/api/video/{video_id}/stream", video_id)) conn.commit() conn.close() except Exception as e: conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('UPDATE videos SET status = "error" WHERE id = ?', (video_id,)) conn.commit() conn.close() @app.route('/api/thumbnail/') def get_thumbnail(video_id): thumbnail_path = os.path.join(THUMBNAIL_DIR, f"{video_id}.jpg") if os.path.exists(thumbnail_path): return send_file(thumbnail_path, mimetype='image/jpeg') return '', 404 @app.route('/api/hls//') def get_hls_segment(video_id, filename): hls_dir = os.path.join(CACHE_DIR, video_id) filepath = os.path.join(hls_dir, filename) if os.path.exists(filepath): return send_file(filepath, mimetype='application/vnd.apple.mpegurl' if filename.endswith('.m3u8') else 'video/MP2T') return jsonify({'error': 'Segment not found'}), 404 @app.route('/api/search', methods=['GET']) def search_videos(): query = request.args.get('q', '').strip() if not query: return get_videos() conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''SELECT id, title, description, filename, size, duration, width, height, views, likes, dislikes, upload_date, format, thumbnail_url, video_url, hls_url, category, tags FROM videos WHERE status = "ready" AND (title LIKE ? OR description LIKE ? OR tags LIKE ?) ORDER BY upload_date DESC LIMIT 50''', (f'%{query}%', f'%{query}%', f'%{query}%')) videos = [] for row in c.fetchall(): videos.append({ 'id': row[0], 'title': row[1], 'description': row[2], 'filename': row[3], 'size': row[4], 'duration': row[5], 'width': row[6], 'height': row[7], 'views': row[8], 'likes': row[9], 'dislikes': row[10], 'upload_date': row[11], 'format': row[12], 'thumbnail_url': row[13] or f"/api/thumbnail/{row[0]}", 'video_url': row[14] or f"/api/video/{row[0]}", 'hls_url': row[15], 'category': row[16] or 'other', 'tags': row[17] or '' }) conn.close() return jsonify({'videos': videos, 'query': query}) @app.route('/api/like/', methods=['POST']) def like_video(video_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('UPDATE videos SET likes = likes + 1 WHERE id = ?', (video_id,)) conn.commit() c.execute('SELECT likes FROM videos WHERE id = ?', (video_id,)) likes = c.fetchone()[0] conn.close() return jsonify({'likes': likes}) @app.route('/api/dislike/', methods=['POST']) def dislike_video(video_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('UPDATE videos SET dislikes = dislikes + 1 WHERE id = ?', (video_id,)) conn.commit() c.execute('SELECT dislikes FROM videos WHERE id = ?', (video_id,)) dislikes = c.fetchone()[0] conn.close() return jsonify({'dislikes': dislikes}) @app.route('/api/comment/', methods=['POST']) def add_comment(video_id): data = request.json text = data.get('text', '').strip() if not text: return jsonify({'error': 'Comment cannot be empty'}), 400 comment_id = str(uuid.uuid4()) conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''INSERT INTO comments (id, video_id, user_id, text, created_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)''', (comment_id, video_id, 'anonymous', text)) conn.commit() c.execute('''SELECT id, text, created_at, likes FROM comments WHERE video_id = ? ORDER BY created_at DESC LIMIT 1''', (video_id,)) row = c.fetchone() conn.close() return jsonify({ 'id': row[0], 'text': row[1], 'created_at': row[2], 'likes': row[3] }) @app.route('/api/comments/', methods=['GET']) def get_comments(video_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''SELECT id, text, created_at, likes FROM comments WHERE video_id = ? ORDER BY created_at DESC LIMIT 100''', (video_id,)) comments = [{'id': row[0], 'text': row[1], 'created_at': row[2], 'likes': row[3]} for row in c.fetchall()] conn.close() return jsonify({'comments': comments}) @app.route('/api/stats') def get_stats(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('SELECT COUNT(*) FROM videos') total_videos = c.fetchone()[0] c.execute('SELECT SUM(size) FROM videos') total_size = c.fetchone()[0] or 0 c.execute('SELECT SUM(views) FROM videos') total_views = c.fetchone()[0] or 0 conn.close() return jsonify({ 'total_videos': total_videos, 'total_size': total_size, 'total_views': total_views, 'video_dir_size': get_dir_size(VIDEO_DIR), 'thumb_dir_size': get_dir_size(THUMBNAIL_DIR), 'cache_dir_size': get_dir_size(CACHE_DIR), 'disk_free': shutil.disk_usage(BASE_DIR).free, 'disk_total': shutil.disk_usage(BASE_DIR).total, 'version': VERSION }) @app.route('/api/delete/', methods=['DELETE']) def delete_video(video_id): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('SELECT filename FROM videos WHERE id = ?', (video_id,)) row = c.fetchone() if not row: conn.close() return jsonify({'error': 'Video not found'}), 404 video_path = os.path.join(VIDEO_DIR, row[0]) if os.path.exists(video_path): os.remove(video_path) thumb_path = os.path.join(THUMBNAIL_DIR, f"{video_id}.jpg") if os.path.exists(thumb_path): os.remove(thumb_path) hls_dir = os.path.join(CACHE_DIR, video_id) if os.path.exists(hls_dir): shutil.rmtree(hls_dir) c.execute('DELETE FROM videos WHERE id = ?', (video_id,)) c.execute('DELETE FROM comments WHERE video_id = ?', (video_id,)) conn.commit() conn.close() return jsonify({'message': 'Video deleted'}) @app.route('/api/categories') def get_categories(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('SELECT DISTINCT category, COUNT(*) FROM videos WHERE status = "ready" GROUP BY category') categories = [{'name': row[0] or 'other', 'count': row[1]} for row in c.fetchall()] conn.close() return jsonify({'categories': categories}) HTML_TEMPLATE = ''' 🎬 HuggingVideo

🎬 HuggingVideo v1.0

📤 Загрузить видео

Перетащите файл или нажмите для выбора (до 500MB)

''' if __name__ == '__main__': app.run(host='0.0.0.0', port=7860, debug=True)