Compare commits

...

6 Commits

22 changed files with 984 additions and 36 deletions

View File

@@ -0,0 +1,179 @@
# Info-Beamer Player Integration Guide
This guide will help you integrate Info-Beamer devices with your content management server.
## Prerequisites
- Info-Beamer device (Raspberry Pi or compatible)
- Network access between the device and your server (IP: 192.168.1.22)
- Device ID from your Info-Beamer device
## Step-by-Step Integration
### 1. Find Your Info-Beamer Device ID
First, you need to get your device's unique ID:
- Access your Info-Beamer device's web interface (usually via its IP address)
- Look for "Device ID" in the settings or dashboard
- Write down this ID - you'll need it for registration
### 2. Register Player in Web Interface
1. Open your browser and navigate to: `http://192.168.1.22`
2. Login with your admin credentials
3. Go to the "Players" section in the admin panel
4. Click "Add Player" and fill in:
- **Device ID**: Your Info-Beamer device's unique ID (from step 1)
- **Player Name**: Descriptive name (e.g., "Lobby Display", "Reception Screen")
- **Channel**: Select which streaming channel this player should display
### 3. Prepare Info-Beamer Package
You need to create a package with these files:
#### Required Files:
- `node.lua` - Main display script (already provided in this repository)
- `config.json` - Configuration file (template provided)
- `roboto.ttf` - Font file for text display (download from Google Fonts)
#### Package Structure:
```
your-info-beamer-package/
├── node.lua
├── config.json
└── roboto.ttf
```
### 4. Configure Your Package
Edit the `config.json` file with your specific settings:
```json
{
"server_url": "http://192.168.1.22",
"player_id": "YOUR_DEVICE_ID_HERE",
"refresh_interval": 30,
"default_duration": 10
}
```
**Configuration Options:**
- `server_url`: Your server's URL (change IP if your server is on a different address)
- `player_id`: Must match exactly the Device ID you registered in step 2
- `refresh_interval`: How often to check for new content (seconds)
- `default_duration`: Default display time per item if not specified (seconds)
### 5. Get the Font File
Download the Roboto font:
1. Go to Google Fonts: https://fonts.google.com/specimen/Roboto
2. Download the font family
3. Extract and copy `Roboto-Regular.ttf` to your package folder
4. Rename it to `roboto.ttf`
### 6. Upload Package to Info-Beamer
#### Option A: Info-Beamer Hosted Service
1. Create account on info-beamer.com
2. Create a new package
3. Upload all three files: `node.lua`, `config.json`, `roboto.ttf`
4. Assign the package to your device
#### Option B: Local Info-Beamer Installation
1. Access your device's web interface
2. Upload the package files
3. Activate the package
### 7. Create and Assign Content
1. **Create a Streaming Channel**:
- In the web interface, go to "Channels"
- Click "Add Channel"
- Enter name and description
- Mark as active
2. **Add Media Content**:
- Go to the "Upload" section
- Upload your images and videos
- Navigate to your channel
- Add uploaded files to the channel
- Set display duration for each item
- Arrange content order
3. **Assign Channel to Player**:
- Go to "Players" section
- Edit your player
- Select the channel you created
### 8. Test the Integration
Your Info-Beamer device should now:
- Connect to the server every 30 seconds
- Display "Waiting for channel content..." if no content is assigned
- Show media files from the assigned channel in sequence
- Display channel name and current time as overlay
- Automatically update when you change content
## API Endpoints
The server provides these endpoints for Info-Beamer integration:
- `GET /api/player/{device_id}/content` - Get content playlist for the player
- `GET /api/player/{device_id}/channel` - Get channel information
- `POST /api/player/{device_id}/heartbeat` - Update player last seen status
## Troubleshooting
### Player Not Connecting
- Verify `server_url` in config.json points to the correct IP address
- Check network connectivity between device and server
- Ensure `player_id` in config.json matches the registered Device ID exactly
- Check firewall settings on the server
### No Content Displaying
- Verify the player is assigned to an active channel
- Check that the channel has content added to it
- Ensure media files are properly uploaded to the server
- Check file permissions in the `/uploads/` directory
### Content Not Updating
- Check the `refresh_interval` setting in config.json
- Verify the server is running and accessible at port 80
- Look for error messages in Info-Beamer device logs
- Test API endpoints manually in browser
### Wrong Content Displaying
- Verify channel assignment for the player
- Check content order in the channel
- Ensure media files are the correct format (JPG, PNG, MP4)
## Player Management
In the admin interface, you can monitor:
- **Player Status**: Online/Offline based on last heartbeat
- **Last Seen**: Timestamp of last communication with server
- **Channel Assignment**: Which content channel is currently assigned
- **Device Information**: Name, location, and other details
You can change channel assignments at any time through the web interface, and players will automatically update their content within the refresh interval.
## Content Management
### Supported Media Types
- **Images**: JPG, PNG, GIF
- **Videos**: MP4, AVI, MOV
### Best Practices
- Use consistent aspect ratios for smooth transitions
- Optimize file sizes for network transfer
- Test content on actual displays before deployment
- Keep refresh intervals reasonable (30-60 seconds)
- Monitor player connectivity regularly
## Security Notes
- Change default admin password immediately
- Use HTTPS in production environments
- Restrict network access to the server
- Regular backup of database and uploaded content
- Monitor API access logs for unusual activity

