Files
tkinter_player/signage_player/get_playlists.py

400 lines
16 KiB
Python

import os
import json
import requests
import bcrypt
import re
import datetime
from logging_config import Logger
# Global variable to track server connectivity status
SERVER_CONNECTION_STATUS = {
'is_online': True,
'last_successful_connection': None,
'last_playlist_update': None,
'error_message': None
}
def get_server_status():
"""Get current server connection status"""
return SERVER_CONNECTION_STATUS.copy()
def get_last_playlist_update_time():
"""Get the timestamp of the last playlist update from filesystem"""
try:
playlist_dir = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
if os.path.exists(playlist_dir):
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
if playlist_files:
# Get the most recent playlist file
latest_file = max([os.path.join(playlist_dir, f) for f in playlist_files], key=os.path.getmtime)
mod_time = os.path.getmtime(latest_file)
return datetime.datetime.fromtimestamp(mod_time)
return None
except Exception as e:
Logger.error(f"Error getting last playlist update time: {e}")
return None
def set_server_offline(error_message=None):
"""Mark server as offline with optional error message"""
global SERVER_CONNECTION_STATUS
SERVER_CONNECTION_STATUS['is_online'] = False
SERVER_CONNECTION_STATUS['error_message'] = error_message
Logger.warning(f"Server marked as offline: {error_message}")
def set_server_online():
"""Mark server as online and update connection time"""
global SERVER_CONNECTION_STATUS
SERVER_CONNECTION_STATUS['is_online'] = True
SERVER_CONNECTION_STATUS['last_successful_connection'] = datetime.datetime.now()
SERVER_CONNECTION_STATUS['error_message'] = None
Logger.info("Server connection restored")
def send_player_feedback(config, message, status="active", playlist_version=None, error_details=None):
"""
Send feedback to the server about player status.
Args:
config (dict): Configuration containing server details
message (str): Main feedback message
status (str): Player status - "active", "playing", "error", "restarting"
playlist_version (int, optional): Current playlist version being played
error_details (str, optional): Error details if status is "error"
Returns:
bool: True if feedback sent successfully, False otherwise
"""
try:
server = config.get("server_ip", "")
host = config.get("screen_name", "")
quick = config.get("quickconnect_key", "")
port = config.get("port", "")
# Construct server URL
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
if re.match(ip_pattern, server):
feedback_url = f'http://{server}:{port}/api/player-feedback'
else:
feedback_url = f'http://{server}/api/player-feedback'
# Prepare feedback data
feedback_data = {
'player_name': host,
'quickconnect_code': quick,
'message': message,
'status': status,
'timestamp': datetime.datetime.now().isoformat(),
'playlist_version': playlist_version,
'error_details': error_details
}
Logger.info(f"Sending feedback to {feedback_url}: {feedback_data}")
# Send POST request
response = requests.post(feedback_url, json=feedback_data, timeout=10)
if response.status_code == 200:
Logger.info(f"Feedback sent successfully: {message}")
# Mark server as online on successful feedback
set_server_online()
return True
else:
Logger.warning(f"Feedback failed with status {response.status_code}: {response.text}")
set_server_offline(f"Feedback failed with status {response.status_code}")
return False
except requests.exceptions.RequestException as e:
Logger.error(f"Failed to send feedback: {e}")
set_server_offline(f"Network error during feedback: {e}")
return False
except Exception as e:
Logger.error(f"Unexpected error sending feedback: {e}")
set_server_offline(f"Unexpected error during feedback: {e}")
return False
def send_playlist_check_feedback(config, playlist_version=None):
"""
Send feedback when server is interrogated for playlist updates.
Args:
config (dict): Configuration containing server details
playlist_version (int, optional): Current playlist version
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
message = f"player {player_name}, server interrogation, checking for updates : {version_info}"
return send_player_feedback(
config=config,
message=message,
status="active",
playlist_version=playlist_version
)
def send_playlist_restart_feedback(config, playlist_version=None):
"""
Send feedback when playlist loop ends and restarts.
Args:
config (dict): Configuration containing server details
playlist_version (int, optional): Current playlist version
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
message = f"player {player_name}, playlist working in loop, cycle completed : {version_info}"
return send_player_feedback(
config=config,
message=message,
status="restarting",
playlist_version=playlist_version
)
def send_player_error_feedback(config, error_message, playlist_version=None):
"""
Send feedback when an error occurs in the player.
Args:
config (dict): Configuration containing server details
error_message (str): Description of the error
playlist_version (int, optional): Current playlist version
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
message = f"player {player_name}, error occurred"
return send_player_feedback(
config=config,
message=message,
status="error",
playlist_version=playlist_version,
error_details=error_message
)
def send_playing_status_feedback(config, playlist_version=None, current_media=None):
"""
Send feedback about playlist starting (first media).
Args:
config (dict): Configuration containing server details
playlist_version (int, optional): Current playlist version
current_media (str, optional): First media file in playlist
Returns:
bool: True if feedback sent successfully, False otherwise
"""
player_name = config.get("screen_name", "unknown")
version_info = f"playlist v{playlist_version}" if playlist_version else "unknown"
message = f"player {player_name}, playlist started : {version_info}"
return send_player_feedback(
config=config,
message=message,
status="playing",
playlist_version=playlist_version
)
def is_playlist_up_to_date(local_playlist_path, config):
"""
Compare the version of the local playlist with the server playlist.
Returns True if up-to-date, False otherwise.
"""
import json
if not os.path.exists(local_playlist_path):
Logger.info(f"Local playlist file not found: {local_playlist_path}")
return False
with open(local_playlist_path, 'r') as f:
local_data = json.load(f)
local_version = local_data.get('version', 0)
server_data = fetch_server_playlist(config)
server_version = server_data.get('version', 0)
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
return local_version == server_version
def fetch_server_playlist(config):
"""Fetch the updated playlist from the server using a config dict."""
server = config.get("server_ip", "")
host = config.get("screen_name", "")
quick = config.get("quickconnect_key", "")
port = config.get("port", "")
try:
ip_pattern = r'^\d+\.\d+\.\d+\.\d+$'
if re.match(ip_pattern, server):
server_url = f'http://{server}:{port}/api/playlists'
else:
server_url = f'http://{server}/api/playlists'
params = {
'hostname': host,
'quickconnect_code': quick
}
Logger.info(f"Fetching playlist from URL: {server_url} with params: {params}")
response = requests.get(server_url, params=params, timeout=15)
if response.status_code == 200:
response_data = response.json()
Logger.info(f"Server response: {response_data}")
playlist = response_data.get('playlist', [])
version = response_data.get('playlist_version', None)
hashed_quickconnect = response_data.get('hashed_quickconnect', None)
if version is not None and hashed_quickconnect is not None:
if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')):
Logger.info("Fetched updated playlist from server.")
# Mark server as online on successful connection
set_server_online()
return {'playlist': playlist, 'version': version}
else:
Logger.error("Quickconnect code validation failed.")
set_server_offline("Authentication failed - invalid quickconnect code")
else:
Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.")
set_server_offline("Invalid server response - missing playlist data")
else:
Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}")
set_server_offline(f"Server returned error code: {response.status_code}")
except requests.exceptions.ConnectTimeout as e:
Logger.error(f"Connection timeout while fetching playlist: {e}")
set_server_offline("Connection timeout - server unreachable")
except requests.exceptions.ConnectionError as e:
Logger.error(f"Connection error while fetching playlist: {e}")
set_server_offline("Connection failed - server unreachable")
except requests.exceptions.RequestException as e:
Logger.error(f"Request error while fetching playlist: {e}")
set_server_offline(f"Network error: {str(e)}")
except Exception as e:
Logger.error(f"Unexpected error while fetching playlist: {e}")
set_server_offline(f"Unexpected error: {str(e)}")
return {'playlist': [], 'version': 0}
def save_playlist_with_version(playlist_data, playlist_dir):
version = playlist_data.get('version', 0)
playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.json')
with open(playlist_file, 'w') as f:
json.dump(playlist_data, f, indent=2)
print(f"Playlist saved to {playlist_file}")
return playlist_file
def download_media_files(playlist, media_dir):
"""Download media files from the server and save them to media_dir."""
if not os.path.exists(media_dir):
os.makedirs(media_dir)
Logger.info(f"Created directory {media_dir} for media files.")
updated_playlist = []
for media in playlist:
file_name = media.get('file_name', '')
file_url = media.get('url', '')
duration = media.get('duration', 10)
local_path = os.path.join(media_dir, file_name)
Logger.info(f"Preparing to download {file_name} from {file_url}...")
if os.path.exists(local_path):
Logger.info(f"File {file_name} already exists. Skipping download.")
else:
try:
response = requests.get(file_url, timeout=10)
if response.status_code == 200:
with open(local_path, 'wb') as file:
file.write(response.content)
Logger.info(f"Successfully downloaded {file_name} to {local_path}")
else:
Logger.error(f"Failed to download {file_name}. Status Code: {response.status_code}")
continue
except requests.exceptions.RequestException as e:
Logger.error(f"Error downloading {file_name}: {e}")
continue
updated_media = {
'file_name': file_name,
'url': os.path.relpath(local_path, os.path.dirname(media_dir)),
'duration': duration
}
updated_playlist.append(updated_media)
return updated_playlist
def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1):
"""
Delete old playlist files and media files not referenced by the latest playlist version.
keep_versions: number of latest versions to keep (default 1)
"""
# Find all playlist files
playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')]
# Keep only the latest N versions
versions = sorted([int(f.split('_v')[-1].split('.json')[0]) for f in playlist_files], reverse=True)
keep = set(versions[:keep_versions])
# Delete old playlist files
for f in playlist_files:
v = int(f.split('_v')[-1].split('.json')[0])
if v not in keep:
os.remove(os.path.join(playlist_dir, f))
# Collect all media files referenced by the kept playlists
referenced = set()
for v in keep:
path = os.path.join(playlist_dir, f'server_playlist_v{v}.json')
if os.path.exists(path):
with open(path, 'r') as f:
data = json.load(f)
for item in data.get('playlist', []):
referenced.add(item.get('file_name'))
# Delete media files not referenced
for f in os.listdir(media_dir):
if f not in referenced:
try:
os.remove(os.path.join(media_dir, f))
except Exception as e:
Logger.warning(f"Failed to delete media file {f}: {e}")
def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir):
"""
Fetch the server playlist once, compare versions, and update if needed.
Returns True if updated, False if already up to date.
Also sends feedback to server about playlist check.
"""
import json
global SERVER_CONNECTION_STATUS
server_data = fetch_server_playlist(config)
server_version = server_data.get('version', 0)
if not os.path.exists(local_playlist_path):
local_version = 0
else:
with open(local_playlist_path, 'r') as f:
local_data = json.load(f)
local_version = local_data.get('version', 0)
Logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}")
# Only send feedback if server is online
if SERVER_CONNECTION_STATUS['is_online']:
send_playlist_check_feedback(config, server_version if server_version > 0 else local_version)
if local_version != server_version and server_version > 0:
if server_data and server_data.get('playlist'):
updated_playlist = download_media_files(server_data['playlist'], media_dir)
server_data['playlist'] = updated_playlist
save_playlist_with_version(server_data, playlist_dir)
# Delete old playlists and unreferenced media
delete_old_playlists_and_media(server_version, playlist_dir, media_dir)
# Update last playlist update time
SERVER_CONNECTION_STATUS['last_playlist_update'] = datetime.datetime.now()
return True
else:
Logger.warning("No playlist data fetched from server or playlist is empty.")
return False
else:
Logger.info("Local playlist is already up to date or server is offline.")
return False