Skip to content

Instantly share code, notes, and snippets.

@germanviscuso
Last active December 10, 2025 18:43
Show Gist options
  • Select an option

  • Save germanviscuso/ddf0200b6369a8012fedf53899e4cfc2 to your computer and use it in GitHub Desktop.

Select an option

Save germanviscuso/ddf0200b6369a8012fedf53899e4cfc2 to your computer and use it in GitHub Desktop.
StreamFlix
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)
}
<!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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
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>
{
"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"
}
]
}
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"POST",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
<!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