Binary file not shown.

View File

@@ -1,6 +1,9 @@
{ {
"server_url": "http://192.168.1.22", "server_url": "http://192.168.1.22",
"player_id": "REPLACE_WITH_YOUR_DEVICE_ID", "player_id": "12131415",
"refresh_interval": 30, "refresh_interval": 30,
"default_duration": 10 "default_duration": 10,
"touch_enabled": true,
"swipe_threshold": 100,
"manual_override_duration": 10
} }

View File

@@ -0,0 +1,34 @@
# Info-Beamer Package for Tv-Anunturi (ID: 12131415)
## Files:
- node.lua (main script)
- config.json (configured for your player)
- roboto.ttf (font file)
- wpa_supplicant.conf (WiFi configuration template)
- configure-wifi.sh (WiFi setup script)
- network-config.txt (network configuration guide)
## WiFi Setup:
1. Run: ./configure-wifi.sh
2. Enter your WiFi network name and password
3. Copy generated wpa_supplicant.conf to SD card /boot/ folder
## Deployment:
1. Configure WiFi (see above)
2. Copy package files to SD card root: node.lua, config.json, roboto.ttf
3. Copy wpa_supplicant.conf to SD card /boot/ folder
4. Insert SD card and power on device
5. Device will connect to WiFi and server at 192.168.1.22
## Features:
- Touch screen swipe navigation (left/right)
- Automatic content updates every 30 seconds
- Shows content from Default Channel
- Manual override with touch control
## Touch Usage:
- Swipe right = next content
- Swipe left = previous content
- Auto-resume after 10 seconds
Server: http://192.168.1.22/admin

View File

@@ -0,0 +1,9 @@
{
"server_url": "http://192.168.1.22",
"player_id": "12131415",
"refresh_interval": 30,
"default_duration": 10,
"touch_enabled": true,
"swipe_threshold": 100,
"manual_override_duration": 10
}

View File

@@ -0,0 +1,44 @@
#!/bin/bash
# WiFi Configuration Script for Info-Beamer Package
echo "=== Info-Beamer WiFi Configuration ==="
echo ""
# Get WiFi network name
read -p "Enter WiFi Network Name (SSID): " WIFI_SSID
# Get WiFi password
read -s -p "Enter WiFi Password: " WIFI_PASSWORD
echo ""
# Get country code
echo ""
echo "Common country codes: US, GB, DE, FR, CA, AU, JP"
read -p "Enter country code [US]: " COUNTRY_CODE
COUNTRY_CODE=${COUNTRY_CODE:-US}
# Generate wpa_supplicant.conf
cat > wpa_supplicant.conf << WIFIEOF
# WiFi Configuration for Info-Beamer Device
country=${COUNTRY_CODE}
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
# Primary WiFi Network
network={
ssid="${WIFI_SSID}"
psk="${WIFI_PASSWORD}"
priority=1
scan_ssid=1
}
WIFIEOF
echo ""
echo "✅ WiFi configuration saved to wpa_supplicant.conf"
echo ""
echo "📋 Deployment Instructions:"
echo "1. Copy wpa_supplicant.conf to SD card at: /boot/wpa_supplicant.conf"
echo "2. Copy other package files (node.lua, config.json, roboto.ttf) to SD card root"
echo "3. Insert SD card and power on device"
echo "4. Device will connect to WiFi: ${WIFI_SSID}"
echo ""

View File

@@ -0,0 +1,29 @@
# Network Configuration Guide for Info-Beamer Device
## WiFi Configuration
1. Edit wpa_supplicant.conf with your WiFi credentials
2. Copy to SD card at: /boot/wpa_supplicant.conf
3. Device will auto-connect on boot
## Static IP Configuration (Optional)
If you want a fixed IP instead of DHCP, create this file on SD card:
File: /boot/dhcpcd.conf
Content:
interface wlan0
static ip_address=192.168.1.100/24
static routers=192.168.1.1
static domain_name_servers=192.168.1.1 8.8.8.8
## Ethernet Configuration
For wired connection, device will use DHCP by default.
No configuration needed if using ethernet cable.
## WiFi Country Codes
US - United States
GB - United Kingdom
DE - Germany
FR - France
CA - Canada
AU - Australia
JP - Japan

