import os import json import time import pandas as pd # Included as requested from flask import Flask, render_template, request, redirect, url_for, session, flash from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError from werkzeug.security import generate_password_hash, check_password_hash from threading import Lock from dotenv import load_dotenv # Load local .env file if present (Good for local testing) load_dotenv() app = Flask(__name__) app.secret_key = os.environ.get("SECRET_KEY", "super_secret_key_123") # --- CONFIGURATION (WITH AUTO-FIX) --- # We use .strip() here to automatically remove invisible spaces # that often happen when pasting into Space Settings. token_env = os.environ.get("HF_TOKEN") HF_TOKEN = token_env.strip() if token_env else None repo_env = os.environ.get("DB_REPO") DB_REPO = repo_env.strip() if repo_env else None DB_FILE = "db.json" # Print to logs so you can verify exactly what the app sees print(f"--- SYSTEM CONFIG ---") print(f"Target Repo: '{DB_REPO}'") print(f"Token Found: {'Yes' if HF_TOKEN else 'NO (Check Secrets!)'}") print(f"---------------------") # Initialize API if HF_TOKEN: api = HfApi(token=HF_TOKEN) else: api = None print("WARNING: No HF_TOKEN found. App is strictly Read-Only and may crash on save.") db_lock = Lock() # --- STARTER DATA --- DEFAULT_PINS = [ {"id": 1, "url": "https://images.unsplash.com/photo-1541963463532-d68292c34b19", "caption": "Nature Vibes", "author": "System"}, {"id": 2, "url": "https://images.unsplash.com/photo-1493246507139-91e8fad9978e", "caption": "Alpine Lake", "author": "System"}, {"id": 3, "url": "https://images.unsplash.com/photo-1511497584788-876760111969", "caption": "Forest Mist", "author": "System"}, {"id": 4, "url": "https://images.unsplash.com/photo-1682687982501-1e58ab814714", "caption": "Desert Life", "author": "System"}, {"id": 5, "url": "https://images.unsplash.com/photo-1472214103451-9374bd1c798e", "caption": "Green Valley", "author": "System"} ] def load_db(): """ Tries to download the DB. If it fails because the file doesn't exist yet, it returns the default data so the app can start (and save it later). """ if not HF_TOKEN or not DB_REPO: print("WARNING: HF_TOKEN or DB_REPO secrets are missing. Site is Read-Only.") return {"users": {}, "pins": DEFAULT_PINS} try: print(f"Attempting to download {DB_FILE} from {DB_REPO}...") path = hf_hub_download(repo_id=DB_REPO, filename=DB_FILE, repo_type="dataset", token=HF_TOKEN) with open(path, 'r') as f: data = json.load(f) # Ensure structure exists if 'pins' not in data: data['pins'] = DEFAULT_PINS if 'users' not in data: data['users'] = {} print("Database loaded successfully.") return data except (EntryNotFoundError, RepositoryNotFoundError, RevisionNotFoundError) as e: print(f"NOTE: Could not load {DB_FILE}. Reason: {e}") print("Initializing with default data (Normal for first run).") return {"users": {}, "pins": DEFAULT_PINS} except Exception as e: print(f"CRITICAL: Unknown DB Load Error: {e}") return {"users": {}, "pins": DEFAULT_PINS} def save_db(data): """ Saves the DB locally and then pushes to Hugging Face. """ if not HF_TOKEN or not DB_REPO: flash("Error: Cannot save. Secrets (HF_TOKEN/DB_REPO) are missing!") return False with db_lock: try: # 1. Save locally with open(DB_FILE, 'w') as f: json.dump(data, f, indent=2) # 2. Push to Hugging Face Dataset if api: print("Syncing to Hugging Face Dataset...") api.upload_file( path_or_fileobj=DB_FILE, path_in_repo=DB_FILE, repo_id=DB_REPO, repo_type="dataset", commit_message="Sync DB via App" ) print("Sync successful!") return True else: flash("Error: API not initialized (Missing Token)") return False except Exception as e: print(f"Sync Error: {e}") flash(f"Database Sync Failed: {str(e)}") return False # Load data on startup DATA_CACHE = load_db() # --- ROUTES --- @app.route('/') def index(): return render_template('index.html', pins=DATA_CACHE.get('pins', []), user=session.get('user')) @app.route('/signup', methods=['POST']) def signup(): username = request.form.get('username') password = request.form.get('password') if not username or not password: flash("Username and password required") return redirect(url_for('index')) # 1. Check if user exists if username in DATA_CACHE.get('users', {}): flash("User already exists!") return redirect(url_for('index')) # 2. Add user DATA_CACHE.setdefault('users', {})[username] = generate_password_hash(password) # 3. Save to DB success = save_db(DATA_CACHE) if success: session['user'] = username flash("Account created successfully!") else: # Revert change if save failed del DATA_CACHE['users'][username] return redirect(url_for('index')) @app.route('/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') users = DATA_CACHE.get('users', {}) if username in users and check_password_hash(users[username], password): session['user'] = username flash("Logged in!") else: # Debug helper: Print what we checked against print(f"Login Failed for {username}. Available users: {list(users.keys())}") flash("Invalid username or password") return redirect(url_for('index')) @app.route('/logout') def logout(): session.pop('user', None) flash("Logged out") return redirect(url_for('index')) @app.route('/add_pin', methods=['POST']) def add_pin(): if 'user' not in session: return redirect(url_for('index')) img_url = request.form.get('img_url') caption = request.form.get('caption') if not img_url: flash("Image URL is required") return redirect(url_for('index')) new_pin = { "id": int(time.time()), "url": img_url, "caption": caption, "author": session['user'] } DATA_CACHE.setdefault('pins', []).insert(0, new_pin) save_db(DATA_CACHE) flash("Pin added!") return redirect(url_for('index')) if __name__ == '__main__': app.run(host='0.0.0.0', port=7860)