Initial clean commit
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
from datetime import datetime
|
||||
from musicround import db
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import uuid
|
||||
|
||||
# User-Role association table for many-to-many relationship
|
||||
user_roles = db.Table('user_roles',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), primary_key=True),
|
||||
db.Column('role_id', db.Integer, db.ForeignKey('role.id', ondelete='CASCADE'), primary_key=True)
|
||||
)
|
||||
|
||||
class Role(db.Model):
|
||||
"""
|
||||
Role model for user permissions
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||
description = db.Column(db.String(255))
|
||||
|
||||
def __repr__(self):
|
||||
return f"Role(id={self.id}, name='{self.name}')"
|
||||
|
||||
|
||||
class UserPreferences(db.Model):
|
||||
"""
|
||||
User preferences model for storing user-specific settings
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), unique=True)
|
||||
default_tts_service = db.Column(db.String(32), default='polly')
|
||||
enable_intro = db.Column(db.Boolean, default=True)
|
||||
theme = db.Column(db.String(16), default='light')
|
||||
user = db.relationship('User', back_populates='preferences')
|
||||
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
"""
|
||||
User model for authentication and user management
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), index=True, unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), index=True, unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(255), nullable=True) # Now nullable for OAuth-only users
|
||||
first_name = db.Column(db.String(50))
|
||||
last_name = db.Column(db.String(50))
|
||||
active = db.Column(db.Boolean, default=True)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_login = db.Column(db.DateTime)
|
||||
|
||||
# Token for password reset, email verification, etc.
|
||||
reset_token = db.Column(db.String(100), index=True, unique=True)
|
||||
reset_token_expiry = db.Column(db.DateTime)
|
||||
|
||||
# Authentication provider info
|
||||
auth_provider = db.Column(db.String(20), default='local') # 'local', 'google', 'authentik'
|
||||
|
||||
# OAuth provider info - Spotify
|
||||
oauth_id = db.Column(db.String(100)) # Spotify user ID
|
||||
spotify_token = db.Column(db.Text) # Store Spotify access token
|
||||
spotify_refresh_token = db.Column(db.Text) # Store Spotify refresh token
|
||||
spotify_token_expiry = db.Column(db.DateTime)
|
||||
|
||||
# OAuth provider info - Google
|
||||
google_id = db.Column(db.String(100)) # Google user ID
|
||||
google_token = db.Column(db.Text) # Store Google access token
|
||||
google_refresh_token = db.Column(db.Text) # Store Google refresh token
|
||||
|
||||
# OAuth provider info - Authentik
|
||||
authentik_id = db.Column(db.String(100)) # Authentik user ID
|
||||
authentik_token = db.Column(db.Text) # Store Authentik access token
|
||||
authentik_refresh_token = db.Column(db.Text) # Store Authentik refresh token
|
||||
|
||||
# OAuth provider info - Dropbox
|
||||
dropbox_id = db.Column(db.String(100)) # Dropbox user ID
|
||||
dropbox_token = db.Column(db.Text) # Store Dropbox access token
|
||||
dropbox_refresh_token = db.Column(db.Text) # Store Dropbox refresh token
|
||||
dropbox_token_expiry = db.Column(db.DateTime) # Dropbox token expiration
|
||||
dropbox_export_path = db.Column(db.String(255), default='/QuizzicalBeats') # User's preferred Dropbox export folder
|
||||
|
||||
# User preferences
|
||||
intro_mp3 = db.Column(db.String(255)) # Custom intro MP3 path
|
||||
outro_mp3 = db.Column(db.String(255)) # Custom outro MP3 path
|
||||
replay_mp3 = db.Column(db.String(255)) # Custom replay MP3 path
|
||||
|
||||
# Relationships
|
||||
roles = db.relationship('Role', secondary=user_roles, backref=db.backref('users', lazy='dynamic'))
|
||||
preferences = db.relationship('UserPreferences', uselist=False, back_populates='user')
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
"""Password getter should raise an error"""
|
||||
raise AttributeError('password is not a readable attribute')
|
||||
|
||||
@password.setter
|
||||
def password(self, password):
|
||||
"""Hash and store the password"""
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check if provided password matches the hash"""
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def set_token(self):
|
||||
"""Generate a unique token for the user"""
|
||||
self.reset_token = str(uuid.uuid4())
|
||||
return self.reset_token
|
||||
|
||||
def has_role(self, role_name):
|
||||
"""Check if user has a specific role"""
|
||||
return any(role.name == role_name for role in self.roles)
|
||||
|
||||
def is_admin(self):
|
||||
"""Check if user is an admin"""
|
||||
return self.has_role('admin')
|
||||
|
||||
def __repr__(self):
|
||||
return f"User(id={self.id}, username='{self.username}', email='{self.email}')"
|
||||
|
||||
|
||||
class Tag(db.Model):
|
||||
"""
|
||||
Tag model for storing song tags
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), unique=True, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Define relationship to songs via SongTag
|
||||
songs = db.relationship('Song', secondary='song_tag', back_populates='tags')
|
||||
|
||||
def __repr__(self):
|
||||
return f"Tag(id={self.id}, name='{self.name}')"
|
||||
|
||||
|
||||
class SongTag(db.Model):
|
||||
"""
|
||||
Association table for Song-Tag many-to-many relationship
|
||||
"""
|
||||
__tablename__ = 'song_tag'
|
||||
|
||||
song_id = db.Column(db.Integer, db.ForeignKey('song.id', ondelete='CASCADE'), primary_key=True)
|
||||
tag_id = db.Column(db.Integer, db.ForeignKey('tag.id', ondelete='CASCADE'), primary_key=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f"SongTag(song_id={self.song_id}, tag_id={self.tag_id})"
|
||||
|
||||
|
||||
class Song(db.Model):
|
||||
"""
|
||||
Song model for storing song details from Spotify and Deezer
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
spotify_id = db.Column(db.String(100), unique=True, nullable=True)
|
||||
deezer_id = db.Column(db.Integer, unique=True, nullable=True)
|
||||
isrc = db.Column(db.String(20), index=True, nullable=True) # Add ISRC code
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
artist = db.Column(db.String(200), nullable=False)
|
||||
album_name = db.Column(db.String(200), nullable=True) # Album name
|
||||
genre = db.Column(db.String(100))
|
||||
year = db.Column(db.Integer)
|
||||
preview_url = db.Column(db.String(500)) # Increased length for longer URLs - primary preview URL
|
||||
cover_url = db.Column(db.String(500)) # Increased length - primary cover URL
|
||||
spotify_preview_url = db.Column(db.String(500), nullable=True) # Spotify-specific preview
|
||||
deezer_preview_url = db.Column(db.String(500), nullable=True) # Deezer-specific preview
|
||||
apple_preview_url = db.Column(db.String(500), nullable=True) # Apple Music preview
|
||||
youtube_preview_url = db.Column(db.String(500), nullable=True) # YouTube preview
|
||||
spotify_cover_url = db.Column(db.String(500), nullable=True) # Spotify cover
|
||||
deezer_cover_url = db.Column(db.String(500), nullable=True) # Deezer cover
|
||||
apple_cover_url = db.Column(db.String(500), nullable=True) # Apple Music cover
|
||||
popularity = db.Column(db.Integer)
|
||||
used_count = db.Column(db.Integer, default=0)
|
||||
source = db.Column(db.String(20), default='spotify') # 'spotify', 'deezer', or 'acrcloud'
|
||||
import_date = db.Column(db.DateTime, default=datetime.utcnow) # Add import date
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_used = db.Column(db.DateTime)
|
||||
metadata_sources = db.Column(db.String(500), nullable=True) # Comma-separated list of metadata sources
|
||||
|
||||
# Spotify Audio Features
|
||||
acousticness = db.Column(db.Float, nullable=True) # Confidence of track being acoustic (0.0 to 1.0)
|
||||
danceability = db.Column(db.Float, nullable=True) # How suitable for dancing (0.0 to 1.0)
|
||||
energy = db.Column(db.Float, nullable=True) # Intensity and activity measure (0.0 to 1.0)
|
||||
instrumentalness = db.Column(db.Float, nullable=True) # Predicts if track has no vocals (0.0 to 1.0)
|
||||
key = db.Column(db.Integer, nullable=True) # Key of the track (standard Pitch Class notation, -1 = no key)
|
||||
liveness = db.Column(db.Float, nullable=True) # Presence of audience in recording (0.0 to 1.0)
|
||||
loudness = db.Column(db.Float, nullable=True) # Overall loudness in dB (-60 to 0 typically)
|
||||
mode = db.Column(db.Integer, nullable=True) # Modality - major (1) or minor (0)
|
||||
speechiness = db.Column(db.Float, nullable=True) # Presence of spoken words (0.0 to 1.0)
|
||||
tempo = db.Column(db.Float, nullable=True) # Estimated tempo in BPM
|
||||
time_signature = db.Column(db.Integer, nullable=True) # Estimated time signature (3 to 7 representing 3/4 to 7/4)
|
||||
valence = db.Column(db.Float, nullable=True) # Musical positiveness (0.0 to 1.0)
|
||||
duration_ms = db.Column(db.Integer, nullable=True) # Duration of track in milliseconds
|
||||
analysis_url = db.Column(db.String(500), nullable=True) # URL to access full audio analysis
|
||||
|
||||
# Additional properties as JSON strings
|
||||
additional_data = db.Column(db.Text, nullable=True) # Store additional data as JSON
|
||||
|
||||
# Relationship with tags
|
||||
tags = db.relationship('Tag', secondary='song_tag', back_populates='songs')
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"Song('{self.id}', '{self.title}', '{self.artist}', "
|
||||
f"'{self.genre}', '{self.year}', source='{self.source}', "
|
||||
f"isrc='{self.isrc}', spotify_id='{self.spotify_id}', deezer_id='{self.deezer_id}')"
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'title': self.title,
|
||||
'artist': self.artist,
|
||||
'album_name': self.album_name,
|
||||
'cover_url': self.cover_url,
|
||||
'preview_url': self.preview_url,
|
||||
'spotify_id': self.spotify_id,
|
||||
'deezer_id': self.deezer_id,
|
||||
'isrc': self.isrc,
|
||||
'year': self.year,
|
||||
'genre': self.genre,
|
||||
'popularity': self.popularity,
|
||||
'used_count': self.used_count,
|
||||
'last_used': self.last_used.strftime('%Y-%m-%d %H:%M:%S') if self.last_used else None,
|
||||
'metadata_sources': self.metadata_sources,
|
||||
'tags': [{'id': tag.id, 'name': tag.name} for tag in self.tags],
|
||||
# Include audio features in the dictionary
|
||||
'acousticness': self.acousticness,
|
||||
'danceability': self.danceability,
|
||||
'energy': self.energy,
|
||||
'instrumentalness': self.instrumentalness,
|
||||
'key': self.key,
|
||||
'liveness': self.liveness,
|
||||
'loudness': self.loudness,
|
||||
'mode': self.mode,
|
||||
'speechiness': self.speechiness,
|
||||
'tempo': self.tempo,
|
||||
'time_signature': self.time_signature,
|
||||
'valence': self.valence,
|
||||
'duration_ms': self.duration_ms
|
||||
}
|
||||
|
||||
|
||||
class Round(db.Model):
|
||||
"""
|
||||
Round model for storing quiz rounds and their associated songs
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(200), nullable=True) # Optional name for the round
|
||||
round_type = db.Column(db.String(50), nullable=False)
|
||||
round_criteria_used = db.Column(db.String(500), nullable=False)
|
||||
songs = db.Column(db.Text, nullable=False) # JSON string of song IDs in order
|
||||
genre = db.Column(db.String(100))
|
||||
decade = db.Column(db.String(10))
|
||||
tag = db.Column(db.String(50))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
|
||||
mp3_generated = db.Column(db.Boolean, default=False) # Track if MP3 has been generated
|
||||
pdf_generated = db.Column(db.Boolean, default=False) # Track if PDF has been generated
|
||||
last_generated_at = db.Column(db.DateTime, nullable=True) # When files were last generated
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"Round('{self.name or self.id}', '{self.round_type}', '{self.round_criteria_used}', "
|
||||
f"'{self.songs}', '{self.created_at}')"
|
||||
)
|
||||
|
||||
@property
|
||||
def song_list(self):
|
||||
"""
|
||||
Returns a list of Song objects associated with this round
|
||||
"""
|
||||
song_ids = self.songs.split(',')
|
||||
return Song.query.filter(Song.id.in_(song_ids)).all()
|
||||
|
||||
def reset_generated_status(self):
|
||||
"""
|
||||
Reset the MP3 and PDF generated flags when a round is modified
|
||||
"""
|
||||
self.mp3_generated = False
|
||||
self.pdf_generated = False
|
||||
|
||||
|
||||
class RoundExport(db.Model):
|
||||
"""
|
||||
Model to track round exports to various destinations (Dropbox, email, etc.)
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
round_id = db.Column(db.Integer, db.ForeignKey('round.id', ondelete='CASCADE'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='SET NULL'), nullable=True)
|
||||
export_type = db.Column(db.String(20), nullable=False) # 'dropbox', 'email', etc.
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
destination = db.Column(db.String(500), nullable=True) # Path, email address, etc.
|
||||
include_mp3s = db.Column(db.Boolean, default=False) # Whether MP3s were included
|
||||
status = db.Column(db.String(20), default='success') # 'success', 'failed'
|
||||
error_message = db.Column(db.Text, nullable=True) # Error details if failed
|
||||
|
||||
# Relationships
|
||||
round = db.relationship('Round', backref=db.backref('exports', lazy='dynamic'))
|
||||
user = db.relationship('User', backref=db.backref('exports', lazy='dynamic'))
|
||||
|
||||
def __repr__(self):
|
||||
return f"RoundExport(id={self.id}, round_id={self.round_id}, type='{self.export_type}', timestamp='{self.timestamp}')"
|
||||
|
||||
|
||||
class SystemSetting(db.Model):
|
||||
__tablename__ = 'system_settings'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.String(64), unique=True, nullable=False)
|
||||
value = db.Column(db.Text, nullable=True)
|
||||
|
||||
@staticmethod
|
||||
def get(key, default=None):
|
||||
setting = SystemSetting.query.filter_by(key=key).first()
|
||||
return setting.value if setting else default
|
||||
|
||||
@staticmethod
|
||||
def set(key, value):
|
||||
setting = SystemSetting.query.filter_by(key=key).first()
|
||||
if not setting:
|
||||
setting = SystemSetting(key=key, value=value)
|
||||
db.session.add(setting)
|
||||
else:
|
||||
setting.value = value
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def all_settings():
|
||||
return {s.key: s.value for s in SystemSetting.query.all()}
|
||||
|
||||
Reference in New Issue
Block a user