207
deployment-package/node.lua Normal file
View File

@@ -0,0 +1,207 @@
-- Info-Beamer script with streaming channel support
gl.setup(NATIVE_WIDTH, NATIVE_HEIGHT)
local json = require "json"
local font = resource.load_font "roboto.ttf"
local server_url = CONFIG.server_url or "http://192.168.1.22"
local player_id = CONFIG.player_id or "default_player"
local refresh_interval = CONFIG.refresh_interval or 30
local default_duration = CONFIG.default_duration or 10
local touch_enabled = CONFIG.touch_enabled ~= false -- Default to true
local swipe_threshold = CONFIG.swipe_threshold or 100
local manual_override_duration = CONFIG.manual_override_duration or 10
local playlist = {}
local current_item = 1
local item_start_time = 0
local last_update = 0
local channel_info = {}
-- Touch and swipe variables
local touch_start_x = 0
local touch_start_y = 0
local touch_start_time = 0
local is_touching = false
local swipe_time_limit = 1.0 -- Maximum time for swipe (seconds)
local manual_override = false
local manual_override_time = 0
function update_playlist()
-- Fetch channel content from server
local url = server_url .. "/api/player/" .. player_id .. "/content"
util.post_and_wait(url, "", function(response)
if response.success then
local data = json.decode(response.content)
if data and data.content then
playlist = data.content
print("Updated channel content: " .. #playlist .. " items")
-- Load assets
for i, item in ipairs(playlist) do
if item.type == "image" then
item.resource = resource.load_image(server_url .. "/uploads/" .. item.filename)
elseif item.type == "video" then
item.resource = resource.load_video(server_url .. "/uploads/" .. item.filename)
end
end
end
end
end)
-- Also fetch channel information
local channel_url = server_url .. "/api/player/" .. player_id .. "/channel"
util.post_and_wait(channel_url, "", function(response)
if response.success then
channel_info = json.decode(response.content) or {}
end
end)
last_update = sys.now()
end
-- Touch event handlers
function node.on_touch(touches)
for touch in touches() do
if touch.type == "start" then
-- Touch started
touch_start_x = touch.x
touch_start_y = touch.y
touch_start_time = sys.now()
is_touching = true
elseif touch.type == "end" and is_touching then
-- Touch ended - check for swipe
local touch_end_x = touch.x
local touch_end_y = touch.y
local touch_duration = sys.now() - touch_start_time
-- Calculate swipe distance and direction
local dx = touch_end_x - touch_start_x
local dy = touch_end_y - touch_start_y
local distance = math.sqrt(dx * dx + dy * dy)
-- Check if it's a valid swipe (horizontal, sufficient distance, quick enough)
if distance > swipe_threshold and
touch_duration < swipe_time_limit and
math.abs(dx) > math.abs(dy) * 2 then -- More horizontal than vertical
if #playlist > 1 then -- Only allow swipe if there's content to switch to
if dx > 0 then
-- Swipe right - next item
next_content()
else
-- Swipe left - previous item
previous_content()
end
-- Enable manual override mode
manual_override = true
manual_override_time = sys.now()
print("Swipe detected: " .. (dx > 0 and "right (next)" or "left (previous)"))
end
end
is_touching = false
end
end
end
-- Function to go to next content item
function next_content()
if #playlist > 0 then
current_item = current_item + 1
if current_item > #playlist then
current_item = 1
end
item_start_time = sys.now()
end
end
-- Function to go to previous content item
function previous_content()
if #playlist > 0 then
current_item = current_item - 1
if current_item < 1 then
current_item = #playlist
end
item_start_time = sys.now()
end
end
function node.render()
-- Update playlist periodically
if sys.now() - last_update > refresh_interval then
update_playlist()
end
-- Check if manual override has expired
if manual_override and sys.now() - manual_override_time > manual_override_duration then
manual_override = false
item_start_time = sys.now() -- Reset automatic timing
end
-- If no playlist items, show waiting message
if #playlist == 0 then
font:write(100, 100, "Waiting for channel content...", 50, 1, 1, 1, 1)
font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1)
font:write(100, 250, "Player ID: " .. player_id, 30, 0.7, 0.7, 0.7, 1)
font:write(100, 300, "Swipe left/right to navigate when content is available", 20, 0.5, 0.5, 0.5, 1)
return
end
local item = playlist[current_item]
if not item then
current_item = 1
item = playlist[current_item]
if not item then return end
end
local duration = item.duration or default_duration
-- Only advance automatically if not in manual override mode
if not manual_override and sys.now() - item_start_time > duration then
next_content()
end
-- Display current item
if item and item.resource then
if item.type == "image" then
item.resource:draw(0, 0, WIDTH, HEIGHT)
elseif item.type == "video" then
item.resource:draw(0, 0, WIDTH, HEIGHT)
end
-- Show channel info overlay
if channel_info.name then
font:write(10, HEIGHT - 80, "Channel: " .. channel_info.name, 20, 1, 1, 1, 0.8)
if channel_info.description then
font:write(10, HEIGHT - 50, channel_info.description, 18, 1, 1, 1, 0.7)
end
end
-- Show content navigation info
if #playlist > 1 then
local nav_text = string.format("%d / %d", current_item, #playlist)
font:write(10, HEIGHT - 20, nav_text, 16, 1, 1, 1, 0.6)
-- Show swipe instructions (fade in/out)
local instructions_alpha = 0.4 + 0.2 * math.sin(sys.now() * 2)
font:write(10, 30, "◀ Swipe to navigate ▶", 18, 1, 1, 1, instructions_alpha)
end
-- Show manual override indicator
if manual_override then
local remaining = manual_override_duration - (sys.now() - manual_override_time)
font:write(WIDTH - 150, HEIGHT - 20, string.format("Manual: %.1fs", remaining), 16, 1, 1, 0, 0.8)
end
end
-- Show current time
local current_time = os.date("%H:%M")
font:write(WIDTH - 100, 20, current_time, 24, 1, 1, 1, 0.9)
end
-- Initial load
update_playlist()

Binary file not shown.

View File

@@ -0,0 +1,12 @@
# WiFi Configuration for Info-Beamer Device
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
# Primary WiFi Network
network={
ssid="HarBAck2.4"
psk="EuropaUnita2020"
priority=1
scan_ssid=1
}

16
device-info-template.txt Normal file
View File

@@ -0,0 +1,16 @@
=== Info-Beamer Device Information Needed ===
1. Device ID: _________________ (from player screen)
2. Device Name: _______________ (what you named it)
3. Resolution: ________________ (from dashboard)
4. Status: ___________________ (should be 'online')
=== Next Steps ===
1. Record your actual Device ID above
2. Update our deployment package if different from '12131415'
3. Test our Flask server connection
4. Deploy our touch-enabled package

View File

@@ -0,0 +1,36 @@
# Info-Beamer OS WiFi Configuration
## Quick Setup (Recommended):
1. After flashing SD card with Info-Beamer OS:
- Don't eject SD card yet
- Open the SD card in file explorer
- Create new file: wpa_supplicant.conf
- Add this content:
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="YOUR_WIFI_NAME"
psk="YOUR_WIFI_PASSWORD"
key_mgmt=WPA-PSK
}
2. Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD
3. Save file and eject SD card
4. Insert in Pi and boot
## Alternative: Pi Imager Method
- Use gear icon in Pi Imager
- Enable "Configure wireless LAN"
- Enter WiFi credentials before flashing
## Country Codes:
US, GB, DE, FR, IT, ES, CA, AU, etc.
## Important:
- Country code is REQUIRED
- File goes in /boot partition
- Use quotes around SSID/password

