Initial commit - Church Music Database
This commit is contained in:
575
legacy-site/backend/._archived_sqlite/app_sqlite_backup.py
Normal file
575
legacy-site/backend/._archived_sqlite/app_sqlite_backup.py
Normal file
@@ -0,0 +1,575 @@
|
||||
from flask import Flask, jsonify, request
|
||||
from flask_cors import CORS
|
||||
from datetime import datetime
|
||||
from models import init_db, SessionLocal, Profile, Song, Plan, PlanSong, ProfileSong
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv() # Loads variables from a .env file if present
|
||||
except ImportError:
|
||||
# dotenv not installed yet; proceed without loading .env
|
||||
pass
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
# Enhanced CORS with wildcard support for all origins
|
||||
CORS(app, resources={
|
||||
r"/api/*": {
|
||||
"origins": "*",
|
||||
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
"allow_headers": ["Content-Type", "Authorization"]
|
||||
}
|
||||
})
|
||||
init_db()
|
||||
FLASK_PORT = int(os.environ.get('FLASK_PORT', '5000'))
|
||||
|
||||
def get_db():
|
||||
"""Create a new database session with automatic cleanup"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
return db
|
||||
except Exception:
|
||||
db.close()
|
||||
raise
|
||||
|
||||
@app.teardown_appcontext
|
||||
def cleanup_session(exception=None):
|
||||
"""Cleanup scoped database sessions after each request to prevent connection pool exhaustion"""
|
||||
SessionLocal.remove()
|
||||
|
||||
def extract_text_from_file(file_storage):
|
||||
"""Extract text from an uploaded file (docx, pdf, image, txt)."""
|
||||
filename = file_storage.filename or ''
|
||||
lower = filename.lower()
|
||||
data = file_storage.read()
|
||||
# Reset stream position if needed for certain libraries
|
||||
import io
|
||||
stream = io.BytesIO(data)
|
||||
text = ''
|
||||
try:
|
||||
if lower.endswith('.txt'):
|
||||
text = data.decode(errors='ignore')
|
||||
elif lower.endswith('.docx'):
|
||||
try:
|
||||
import docx
|
||||
doc = docx.Document(stream)
|
||||
text = '\n'.join(p.text for p in doc.paragraphs)
|
||||
except Exception:
|
||||
text = ''
|
||||
elif lower.endswith('.pdf'):
|
||||
try:
|
||||
import PyPDF2
|
||||
reader = PyPDF2.PdfReader(stream)
|
||||
parts = []
|
||||
for page in reader.pages[:20]: # safety limit
|
||||
try:
|
||||
parts.append(page.extract_text() or '')
|
||||
except Exception:
|
||||
continue
|
||||
text = '\n'.join(parts)
|
||||
# OCR fallback if minimal text extracted (likely scanned PDF)
|
||||
if len(text.strip()) < 50:
|
||||
try:
|
||||
from pdf2image import convert_from_bytes
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
images = convert_from_bytes(data, first_page=1, last_page=min(10, len(reader.pages)))
|
||||
ocr_parts = []
|
||||
for img in images:
|
||||
ocr_parts.append(pytesseract.image_to_string(img))
|
||||
ocr_text = '\n'.join(ocr_parts)
|
||||
if len(ocr_text.strip()) > len(text.strip()):
|
||||
text = ocr_text
|
||||
except Exception:
|
||||
pass # Fall back to original text layer extraction
|
||||
except Exception:
|
||||
text = ''
|
||||
elif lower.endswith(('.png','.jpg','.jpeg','.tif','.tiff')):
|
||||
try:
|
||||
from PIL import Image
|
||||
import pytesseract
|
||||
img = Image.open(stream)
|
||||
text = pytesseract.image_to_string(img)
|
||||
except Exception:
|
||||
text = ''
|
||||
else:
|
||||
# Attempt generic decode
|
||||
try:
|
||||
text = data.decode(errors='ignore')
|
||||
except Exception:
|
||||
text = ''
|
||||
except Exception:
|
||||
text = ''
|
||||
# Basic cleanup
|
||||
cleaned = '\n'.join(line.rstrip() for line in (text or '').splitlines())
|
||||
return cleaned.strip()
|
||||
|
||||
# Admin restore from data.json (profiles and songs)
|
||||
@app.route('/api/admin/restore', methods=['POST'])
|
||||
def admin_restore():
|
||||
# Determine data.json location: prefer backend/data.json, fallback to project root
|
||||
backend_path = os.path.join(os.path.dirname(__file__), 'data.json')
|
||||
root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'data.json'))
|
||||
source_path = backend_path if os.path.exists(backend_path) else root_path if os.path.exists(root_path) else None
|
||||
if not source_path:
|
||||
return jsonify({ 'ok': False, 'error': 'data.json not found in backend/ or project root' }), 404
|
||||
|
||||
try:
|
||||
with open(source_path, 'r', encoding='utf-8') as f:
|
||||
payload = json.load(f)
|
||||
except Exception as e:
|
||||
return jsonify({ 'ok': False, 'error': f'Failed to read data.json: {str(e)}' }), 400
|
||||
|
||||
db = SessionLocal()
|
||||
created = { 'profiles': 0, 'songs': 0 }
|
||||
updated = { 'profiles': 0, 'songs': 0 }
|
||||
|
||||
try:
|
||||
import uuid
|
||||
# Restore profiles
|
||||
profiles = payload.get('profiles', [])
|
||||
for p in profiles:
|
||||
name = (p.get('name') or f"{(p.get('first_name') or '').strip()} {(p.get('last_name') or '').strip()}" ).strip()
|
||||
if not name:
|
||||
continue
|
||||
existing = db.query(Profile).filter(Profile.name == name).first()
|
||||
if existing:
|
||||
changed = False
|
||||
for key in ['email','contact_number','default_key','notes']:
|
||||
if p.get(key) and getattr(existing, key, None) != p.get(key):
|
||||
setattr(existing, key, p.get(key))
|
||||
changed = True
|
||||
if changed:
|
||||
updated['profiles'] += 1
|
||||
else:
|
||||
profile_id = str(p.get('id')) if p.get('id') else str(uuid.uuid4())
|
||||
new_p = Profile(
|
||||
id=profile_id,
|
||||
name=name,
|
||||
email=p.get('email') or '',
|
||||
contact_number=p.get('contact_number') or '',
|
||||
default_key=p.get('default_key') or 'C',
|
||||
notes=p.get('notes') or ''
|
||||
)
|
||||
db.add(new_p)
|
||||
created['profiles'] += 1
|
||||
|
||||
# Restore songs
|
||||
songs = payload.get('songs', [])
|
||||
for s in songs:
|
||||
title = (s.get('title') or '').strip()
|
||||
if not title:
|
||||
continue
|
||||
existing = db.query(Song).filter(Song.title == title).first()
|
||||
lyrics = s.get('lyrics') or s.get('content') or ''
|
||||
chords = s.get('chords') or ''
|
||||
singer = s.get('singer') or s.get('artist') or ''
|
||||
artist = s.get('artist') or ''
|
||||
band = s.get('band') or ''
|
||||
if existing:
|
||||
changed = False
|
||||
if lyrics and (existing.lyrics or '') != lyrics:
|
||||
existing.lyrics = lyrics
|
||||
changed = True
|
||||
if chords and (existing.chords or '') != chords:
|
||||
existing.chords = chords
|
||||
changed = True
|
||||
if singer and (existing.singer or '') != singer:
|
||||
existing.singer = singer
|
||||
changed = True
|
||||
if artist and (existing.artist or '') != artist:
|
||||
existing.artist = artist
|
||||
changed = True
|
||||
if band and (existing.band or '') != band:
|
||||
existing.band = band
|
||||
changed = True
|
||||
if changed:
|
||||
updated['songs'] += 1
|
||||
else:
|
||||
song_id = str(s.get('id')) if s.get('id') else str(uuid.uuid4())
|
||||
new_s = Song(id=song_id, title=title, artist=artist, band=band, singer=singer, lyrics=lyrics, chords=chords)
|
||||
db.add(new_s)
|
||||
created['songs'] += 1
|
||||
|
||||
db.commit()
|
||||
return jsonify({ 'ok': True, 'profiles_created': created['profiles'], 'profiles_updated': updated['profiles'], 'songs_created': created['songs'], 'songs_updated': updated['songs'], 'source': source_path })
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({ 'ok': False, 'error': str(e) }), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@app.route('/api/upload_lyric', methods=['POST'])
|
||||
def upload_lyric():
|
||||
"""Accept a lyric file and return extracted text. Does NOT create a Song automatically."""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'error':'file_missing'}), 400
|
||||
f = request.files['file']
|
||||
if not f.filename:
|
||||
return jsonify({'error':'empty_filename'}), 400
|
||||
# Optional metadata fields from form
|
||||
title = request.form.get('title') or f.filename.rsplit('.',1)[0]
|
||||
artist = request.form.get('artist') or ''
|
||||
band = request.form.get('band') or ''
|
||||
extracted = extract_text_from_file(f)
|
||||
if not extracted:
|
||||
return jsonify({'error':'extraction_failed','title':title}), 422
|
||||
# Return without saving; front-end will present in modal for editing & saving
|
||||
sample = extracted[:150] + ('...' if len(extracted) > 150 else '')
|
||||
return jsonify({
|
||||
'status':'ok',
|
||||
'title': title,
|
||||
'artist': artist,
|
||||
'band': band,
|
||||
'lyrics': extracted,
|
||||
'preview': sample,
|
||||
'length': len(extracted)
|
||||
})
|
||||
|
||||
@app.route('/api/providers')
|
||||
def providers():
|
||||
"""Report which external provider tokens are configured (OpenAI removed)."""
|
||||
return jsonify({
|
||||
'local_db': True,
|
||||
'chartlyrics': True,
|
||||
'lifeway_link': True
|
||||
})
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return jsonify({'message': 'House of Prayer Song Lyrics API (Flask)', 'port': FLASK_PORT})
|
||||
|
||||
@app.route('/api/health')
|
||||
def health():
|
||||
return jsonify({'status': 'ok', 'ts': datetime.utcnow().isoformat()})
|
||||
|
||||
# Profile Selection Management
|
||||
@app.route('/api/profile-selection/clear', methods=['POST', 'OPTIONS'])
|
||||
def clear_profile_selection():
|
||||
"""Clear profile selection (frontend manages this in localStorage, but endpoint needed for API compatibility)"""
|
||||
if request.method == 'OPTIONS':
|
||||
return '', 204
|
||||
return jsonify({'status': 'ok', 'message': 'Profile selection cleared'})
|
||||
|
||||
# Profiles CRUD
|
||||
@app.route('/api/profiles', methods=['GET','POST'])
|
||||
def profiles():
|
||||
db = get_db()
|
||||
if request.method == 'GET':
|
||||
items = db.query(Profile).all()
|
||||
result = jsonify([{'id':p.id,'name':p.name,'email':p.email,'contact_number':p.contact_number,'default_key':p.default_key,'notes':p.notes} for p in items])
|
||||
db.close()
|
||||
return result
|
||||
import uuid
|
||||
data = request.get_json() or {}
|
||||
profile_id = data.get('id') or str(uuid.uuid4())
|
||||
p = Profile(id=profile_id, name=data.get('name'), email=data.get('email') or '', contact_number=data.get('contact_number') or '', default_key=data.get('default_key') or 'C', notes=data.get('notes') or '')
|
||||
db.add(p); db.commit();
|
||||
result = jsonify({'id':p.id})
|
||||
db.close()
|
||||
return result
|
||||
|
||||
@app.route('/api/profiles/<pid>', methods=['PUT','DELETE'])
|
||||
def profile_item(pid):
|
||||
db = get_db(); p = db.query(Profile).get(pid)
|
||||
if not p: return jsonify({'error':'not_found'}),404
|
||||
if request.method == 'PUT':
|
||||
d = request.get_json() or {}
|
||||
p.name = d.get('name', p.name)
|
||||
p.email = d.get('email', p.email)
|
||||
p.contact_number = d.get('contact_number', p.contact_number)
|
||||
p.default_key = d.get('default_key', p.default_key)
|
||||
p.notes = d.get('notes', p.notes)
|
||||
db.commit(); return jsonify({'status':'ok'})
|
||||
db.delete(p); db.commit(); return jsonify({'status':'deleted'})
|
||||
|
||||
# Songs CRUD + search (local)
|
||||
@app.route('/api/songs', methods=['GET','POST'])
|
||||
def songs():
|
||||
db = get_db()
|
||||
if request.method == 'GET':
|
||||
q = request.args.get('q','').lower()
|
||||
items = db.query(Song).all()
|
||||
def match(s):
|
||||
return (q in (s.title or '').lower() or q in (s.artist or '').lower() or q in (s.band or '').lower() or q in (s.singer or '').lower()) if q else True
|
||||
# Include lyrics preview (first 200 chars) for Database component
|
||||
return jsonify([{
|
||||
'id':s.id,
|
||||
'title':s.title,
|
||||
'artist':s.artist,
|
||||
'band':s.band,
|
||||
'singer':s.singer,
|
||||
'lyrics':(s.lyrics or '')[:200] if s.lyrics else '',
|
||||
'chords':(s.chords or '')[:100] if s.chords else ''
|
||||
} for s in items if match(s)])
|
||||
import uuid
|
||||
d = request.get_json() or {}
|
||||
song_id = d.get('id') or str(uuid.uuid4())
|
||||
s = Song(id=song_id, title=d.get('title') or 'Untitled', artist=d.get('artist') or '', band=d.get('band') or '', singer=d.get('singer') or '', lyrics=d.get('lyrics') or '', chords=d.get('chords') or '')
|
||||
db.add(s); db.commit(); return jsonify({'id':s.id})
|
||||
|
||||
@app.route('/api/songs/<sid>', methods=['GET','PUT','DELETE'])
|
||||
def song_item(sid):
|
||||
db = get_db(); s = db.query(Song).get(sid)
|
||||
if not s: return jsonify({'error':'not_found'}),404
|
||||
if request.method == 'GET':
|
||||
return jsonify({'id':s.id,'title':s.title,'artist':s.artist,'band':s.band,'singer':s.singer,'lyrics':s.lyrics,'chords':s.chords})
|
||||
if request.method == 'PUT':
|
||||
d = request.get_json() or {}
|
||||
s.title = d.get('title', s.title)
|
||||
s.artist = d.get('artist', s.artist)
|
||||
s.band = d.get('band', s.band)
|
||||
s.singer = d.get('singer', s.singer)
|
||||
s.lyrics = d.get('lyrics', s.lyrics)
|
||||
s.chords = d.get('chords', s.chords)
|
||||
db.commit(); return jsonify({'status':'ok'})
|
||||
db.delete(s); db.commit(); return jsonify({'status':'deleted'})
|
||||
|
||||
# Planning (date-based)
|
||||
@app.route('/api/plans', methods=['GET','POST'])
|
||||
def plans():
|
||||
db = get_db()
|
||||
if request.method == 'GET':
|
||||
items = db.query(Plan).all()
|
||||
return jsonify([{'id':p.id,'date':p.date.isoformat(),'profile_id':p.profile_id,'memo':p.memo} for p in items])
|
||||
d = request.get_json() or {}
|
||||
date_str = d.get('date'); date = datetime.fromisoformat(date_str).date() if date_str else datetime.now().date()
|
||||
plan = Plan(date=date, profile_id=d.get('profile_id'), memo=d.get('memo') or '')
|
||||
db.add(plan); db.commit(); return jsonify({'id':plan.id})
|
||||
|
||||
@app.route('/api/plans/<int:pid>/songs', methods=['GET','POST'])
|
||||
def plan_songs(pid):
|
||||
db = get_db(); plan = db.query(Plan).get(pid)
|
||||
if not plan: return jsonify({'error':'plan_not_found'}),404
|
||||
if request.method == 'GET':
|
||||
links = db.query(PlanSong).filter(PlanSong.plan_id==pid).order_by(PlanSong.order_index).all()
|
||||
return jsonify([{'id':l.id,'song_id':l.song_id,'order_index':l.order_index} for l in links])
|
||||
d = request.get_json() or {}
|
||||
link = PlanSong(plan_id=pid, song_id=d.get('song_id'), order_index=d.get('order_index') or 0)
|
||||
db.add(link); db.commit(); return jsonify({'id':link.id})
|
||||
|
||||
# Profile Songs endpoints
|
||||
@app.route('/api/profiles/<pid>/songs', methods=['GET','POST'])
|
||||
def profile_songs(pid):
|
||||
db = get_db()
|
||||
try:
|
||||
profile = db.query(Profile).get(pid)
|
||||
if not profile:
|
||||
return jsonify({'error':'profile_not_found'}),404
|
||||
if request.method == 'GET':
|
||||
links = db.query(ProfileSong).filter(ProfileSong.profile_id==pid).all()
|
||||
return jsonify([{'id':l.id,'profile_id':l.profile_id,'song_id':l.song_id,'song_key':l.song_key} for l in links])
|
||||
d = request.get_json() or {}
|
||||
# Check if association already exists
|
||||
existing = db.query(ProfileSong).filter(ProfileSong.profile_id==pid, ProfileSong.song_id==d.get('song_id')).first()
|
||||
if existing:
|
||||
return jsonify({'id':existing.id, 'exists':True})
|
||||
link = ProfileSong(profile_id=pid, song_id=d.get('song_id'), song_key=d.get('song_key') or 'C')
|
||||
db.add(link); db.commit()
|
||||
return jsonify({'id':link.id})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@app.route('/api/profiles/<pid>/songs/<sid>', methods=['GET','PUT','DELETE'])
|
||||
def profile_song_item(pid, sid):
|
||||
db = get_db()
|
||||
try:
|
||||
link = db.query(ProfileSong).filter(ProfileSong.profile_id==pid, ProfileSong.song_id==sid).first()
|
||||
if not link:
|
||||
return jsonify({'error':'not_found'}),404
|
||||
if request.method == 'GET':
|
||||
return jsonify({'id':link.id,'profile_id':link.profile_id,'song_id':link.song_id,'song_key':link.song_key})
|
||||
if request.method == 'PUT':
|
||||
d = request.get_json() or {}
|
||||
link.song_key = d.get('song_key', link.song_key)
|
||||
db.commit()
|
||||
return jsonify({'status':'ok'})
|
||||
db.delete(link); db.commit()
|
||||
return jsonify({'status':'deleted'})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# External search using ChartLyrics API + Local Database
|
||||
@app.route('/api/search_external')
|
||||
def search_external():
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
q = request.args.get('q', '')
|
||||
filter_type = request.args.get('filter', 'all')
|
||||
|
||||
if not q:
|
||||
return jsonify({'query': q, 'results': []})
|
||||
|
||||
results = []
|
||||
|
||||
# First, search local database
|
||||
db = get_db()
|
||||
local_songs = db.query(Song).all()
|
||||
|
||||
def match_local(s):
|
||||
q_lower = q.lower()
|
||||
if filter_type == 'title':
|
||||
return q_lower in (s.title or '').lower()
|
||||
elif filter_type == 'artist':
|
||||
return q_lower in (s.artist or '').lower()
|
||||
elif filter_type == 'band':
|
||||
return q_lower in (s.band or '').lower()
|
||||
else: # all
|
||||
return (q_lower in (s.title or '').lower() or
|
||||
q_lower in (s.artist or '').lower() or
|
||||
q_lower in (s.band or '').lower())
|
||||
|
||||
for s in local_songs:
|
||||
if match_local(s):
|
||||
snippet = (s.lyrics or '')[:150] + '...' if len(s.lyrics or '') > 150 else (s.lyrics or '')
|
||||
results.append({
|
||||
'title': s.title,
|
||||
'artist': s.artist,
|
||||
'band': s.band,
|
||||
'snippet': snippet,
|
||||
'source': 'local_db'
|
||||
})
|
||||
|
||||
# Then search ChartLyrics (free API)
|
||||
try:
|
||||
# ChartLyrics search - expects artist and song
|
||||
# We'll try different combinations based on filter
|
||||
search_terms = []
|
||||
|
||||
if filter_type == 'artist':
|
||||
search_terms.append({'artist': q, 'song': ''})
|
||||
elif filter_type == 'title':
|
||||
search_terms.append({'artist': '', 'song': q})
|
||||
else:
|
||||
# For 'all' and 'band', try both
|
||||
search_terms.append({'artist': q, 'song': ''})
|
||||
search_terms.append({'artist': '', 'song': q})
|
||||
|
||||
for term in search_terms:
|
||||
try:
|
||||
artist_encoded = urllib.parse.quote(term['artist'])
|
||||
song_encoded = urllib.parse.quote(term['song'])
|
||||
url = f'http://api.chartlyrics.com/apiv1.asmx/SearchLyric?artist={artist_encoded}&song={song_encoded}'
|
||||
|
||||
with urllib.request.urlopen(url, timeout=5) as response:
|
||||
xml_data = response.read()
|
||||
root = ET.fromstring(xml_data)
|
||||
|
||||
# Parse XML results (ChartLyrics returns XML)
|
||||
namespace = {'cl': 'http://api.chartlyrics.com/'}
|
||||
for item in root.findall('.//cl:SearchLyricResult', namespace):
|
||||
title = item.find('cl:Song', namespace)
|
||||
artist = item.find('cl:Artist', namespace)
|
||||
lyric_id = item.find('cl:LyricId', namespace)
|
||||
|
||||
if title is not None and artist is not None:
|
||||
# Get actual lyrics
|
||||
snippet = "Preview available - click to view full lyrics"
|
||||
|
||||
results.append({
|
||||
'title': title.text or 'Unknown',
|
||||
'artist': artist.text or 'Unknown',
|
||||
'band': artist.text or 'Unknown',
|
||||
'snippet': snippet,
|
||||
'source': 'chartlyrics',
|
||||
'lyric_id': lyric_id.text if lyric_id is not None else None
|
||||
})
|
||||
|
||||
# Limit to 5 results per search
|
||||
if len([r for r in results if r.get('source') == 'chartlyrics']) >= 5:
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
# If external API fails, we still have local results
|
||||
pass
|
||||
|
||||
# (Genius integration removed per user request)
|
||||
|
||||
# Remove duplicates and limit total results
|
||||
seen = set()
|
||||
unique_results = []
|
||||
for r in results:
|
||||
key = (r['title'].lower(), r['artist'].lower())
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_results.append(r)
|
||||
if len(unique_results) >= 10:
|
||||
break
|
||||
|
||||
# Add Lifeway Worship search link as an additional source entry
|
||||
try:
|
||||
import urllib.parse as _urlparse
|
||||
lifeway_url = f"https://worship.lifeway.com/findAndBuy/home?findMappable=false&searchString={_urlparse.quote(q)}"
|
||||
unique_results.append({
|
||||
'title': f'Search on Lifeway: {q}',
|
||||
'artist': '',
|
||||
'band': 'Lifeway Worship',
|
||||
'snippet': 'Open Lifeway Worship results for this query',
|
||||
'source': 'lifeway',
|
||||
'url': lifeway_url
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
'query': q,
|
||||
'filter': filter_type,
|
||||
'results': unique_results,
|
||||
'sources': 'Local DB + ChartLyrics (+ Lifeway link)'
|
||||
})
|
||||
|
||||
@app.route('/api/export/<int:plan_id>')
|
||||
def export_plan(plan_id):
|
||||
"""Export a plan's songs as a formatted text document."""
|
||||
db = get_db()
|
||||
plan = db.query(Plan).get(plan_id)
|
||||
if not plan:
|
||||
return jsonify({'error':'plan_not_found'}), 404
|
||||
|
||||
profile = db.query(Profile).get(plan.profile_id) if plan.profile_id else None
|
||||
plan_songs = db.query(PlanSong).filter(PlanSong.plan_id==plan_id).order_by(PlanSong.order_index).all()
|
||||
|
||||
lines = []
|
||||
lines.append('=' * 60)
|
||||
lines.append(f'WORSHIP SETLIST: {plan.date.strftime("%B %d, %Y")}')
|
||||
if profile:
|
||||
lines.append(f'Profile: {profile.name} (Key: {profile.default_key})')
|
||||
if plan.memo:
|
||||
lines.append(f'Notes: {plan.memo}')
|
||||
lines.append('=' * 60)
|
||||
lines.append('')
|
||||
|
||||
for idx, ps in enumerate(plan_songs, 1):
|
||||
song = db.query(Song).get(ps.song_id)
|
||||
if not song:
|
||||
continue
|
||||
lines.append(f'{idx}. {song.title}')
|
||||
lines.append(f' Artist: {song.artist or "Unknown"} | Band: {song.band or "Unknown"}')
|
||||
lines.append('')
|
||||
if song.lyrics:
|
||||
lines.append(' LYRICS:')
|
||||
for line in song.lyrics.splitlines():
|
||||
lines.append(f' {line}')
|
||||
lines.append('')
|
||||
if song.chords:
|
||||
lines.append(' CHORDS:')
|
||||
for line in song.chords.splitlines():
|
||||
lines.append(f' {line}')
|
||||
lines.append('')
|
||||
lines.append('-' * 60)
|
||||
lines.append('')
|
||||
|
||||
content = '\n'.join(lines)
|
||||
response = app.make_response(content)
|
||||
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="setlist_{plan.date.isoformat()}.txt"'
|
||||
return response
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Bind to all interfaces for optional external testing on alternate port
|
||||
app.run(host='0.0.0.0', port=FLASK_PORT, debug=True)
|
||||
59
legacy-site/backend/._archived_sqlite/models.py
Normal file
59
legacy-site/backend/._archived_sqlite/models.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Text, Date, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, scoped_session
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'app.db')
|
||||
engine = create_engine(f'sqlite:///{DB_PATH}', echo=False, future=True, pool_pre_ping=True, pool_recycle=3600)
|
||||
SessionLocal = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False))
|
||||
Base = declarative_base()
|
||||
|
||||
class Profile(Base):
|
||||
__tablename__ = 'profiles'
|
||||
id = Column(String(200), primary_key=True) # Support both int and UUID strings
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
email = Column(String(100), default='')
|
||||
contact_number = Column(String(50), default='')
|
||||
default_key = Column(String(10), default='C')
|
||||
notes = Column(Text, default='')
|
||||
|
||||
class Song(Base):
|
||||
__tablename__ = 'songs'
|
||||
id = Column(String(200), primary_key=True) # Support UUID strings
|
||||
title = Column(String(200), nullable=False)
|
||||
artist = Column(String(200), default='')
|
||||
band = Column(String(200), default='')
|
||||
singer = Column(String(200), default='')
|
||||
lyrics = Column(Text, default='')
|
||||
chords = Column(Text, default='')
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Plan(Base):
|
||||
__tablename__ = 'plans'
|
||||
id = Column(Integer, primary_key=True)
|
||||
date = Column(Date, nullable=False)
|
||||
profile_id = Column(String(200), ForeignKey('profiles.id'))
|
||||
memo = Column(Text, default='')
|
||||
profile = relationship('Profile')
|
||||
|
||||
class PlanSong(Base):
|
||||
__tablename__ = 'plan_songs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
plan_id = Column(Integer, ForeignKey('plans.id'))
|
||||
song_id = Column(String(200), ForeignKey('songs.id'))
|
||||
order_index = Column(Integer, default=0)
|
||||
plan = relationship('Plan')
|
||||
song = relationship('Song')
|
||||
|
||||
class ProfileSong(Base):
|
||||
__tablename__ = 'profile_songs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
profile_id = Column(String(200), ForeignKey('profiles.id'))
|
||||
song_id = Column(String(200), ForeignKey('songs.id'))
|
||||
song_key = Column(String(10), default='C')
|
||||
profile = relationship('Profile')
|
||||
song = relationship('Song')
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(engine)
|
||||
@@ -0,0 +1,59 @@
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Text, Date, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, scoped_session
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'app.db')
|
||||
engine = create_engine(f'sqlite:///{DB_PATH}', echo=False, future=True, pool_pre_ping=True, pool_recycle=3600)
|
||||
SessionLocal = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False))
|
||||
Base = declarative_base()
|
||||
|
||||
class Profile(Base):
|
||||
__tablename__ = 'profiles'
|
||||
id = Column(String(200), primary_key=True) # Support both int and UUID strings
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
email = Column(String(100), default='')
|
||||
contact_number = Column(String(50), default='')
|
||||
default_key = Column(String(10), default='C')
|
||||
notes = Column(Text, default='')
|
||||
|
||||
class Song(Base):
|
||||
__tablename__ = 'songs'
|
||||
id = Column(String(200), primary_key=True) # Support UUID strings
|
||||
title = Column(String(200), nullable=False)
|
||||
artist = Column(String(200), default='')
|
||||
band = Column(String(200), default='')
|
||||
singer = Column(String(200), default='')
|
||||
lyrics = Column(Text, default='')
|
||||
chords = Column(Text, default='')
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Plan(Base):
|
||||
__tablename__ = 'plans'
|
||||
id = Column(Integer, primary_key=True)
|
||||
date = Column(Date, nullable=False)
|
||||
profile_id = Column(String(200), ForeignKey('profiles.id'))
|
||||
memo = Column(Text, default='')
|
||||
profile = relationship('Profile')
|
||||
|
||||
class PlanSong(Base):
|
||||
__tablename__ = 'plan_songs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
plan_id = Column(Integer, ForeignKey('plans.id'))
|
||||
song_id = Column(String(200), ForeignKey('songs.id'))
|
||||
order_index = Column(Integer, default=0)
|
||||
plan = relationship('Plan')
|
||||
song = relationship('Song')
|
||||
|
||||
class ProfileSong(Base):
|
||||
__tablename__ = 'profile_songs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
profile_id = Column(String(200), ForeignKey('profiles.id'))
|
||||
song_id = Column(String(200), ForeignKey('songs.id'))
|
||||
song_key = Column(String(10), default='C')
|
||||
profile = relationship('Profile')
|
||||
song = relationship('Song')
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(engine)
|
||||
Reference in New Issue
Block a user