import os import json import re import random import time # for generating a simple unique id from datetime import datetime, timezone from flask import Flask, render_template, request, url_for, jsonify from flask_socketio import SocketIO from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) # Adjust SERVER_NAME and SQLALCHEMY_DATABASE_URI as needed. app.config['SERVER_NAME'] = '10.30.0.2:5000' app.config['PREFERRED_URL_SCHEME'] = 'http' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////Users/mediarogc/Documents/Annoucements Web/bible.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False CORS(app) socketio = SocketIO(app, cors_allowed_origins="*") db = SQLAlchemy(app) # --------------------------- # Global configuration & Folders # --------------------------- # Bible verse linking and frequency features have been removed. STATUS_FILE = 'slides_status.json' SLIDES_FOLDER = os.path.join('static', 'slides') ALLOWED_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif'} # Folder for Bible backgrounds (used elsewhere). BIBLE_BACKGROUNDS_FOLDER = os.path.join('static', 'bible_backgrounds') ALLOWED_BG_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif'} # Bible JSON file for added Bible verses (will store verse text and reference) BIBLE_JSON_FILE = 'bible.json' def get_slides(): if not os.path.exists(SLIDES_FOLDER): os.makedirs(SLIDES_FOLDER) slides = [] for filename in os.listdir(SLIDES_FOLDER): ext = os.path.splitext(filename)[1].lower() if ext in ALLOWED_EXTENSIONS: slides.append(filename) return slides def load_slide_statuses(): if os.path.exists(STATUS_FILE): with open(STATUS_FILE, 'r') as f: return json.load(f) return {} def save_slide_statuses(statuses): with open(STATUS_FILE, 'w') as f: json.dump(statuses, f) def get_slide_statuses(): statuses = load_slide_statuses() slides = get_slides() new_statuses = {} max_order = max((data.get("order", 0) for data in statuses.values()), default=0) for slide in slides: if slide in statuses: new_statuses[slide] = statuses[slide] else: max_order += 1 new_statuses[slide] = { "active": True, "order": max_order, "duration": 7, "title": slide.split('.')[0] } save_slide_statuses(new_statuses) return new_statuses # --------------------------- # Announcement Endpoints # --------------------------- @app.route('/') def index(): statuses = get_slide_statuses() sorted_slides = sorted(statuses.items(), key=lambda x: x[1]['order']) active_slides = [slide for slide, data in sorted_slides if data.get("active", False)] slide_urls = [url_for('static', filename='slides/' + slide) for slide in active_slides] return render_template('index.html', slide_urls=slide_urls) @app.route('/control') def control(): statuses = get_slide_statuses() slide_urls = {slide: url_for('static', filename='slides/' + slide) for slide in statuses} sorted_statuses = sorted(statuses.items(), key=lambda item: item[1]["order"]) return render_template('control.html', statuses=sorted_statuses, slide_urls=slide_urls) @app.route('/update_order', methods=['POST']) def update_order(): statuses = get_slide_statuses() data = request.get_json() order_list = data.get("order", []) counter = 1 for slide in order_list: if slide in statuses: statuses[slide]["order"] = counter counter += 1 remaining = [slide for slide in statuses if slide not in order_list] remaining.sort(key=lambda s: statuses[s]["order"]) for slide in remaining: statuses[slide]["order"] = counter counter += 1 save_slide_statuses(statuses) socketio.emit('refresh_index') return jsonify({"status": "success"}) @app.route('/update_active', methods=['POST']) def update_active(): statuses = get_slide_statuses() data = request.get_json() slide = data.get("slide") active = data.get("active") if slide in statuses: statuses[slide]["active"] = active save_slide_statuses(statuses) return jsonify({"status": "success"}) return jsonify({"status": "error", "message": "Slide not found."}), 400 @app.route('/update_duration', methods=['POST']) def update_duration(): statuses = get_slide_statuses() data = request.get_json() slide = data.get("slide") duration = data.get("duration") if slide in statuses: try: duration_val = float(duration) except ValueError: return jsonify({"status": "error", "message": "Invalid duration."}), 400 statuses[slide]["duration"] = duration_val save_slide_statuses(statuses) return jsonify({"status": "success"}) return jsonify({"status": "error", "message": "Slide not found."}), 400 @app.route('/update_title', methods=['POST']) def update_title(): statuses = get_slide_statuses() data = request.get_json() slide = data.get("slide") title = data.get("title") if slide in statuses: statuses[slide]["title"] = title save_slide_statuses(statuses) return jsonify({"status": "success"}) return jsonify({"status": "error", "message": "Slide not found."}), 400 @app.route('/update_end_date', methods=['POST']) def update_end_date(): statuses = get_slide_statuses() data = request.get_json() slide = data.get("slide") end_date = data.get("end_date") if slide in statuses: statuses[slide]["end_date"] = end_date save_slide_statuses(statuses) return jsonify({"status": "success"}) return jsonify({"status": "error", "message": "Slide not found."}), 400 @app.route('/delete_slide', methods=['POST']) def delete_slide(): statuses = get_slide_statuses() data = request.get_json() slide = data.get("slide") if slide in statuses: del statuses[slide] save_slide_statuses(statuses) file_path = os.path.join(SLIDES_FOLDER, slide) if os.path.exists(file_path): os.remove(file_path) return jsonify({"status": "success"}) return jsonify({"status": "error", "message": "Slide not found."}), 400 # --------------------------- # Bible Verses Integration (Library and Addition to Announcements) # --------------------------- class BibleVerse(db.Model): __tablename__ = 'bible_verse' id = db.Column(db.Integer, primary_key=True) book = db.Column(db.String(64), nullable=False) chapter = db.Column(db.Integer, nullable=False) verse = db.Column(db.Integer, nullable=False) text = db.Column(db.Text, nullable=False) translation = db.Column(db.String(64), nullable=False, default='KJV') # Removed color and background_url since background handling is via bible.json active_in_rotation = db.Column(db.Boolean, default=False) @app.route('/search_verses') def search_verses(): query = request.args.get('q', '').strip() translation = request.args.get('translation', None) pattern = r'^([A-Za-z0-9\s]+)\s+(\d+):(\d+)$' if re.match(pattern, query): match = re.match(pattern, query) book, chapter, verse = match.groups() chapter = int(chapter) verse = int(verse) q = BibleVerse.query.filter( BibleVerse.book.ilike(book.strip()), BibleVerse.chapter == chapter, BibleVerse.verse == verse ) if translation: q = q.filter(BibleVerse.translation == translation) verses = q.all() else: if query == "": verses = BibleVerse.query.filter_by(active_in_rotation=True).all() else: q = BibleVerse.query.filter(BibleVerse.text.ilike(f'%{query}%')) if translation: q = q.filter(BibleVerse.translation == translation) verses = q.all() # Only return the required fields. results = [{ 'book': v.book, 'chapter': v.chapter, 'verse': v.verse, 'text': v.text, 'translation': v.translation } for v in verses] return jsonify(results) def load_bible_json(): if os.path.exists(BIBLE_JSON_FILE): with open(BIBLE_JSON_FILE, 'r') as f: try: return json.load(f) except Exception as e: print("Error loading bible.json:", e) return [] return [] def save_bible_json(data): with open(BIBLE_JSON_FILE, 'w') as f: json.dump(data, f) @app.route('/add_bible_verse', methods=['POST']) def add_bible_verse(): """ Expects JSON payload: { "verse": "For God so loved the word that whomever believes in Him shall not perish but have everlasting life.", "reference": "John 3:16" } """ data = request.get_json() new_entry = { "id": int(time.time()), # Simple unique id generation; adjust if needed. "verse": data.get("verse"), "reference": data.get("reference"), "background": "" # By default, an empty background. } bible_data = load_bible_json() bible_data.append(new_entry) save_bible_json(bible_data) return jsonify({"status": "success", "entry": new_entry}) @app.route('/remove_bible_verse', methods=['POST']) def remove_bible_verse(): """ Expects JSON payload: { "id": 1234567890 } """ data = request.get_json() remove_id = data.get("id") bible_data = load_bible_json() # Filter out the entry with matching id new_data = [entry for entry in bible_data if entry.get("id") != remove_id] save_bible_json(new_data) return jsonify({"status": "success"}) @app.route('/bible_registry') def bible_registry(): """ Returns the current list of added bible verses (from bible.json). """ data = load_bible_json() return jsonify(data) # --------------------------- # Bible Background Helpers # --------------------------- def get_bible_background_images(): if not os.path.exists(BIBLE_BACKGROUNDS_FOLDER): os.makedirs(BIBLE_BACKGROUNDS_FOLDER) images = [] for filename in os.listdir(BIBLE_BACKGROUNDS_FOLDER): ext = os.path.splitext(filename)[1].lower() if ext in ALLOWED_BG_EXTENSIONS: images.append(filename) return images def get_random_bible_background(): imgs = get_bible_background_images() if imgs: bg = random.choice(imgs) return url_for('static', filename='bible_backgrounds/' + bg) return "" # --------------------------- # Background Tasks # --------------------------- current_slide_index = 0 def background_slide_sync(): global current_slide_index while True: with app.app_context(): statuses = get_slide_statuses() sorted_slides = sorted(statuses.items(), key=lambda x: x[1]['order']) active_list = [(slide, data) for slide, data in sorted_slides if data.get("active", False)] if active_list: current_slide_index = (current_slide_index + 1) % len(active_list) slide, data = active_list[current_slide_index] slide_url = url_for('static', filename='slides/' + slide) socketio.emit('sync_slide', {"current_slide_index": current_slide_index, "slide_url": slide_url}) duration = float(data.get("duration", 7)) else: duration = 7 socketio.sleep(duration) @socketio.on('connect') def on_connect(auth=None): global current_slide_index with app.app_context(): statuses = get_slide_statuses() sorted_slides = sorted(statuses.items(), key=lambda x: x[1]['order']) active_list = [(slide, data) for slide, data in sorted_slides if data.get("active", False)] if active_list: index = current_slide_index % len(active_list) slide, data = active_list[index] slide_url = url_for('static', filename='slides/' + slide) socketio.emit('sync_slide', {"current_slide_index": index, "slide_url": slide_url}) def scheduled_deletion_check(): while True: with app.app_context(): statuses = get_slide_statuses() to_delete = [] now = datetime.now(timezone.utc) for slide, data in statuses.items(): end_date_str = data.get("end_date") if end_date_str: try: end_date = datetime.fromisoformat(end_date_str) if end_date.tzinfo is None: end_date = end_date.replace(tzinfo=timezone.utc) if now >= end_date: to_delete.append(slide) except Exception as e: print(f"Error parsing end_date for {slide}: {e}") if to_delete: for slide in to_delete: del statuses[slide] file_path = os.path.join(SLIDES_FOLDER, slide) if os.path.exists(file_path): os.remove(file_path) save_slide_statuses(statuses) socketio.emit('refresh_index') socketio.sleep(60) if __name__ == '__main__': # Uncomment the following line on first run (or when models change) # db.create_all() socketio.start_background_task(background_slide_sync) socketio.start_background_task(scheduled_deletion_check) socketio.run(app, host='0.0.0.0', port=5000, debug=True, use_reloader=False)