View File

@@ -0,0 +1,33 @@
# Info-Beamer Player Setup Checklist
## 1. Connect Device to Account:
- Go to: https://info-beamer.com/
- Create account or login
- Click "Add Device"
- Enter the ID shown on your player screen
- Device should appear in your dashboard
## 2. Verify Device Status:
- Device shows "online" status in dashboard
- Last seen timestamp is recent
- Device info shows correct hardware details
## 3. Test Basic Functionality:
- Upload a simple image to test
- Assign to your device
- Image should display on screen within 30 seconds
## 4. Configure for Our Flask Server:
- Device ID needed: Record the ID from screen
- Update our server.py with correct device ID
- Test API endpoints work
## 5. Deploy Our Package:
- Upload our node.lua package to Info-Beamer
- Assign package to device
- Device should connect to Flask server at 192.168.1.22
## Troubleshooting:
- If device offline: Check WiFi, restart device
- If package not loading: Check node.lua syntax
- If server connection fails: Check Flask server running on port 80

122
node.lua
View File

@@ -8,6 +8,9 @@ local server_url = CONFIG.server_url or "http://192.168.1.22"
local player_id = CONFIG.player_id or "default_player" local player_id = CONFIG.player_id or "default_player"
local refresh_interval = CONFIG.refresh_interval or 30 local refresh_interval = CONFIG.refresh_interval or 30
local default_duration = CONFIG.default_duration or 10 local default_duration = CONFIG.default_duration or 10
local touch_enabled = CONFIG.touch_enabled ~= false -- Default to true
local swipe_threshold = CONFIG.swipe_threshold or 100
local manual_override_duration = CONFIG.manual_override_duration or 10
local playlist = {} local playlist = {}
local current_item = 1 local current_item = 1
@@ -15,6 +18,15 @@ local item_start_time = 0
local last_update = 0 local last_update = 0
local channel_info = {} local channel_info = {}
-- Touch and swipe variables
local touch_start_x = 0
local touch_start_y = 0
local touch_start_time = 0
local is_touching = false
local swipe_time_limit = 1.0 -- Maximum time for swipe (seconds)
local manual_override = false
local manual_override_time = 0
function update_playlist() function update_playlist()
-- Fetch channel content from server -- Fetch channel content from server
local url = server_url .. "/api/player/" .. player_id .. "/content" local url = server_url .. "/api/player/" .. player_id .. "/content"
@@ -48,17 +60,94 @@ function update_playlist()
last_update = sys.now() last_update = sys.now()
end end
-- Touch event handlers
function node.on_touch(touches)
for touch in touches() do
if touch.type == "start" then
-- Touch started
touch_start_x = touch.x
touch_start_y = touch.y
touch_start_time = sys.now()
is_touching = true
elseif touch.type == "end" and is_touching then
-- Touch ended - check for swipe
local touch_end_x = touch.x
local touch_end_y = touch.y
local touch_duration = sys.now() - touch_start_time
-- Calculate swipe distance and direction
local dx = touch_end_x - touch_start_x
local dy = touch_end_y - touch_start_y
local distance = math.sqrt(dx * dx + dy * dy)
-- Check if it's a valid swipe (horizontal, sufficient distance, quick enough)
if distance > swipe_threshold and
touch_duration < swipe_time_limit and
math.abs(dx) > math.abs(dy) * 2 then -- More horizontal than vertical
if #playlist > 1 then -- Only allow swipe if there's content to switch to
if dx > 0 then
-- Swipe right - next item
next_content()
else
-- Swipe left - previous item
previous_content()
end
-- Enable manual override mode
manual_override = true
manual_override_time = sys.now()
print("Swipe detected: " .. (dx > 0 and "right (next)" or "left (previous)"))
end
end
is_touching = false
end
end
end
-- Function to go to next content item
function next_content()
if #playlist > 0 then
current_item = current_item + 1
if current_item > #playlist then
current_item = 1
end
item_start_time = sys.now()
end
end
-- Function to go to previous content item
function previous_content()
if #playlist > 0 then
current_item = current_item - 1
if current_item < 1 then
current_item = #playlist
end
item_start_time = sys.now()
end
end
function node.render() function node.render()
-- Update playlist periodically -- Update playlist periodically
if sys.now() - last_update > refresh_interval then if sys.now() - last_update > refresh_interval then
update_playlist() update_playlist()
end end
-- Check if manual override has expired
if manual_override and sys.now() - manual_override_time > manual_override_duration then
manual_override = false
item_start_time = sys.now() -- Reset automatic timing
end
-- If no playlist items, show waiting message -- If no playlist items, show waiting message
if #playlist == 0 then if #playlist == 0 then
font:write(100, 100, "Waiting for channel content...", 50, 1, 1, 1, 1) font:write(100, 100, "Waiting for channel content...", 50, 1, 1, 1, 1)
font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1) font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1)
font:write(100, 250, "Player ID: " .. player_id, 30, 0.7, 0.7, 0.7, 1) font:write(100, 250, "Player ID: " .. player_id, 30, 0.7, 0.7, 0.7, 1)
font:write(100, 300, "Swipe left/right to navigate when content is available", 20, 0.5, 0.5, 0.5, 1)
return return
end end
@@ -71,14 +160,9 @@ function node.render()
local duration = item.duration or default_duration local duration = item.duration or default_duration
-- Check if it's time to move to next item -- Only advance automatically if not in manual override mode
if sys.now() - item_start_time > duration then if not manual_override and sys.now() - item_start_time > duration then
current_item = current_item + 1 next_content()
if current_item > #playlist then
current_item = 1
end
item_start_time = sys.now()
item = playlist[current_item]
end end
-- Display current item -- Display current item
@@ -89,13 +173,29 @@ function node.render()
item.resource:draw(0, 0, WIDTH, HEIGHT) item.resource:draw(0, 0, WIDTH, HEIGHT)
end end
-- Show channel info overlay (optional) -- Show channel info overlay
if channel_info.name then if channel_info.name then
font:write(10, HEIGHT - 60, "Channel: " .. channel_info.name, 20, 1, 1, 1, 0.8) font:write(10, HEIGHT - 80, "Channel: " .. channel_info.name, 20, 1, 1, 1, 0.8)
if channel_info.description then if channel_info.description then
font:write(10, HEIGHT - 30, channel_info.description, 20, 1, 1, 1, 0.8) font:write(10, HEIGHT - 50, channel_info.description, 18, 1, 1, 1, 0.7)
end end
end end
-- Show content navigation info
if #playlist > 1 then
local nav_text = string.format("%d / %d", current_item, #playlist)
font:write(10, HEIGHT - 20, nav_text, 16, 1, 1, 1, 0.6)
-- Show swipe instructions (fade in/out)
local instructions_alpha = 0.4 + 0.2 * math.sin(sys.now() * 2)
font:write(10, 30, "◀ Swipe to navigate ▶", 18, 1, 1, 1, instructions_alpha)
end
-- Show manual override indicator
if manual_override then
local remaining = manual_override_duration - (sys.now() - manual_override_time)
font:write(WIDTH - 150, HEIGHT - 20, string.format("Manual: %.1fs", remaining), 16, 1, 1, 0, 0.8)
end
end end
-- Show current time -- Show current time

178
server.py
View File

@@ -199,13 +199,19 @@ def admin():
@app.route('/user') @app.route('/user')
@login_required @login_required
def user_dashboard(): def user_dashboard():
# Get user's files # Get all active files (for signage systems, users should see all available media)
user_files = MediaFile.query.filter_by(uploaded_by=current_user.id, is_active=True).order_by(MediaFile.upload_date.desc()).all() user_files = MediaFile.query.filter_by(is_active=True).order_by(MediaFile.upload_date.desc()).all()
# Get all active channels
channels = StreamingChannel.query.filter_by(is_active=True).order_by(StreamingChannel.name).all()
# Get all users for display in file info
users = User.query.all()
# Get user's activity # Get user's activity
user_activity = ActivityLog.query.filter_by(user_id=current_user.id).order_by(ActivityLog.timestamp.desc()).limit(10).all() user_activity = ActivityLog.query.filter_by(user_id=current_user.id).order_by(ActivityLog.timestamp.desc()).limit(10).all()
return render_template('user.html', user_files=user_files, user_activity=user_activity) return render_template('user.html', user_files=user_files, user_activity=user_activity, channels=channels, users=users)
# File Management Routes # File Management Routes
@app.route('/upload', methods=['POST']) @app.route('/upload', methods=['POST'])
@@ -243,8 +249,39 @@ def upload_file():
db.session.add(media_file) db.session.add(media_file)
db.session.commit() db.session.commit()
# If channel is selected, add to channel
channel_id = request.form.get('channel_id')
display_duration = request.form.get('display_duration', 10)
if channel_id and channel_id.strip():
try:
channel_id = int(channel_id)
channel = StreamingChannel.query.get(channel_id)
if channel and channel.is_active:
# Get next order number for this channel
max_order = db.session.query(db.func.max(ChannelContent.order_index)).filter_by(channel_id=channel_id).scalar() or 0
# Add to channel
channel_content = ChannelContent(
channel_id=channel_id,
media_file_id=media_file.id,
display_duration=int(display_duration),
order_index=max_order + 1
)
db.session.add(channel_content)
db.session.commit()
log_activity('Content added to channel', f'Added {file.filename} to channel {channel.name}')
flash(f'File "{file.filename}" uploaded and added to channel "{channel.name}"!', 'success')
else:
flash(f'File "{file.filename}" uploaded but channel not found!', 'warning')
except (ValueError, TypeError):
flash(f'File "{file.filename}" uploaded but invalid channel selected!', 'warning')
else:
flash(f'File "{file.filename}" uploaded successfully!', 'success')
log_activity('File uploaded', f'Uploaded: {file.filename}') log_activity('File uploaded', f'Uploaded: {file.filename}')
flash(f'File "{file.filename}" uploaded successfully!', 'success')
else: else:
flash('Invalid file type. Please upload images, videos, PDFs, or JSON files.', 'error') flash('Invalid file type. Please upload images, videos, PDFs, or JSON files.', 'error')
@@ -316,6 +353,139 @@ def add_channel():
return redirect(url_for('view_channels')) return redirect(url_for('view_channels'))
@app.route('/delete_channel/<int:channel_id>')
@login_required
def delete_channel(channel_id):
channel = StreamingChannel.query.get_or_404(channel_id)
# Check permissions
if current_user.role != 'admin' and channel.created_by != current_user.id:
flash('Access denied. You can only delete your own channels.', 'error')
return redirect(url_for('view_channels'))
# Prevent deleting default channel
if channel.is_default:
flash('Cannot delete the default channel.', 'error')
return redirect(url_for('view_channels'))
# Check if any players are assigned to this channel
assigned_players = Player.query.filter_by(channel_id=channel_id).count()
if assigned_players > 0:
flash(f'Cannot delete channel. {assigned_players} player(s) are assigned to this channel.', 'error')
return redirect(url_for('view_channels'))
channel_name = channel.name
db.session.delete(channel)
db.session.commit()
log_activity('Channel deleted', f'Deleted channel: {channel_name}')
flash(f'Channel "{channel_name}" deleted successfully!', 'success')
return redirect(url_for('view_channels'))
@app.route('/edit_channel/<int:channel_id>', methods=['GET', 'POST'])
@login_required
def edit_channel(channel_id):
channel = StreamingChannel.query.get_or_404(channel_id)
# Check permissions
if current_user.role != 'admin' and channel.created_by != current_user.id:
flash('Access denied. You can only edit your own channels.', 'error')
return redirect(url_for('view_channels'))
if request.method == 'POST':
try:
# If setting as default, unset other defaults
is_default = 'is_default' in request.form
if is_default and not channel.is_default:
StreamingChannel.query.filter_by(is_default=True).update({'is_default': False})
channel.name = request.form['name']
channel.description = request.form.get('description', '')
channel.is_default = is_default
channel.is_active = 'is_active' in request.form
db.session.commit()
log_activity('Channel updated', f'Updated channel: {channel.name}')
flash(f'Channel "{channel.name}" updated successfully!', 'success')
except Exception as e:
flash(f'Error updating channel: {str(e)}', 'error')
return redirect(url_for('view_channels'))
return render_template('edit_channel.html', channel=channel)
@app.route('/manage_content/<int:channel_id>')
@login_required
def manage_content(channel_id):
channel = StreamingChannel.query.get_or_404(channel_id)
# Check permissions
if current_user.role != 'admin' and channel.created_by != current_user.id:
flash('Access denied. You can only manage content of your own channels.', 'error')
return redirect(url_for('view_channels'))
# Get all available media files
available_media = MediaFile.query.filter_by(is_active=True).order_by(MediaFile.upload_date.desc()).all()
# Get current channel content with media file details
channel_content = ChannelContent.query.filter_by(channel_id=channel_id).order_by(ChannelContent.order_index).all()
return render_template('manage_content.html', channel=channel, available_media=available_media, channel_content=channel_content)
@app.route('/add_content_to_channel', methods=['POST'])
@login_required
def add_content_to_channel():
channel_id = request.form['channel_id']
media_file_id = request.form['media_file_id']
duration = request.form.get('duration', 10)
channel = StreamingChannel.query.get_or_404(channel_id)
# Check permissions
if current_user.role != 'admin' and channel.created_by != current_user.id:
flash('Access denied.', 'error')
return redirect(url_for('view_channels'))
# Get next order number
max_order = db.session.query(db.func.max(ChannelContent.order_index)).filter_by(channel_id=channel_id).scalar() or 0
# Add content
content = ChannelContent(
channel_id=channel_id,
media_file_id=media_file_id,
display_duration=int(duration),
order_index=max_order + 1
)
db.session.add(content)
db.session.commit()
media_file = MediaFile.query.get(media_file_id)
log_activity('Content added to channel', f'Added {media_file.original_name} to {channel.name}')
flash('Content added to channel successfully!', 'success')
return redirect(url_for('manage_content', channel_id=channel_id))
@app.route('/remove_content_from_channel/<int:content_id>')
@login_required
def remove_content_from_channel(content_id):
content = ChannelContent.query.get_or_404(content_id)
channel = content.channel
# Check permissions
if current_user.role != 'admin' and channel.created_by != current_user.id:
flash('Access denied.', 'error')
return redirect(url_for('view_channels'))
db.session.delete(content)
db.session.commit()
log_activity('Content removed from channel', f'Removed content from {channel.name}')
flash('Content removed from channel successfully!', 'success')
return redirect(url_for('manage_content', channel_id=channel.id))
# User Management Routes # User Management Routes
@app.route('/add_user', methods=['POST']) @app.route('/add_user', methods=['POST'])
@login_required @login_required

View File

@@ -57,7 +57,11 @@
<i class="bi bi-person-circle"></i> {{ current_user.username }} <i class="bi bi-person-circle"></i> {{ current_user.username }}
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('admin') }}"><i class="bi bi-house"></i> Dashboard</a></li> {% if current_user.role == 'admin' %}
<li><a class="dropdown-item" href="{{ url_for('admin') }}"><i class="bi bi-house"></i> Admin Dashboard</a></li>
{% else %}
<li><a class="dropdown-item" href="{{ url_for('user_dashboard') }}"><i class="bi bi-house"></i> User Dashboard</a></li>
{% endif %}
<li><a class="dropdown-item" href="{{ url_for('view_schedules') }}"><i class="bi bi-calendar"></i> Schedules</a></li> <li><a class="dropdown-item" href="{{ url_for('view_schedules') }}"><i class="bi bi-calendar"></i> Schedules</a></li>
<li><a class="dropdown-item" href="{{ url_for('view_channels') }}"><i class="bi bi-collection-play"></i> Channels</a></li> <li><a class="dropdown-item" href="{{ url_for('view_channels') }}"><i class="bi bi-collection-play"></i> Channels</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -259,16 +263,16 @@
} }
function editChannel(channelId) { function editChannel(channelId) {
alert('Edit channel functionality coming soon!'); window.location.href = `/edit_channel/${channelId}`;
} }
function manageContent(channelId) { function manageContent(channelId) {
alert('Content management functionality coming soon!'); window.location.href = `/manage_content/${channelId}`;
} }
function deleteChannel(channelId) { function deleteChannel(channelId) {
if (confirm('Are you sure you want to delete this channel?')) { if (confirm('Are you sure you want to delete this channel? This action cannot be undone.')) {
alert('Delete channel functionality coming soon!'); window.location.href = `/delete_channel/${channelId}`;
} }
} }

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Channel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h2>Edit Channel</h2>
<form method="post">
<div class="mb-3">
<label for="name" class="form-label">Channel Name</label>
<input type="text" class="form-control" name="name" value="{{ channel.name }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" name="description">{{ channel.description or '' }}</textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_default" {{ 'checked' if channel.is_default else '' }}>
<label class="form-check-label">Set as default channel</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_active" {{ 'checked' if channel.is_active else '' }}>
<label class="form-check-label">Channel is active</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Update Channel</button>
<a href="{{ url_for('view_channels') }}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head><title>Manage Content</title><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"></head>
<body><div class="container mt-4"><h2>{{ channel.name }} - Content Management</h2>
<div class="row"><div class="col-md-6"><h4>Current Content</h4>
{% for content in channel_content %}<div class="card mb-2"><div class="card-body">{{ content.media_file.original_name }} ({{ content.display_duration }}s)
<a href="/remove_content_from_channel/{{ content.id }}" class="btn btn-sm btn-danger float-end">Remove</a></div></div>{% endfor %}
</div><div class="col-md-6"><h4>Add Content</h4><form method="post" action="/add_content_to_channel">
<input type="hidden" name="channel_id" value="{{ channel.id }}"><select name="media_file_id" class="form-select mb-2">
{% for media in available_media %}<option value="{{ media.id }}">{{ media.original_name }}</option>{% endfor %}</select>
<input type="number" name="duration" value="10" class="form-control mb-2" placeholder="Duration">
<button type="submit" class="btn btn-success">Add</button></form></div></div>
<a href="/channels" class="btn btn-primary mt-3">Back</a></div></body></html>

