ds-play created

This commit is contained in:
2025-05-11 16:09:49 +03:00
commit 86cefde130
15 changed files with 692 additions and 0 deletions

Binary file not shown.

223
app/app.py Normal file
View File

@@ -0,0 +1,223 @@
from flask import Flask, jsonify, request, send_from_directory
import vlc
import os
import json
import requests
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
Logger = logging.getLogger(__name__)
app = Flask(__name__, static_folder='static')
# VLC Media Player instance
vlc_instance = vlc.Instance()
player = vlc_instance.media_player_new()
# File paths
PLAYLIST_FILE = './static/resurse/playlist.json'
APP_CONFIG_FILE = './app_config.json'
RESOURCES_FOLDER = './static/resurse'
# Ensure the resources folder exists
os.makedirs(RESOURCES_FOLDER, exist_ok=True)
# Load playlist
def load_playlist():
if os.path.exists(PLAYLIST_FILE):
with open(PLAYLIST_FILE, 'r') as file:
return json.load(file)
return []
# Save playlist
def save_playlist(playlist):
with open(PLAYLIST_FILE, 'w') as file:
json.dump(playlist, file)
# Load app configuration
def load_app_config():
if os.path.exists(APP_CONFIG_FILE):
with open(APP_CONFIG_FILE, 'r') as file:
return json.load(file)
return {
"player_orientation": "portrait",
"player_name": "",
"quickconnect_code": "",
"server_address": "",
"port": 1025
}
# Save app configuration
def save_app_config(config):
with open(APP_CONFIG_FILE, 'w') as file:
json.dump(config, file)
# Download media files
def download_media_files(playlist):
Logger.info("Starting media file download...")
for media in playlist:
file_name = media.get('file_name', '')
file_url = media.get('url', '')
file_path = os.path.join(RESOURCES_FOLDER, file_name)
try:
response = requests.get(file_url)
if response.status_code == 200:
with open(file_path, 'wb') as file:
file.write(response.content)
Logger.info(f"Downloaded {file_name} to {file_path}")
else:
Logger.error(f"Failed to download {file_name}: {response.status_code}")
except requests.exceptions.RequestException as e:
Logger.error(f"Failed to download {file_name}: {e}")
# Download playlist files from server
def download_playlist_files_from_server():
Logger.info("Starting playlist file download using app configuration...")
# Load app configuration
app_config = load_app_config()
server_address = app_config.get('server_address', '')
port = app_config.get('port', 1025)
hostname = app_config.get('player_name', '')
quickconnect_code = app_config.get('quickconnect_code', '')
if not server_address or not hostname or not quickconnect_code:
Logger.error("Missing required configuration values.")
return
# Construct the request URL and parameters
server_ip = f'{server_address}:{port}'
url = f'http://{server_ip}/api/playlists'
params = {
'hostname': hostname,
'quickconnect_code': quickconnect_code
}
try:
# Send request to fetch the playlist
response = requests.get(url, params=params)
Logger.debug(f"Status Code: {response.status_code}")
Logger.debug(f"Response Content: {response.text}")
if response.status_code == 200:
try:
playlist = response.json().get('playlist', [])
Logger.info("Playlist retrieved successfully.")
save_playlist(playlist) # Save the playlist locally
download_media_files(playlist) # Download media files
except json.JSONDecodeError as e:
Logger.error(f"Failed to parse playlist JSON: {e}")
else:
Logger.error(f"Failed to retrieve playlist: {response.text}")
except requests.exceptions.RequestException as e:
Logger.error(f"Failed to connect to server: {e}")
@app.route('/')
def serve_index():
"""Serve the main index.html page."""
return send_from_directory(app.static_folder, 'index.html')
@app.route('/api/playlist', methods=['GET'])
def get_playlist():
"""Get the current playlist."""
playlist = load_playlist()
return jsonify({'playlist': playlist})
@app.route('/api/playlist', methods=['POST'])
def update_playlist():
"""Update the playlist."""
playlist = request.json.get('playlist', [])
save_playlist(playlist)
return jsonify({'status': 'success'})
@app.route('/api/play', methods=['POST'])
def play_media():
"""Play a media file."""
file_path = request.json.get('file_path')
if not os.path.exists(file_path):
return jsonify({'error': 'File not found'}), 404
media = vlc_instance.media_new(file_path)
player.set_media(media)
player.play()
return jsonify({'status': 'playing', 'file': file_path})
@app.route('/api/stop', methods=['POST'])
def stop_media():
"""Stop media playback."""
player.stop()
return jsonify({'status': 'stopped'})
@app.route('/api/config', methods=['GET'])
def get_config():
"""Get the app configuration."""
config = load_app_config()
return jsonify(config)
@app.route('/api/config', methods=['POST'])
def update_config():
"""Update the app configuration."""
config = request.json
save_app_config(config)
return jsonify({'status': 'success'})
@app.route('/api/download_playlist', methods=['POST'])
def download_playlist():
"""Download playlist files from the server."""
try:
download_playlist_files_from_server()
return jsonify({'status': 'success', 'message': 'Playlist files downloaded successfully.'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/updated_playlist.json')
def serve_updated_playlist():
"""Serve the updated playlist file."""
return send_from_directory('.', 'updated_playlist.json')
def create_updated_playlist():
"""Create a new playlist file with local file paths."""
Logger.info("Creating updated playlist with local file paths...")
# Load the existing playlist
if not os.path.exists(PLAYLIST_FILE):
Logger.error(f"Playlist file not found: {PLAYLIST_FILE}")
return
with open(PLAYLIST_FILE, 'r') as file:
playlist = json.load(file)
# Update the playlist with local file paths
updated_playlist = []
for media in playlist:
file_name = media.get('file_name', '')
local_path = f"/static/resurse/{file_name}" # Use Flask's static folder path
if os.path.exists(os.path.join(RESOURCES_FOLDER, file_name)):
updated_media = {
"type": "image" if file_name.lower().endswith(('.jpg', '.jpeg', '.png')) else "video",
"url": local_path,
"duration": media.get('duration', 0) # Keep the duration for images
}
updated_playlist.append(updated_media)
else:
Logger.warning(f"File not found in resurse folder: {file_name}")
# Save the updated playlist to the root folder
updated_playlist_file = './updated_playlist.json'
with open(updated_playlist_file, 'w') as file:
json.dump(updated_playlist, file, indent=4)
Logger.info(f"Updated playlist saved to {updated_playlist_file}")
# Check and download playlist on app startup
def initialize_playlist():
Logger.info("Initializing playlist...")
download_playlist_files_from_server()
Logger.info("Playlist initialization complete.")
if __name__ == '__main__':
initialize_playlist() # Check and download playlist on startup
create_updated_playlist() # Create the updated playlist
app.run(host='0.0.0.0', port=1025)

1
app/app_config.json Normal file
View File

@@ -0,0 +1 @@
{"player_orientation": "portrait", "player_name": "tv-terasa", "quickconnect_code": "8887779", "server_address": "digi-signage.moto-adv.com", "port": 80}

15
app/install.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Exit immediately if a command exits with a non-zero status
set -e
echo "Updating package list..."
sudo apt update
echo "Installing system dependencies..."
sudo apt install -y python3 python3-pip vlc libvlc-dev libvlccore-dev pulseaudio
echo "Installing Python dependencies..."
pip3 install -r requirements.txt
echo "Setup complete. You can now run the app using: python3 app.py or gunicorn -w 4 -b 0.0.0.0:1025 app:app"

4
app/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
Flask==2.3.2
gunicorn==20.1.0
python-vlc==3.0.18121
requests==2.31.0

5
app/run_gunicorn.py Normal file
View File

@@ -0,0 +1,5 @@
import os
import sys
if __name__ == "__main__":
os.system("gunicorn -w 4 -b 0.0.0.0:1025 app:app")

Binary file not shown.

160
app/static/functions.py Normal file
View File

@@ -0,0 +1,160 @@
import os
import json
import requests
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
Logger = logging.getLogger(__name__)
def load_playlist():
"""Load playlist from the server or local storage."""
try:
Logger.info("python_functions: Attempting to load playlist from server...")
# Load app configuration
app_config_path = os.path.join(os.path.dirname(__file__), '../../app_config.json')
if not os.path.exists(app_config_path):
Logger.error("python_functions: App configuration file not found.")
return []
with open(app_config_path, 'r') as config_file:
app_config = json.load(config_file)
server = app_config.get('server_address', '')
port = app_config.get('port', 1025)
host = app_config.get('player_name', '')
quick = app_config.get('quickconnect_code', '')
if not server or not host or not quick:
Logger.error("python_functions: Missing required configuration values.")
return []
# Construct the server IP and request URL
server_ip = f'{server}:{port}'
url = f'http://{server_ip}/api/playlists'
params = {
'hostname': host,
'quickconnect_code': quick
}
# Send the request
response = requests.get(url, params=params)
# Debugging logs
Logger.debug(f"python_functions: Status Code: {response.status_code}")
Logger.debug(f"python_functions: Response Content: {response.text}")
if response.status_code == 200:
try:
playlist = response.json().get('playlist', [])
Logger.info("python_functions: Playlist loaded successfully.")
Logger.debug(f"python_functions: Loaded playlist: {playlist}")
return playlist
except json.JSONDecodeError as e:
Logger.error(f"python_functions: Failed to parse JSON response: {e}")
else:
Logger.error(f"python_functions: Failed to retrieve playlist: {response.text}")
except requests.exceptions.RequestException as e:
Logger.error(f"python_functions: Failed to load playlist: {e}")
return []
def download_media_files(playlist):
"""Download media files from the playlist."""
Logger.info("python_functions: Starting media file download...")
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') # Update this to the correct path
if not os.path.exists(base_dir):
os.makedirs(base_dir)
Logger.info(f"python_functions: Created directory {base_dir} for media files.")
for media in playlist:
file_name = media.get('file_name', '')
file_url = media.get('url', '')
file_path = os.path.join(base_dir, file_name)
try:
response = requests.get(file_url)
if response.status_code == 200:
with open(file_path, 'wb') as file:
file.write(response.content)
Logger.info(f"python_functions: Downloaded {file_name} to {file_path}")
else:
Logger.error(f"python_functions: Failed to download {file_name}: {response.status_code}")
except requests.exceptions.RequestException as e:
Logger.error(f"python_functions: Failed to download {file_name}: {e}")
def download_playlist_files_from_server():
"""Download playlist files using app configuration."""
Logger.info("python_functions: Starting playlist file download using app configuration...")
# Load app configuration
app_config_path = os.path.join(os.path.dirname(__file__), '../../app_config.json')
if not os.path.exists(app_config_path):
Logger.error("python_functions: App configuration file not found.")
return
try:
with open(app_config_path, 'r') as config_file:
app_config = json.load(config_file)
except json.JSONDecodeError as e:
Logger.error(f"python_functions: Failed to load app configuration: {e}")
return
# Extract configuration values
server_address = app_config.get('server_address', '')
port = app_config.get('port', 1025)
hostname = app_config.get('player_name', '')
quickconnect_code = app_config.get('quickconnect_code', '')
if not server_address or not hostname or not quickconnect_code:
Logger.error("python_functions: Missing required configuration values.")
return
# Construct the request URL and parameters
server_ip = f'{server_address}:{port}'
url = f'http://{server_ip}/api/playlists'
params = {
'hostname': hostname,
'quickconnect_code': quickconnect_code
}
try:
# Send request to fetch the playlist
response = requests.get(url, params=params)
Logger.debug(f"python_functions: Status Code: {response.status_code}")
Logger.debug(f"python_functions: Response Content: {response.text}")
if response.status_code == 200:
try:
playlist = response.json().get('playlist', [])
Logger.info("python_functions: Playlist retrieved successfully.")
Logger.debug(f"python_functions: Playlist: {playlist}")
# Download media files from the playlist
base_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse')
if not os.path.exists(base_dir):
os.makedirs(base_dir)
Logger.info(f"python_functions: Created directory {base_dir} for media files.")
for media in playlist:
file_name = media.get('file_name', '')
file_url = media.get('url', '')
file_path = os.path.join(base_dir, file_name)
try:
file_response = requests.get(file_url)
if file_response.status_code == 200:
with open(file_path, 'wb') as file:
file.write(file_response.content)
Logger.info(f"python_functions: Downloaded {file_name} to {file_path}")
else:
Logger.error(f"python_functions: Failed to download {file_name}: {file_response.status_code}")
except requests.exceptions.RequestException as e:
Logger.error(f"python_functions: Failed to download {file_name}: {e}")
except json.JSONDecodeError as e:
Logger.error(f"python_functions: Failed to parse playlist JSON: {e}")
else:
Logger.error(f"python_functions: Failed to retrieve playlist: {response.text}")
except requests.exceptions.RequestException as e:
Logger.error(f"python_functions: Failed to connect to server: {e}")
download_playlist_files_from_server()

182
app/static/index.html Normal file
View File

@@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Player</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: black;
color: white;
display: flex;
flex-direction: column;
height: 100vh;
}
.playlist-container {
flex: 1;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: black;
}
ul {
list-style-type: none;
padding: 0;
display: none; /* Hide the playlist list */
}
.controls-wrapper {
display: flex;
justify-content: center;
width: 33.33%; /* 1/3 of the page width */
margin: 0 auto;
transition: opacity 0.5s ease; /* Smooth fade effect */
}
.controls-wrapper.hidden {
opacity: 0; /* Hide the buttons */
pointer-events: none; /* Disable interaction when hidden */
}
.controls {
display: flex;
justify-content: center;
gap: 15px; /* Space between buttons */
padding: 10px;
background-color: #222;
}
button {
margin: 5px;
padding: 10px;
background-color: #444;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 20px;
}
button:hover {
background-color: #666;
}
button i {
pointer-events: none;
}
img, video {
max-width: 100%;
max-height: 100%;
}
</style>
<!-- Add Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div class="playlist-container" id="playlist-container">
<!-- Content will be dynamically added here -->
</div>
<div class="controls-wrapper" id="controls-wrapper">
<div class="controls">
<button onclick="previousMedia()"><i class="fas fa-step-backward"></i></button> <!-- Previous -->
<button onclick="loadPlaylist()"><i class="fas fa-sync-alt"></i></button> <!-- Refresh Playlist -->
<button onclick="playMedia()"><i class="fas fa-play"></i></button> <!-- Play -->
<button onclick="nextMedia()"><i class="fas fa-step-forward"></i></button> <!-- Next -->
<button onclick="stopMedia()"><i class="fas fa-stop"></i></button> <!-- Stop -->
<button onclick="goToSettings()"><i class="fas fa-cog"></i></button> <!-- Settings -->
</div>
</div>
<script>
const apiBase = 'http://localhost:1025'; // Update to match your Flask app's port
const playlistContainer = document.getElementById('playlist-container');
let playlist = [];
let currentIndex = 0;
let playbackInterval;
// Function to load the playlist from updated_playlist.json
async function loadPlaylist() {
try {
const response = await fetch(`${apiBase}/updated_playlist.json`);
if (!response.ok) {
throw new Error(`Failed to load playlist: ${response.statusText}`);
}
const data = await response.json();
playlist = data; // Use the updated playlist
console.log("Loaded playlist:", playlist); // Debug log
startPlaylist(); // Start playing the playlist after loading
} catch (error) {
console.error("Error loading playlist:", error);
}
}
// Function to start playing the playlist
function startPlaylist() {
if (playlist.length === 0) {
console.error("No items in the playlist.");
return;
}
playCurrentItem();
}
// Function to play the current item in the playlist
function playCurrentItem() {
if (currentIndex >= playlist.length) {
currentIndex = 0; // Loop back to the beginning
}
const currentItem = playlist[currentIndex];
playlistContainer.innerHTML = ''; // Clear the container
if (currentItem.type === 'image') {
const img = document.createElement('img');
img.src = currentItem.url;
playlistContainer.appendChild(img);
// Display the image for the specified duration
playbackInterval = setTimeout(() => {
currentIndex++;
playCurrentItem();
}, currentItem.duration * 1000);
} else if (currentItem.type === 'video') {
const video = document.createElement('video');
video.src = currentItem.url;
video.autoplay = true;
video.controls = false;
playlistContainer.appendChild(video);
// Play the video and move to the next item after it ends
video.onended = () => {
currentIndex++;
playCurrentItem();
};
}
}
// Function to stop playback
function stopMedia() {
clearTimeout(playbackInterval);
playlistContainer.innerHTML = ''; // Clear the container
}
// Function to play the previous item
function previousMedia() {
stopMedia();
currentIndex = (currentIndex - 1 + playlist.length) % playlist.length;
playCurrentItem();
}
// Function to play the next item
function nextMedia() {
stopMedia();
currentIndex = (currentIndex + 1) % playlist.length;
playCurrentItem();
}
function goToSettings() {
window.location.href = '/static/settings.html';
}
// Load playlist on page load
loadPlaylist();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

View File

@@ -0,0 +1 @@
[{"duration": 15, "file_name": "IMG_20250503_220547.jpg", "url": "http://digi-signage.moto-adv.com/media/IMG_20250503_220547.jpg"}, {"duration": 15, "file_name": "IMG_20250506_080609.jpg", "url": "http://digi-signage.moto-adv.com/media/IMG_20250506_080609.jpg"}, {"duration": 15, "file_name": "VID_20250501_184228.mp4", "url": "http://digi-signage.moto-adv.com/media/VID_20250501_184228.mp4"}]

84
app/static/settings.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
label { display: block; margin: 10px 0 5px; }
input, select { width: 100%; padding: 8px; margin-bottom: 10px; }
button { padding: 10px 15px; }
.home-button { position: fixed; bottom: 20px; right: 20px; }
</style>
</head>
<body>
<h1>Settings</h1>
<form id="settings-form">
<label for="player_orientation">Player Orientation</label>
<select id="player_orientation" name="player_orientation">
<option value="portrait">Portrait</option>
<option value="landscape">Landscape</option>
</select>
<label for="player_name">Player Name</label>
<input type="text" id="player_name" name="player_name">
<label for="quickconnect_code">QuickConnect Code</label>
<input type="text" id="quickconnect_code" name="quickconnect_code">
<label for="server_address">Server Address</label>
<input type="text" id="server_address" name="server_address">
<label for="port">Port</label>
<input type="number" id="port" name="port">
<button type="button" onclick="saveConfig()">Save Settings</button>
</form>
<!-- Home button -->
<button class="home-button" onclick="goHome()">🏠 Home</button>
<script>
const apiBase = '/api';
// Load configuration on page load
async function loadConfig() {
const response = await fetch(`${apiBase}/config`);
const config = await response.json();
document.getElementById('player_orientation').value = config.player_orientation;
document.getElementById('player_name').value = config.player_name;
document.getElementById('quickconnect_code').value = config.quickconnect_code;
document.getElementById('server_address').value = config.server_address;
document.getElementById('port').value = config.port;
}
// Save configuration
async function saveConfig() {
const config = {
player_orientation: document.getElementById('player_orientation').value,
player_name: document.getElementById('player_name').value,
quickconnect_code: document.getElementById('quickconnect_code').value,
server_address: document.getElementById('server_address').value,
port: parseInt(document.getElementById('port').value, 10)
};
await fetch(`${apiBase}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
alert('Settings saved successfully!');
}
// Navigate back to the home page
function goHome() {
window.location.href = '/';
}
// Load configuration when the page loads
loadConfig();
</script>
</body>
</html>

17
app/updated_playlist.json Normal file
View File

@@ -0,0 +1,17 @@
[
{
"type": "image",
"url": "/static/resurse/IMG_20250503_220547.jpg",
"duration": 15
},
{
"type": "image",
"url": "/static/resurse/IMG_20250506_080609.jpg",
"duration": 15
},
{
"type": "video",
"url": "/static/resurse/VID_20250501_184228.mp4",
"duration": 15
}
]