Last active
December 10, 2025 18:43
-
-
Save germanviscuso/ddf0200b6369a8012fedf53899e4cfc2 to your computer and use it in GitHub Desktop.
StreamFlix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| import logging | |
| import pymysql | |
| import json | |
| import os | |
| import boto3 | |
| import datetime | |
| import base64 | |
| import hashlib | |
| import hmac | |
| # --- CONFIGURACIÓN RDS (Desde Variables de Entorno) --- | |
| rds_host = os.environ.get('RDS_HOST') | |
| name = os.environ.get('DB_USER') | |
| password = os.environ.get('DB_PASSWORD') | |
| db_name = os.environ.get('DB_NAME') | |
| # --- CONFIGURACIÓN S3 --- | |
| bucket = os.environ.get('BUCKET_NAME') | |
| region = 'us-east-1' | |
| logger = logging.getLogger() | |
| logger.setLevel(logging.INFO) | |
| # --- CONEXIÓN A BASE DE DATOS --- | |
| try: | |
| conn = pymysql.connect(host=rds_host, user=name, passwd=password, db=db_name, connect_timeout=5, cursorclass=pymysql.cursors.DictCursor) | |
| logger.info("SUCCESS: Connection to RDS MySQL instance succeeded") | |
| except Exception as e: | |
| logger.error("ERROR: Unexpected error: Could not connect to MySQL instance.") | |
| logger.error(e) | |
| sys.exit() | |
| # --- FUNCIONES AUXILIARES S3 --- | |
| def sign(key, msg): | |
| return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest() | |
| def getSignatureKey(key, dateStamp, regionName, serviceName): | |
| kDate = sign(('AWS4' + key).encode('utf-8'), dateStamp) | |
| kRegion = sign(kDate, regionName) | |
| kService = sign(kRegion, serviceName) | |
| kSigning = sign(kService, 'aws4_request') | |
| return kSigning | |
| # --- HANDLER PRINCIPAL --- | |
| def lambda_handler(event, context): | |
| # Detectar qué quiere hacer el usuario | |
| # Puede venir por queryStringParameters (GET) o body (POST) | |
| action = "" | |
| # Intento leer 'action' de QueryString (para GET) | |
| if event.get("queryStringParameters") and "action" in event["queryStringParameters"]: | |
| action = event["queryStringParameters"]["action"] | |
| # Intento leer 'action' del Body (para POST - reviews) | |
| body_data = {} | |
| if event.get("body"): | |
| try: | |
| body_data = json.loads(event["body"]) | |
| if "action" in body_data: | |
| action = body_data["action"] | |
| except: | |
| pass | |
| logger.info(f"Accion solicitada: {action}") | |
| # 1. FIRMAR SUBIDA S3 (Tu código original adaptado) | |
| if action == "sign_upload": | |
| # Necesitamos credenciales del rol actual | |
| session = boto3.Session() | |
| creds = session.get_credentials().get_frozen_credentials() | |
| t = datetime.datetime.utcnow() | |
| amzDate = t.strftime('%Y%m%dT%H%M%SZ') | |
| dateStamp = t.strftime('%Y%m%d') | |
| policy_structure = { | |
| "expiration": "2025-12-30T12:00:00.000Z", | |
| "conditions": [ | |
| {"bucket": bucket}, | |
| ["starts-with", "$key", ""], | |
| {"success_action_status": "201"}, | |
| {"x-amz-credential": creds.access_key + "/" + dateStamp + "/" + region + "/s3/aws4_request"}, | |
| {"x-amz-algorithm": "AWS4-HMAC-SHA256"}, | |
| {"x-amz-date": amzDate}, | |
| {"x-amz-security-token": creds.token} | |
| ] | |
| } | |
| policy_json = json.dumps(policy_structure) | |
| stringToSign = base64.b64encode(policy_json.encode("utf-8")) | |
| signing_key = getSignatureKey(creds.secret_key, dateStamp, region, 's3') | |
| signature = hmac.new(signing_key, stringToSign, hashlib.sha256).hexdigest() | |
| return response(200, { | |
| 'stringSigned': signature, | |
| 'stringToSign': stringToSign.decode('utf-8'), | |
| 'xAmzCredential': creds.access_key + "/" + dateStamp + "/" + region + "/s3/aws4_request", | |
| 'dateStamp': dateStamp, | |
| 'amzDate': amzDate, | |
| 'securityToken': creds.token | |
| }) | |
| # 2. OBTENER REVIEWS (GET) | |
| elif action == "get_reviews": | |
| video_filename = event["queryStringParameters"].get("video") | |
| rows = [] | |
| with conn.cursor() as cur: | |
| # Ordenamos por fecha descendente | |
| cur.execute("SELECT user_name, comment, created_at FROM reviews WHERE video_filename = %s ORDER BY id DESC", (video_filename,)) | |
| rows = cur.fetchall() | |
| # Convertir fechas a string para JSON | |
| for row in rows: | |
| row['created_at'] = str(row['created_at']) | |
| return response(200, rows) | |
| # 3. GUARDAR REVIEW (POST) | |
| elif action == "add_review": | |
| video_filename = body_data.get("video") | |
| user = body_data.get("user") | |
| comment = body_data.get("comment") | |
| with conn.cursor() as cur: | |
| cur.execute("INSERT INTO reviews (video_filename, user_name, comment) VALUES (%s, %s, %s)", (video_filename, user, comment)) | |
| conn.commit() | |
| return response(200, {"message": "Review guardada"}) | |
| else: | |
| return response(400, {"error": "Accion no valida o no especificada"}) | |
| def response(status, body): | |
| return { | |
| 'statusCode': status, | |
| 'headers': { | |
| 'Access-Control-Allow-Origin': '*', | |
| 'Access-Control-Allow-Headers': 'Content-Type', | |
| 'Access-Control-Allow-Methods': 'OPTIONS,POST,GET' | |
| }, | |
| 'body': json.dumps(body) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>StreamFlix con Reviews</title> | |
| <style> | |
| body { font-family: 'Segoe UI', sans-serif; display: flex; flex-direction: column; align-items: center; background: #141414; color: #fff; margin:0; padding:20px; } | |
| header { width: 100%; max-width: 1000px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } | |
| h2 { color: #e50914; margin: 0; font-size: 2rem; } | |
| .btn-upload { background-color: #e50914; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; font-weight: bold; transition: 0.3s; } | |
| .btn-upload:hover { background-color: #f40612; } | |
| .container { display: flex; width: 100%; max-width: 1000px; gap: 20px; flex-wrap: wrap; } | |
| .main-content { flex: 3; min-width: 600px; } | |
| .video-wrapper { width: 100%; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.5); background: #000; margin-bottom: 20px;} | |
| video { width: 100%; display: block; } | |
| .playlist-section { flex: 1; background: #222; padding: 15px; border-radius: 8px; max-height: 500px; overflow-y: auto; min-width: 250px; } | |
| .playlist-section h3 { margin-top: 0; color: #aaa; font-size: 1rem; border-bottom: 1px solid #444; padding-bottom: 10px; } | |
| .video-item { display: block; width: 100%; padding: 12px; margin-bottom: 8px; background: #333; color: #ddd; border: none; text-align: left; cursor: pointer; border-radius: 4px; transition: 0.2s; } | |
| .video-item:hover { background: #444; } | |
| .video-item.active { background: #e50914; color: white; font-weight: bold; } | |
| /* --- SECCIÓN REVIEWS --- */ | |
| .reviews-section { background: #1f1f1f; padding: 20px; border-radius: 8px; margin-top: 20px; } | |
| .reviews-header { font-size: 1.2rem; margin-bottom: 15px; border-bottom: 1px solid #333; padding-bottom: 10px; } | |
| .review-card { background: #333; padding: 15px; border-radius: 4px; margin-bottom: 10px; } | |
| .review-user { color: #e50914; font-weight: bold; margin-bottom: 5px; font-size: 0.9rem; } | |
| .review-date { float: right; color: #777; font-size: 0.8rem; font-weight: normal; } | |
| .review-text { color: #ddd; font-size: 0.95rem; line-height: 1.4; } | |
| .review-form { margin-top: 20px; padding-top: 20px; border-top: 1px solid #333; } | |
| .review-form input, .review-form textarea { width: 100%; padding: 10px; margin-bottom: 10px; background: #141414; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box;} | |
| .review-form button { background: #e50914; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold; } | |
| .review-form button:hover { background: #f40612; } | |
| /* Toast */ | |
| #toast { visibility: hidden; min-width: 250px; background-color: #4CAF50; color: #fff; text-align: center; border-radius: 4px; padding: 16px; position: fixed; z-index: 10; left: 50%; bottom: 30px; transform: translateX(-50%); opacity: 0; transition: opacity 0.5s, bottom 0.5s; } | |
| #toast.show { visibility: visible; opacity: 1; bottom: 50px; } | |
| @media (max-width: 768px) { .container { flex-direction: column; } .main-content { min-width: 100%; } } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h2>🎥 StreamFlix</h2> | |
| <a href="upload.html" class="btn-upload">☁ Subir Video</a> | |
| </header> | |
| <div class="container"> | |
| <div class="main-content"> | |
| <div class="video-wrapper"> | |
| <video id="videoPlayer" controls autoplay> | |
| <source src="" type="video/mp4"> | |
| Tu navegador no soporta video. | |
| </video> | |
| </div> | |
| <h2 id="currentTitle" style="margin-top:0">Selecciona un video...</h2> | |
| <div class="reviews-section" id="reviewsArea" style="display:none;"> | |
| <div class="reviews-header">Opiniones de la Comunidad</div> | |
| <div id="reviewsList"> | |
| <p style="color:#777">Cargando opiniones...</p> | |
| </div> | |
| <div class="review-form"> | |
| <h4>Deja tu opinión</h4> | |
| <input type="text" id="reviewName" placeholder="Tu nombre" required> | |
| <textarea id="reviewComment" rows="3" placeholder="¿Qué te pareció el video?" required></textarea> | |
| <button onclick="postReview()">Publicar Comentario</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="playlist-section"> | |
| <h3>VIDEOS DISPONIBLES</h3> | |
| <div id="playlist">Cargando...</div> | |
| </div> | |
| </div> | |
| <div id="toast">Notificación</div> | |
| <script> | |
| // --- CONFIGURACIÓN --- | |
| const BUCKET_NAME = "MI_NOMBRE_BUCKET_S3"; // <--- EDITAR | |
| const LAMBDA_URL = "https://xxxxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/"; // <--- EDITAR | |
| const REGION = "us-east-1"; | |
| const BUCKET_URL = `https://${BUCKET_NAME}.s3.${REGION}.amazonaws.com/`; | |
| let currentVideoFile = ""; | |
| window.onload = async function() { | |
| checkToast(); | |
| loadPlaylist(); | |
| }; | |
| // 1. CARGAR LISTA DE VIDEOS (S3) | |
| async function loadPlaylist() { | |
| try { | |
| const response = await fetch(BUCKET_URL); | |
| if (!response.ok) throw new Error("Error bucket"); | |
| const text = await response.text(); | |
| const parser = new DOMParser(); | |
| const xmlDoc = parser.parseFromString(text, "text/xml"); | |
| const files = xmlDoc.getElementsByTagName("Key"); | |
| const playlistDiv = document.getElementById("playlist"); | |
| playlistDiv.innerHTML = ""; | |
| let count = 0; | |
| for (let i = 0; i < files.length; i++) { | |
| const filename = files[i].childNodes[0].nodeValue; | |
| if (filename.match(/\.(mp4|webm|mov|mkv)$/i)) { | |
| createButton(filename, playlistDiv); | |
| count++; | |
| } | |
| } | |
| if (count === 0) playlistDiv.innerHTML = "<p>No hay videos.</p>"; | |
| } catch (error) { | |
| console.error(error); | |
| document.getElementById("playlist").innerText = "Error cargando lista."; | |
| } | |
| } | |
| // 2. CREAR BOTÓN DE VIDEO | |
| function createButton(filename, container) { | |
| const btn = document.createElement("button"); | |
| btn.className = "video-item"; | |
| btn.innerText = "▶ " + filename; | |
| btn.onclick = () => playVideo(filename, btn); | |
| container.appendChild(btn); | |
| } | |
| // 3. REPRODUCIR VIDEO Y CARGAR REVIEWS | |
| function playVideo(filename, btnElement) { | |
| currentVideoFile = filename; | |
| // UI Video | |
| document.getElementById("videoPlayer").src = BUCKET_URL + filename; | |
| document.getElementById("currentTitle").innerText = filename; | |
| document.querySelectorAll('.video-item').forEach(b => b.classList.remove('active')); | |
| if(btnElement) btnElement.classList.add('active'); | |
| // Mostrar área de reviews y cargar datos | |
| document.getElementById("reviewsArea").style.display = "block"; | |
| loadReviews(filename); | |
| } | |
| // 4. CARGAR REVIEWS (GET Lambda) | |
| async function loadReviews(filename) { | |
| const listDiv = document.getElementById("reviewsList"); | |
| listDiv.innerHTML = "<p style='color:#777'>Cargando...</p>"; | |
| try { | |
| // Llamamos al Lambda con action=get_reviews | |
| const url = `${LAMBDA_URL}?action=get_reviews&video=${encodeURIComponent(filename)}`; | |
| const response = await fetch(url); | |
| const reviews = await response.json(); | |
| listDiv.innerHTML = ""; // Limpiar | |
| if (reviews.length === 0) { | |
| listDiv.innerHTML = "<p style='color:#555; font-style:italic;'>Sé el primero en comentar.</p>"; | |
| return; | |
| } | |
| reviews.forEach(r => { | |
| const card = document.createElement("div"); | |
| card.className = "review-card"; | |
| card.innerHTML = ` | |
| <div class="review-user"> | |
| ${escapeHtml(r.user_name)} | |
| <span class="review-date">${formatDate(r.created_at)}</span> | |
| </div> | |
| <div class="review-text">${escapeHtml(r.comment)}</div> | |
| `; | |
| listDiv.appendChild(card); | |
| }); | |
| } catch (error) { | |
| console.error(error); | |
| listDiv.innerHTML = "<p style='color:red'>Error cargando comentarios.</p>"; | |
| } | |
| } | |
| // 5. PUBLICAR REVIEW (POST Lambda) | |
| async function postReview() { | |
| const name = document.getElementById("reviewName").value; | |
| const comment = document.getElementById("reviewComment").value; | |
| if (!name || !comment) { | |
| alert("Por favor completa nombre y comentario."); | |
| return; | |
| } | |
| const btn = document.querySelector(".review-form button"); | |
| btn.disabled = true; | |
| btn.innerText = "Enviando..."; | |
| try { | |
| const payload = { | |
| action: "add_review", | |
| video: currentVideoFile, | |
| user: name, | |
| comment: comment | |
| }; | |
| const response = await fetch(LAMBDA_URL, { | |
| method: "POST", | |
| body: JSON.stringify(payload) | |
| }); | |
| if (response.ok) { | |
| // Limpiar form y recargar lista | |
| document.getElementById("reviewComment").value = ""; | |
| loadReviews(currentVideoFile); | |
| showToast("¡Comentario publicado!"); | |
| } else { | |
| alert("Error guardando comentario."); | |
| } | |
| } catch (error) { | |
| console.error(error); | |
| alert("Error de conexión."); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerText = "Publicar Comentario"; | |
| } | |
| } | |
| // --- UTILIDADES --- | |
| function escapeHtml(text) { | |
| if (!text) return ""; | |
| return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); | |
| } | |
| function formatDate(dateStr) { | |
| if(!dateStr) return ""; | |
| return dateStr.substring(0, 16); // Simple recorte de fecha | |
| } | |
| function checkToast() { | |
| const params = new URLSearchParams(window.location.search); | |
| if (params.get('upload') === 'success') { | |
| showToast("¡Video subido correctamente!"); | |
| window.history.replaceState({}, document.title, window.location.pathname); | |
| } | |
| } | |
| function showToast(msg) { | |
| const t = document.getElementById("toast"); | |
| t.innerText = msg; | |
| t.className = "show"; | |
| setTimeout(() => t.className = "", 3000); | |
| } | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "Version": "2012-10-17", | |
| "Statement": [ | |
| { | |
| "Sid": "PublicReadGetObject", | |
| "Effect": "Allow", | |
| "Principal": "*", | |
| "Action": "s3:GetObject", | |
| "Resource": "arn:aws:s3:::MI_NOMBRE_BUCKET_S3/*" | |
| }, | |
| { | |
| "Sid": "PublicListBucket", | |
| "Effect": "Allow", | |
| "Principal": "*", | |
| "Action": "s3:ListBucket", | |
| "Resource": "arn:aws:s3:::MI_NOMBRE_BUCKET_S3" | |
| } | |
| ] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [ | |
| { | |
| "AllowedHeaders": [ | |
| "*" | |
| ], | |
| "AllowedMethods": [ | |
| "GET", | |
| "POST", | |
| "HEAD" | |
| ], | |
| "AllowedOrigins": [ | |
| "*" | |
| ], | |
| "ExposeHeaders": [] | |
| } | |
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Subir Video</title> | |
| <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> | |
| <style> | |
| body { font-family: 'Segoe UI', sans-serif; background: #141414; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; } | |
| .container { background: #222; padding: 2.5rem; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); width: 400px; text-align: center; } | |
| h2 { color: #e50914; margin-top: 0; } | |
| input[type="file"] { margin: 20px 0; padding: 10px; background: #333; border-radius: 4px; width: 100%; box-sizing: border-box; color: #ddd; } | |
| .btn-group { display: flex; gap: 10px; justify-content: center; margin-top: 20px; } | |
| button { padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: bold; width: 100%; } | |
| .btn-submit { background-color: #e50914; color: white; transition: 0.3s; } | |
| .btn-submit:hover { background-color: #f40612; } | |
| .btn-submit:disabled { background-color: #555; cursor: not-allowed; } | |
| .btn-cancel { background-color: transparent; border: 1px solid #555; color: #aaa; text-decoration: none; padding: 10px 20px; display: inline-block; border-radius: 4px; } | |
| .btn-cancel:hover { color: white; border-color: white; } | |
| #progress-wrapper { display: none; margin-top: 25px; } | |
| progress { width: 100%; height: 20px; accent-color: #e50914; } | |
| #status { margin-top: 10px; font-size: 0.9rem; color: #aaa; } | |
| </style> | |
| <script> | |
| // --- CONFIGURACIÓN --- | |
| const BUCKET_NAME = "MI_NOMBRE_BUCKET_S3"; // <--- EDITAR | |
| const LAMBDA_URL = "xxxxxxxxxxxxx.lambda-url.us-east-1.on.aws/"; // <--- EDITAR | |
| const REGION = "us-east-1"; | |
| const BUCKET_URL = `https://${BUCKET_NAME}.s3.${REGION}.amazonaws.com/`; | |
| function getAWSKeys() { | |
| // CAMBIO CLAVE: Añadimos ?action=sign_upload | |
| $.get(LAMBDA_URL + "?action=sign_upload", {}, function(json) { | |
| document.getElementById("Policy").value = json.stringToSign; | |
| document.getElementById("X-Amz-Credential").value = json.xAmzCredential; | |
| document.getElementById("X-Amz-Date").value = json.amzDate; | |
| document.getElementById("X-Amz-Signature").value = json.stringSigned; | |
| document.getElementById("X-Amz-Security-Token").value = json.securityToken; | |
| document.getElementById("btnSubmit").disabled = false; | |
| document.getElementById("btnSubmit").innerText = "Subir Video"; | |
| }).fail(function() { alert("Error contactando Lambda. Revisa la URL."); }); | |
| } | |
| function uploadFile() { | |
| var file = document.getElementById("file").files[0]; | |
| if (!file) { alert("Selecciona un archivo."); return; } | |
| document.getElementById("key").value = file.name; | |
| var formData = new FormData(document.getElementById("uploadForm")); | |
| var xhr = new XMLHttpRequest(); | |
| xhr.open("POST", BUCKET_URL, true); | |
| xhr.upload.onprogress = function(e) { | |
| if (e.lengthComputable) { | |
| var percent = Math.round((e.loaded / e.total) * 100); | |
| document.getElementById("progress-wrapper").style.display = "block"; | |
| document.getElementById("progressBar").value = percent; | |
| document.getElementById("status").innerText = percent + "% subido..."; | |
| } | |
| }; | |
| xhr.onload = function() { | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| document.getElementById("status").innerText = "¡Procesando..."; | |
| window.location.href = "player.html?upload=success"; | |
| } else { | |
| console.error(xhr.responseText); | |
| alert("Error al subir. Revisa consola."); | |
| } | |
| }; | |
| document.getElementById("btnSubmit").disabled = true; | |
| xhr.send(formData); | |
| } | |
| </script> | |
| </head> | |
| <body onload="getAWSKeys()"> | |
| <div class="container"> | |
| <h2>Subir Video</h2> | |
| <form id="uploadForm" enctype="multipart/form-data"> | |
| <input type="hidden" id="X-Amz-Credential" name="X-Amz-Credential" /> | |
| <input type="hidden" id="X-Amz-Date" name="X-Amz-Date" /> | |
| <input type="hidden" id="Policy" name="Policy" /> | |
| <input type="hidden" id="X-Amz-Signature" name="X-Amz-Signature" /> | |
| <input type="hidden" id="X-Amz-Security-Token" name="X-Amz-Security-Token" /> | |
| <input type="hidden" id="key" name="key" /> | |
| <input type="hidden" name="X-Amz-Algorithm" value="AWS4-HMAC-SHA256" /> | |
| <input type="hidden" name="success_action_status" value="201" /> | |
| <input type="file" name="file" id="file" accept="video/*" required> | |
| <div class="btn-group"> | |
| <a href="player.html" class="btn-cancel">Cancelar</a> | |
| <button type="button" id="btnSubmit" class="btn-submit" onclick="uploadFile()" disabled>Cargando...</button> | |
| </div> | |
| </form> | |
| <div id="progress-wrapper"> | |
| <progress id="progressBar" value="0" max="100"></progress> | |
| <div id="status">0%</div> | |
| </div> | |
| </div> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment