from models import Player, Group, Content from extensions import db from utils.logger import ( log_group_created, log_group_edited, log_group_deleted, log_player_created, log_player_edited, log_player_deleted, log_player_added_to_group, log_player_removed_from_group, log_player_unlocked, log_content_reordered, log_content_duration_changed, log_content_added ) def create_group(name, player_ids, orientation='Landscape'): """ Create a new group with the given name, orientation, and add selected players. Only players with the same orientation can be added. """ # 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: new_group.players.append(player) Content.query.filter_by(player_id=player.id).delete() player.locked_to_group_id = new_group.id db.session.commit() log_group_created(name) return new_group def edit_group(group_id, name, player_ids): """ Edit an existing group, updating its name and players. Handles locking/unlocking players appropriately. """ group = Group.query.get_or_404(group_id) old_name = group.name # Store old name in case it changes group.name = name # Get current players in the group current_player_ids = [player.id for player in group.players] # Determine players to add and remove players_to_add = [pid for pid in player_ids if pid not in current_player_ids] players_to_remove = [pid for pid in current_player_ids if pid not in player_ids] # Handle players to add for player_id in players_to_add: player = Player.query.get(player_id) if player: # Add to group group.players.append(player) # Delete individual playlist Content.query.filter_by(player_id=player.id).delete() # Lock to group player.locked_to_group_id = group.id # Log this action log_player_added_to_group(player.username, name) # Handle players to remove for player_id in players_to_remove: player = Player.query.get(player_id) if player: # Remove from group group.players.remove(player) # Unlock from group player.locked_to_group_id = None # Log this action log_player_removed_from_group(player.username, name) log_player_unlocked(player.username) db.session.commit() # Log the group edit if old_name != name: log_group_edited(f"{old_name} → {name}") else: log_group_edited(name) return group def delete_group(group_id): """ Delete a group and unlock all associated players. """ group = Group.query.get_or_404(group_id) group_name = group.name # Unlock all players in the group for player in group.players: player.locked_to_group_id = None log_player_unlocked(player.username) db.session.delete(group) db.session.commit() log_group_deleted(group_name) def add_player(username, hostname, password, quickconnect_password, orientation='Landscape'): """ Add a new player with the given details. """ from flask_bcrypt import Bcrypt bcrypt = Bcrypt() hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') hashed_quickconnect = bcrypt.generate_password_hash(quickconnect_password).decode('utf-8') new_player = Player( username=username, hostname=hostname, password=hashed_password, quickconnect_password=hashed_quickconnect, orientation=orientation ) db.session.add(new_player) db.session.commit() log_player_created(username, hostname) return new_player def edit_player(player_id, username, hostname, password=None, quickconnect_password=None, orientation=None): """ Edit an existing player's details. """ from flask_bcrypt import Bcrypt bcrypt = Bcrypt() player = Player.query.get_or_404(player_id) player.username = username player.hostname = hostname if password: player.password = bcrypt.generate_password_hash(password).decode('utf-8') if quickconnect_password: player.quickconnect_password = bcrypt.generate_password_hash(quickconnect_password).decode('utf-8') if orientation: player.orientation = orientation db.session.commit() log_player_edited(username) return player def delete_player(player_id): """ Delete a player and all its content. """ player = Player.query.get_or_404(player_id) username = player.username # Delete all media related to the player Content.query.filter_by(player_id=player_id).delete() # Delete the player db.session.delete(player) db.session.commit() log_player_deleted(username) def get_group_content(group_id): """ Get content for all players in a group, ordered by position. """ from models import Group, Content group = Group.query.get_or_404(group_id) # Get all player IDs in the group player_ids = [player.id for player in group.players] # Get unique content based on file_name, preserving position unique_content = {} # For each player, get their content for player_id in player_ids: # Get content for this player, ordered by position player_content = Content.query.filter_by(player_id=player_id).order_by(Content.position).all() for content in player_content: if content.file_name not in unique_content: unique_content[content.file_name] = content # Sort the unique content by position return sorted(unique_content.values(), key=lambda c: c.position) def get_player_content(player_id): """ Get content for a specific player, ordered by position. """ from models import Content return Content.query.filter_by(player_id=player_id).order_by(Content.position).all() def update_player_content_order(player_id, items): """ Update the order of content items for a player. Args: player_id (int): ID of the player items (list): List of items with id and position Returns: tuple: (success, error_message, new_version) """ from models import Player, Content from extensions import db player = Player.query.get_or_404(player_id) try: # Update the position field for each content item for item in items: content_id = int(item['id']) position = int(item['position']) content = Content.query.get_or_404(content_id) if content.player_id != player_id: continue # Skip if not for this player content.position = position # Force increment the playlist version to trigger client refresh player.playlist_version = (player.playlist_version or 0) + 1 db.session.commit() # Log the reordering action log_content_reordered("player", player.username) return True, None, player.playlist_version except Exception as e: db.session.rollback() return False, str(e), None def update_group_content_order(group_id, items): """ Update the order of content items for all players in a group. Args: group_id (int): ID of the group items (list): List of items with id and position Returns: tuple: (success, error_message) """ from models import Group, Content from extensions import db group = Group.query.get_or_404(group_id) try: # Get file names corresponding to the content IDs content_files = {} for item in items: content_id = int(item['id']) position = int(item['position']) content = Content.query.get_or_404(content_id) content_files[content.file_name] = position # Update all content items for all players in this group for player in group.players: for content in Content.query.filter_by(player_id=player.id).all(): if content.file_name in content_files: content.position = content_files[content.file_name] # Force increment the playlist version to trigger client refresh player.playlist_version = (player.playlist_version or 0) + 1 db.session.commit() # Log the reordering action log_content_reordered("group", group.name) return True, None except Exception as e: db.session.rollback() return False, str(e) def edit_group_media(group_id, content_id, new_duration): """ Update the duration for all instances of a media item across all players in a group. Args: group_id (int): ID of the group content_id (int): ID of the content item new_duration (int): New duration in seconds Returns: bool: Success or failure """ from models import Group, Content from extensions import db group = Group.query.get_or_404(group_id) content = Content.query.get(content_id) file_name = content.file_name old_duration = content.duration try: # Update the duration for all players in the group for player in group.players: content = Content.query.filter_by(player_id=player.id, file_name=file_name).first() if content: content.duration = new_duration db.session.commit() # Log the duration change log_content_duration_changed(file_name, old_duration, new_duration, "group", group.name) return True except Exception as e: db.session.rollback() return False def delete_group_media(group_id, content_id): """ Delete a media item from all players in a group. Args: group_id (int): ID of the group content_id (int): ID of the content item Returns: bool: Success or failure """ from models import Group, Content from extensions import db group = Group.query.get_or_404(group_id) content = Content.query.get(content_id) file_name = content.file_name try: # Delete the media for all players in the group count = 0 for player in group.players: content = Content.query.filter_by(player_id=player.id, file_name=file_name).first() if content: db.session.delete(content) count += 1 db.session.commit() # Log the content deletion log_content_deleted(file_name, "group", group.name) return True except Exception as e: db.session.rollback() return False