diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..703e8a4 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# .env - Flask environment variables + +# Flask secret key (change this to something secure in production) +SECRET_KEY=Ana_Are_Multe_Mere-Si_Nu_Are_Pere + +# Flask environment: development or production +FLASK_ENV=development + +# Database location (optional, defaults to instance/dashboard.db) +# SQLALCHEMY_DATABASE_URI=sqlite:///instance/dashboard.db + +# Default admin user credentials (used for auto-creation) +DEFAULT_USER=admin +DEFAULT_PASSWORD=1234 + +# Flask server settings +HOST=0.0.0.0 +PORT=5000 + +# Maximum upload size (in bytes, 2GB) +MAX_CONTENT_LENGTH=2147483648 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e4e193..fd38011 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ digiscreen/ +.env diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc index 20f006d..b0108ec 100644 Binary files a/__pycache__/app.cpython-311.pyc and b/__pycache__/app.cpython-311.pyc differ diff --git a/__pycache__/models.cpython-311.pyc b/__pycache__/models.cpython-311.pyc index 1530083..30219bd 100644 Binary files a/__pycache__/models.cpython-311.pyc and b/__pycache__/models.cpython-311.pyc differ diff --git a/app.py b/app.py index 678a977..78d263c 100755 --- a/app.py +++ b/app.py @@ -7,9 +7,13 @@ from werkzeug.utils import secure_filename from functools import wraps from extensions import db, bcrypt, login_manager from sqlalchemy import text +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() # First import models -from models import User, Player, Content, Group, ServerLog +from models import User, Player, Group, Content, ServerLog, group_player # Then import utilities that use the models from flask_login import login_user, logout_user, login_required, current_user @@ -47,8 +51,10 @@ app = Flask(__name__, instance_relative_config=True) # Set the secret key from environment variable or use a default value app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'Ana_Are_Multe_Mere-Si_Nu_Are_Pere') -# Configure the database location to be in the instance folder -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(app.instance_path, 'dashboard.db') +instance_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'instance')) +os.makedirs(instance_dir, exist_ok=True) +db_path = os.path.join(instance_dir, 'dashboard.db') +app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Set maximum content length to 1GB @@ -56,6 +62,7 @@ app.config['MAX_CONTENT_LENGTH'] = 2048 * 2048 * 2048 # 2GB, adjust as needed # Ensure the instance folder exists os.makedirs(app.instance_path, exist_ok=True) +os.makedirs(instance_dir, exist_ok=True) db.init_app(app) bcrypt.init_app(app) @@ -68,8 +75,9 @@ app.config['UPLOAD_FOLDERLOGO'] = UPLOAD_FOLDERLOGO # Ensure the upload folder exists if not os.path.exists(UPLOAD_FOLDER): - os.makedirs(UPLOAD_FOLDER) - os.makedirs(UPLOAD_FOLDERLOGO) + os.makedirs(UPLOAD_FOLDER, exist_ok=True) +if not os.path.exists(UPLOAD_FOLDERLOGO): + os.makedirs(UPLOAD_FOLDERLOGO, exist_ok=True) login_manager.login_view = 'login' @@ -303,7 +311,8 @@ def add_player(): hostname = request.form['hostname'] password = bcrypt.generate_password_hash(request.form['password']).decode('utf-8') quickconnect_password = bcrypt.generate_password_hash(request.form['quickconnect_password']).decode('utf-8') - add_player_util(username, hostname, password, quickconnect_password) + orientation = request.form.get('orientation', 'Landscape') # <-- Get orientation + add_player_util(username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation flash(f'Player "{username}" added successfully.', 'success') return redirect(url_for('dashboard')) return render_template('add_player.html') @@ -637,6 +646,16 @@ def create_admin(username, password): db.session.commit() print(f"Admin user '{username}' created successfully.") +from models.create_default_user import create_default_user + +with app.app_context(): + try: + db.session.execute(db.select(User).limit(1)) + except Exception as e: + print("Database not initialized or missing tables. Re-initializing...") + db.create_all() + # Always ensure default user exists + create_default_user(db, User, bcrypt) # Add this at the end of app.py if __name__ == '__main__': diff --git a/clear_db.py b/clear_db.py deleted file mode 100644 index e6b9e04..0000000 --- a/clear_db.py +++ /dev/null @@ -1,14 +0,0 @@ -import os -from flask import Flask -from flask_sqlalchemy import SQLAlchemy - -# Create a minimal Flask app just for clearing the database -app = Flask(__name__) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///instance/dashboard.db' -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -db = SQLAlchemy(app) - -with app.app_context(): - db.reflect() # This loads all tables from the database - db.drop_all() - print("Dropped all tables successfully.") \ No newline at end of file diff --git a/create_default_user.py b/create_default_user.py deleted file mode 100644 index d6b822a..0000000 --- a/create_default_user.py +++ /dev/null @@ -1,20 +0,0 @@ -from app import app, db, User, bcrypt - -# Create the default user -username = 'admin' -password = '1234' -hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') - -with app.app_context(): - # Delete the existing user if it exists - existing_user = User.query.filter_by(username=username).first() - if existing_user: - db.session.delete(existing_user) - db.session.commit() - - # Add the new user to the database - default_user = User(username=username, password=hashed_password, role='admin') - db.session.add(default_user) - db.session.commit() - -print(f"Default user '{username}' created with password '{password}'") \ No newline at end of file diff --git a/instance/dashboard.db b/instance/dashboard.db index 6abf2db..9fbfd2a 100644 Binary files a/instance/dashboard.db and b/instance/dashboard.db differ diff --git a/models.py b/models.py deleted file mode 100644 index a08713d..0000000 --- a/models.py +++ /dev/null @@ -1,77 +0,0 @@ -from extensions import db -from flask_bcrypt import Bcrypt -from flask_login import UserMixin -import datetime # Add this import - -bcrypt = Bcrypt() - -# Add this new model -class ServerLog(db.Model): - id = db.Column(db.Integer, primary_key=True) - action = db.Column(db.String(255), nullable=False) - timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow) - - def __repr__(self): - return f"" - -class Content(db.Model): - id = db.Column(db.Integer, primary_key=True) - file_name = db.Column(db.String(255), nullable=False) - duration = db.Column(db.Integer, nullable=False) - player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=False) - position = db.Column(db.Integer, default=0) # This field must exist - -class Player(db.Model): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(255), nullable=False) - hostname = db.Column(db.String(255), nullable=False) - password = db.Column(db.String(255), nullable=False) - quickconnect_password = db.Column(db.String(255), nullable=True) - playlist_version = db.Column(db.Integer, default=1) # Make sure this exists - locked_to_group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=True) - locked_to_group = db.relationship('Group', foreign_keys=[locked_to_group_id], backref='locked_players') - - def verify_quickconnect_code(self, code): - return bcrypt.check_password_hash(self.quickconnect_password, code) - -class User(db.Model, UserMixin): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(80), unique=True, nullable=False) - password = db.Column(db.String(120), nullable=False) - role = db.Column(db.String(80), nullable=False) - theme = db.Column(db.String(80), default='light') - - def set_password(self, password): - self.password = bcrypt.generate_password_hash(password).decode('utf-8') - - def check_password(self, password): - return bcrypt.check_password_hash(self.password, password) - - @property - def is_active(self): - return True - - @property - def is_authenticated(self): - return True - - @property - def is_anonymous(self): - return False - - def get_id(self): - return str(self.id) - -class Group(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False, unique=True) - players = db.relationship('Player', secondary='group_player', backref='groups') - playlist_version = db.Column(db.Integer, default=0) # Playlist version counter - -# Association table for many-to-many relationship between Group and Player -group_player = db.Table('group_player', - db.Column('group_id', db.Integer, db.ForeignKey('group.id'), primary_key=True), - db.Column('player_id', db.Integer, db.ForeignKey('player.id'), primary_key=True) -) - -# other models... \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..8bea2f1 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +from .user import User +from .player import Player +from .group import Group, group_player +from .content import Content +from .server_log import ServerLog \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-311.pyc b/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1135501 Binary files /dev/null and b/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/models/__pycache__/content.cpython-311.pyc b/models/__pycache__/content.cpython-311.pyc new file mode 100644 index 0000000..d1742f7 Binary files /dev/null and b/models/__pycache__/content.cpython-311.pyc differ diff --git a/models/__pycache__/create_default_user.cpython-311.pyc b/models/__pycache__/create_default_user.cpython-311.pyc new file mode 100644 index 0000000..7d01eaa Binary files /dev/null and b/models/__pycache__/create_default_user.cpython-311.pyc differ diff --git a/models/__pycache__/group.cpython-311.pyc b/models/__pycache__/group.cpython-311.pyc new file mode 100644 index 0000000..353c366 Binary files /dev/null and b/models/__pycache__/group.cpython-311.pyc differ diff --git a/models/__pycache__/player.cpython-311.pyc b/models/__pycache__/player.cpython-311.pyc new file mode 100644 index 0000000..bff7c05 Binary files /dev/null and b/models/__pycache__/player.cpython-311.pyc differ diff --git a/models/__pycache__/server_log.cpython-311.pyc b/models/__pycache__/server_log.cpython-311.pyc new file mode 100644 index 0000000..daa62a6 Binary files /dev/null and b/models/__pycache__/server_log.cpython-311.pyc differ diff --git a/models/__pycache__/user.cpython-311.pyc b/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..09c1534 Binary files /dev/null and b/models/__pycache__/user.cpython-311.pyc differ diff --git a/models/clear_db.py b/models/clear_db.py new file mode 100644 index 0000000..994a0b7 --- /dev/null +++ b/models/clear_db.py @@ -0,0 +1,21 @@ +import os +from flask import Flask +from flask_sqlalchemy import SQLAlchemy + +# Ensure the instance directory exists (relative to project root) +instance_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'instance')) +os.makedirs(instance_dir, exist_ok=True) + +# Set the correct database URI +db_path = os.path.join(instance_dir, 'dashboard.db') +print(f"Using database at: {db_path}") + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) + +with app.app_context(): + db.reflect() # This loads all tables from the database + db.drop_all() + print("Dropped all tables successfully.") \ No newline at end of file diff --git a/models/content.py b/models/content.py new file mode 100644 index 0000000..ff2fdb8 --- /dev/null +++ b/models/content.py @@ -0,0 +1,8 @@ +from extensions import db + +class Content(db.Model): + id = db.Column(db.Integer, primary_key=True) + file_name = db.Column(db.String(255), nullable=False) + duration = db.Column(db.Integer, nullable=False) + player_id = db.Column(db.Integer, db.ForeignKey('player.id'), nullable=False) + position = db.Column(db.Integer, default=0) \ No newline at end of file diff --git a/models/create_default_user.py b/models/create_default_user.py new file mode 100644 index 0000000..1e8d2fa --- /dev/null +++ b/models/create_default_user.py @@ -0,0 +1,18 @@ +#from app import app, db, User, bcrypt +import os + +def create_default_user(db, User, bcrypt): + username = os.getenv('DEFAULT_USER', 'admin') + password = os.getenv('DEFAULT_PASSWORD', '1234') + hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') + existing_user = User.query.filter_by(username=username).first() + if not existing_user: + default_user = User(username=username, password=hashed_password, role='admin') + db.session.add(default_user) + db.session.commit() + print(f"Default user '{username}' created with password '{password}'") + else: + print(f"Default user '{username}' already exists.") + +#with app.app_context(): +# create_default_user(db, User, bcrypt) \ No newline at end of file diff --git a/models/group.py b/models/group.py new file mode 100644 index 0000000..f73ad4f --- /dev/null +++ b/models/group.py @@ -0,0 +1,13 @@ +from extensions import db + +class Group(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, unique=True) + orientation = db.Column(db.String(16), nullable=False, default='Landscape') # <-- Add this line + players = db.relationship('Player', secondary='group_player', backref='groups') + playlist_version = db.Column(db.Integer, default=0) + +group_player = db.Table('group_player', + db.Column('group_id', db.Integer, db.ForeignKey('group.id'), primary_key=True), + db.Column('player_id', db.Integer, db.ForeignKey('player.id'), primary_key=True) +) \ No newline at end of file diff --git a/init_db.py b/models/init_db.py similarity index 100% rename from init_db.py rename to models/init_db.py diff --git a/models/player.py b/models/player.py new file mode 100644 index 0000000..45fef6b --- /dev/null +++ b/models/player.py @@ -0,0 +1,18 @@ +from extensions import db +from flask_bcrypt import Bcrypt + +bcrypt = Bcrypt() + +class Player(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(255), nullable=False) + hostname = db.Column(db.String(255), nullable=False) + password = db.Column(db.String(255), nullable=False) + quickconnect_password = db.Column(db.String(255), nullable=True) + playlist_version = db.Column(db.Integer, default=1) + locked_to_group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=True) + locked_to_group = db.relationship('Group', foreign_keys=[locked_to_group_id], backref='locked_players') + orientation = db.Column(db.String(16), nullable=False, default='Landscape') # <-- Add this line + + def verify_quickconnect_code(self, code): + return bcrypt.check_password_hash(self.quickconnect_password, code) \ No newline at end of file diff --git a/models/server_log.py b/models/server_log.py new file mode 100644 index 0000000..1907f2e --- /dev/null +++ b/models/server_log.py @@ -0,0 +1,10 @@ +from extensions import db +import datetime + +class ServerLog(db.Model): + id = db.Column(db.Integer, primary_key=True) + action = db.Column(db.String(255), nullable=False) + timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..fbedab2 --- /dev/null +++ b/models/user.py @@ -0,0 +1,33 @@ +from extensions import db +from flask_bcrypt import Bcrypt +from flask_login import UserMixin + +bcrypt = Bcrypt() + +class User(db.Model, UserMixin): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(120), nullable=False) + role = db.Column(db.String(80), nullable=False) + theme = db.Column(db.String(80), default='light') + + def set_password(self, password): + self.password = bcrypt.generate_password_hash(password).decode('utf-8') + + def check_password(self, password): + return bcrypt.check_password_hash(self.password, password) + + @property + def is_active(self): + return True + + @property + def is_authenticated(self): + return True + + @property + def is_anonymous(self): + return False + + def get_id(self): + return str(self.id) \ No newline at end of file diff --git a/templates/add_player.html b/templates/add_player.html index d688465..c81f1c5 100644 --- a/templates/add_player.html +++ b/templates/add_player.html @@ -60,6 +60,17 @@ +
+
+
+ + +
+
+
Back to Dashboard diff --git a/test_api.py b/test_api.py deleted file mode 100644 index 5bf5bb7..0000000 --- a/test_api.py +++ /dev/null @@ -1,60 +0,0 @@ -import requests -import os - -# Replace with the actual server IP address or domain name, hostname, and quick connect code -server_ip = 'http://localhost:5000' -hostname = 'rpi-tv11' -quickconnect_code = '8887779' - -# Construct the URL for the playlist API -url = f'{server_ip}/api/playlists' -params = { - 'hostname': hostname, - 'quickconnect_code': quickconnect_code -} - -# Make the GET request to the API -response = requests.get(url, params=params) - -# Print the raw response content and status code for debugging -print(f'Status Code: {response.status_code}') -print(f'Response Content: {response.text}') - -# Check if the request was successful -if response.status_code == 200: - try: - # Parse the JSON response - response_data = response.json() - playlist = response_data.get('playlist', []) - playlist_version = response_data.get('playlist_version', None) - - print(f'Playlist Version: {playlist_version}') - print(f'Playlist: {playlist}') - - # Define the local folder for saving files - local_folder = './static/resurse' - if not os.path.exists(local_folder): - os.makedirs(local_folder) - - # Download each file in the playlist - for media in playlist: - file_name = media.get('file_name', '') - file_url = media.get('url', '') - duration = media.get('duration', 10) # Default duration if not provided - local_file_path = os.path.join(local_folder, file_name) - - print(f'Downloading {file_name} from {file_url}...') - try: - file_response = requests.get(file_url, timeout=10) - if file_response.status_code == 200: - with open(local_file_path, 'wb') as file: - file.write(file_response.content) - print(f'Successfully downloaded {file_name} to {local_file_path}') - else: - print(f'Failed to download {file_name}. Status Code: {file_response.status_code}') - except requests.exceptions.RequestException as e: - print(f'Error downloading {file_name}: {e}') - except requests.exceptions.JSONDecodeError as e: - print(f'Failed to parse JSON response: {e}') -else: - print(f'Failed to retrieve playlist. Status Code: {response.status_code}') \ No newline at end of file diff --git a/utils/__pycache__/group_player_management.cpython-311.pyc b/utils/__pycache__/group_player_management.cpython-311.pyc index 71a5c79..830f25c 100644 Binary files a/utils/__pycache__/group_player_management.cpython-311.pyc and b/utils/__pycache__/group_player_management.cpython-311.pyc differ diff --git a/utils/__pycache__/logger.cpython-311.pyc b/utils/__pycache__/logger.cpython-311.pyc index 9f64de5..675508c 100644 Binary files a/utils/__pycache__/logger.cpython-311.pyc and b/utils/__pycache__/logger.cpython-311.pyc differ diff --git a/utils/__pycache__/uploads.cpython-311.pyc b/utils/__pycache__/uploads.cpython-311.pyc index 4b81569..22b1f34 100644 Binary files a/utils/__pycache__/uploads.cpython-311.pyc and b/utils/__pycache__/uploads.cpython-311.pyc differ diff --git a/utils/group_player_management.py b/utils/group_player_management.py index cf50be9..cdd1d32 100644 --- a/utils/group_player_management.py +++ b/utils/group_player_management.py @@ -8,28 +8,29 @@ from utils.logger import ( log_content_duration_changed, log_content_added ) -def create_group(name, player_ids): +def create_group(name, player_ids, orientation='Landscape'): """ - Create a new group with the given name and add selected players to it. - Clears individual playlists of players and locks them to the group. + Create a new group with the given name, orientation, and add selected players. + Only players with the same orientation can be added. """ - new_group = Group(name=name) + # Check all players have the same orientation + for player_id in player_ids: + player = Player.query.get(player_id) + if player and player.orientation != orientation: + raise ValueError(f"Player '{player.username}' has orientation '{player.orientation}', which does not match group orientation '{orientation}'.") + + new_group = Group(name=name, orientation=orientation) db.session.add(new_group) db.session.flush() # Get the group ID - + # Add players to the group and lock them for player_id in player_ids: player = Player.query.get(player_id) if player: - # Add player to group new_group.players.append(player) - - # Delete player's individual playlist Content.query.filter_by(player_id=player.id).delete() - - # Lock player to this group player.locked_to_group_id = new_group.id - + db.session.commit() log_group_created(name) return new_group @@ -106,7 +107,7 @@ def delete_group(group_id): db.session.commit() log_group_deleted(group_name) -def add_player(username, hostname, password, quickconnect_password): +def add_player(username, hostname, password, quickconnect_password, orientation='Landscape'): """ Add a new player with the given details. """ @@ -120,7 +121,8 @@ def add_player(username, hostname, password, quickconnect_password): username=username, hostname=hostname, password=hashed_password, - quickconnect_password=hashed_quickconnect + quickconnect_password=hashed_quickconnect, + orientation=orientation ) db.session.add(new_player)