View File

@@ -74,18 +74,29 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" enctype="multipart/form-data" action="/upload" class="row g-3"> <form method="post" enctype="multipart/form-data" action="/upload" class="row g-3">
<div class="col-md-8"> <div class="col-md-12">
<div class="input-group"> <label for="file" class="form-label">Select Media File</label>
<input type="file" class="form-control" name="file" accept=".png,.jpg,.jpeg,.gif,.mp4,.avi,.mov,.pdf" required> <input type="file" class="form-control" name="file" id="file" accept=".png,.jpg,.jpeg,.gif,.mp4,.avi,.mov,.pdf" required>
<button type="submit" class="btn btn-success"> <small class="text-muted">Supported: Images, Videos, PDFs</small>
<i class="bi bi-upload"></i> Upload
</button>
</div>
</div> </div>
<div class="col-md-4"> <div class="col-md-6">
<small class="text-muted"> <label for="channel_id" class="form-label">Add to Channel (Optional)</label>
Supported: Images, Videos, PDFs <select class="form-select" name="channel_id" id="channel_id">
</small> <option value="">Select Channel...</option>
{% for channel in channels %}
<option value="{{ channel.id }}">{{ channel.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="display_duration" class="form-label">Display Duration (seconds)</label>
<input type="number" class="form-control" name="display_duration" id="display_duration" min="1" max="300" value="10" placeholder="10">
<small class="text-muted">How long to display this media (1-300 seconds)</small>
</div>
<div class="col-12">
<button type="submit" class="btn btn-success">
<i class="bi bi-upload"></i> Upload Media
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -99,12 +110,12 @@
<div class="card"> <div class="card">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center"> <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-file-earmark-image"></i> All Media Files</h5> <h5 class="mb-0"><i class="bi bi-file-earmark-image"></i> All Media Files</h5>
<span class="badge bg-light text-dark">{{ files|length }} files</span> <span class="badge bg-light text-dark">{{ user_files|length }} files</span>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if files %} {% if user_files %}
<div class="media-grid"> <div class="media-grid">
{% for file in files %} {% for file in user_files %}
<div class="card media-card"> <div class="card media-card">
<div class="file-preview"> <div class="file-preview">
{% if file.file_type in ['jpg', 'jpeg', 'png', 'gif'] %} {% if file.file_type in ['jpg', 'jpeg', 'png', 'gif'] %}

Binary file not shown.

View File

@@ -0,0 +1,9 @@
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
ssid="YOUR_WIFI_NETWORK_NAME"
psk="YOUR_WIFI_PASSWORD"
key_mgmt=WPA-PSK
}