Files
2026-05-04 19:15:00 +02:00

372 lines
12 KiB
Python

import sqlite3
import os
import json
import threading
import secrets
from datetime import datetime, timedelta
import random
DATA_DIR = os.environ.get('DATA_DIR', '/app/data')
DB_PATH = os.path.join(DATA_DIR, 'anuncios.db')
OLD_JSON_PATH = os.path.join(DATA_DIR, 'anuncios.json')
_lock = threading.Lock()
def _get_conn():
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
return conn
def _add_column_if_missing(conn, table, column, col_type):
"""SQLite-safe ALTER TABLE ADD COLUMN if the column does not exist."""
cur = conn.execute(f"PRAGMA table_info({table})")
cols = [row['name'] for row in cur.fetchall()]
if column not in cols:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}")
def init_db():
os.makedirs(DATA_DIR, exist_ok=True)
with _lock:
conn = _get_conn()
conn.execute('''
CREATE TABLE IF NOT EXISTS anuncios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
contenido TEXT NOT NULL,
categoria TEXT NOT NULL DEFAULT 'general',
contacto TEXT NOT NULL DEFAULT 'madrid@craigslist.local',
ciudad TEXT NOT NULL DEFAULT 'madrid',
fecha TEXT NOT NULL
)
''')
_add_column_if_missing(conn, 'anuncios', 'ciudad', "TEXT NOT NULL DEFAULT 'madrid'")
_add_column_if_missing(conn, 'anuncios', 'token', "TEXT")
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_categoria ON anuncios(categoria)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_ciudad ON anuncios(ciudad)
''')
conn.execute('''
CREATE INDEX IF NOT EXISTS idx_fecha ON anuncios(fecha DESC)
''')
conn.commit()
conn.close()
_migrate_json()
def _migrate_json():
if not os.path.exists(OLD_JSON_PATH):
return
with _lock:
conn = _get_conn()
cur = conn.execute('SELECT COUNT(*) as total FROM anuncios')
if cur.fetchone()['total'] > 0:
conn.close()
return
with open(OLD_JSON_PATH, 'r', encoding='utf-8') as f:
anuncios = json.load(f)
for a in anuncios:
conn.execute('''
INSERT INTO anuncios (id, titulo, contenido, categoria, contacto, ciudad, fecha)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
a.get('id'),
a.get('titulo', ''),
a.get('contenido', ''),
a.get('categoria', 'general'),
a.get('contacto', 'madrid@craigslist.local'),
a.get('ciudad', 'madrid'),
a.get('fecha', datetime.now().strftime('%Y-%m-%d %H:%M'))
))
conn.execute("DELETE FROM sqlite_sequence WHERE name='anuncios'")
if anuncios:
max_id = max(a.get('id', 0) for a in anuncios)
conn.execute("INSERT INTO sqlite_sequence(name,seq) VALUES ('anuncios', ?)", (max_id,))
conn.commit()
conn.close()
try:
os.rename(OLD_JSON_PATH, OLD_JSON_PATH + '.backup')
except OSError:
pass
def listar_anuncios(categoria=None, busqueda=None, ciudad=None):
query = 'SELECT * FROM anuncios WHERE 1=1'
params = []
if ciudad:
query += ' AND ciudad = ?'
params.append(ciudad)
if categoria:
query += ' AND categoria = ?'
params.append(categoria)
if busqueda:
query += ' AND (titulo LIKE ? OR contenido LIKE ?)'
params.extend([f'%{busqueda}%', f'%{busqueda}%'])
query += ' ORDER BY fecha DESC'
with _lock:
conn = _get_conn()
cur = conn.execute(query, params)
rows = [dict(r) for r in cur.fetchall()]
conn.close()
return rows
def obtener_anuncio(anuncio_id):
with _lock:
conn = _get_conn()
cur = conn.execute('SELECT * FROM anuncios WHERE id = ?', (anuncio_id,))
row = cur.fetchone()
conn.close()
return dict(row) if row else None
def crear_anuncio(titulo, contenido, categoria, contacto, ciudad='madrid'):
fecha = datetime.now().strftime('%Y-%m-%d %H:%M')
token = secrets.token_urlsafe(16)
with _lock:
conn = _get_conn()
cur = conn.execute('''
INSERT INTO anuncios (titulo, contenido, categoria, contacto, ciudad, fecha, token)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (titulo, contenido, categoria, contacto, ciudad, fecha, token))
anuncio_id = cur.lastrowid
conn.commit()
conn.close()
return anuncio_id, token
def borrar_anuncio(anuncio_id, token=None):
with _lock:
conn = _get_conn()
cur = conn.execute('SELECT token FROM anuncios WHERE id = ?', (anuncio_id,))
row = cur.fetchone()
if not row:
conn.close()
return False
stored_token = row['token']
if stored_token and stored_token != token:
conn.close()
return False
cur = conn.execute('DELETE FROM anuncios WHERE id = ?', (anuncio_id,))
deleted = cur.rowcount
conn.commit()
conn.close()
return deleted > 0
def actualizar_anuncio(anuncio_id, titulo=None, contenido=None, categoria=None, contacto=None, ciudad=None):
campos = []
valores = []
if titulo is not None:
campos.append('titulo = ?')
valores.append(titulo)
if contenido is not None:
campos.append('contenido = ?')
valores.append(contenido)
if categoria is not None:
campos.append('categoria = ?')
valores.append(categoria)
if contacto is not None:
campos.append('contacto = ?')
valores.append(contacto)
if ciudad is not None:
campos.append('ciudad = ?')
valores.append(ciudad)
if not campos:
return False
valores.append(anuncio_id)
with _lock:
conn = _get_conn()
cur = conn.execute(
f"UPDATE anuncios SET {', '.join(campos)} WHERE id = ?",
valores
)
updated = cur.rowcount
conn.commit()
conn.close()
return updated > 0
def listar_categorias(ciudad=None):
query = 'SELECT DISTINCT categoria FROM anuncios WHERE 1=1'
params = []
if ciudad:
query += ' AND ciudad = ?'
params.append(ciudad)
query += ' ORDER BY categoria'
with _lock:
conn = _get_conn()
cur = conn.execute(query, params)
rows = [r['categoria'] for r in cur.fetchall()]
conn.close()
return rows
# ---------------------------------------------------------------------------
# Seed / migración helpers
# ---------------------------------------------------------------------------
_TITULOS = {
'trabajo': [
'Desarrollador backend senior',
'Community manager freelance',
'Repartidor con moto propia',
'Camarero/a para fin de semana',
'Diseñador UX/UI remoto',
'Ingeniero devops',
'Profesor de inglés nativo',
'Ayudante de almacén',
],
'venta': [
'Sofa chaise longue casi nuevo',
'iPhone 14 pro 256gb',
'Bicicleta de montaña trek',
'Libros de programación pack',
'Monitor 27 pulgadas 4k',
'Nevera combi no frost',
'Coche citroen c3 2019',
'Patinete electrico xiaomi',
],
'servicios': [
'Limpieza de hogar por horas',
'Reparación de electrodomesticos',
'Clases particulares de mates',
'Diseño web profesional',
'Fontanero económico',
'Paseador de perros',
'Traducciones inglés-español',
'Electricista autorizado',
],
'alquiler': [
'Piso centro 2 habitaciones',
'Habitación en piso compartido',
'Estudio luminoso con terraza',
'Alquiler temporal verano',
'Duplex con parking incluido',
'Chalet adosado zona norte',
'Loft industrial reformado',
'Apartamento playa primera línea',
],
'compra': [
'Busco piso en el centro',
'Compro coche gasolina manual',
'Se busca mobiliario vintage',
'Quiero comprar portatil gaming',
'Busco terreno para autocaravana',
'Compro material de escalada',
'Se busca piano de pared',
'Quiero comprar patinete eléctrico',
],
'eventos': [
'Concierto indie en la sala',
'Fiesta de inauguración local',
'Mercadillo artesanal domingo',
'Taller de cocina vegana',
'Ruta senderista guiada',
'Exposición fotográfica gratis',
'Open mic comedy night',
'Yoga en el parque sábados',
],
'tecnologia': [
'Raspberry pi 4 kit completo',
'Teclado mecánico custom',
'Router mesh wifi 6',
'Gafas realidad virtual',
'Disco duro externo 2tb',
'Webcam 4k para streaming',
'Impresora 3d ender 3',
'Switch oled con juegos',
],
'hogar': [
'Lámpara de pie nordica',
'Juego de sillas de comedor',
'Cortinas blackout a medida',
'Aspirador robot con mapeo',
'Planta monstera grande',
'Mesa de centro elevable',
'Colchón viscoelástico 150',
'Cubertería acero inox 72 piezas',
],
'ocio': [
'Entradas cine miercoles',
'Mazo cartas magic coleccion',
'Patines en línea talla 42',
'Videojuego elden ring ps5',
'Kitesurf completo principiante',
'Libro cocina japonesa',
'Entrada festival música',
'Guitarra acústica yamaha',
],
'contactos': [
'Busco compañero de piso',
'Quedada rol en la taberna',
'Club de lectura mensual',
'Grupo de running mañanas',
'Busco duo para ranked lol',
'Quedada idiomas intercambio',
'Compañero de escalada',
'Busco banda para batería',
],
}
_DESCRIPCIONES = {
'trabajo': 'Horario flexible. Se valora experiencia previa. Envía CV por correo.',
'venta': 'En perfecto estado. Precio negociable. Entrega en mano preferible.',
'servicios': 'Presupuesto sin compromiso. Atención rápida y profesional.',
'alquiler': 'Sin comisión de agencia. Gastos incluidos o no según caso.',
'compra': 'Busco buen estado y precio razonable. Pago al contado.',
'eventos': 'Entrada libre hasta completar aforo. Más info por privado.',
'tecnologia': 'Con garantía y caja original. Factura disponible.',
'hogar': 'Muy cuidado, sin ralladuras. Recogida en domicilio.',
'ocio': 'Perfecto para regalo o uso personal. Consulta detalles.',
'contactos': 'Gente con buen rollo y ganas de pasarlo bien. Únete!',
}
_CATEGORIAS = list(_TITULOS.keys())
def _random_date(days_back=90):
delta = timedelta(days=random.randint(0, days_back), hours=random.randint(0, 23))
return (datetime.now() - delta).strftime('%Y-%m-%d %H:%M')
def seed_anuncios(ciudades, por_ciudad=4):
"""Genera anuncios aleatorios distribuidos por ciudades."""
with _lock:
conn = _get_conn()
for ciudad in ciudades:
for _ in range(por_ciudad):
cat = random.choice(_CATEGORIAS)
titulo = random.choice(_TITULOS[cat])
contenido = _DESCRIPCIONES[cat]
contacto = f"{ciudad}@craigslist.local"
fecha = _random_date()
conn.execute('''
INSERT INTO anuncios (titulo, contenido, categoria, contacto, ciudad, fecha)
VALUES (?, ?, ?, ?, ?, ?)
''', (titulo, contenido, cat, contacto, ciudad, fecha))
conn.commit()
conn.close()
def migrar_ciudad_existentes(ciudad_default='madrid'):
"""Asigna ciudad a anuncios que aún no la tengan definida."""
with _lock:
conn = _get_conn()
conn.execute('''
UPDATE anuncios SET ciudad = ? WHERE ciudad IS NULL OR ciudad = ''
''', (ciudad_default,))
conn.commit()
conn.close()