Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
MBetterc
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mbetter
MBetterc
Commits
4d37119a
Commit
4d37119a
authored
Sep 27, 2025
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update player intro
parent
b43952ee
Changes
14
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
2545 additions
and
234 deletions
+2545
-234
INTRO.mp4
assets/INTRO.mp4
+0
-0
main.py
main.py
+22
-1
settings.py
mbetterclient/config/settings.py
+8
-1
application.py
mbetterclient/core/application.py
+11
-4
games_thread.py
mbetterclient/core/games_thread.py
+113
-1
message_bus.py
mbetterclient/core/message_bus.py
+55
-1
player.py
mbetterclient/qt_player/player.py
+656
-123
default.html
mbetterclient/qt_player/templates/default.html
+328
-0
match.html
mbetterclient/qt_player/templates/match.html
+650
-0
api.py
mbetterclient/web_dashboard/api.py
+361
-21
routes.py
mbetterclient/web_dashboard/routes.py
+167
-62
fixtures.html
...terclient/web_dashboard/templates/dashboard/fixtures.html
+1
-1
index.html
mbetterclient/web_dashboard/templates/dashboard/index.html
+124
-5
templates.html
...erclient/web_dashboard/templates/dashboard/templates.html
+49
-14
No files found.
dist/MBetterDiscovery.exe
→
assets/INTRO.mp4
View file @
4d37119a
No preview for this file type
main.py
View file @
4d37119a
...
...
@@ -143,6 +143,24 @@ Examples:
help
=
'Enable debug mode showing only message bus messages'
)
parser
.
add_argument
(
'--debug-messages'
,
action
=
'store_true'
,
help
=
'Show all messages passing through the message bus on screen'
)
parser
.
add_argument
(
'--debug-player'
,
action
=
'store_true'
,
help
=
'Enable debug mode for Qt player component'
)
parser
.
add_argument
(
'--debug-overlay'
,
action
=
'store_true'
,
help
=
'Enable debug mode for overlay rendering'
)
parser
.
add_argument
(
'--no-qt'
,
action
=
'store_true'
,
...
...
@@ -200,7 +218,7 @@ Examples:
'--start-timer'
,
type
=
int
,
default
=
None
,
help
=
'Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received (default: 4 minutes)'
help
=
'Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received
. Use 0 for 10-second delay
(default: 4 minutes)'
)
parser
.
add_argument
(
...
...
@@ -252,6 +270,9 @@ def main():
settings
.
web_port
=
args
.
web_port
settings
.
debug_mode
=
args
.
debug
or
args
.
dev_mode
settings
.
dev_message
=
args
.
dev_message
settings
.
debug_messages
=
args
.
debug_messages
settings
.
debug_player
=
args
.
debug_player
settings
.
debug_overlay
=
args
.
debug_overlay
settings
.
enable_qt
=
not
args
.
no_qt
settings
.
enable_web
=
not
args
.
no_web
settings
.
qt
.
use_native_overlay
=
args
.
native_overlay
...
...
mbetterclient/config/settings.py
View file @
4d37119a
...
...
@@ -369,6 +369,9 @@ class AppSettings:
version
:
str
=
"1.0.0"
debug_mode
:
bool
=
False
dev_message
:
bool
=
False
# Enable debug mode showing only message bus messages
debug_messages
:
bool
=
False
# Show all messages passing through the message bus on screen
debug_player
:
bool
=
False
# Enable debug mode for Qt player component
debug_overlay
:
bool
=
False
# Enable debug mode for overlay rendering
enable_web
:
bool
=
True
enable_qt
:
bool
=
True
enable_api_client
:
bool
=
True
...
...
@@ -408,6 +411,10 @@ class AppSettings:
"timer"
:
self
.
timer
.
__dict__
,
"version"
:
self
.
version
,
"debug_mode"
:
self
.
debug_mode
,
"dev_message"
:
self
.
dev_message
,
"debug_messages"
:
self
.
debug_messages
,
"debug_player"
:
self
.
debug_player
,
"debug_overlay"
:
self
.
debug_overlay
,
"enable_web"
:
self
.
enable_web
,
"enable_qt"
:
self
.
enable_qt
,
"enable_api_client"
:
self
.
enable_api_client
,
...
...
@@ -438,7 +445,7 @@ class AppSettings:
settings
.
timer
=
TimerConfig
(
**
data
[
"timer"
])
# Update app settings
for
key
in
[
"version"
,
"debug_mode"
,
"enable_web"
,
"enable_qt"
,
"enable_api_client"
,
"enable_screen_cast"
]:
for
key
in
[
"version"
,
"debug_mode"
,
"
dev_message"
,
"debug_messages"
,
"debug_player"
,
"debug_overlay"
,
"
enable_web"
,
"enable_qt"
,
"enable_api_client"
,
"enable_screen_cast"
]:
if
key
in
data
:
setattr
(
settings
,
key
,
data
[
key
])
...
...
mbetterclient/core/application.py
View file @
4d37119a
...
...
@@ -165,7 +165,7 @@ class MbetterClientApplication:
def
_initialize_message_bus
(
self
)
->
bool
:
"""Initialize message bus"""
try
:
self
.
message_bus
=
MessageBus
(
max_queue_size
=
1000
,
dev_message
=
self
.
settings
.
dev_message
)
self
.
message_bus
=
MessageBus
(
max_queue_size
=
1000
,
dev_message
=
self
.
settings
.
dev_message
,
debug_messages
=
self
.
settings
.
debug_messages
)
# Register core component
self
.
message_bus
.
register_component
(
"core"
)
...
...
@@ -322,7 +322,9 @@ class MbetterClientApplication:
self
.
qt_player
=
QtVideoPlayer
(
message_bus
=
self
.
message_bus
,
settings
=
self
.
settings
.
qt
settings
=
self
.
settings
.
qt
,
debug_player
=
self
.
settings
.
debug_player
,
debug_overlay
=
self
.
settings
.
debug_overlay
)
# Don't register with thread manager since QtPlayer no longer inherits from ThreadedComponent
...
...
@@ -760,6 +762,11 @@ class MbetterClientApplication:
if
self
.
_start_timer_minutes
is
None
:
return
# Special case: --start-timer 0 means 10 seconds delay for system initialization
if
self
.
_start_timer_minutes
==
0
:
delay_seconds
=
10
logger
.
info
(
f
"Starting command line game timer: --start-timer 0 = 10 seconds delay for system initialization"
)
else
:
delay_seconds
=
self
.
_start_timer_minutes
*
60
logger
.
info
(
f
"Starting command line game timer: {self._start_timer_minutes} minutes ({delay_seconds} seconds)"
)
...
...
mbetterclient/core/games_thread.py
View file @
4d37119a
...
...
@@ -685,6 +685,10 @@ class GamesThread(ThreadedComponent):
logger
.
info
(
f
"⏰ Starting match timer for fixture {fixture_id}"
)
self
.
_start_match_timer
(
fixture_id
)
# Dispatch START_INTRO message
logger
.
info
(
f
"🎬 Dispatching START_INTRO message for fixture {fixture_id}"
)
self
.
_dispatch_start_intro
(
fixture_id
)
# Refresh dashboard statuses
self
.
_refresh_dashboard_statuses
()
...
...
@@ -897,6 +901,114 @@ class GamesThread(ThreadedComponent):
except
Exception
as
e
:
logger
.
error
(
f
"Failed to start match timer: {e}"
)
def
_dispatch_start_intro
(
self
,
fixture_id
:
str
):
"""Dispatch START_INTRO message to trigger intro content"""
try
:
from
.message_bus
import
MessageBuilder
# Find the first match that was set to 'bet' status
first_bet_match_id
=
self
.
_get_first_bet_match_id
(
fixture_id
)
# Unzip the ZIP file of the first match if it exists
self
.
_unzip_match_zip_file
(
first_bet_match_id
)
# Create and send START_INTRO message
start_intro_message
=
MessageBuilder
.
start_intro
(
sender
=
self
.
name
,
fixture_id
=
fixture_id
,
match_id
=
first_bet_match_id
)
self
.
message_bus
.
publish
(
start_intro_message
,
broadcast
=
True
)
logger
.
info
(
f
"START_INTRO message dispatched for fixture {fixture_id}, match {first_bet_match_id}"
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to dispatch START_INTRO message: {e}"
)
def
_get_first_bet_match_id
(
self
,
fixture_id
:
str
)
->
Optional
[
int
]:
"""Get the ID of the first match set to 'bet' status in the fixture"""
try
:
session
=
self
.
db_manager
.
get_session
()
try
:
from
..database.models
import
MatchModel
# Find the first match with 'bet' status in this fixture
first_bet_match
=
session
.
query
(
MatchModel
)
.
filter
(
MatchModel
.
fixture_id
==
fixture_id
,
MatchModel
.
status
==
'bet'
,
MatchModel
.
active_status
==
True
)
.
order_by
(
MatchModel
.
match_number
.
asc
())
.
first
()
if
first_bet_match
:
return
first_bet_match
.
id
else
:
logger
.
warning
(
f
"No match with 'bet' status found in fixture {fixture_id}"
)
return
None
finally
:
session
.
close
()
except
Exception
as
e
:
logger
.
error
(
f
"Failed to get first bet match ID for fixture {fixture_id}: {e}"
)
return
None
def
_unzip_match_zip_file
(
self
,
match_id
:
int
):
"""Unzip the ZIP file associated with a match to a temporary directory"""
try
:
import
zipfile
import
tempfile
import
os
from
pathlib
import
Path
session
=
self
.
db_manager
.
get_session
()
try
:
# Get the match from database
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
match_id
)
.
first
()
if
not
match
:
logger
.
warning
(
f
"Match {match_id} not found, skipping ZIP extraction"
)
return
if
not
match
.
zip_filename
:
logger
.
info
(
f
"Match {match_id} has no associated ZIP file, skipping extraction"
)
return
# Determine ZIP file location (ZIP files are stored in the zip_files directory)
from
..config.settings
import
get_user_data_dir
user_data_dir
=
get_user_data_dir
()
zip_file_path
=
user_data_dir
/
"zip_files"
/
match
.
zip_filename
if
not
zip_file_path
.
exists
():
logger
.
warning
(
f
"ZIP file not found: {zip_file_path}"
)
return
# Create temporary directory for extraction
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
f
"match_{match_id}_"
))
logger
.
info
(
f
"Extracting ZIP file {zip_file_path} to temporary directory: {temp_dir}"
)
# Extract the ZIP file
with
zipfile
.
ZipFile
(
str
(
zip_file_path
),
'r'
)
as
zip_ref
:
zip_ref
.
extractall
(
str
(
temp_dir
))
# Log extraction results
extracted_files
=
list
(
temp_dir
.
rglob
(
"*"
))
logger
.
info
(
f
"Successfully extracted {len(extracted_files)} files from {match.zip_filename}"
)
# Store the temporary directory path for potential cleanup
# In a real implementation, you might want to track this for cleanup
match
.
temp_extract_path
=
str
(
temp_dir
)
# Update match in database with temp path (optional)
session
.
commit
()
logger
.
info
(
f
"ZIP extraction completed for match {match_id}"
)
finally
:
session
.
close
()
except
Exception
as
e
:
logger
.
error
(
f
"Failed to unzip ZIP file for match {match_id}: {e}"
)
def
_cleanup
(
self
):
"""Perform cleanup operations"""
try
:
...
...
mbetterclient/core/message_bus.py
View file @
4d37119a
...
...
@@ -64,7 +64,10 @@ class MessageType(Enum):
START_GAME
=
"START_GAME"
SCHEDULE_GAMES
=
"SCHEDULE_GAMES"
START_GAME_DELAYED
=
"START_GAME_DELAYED"
START_INTRO
=
"START_INTRO"
MATCH_START
=
"MATCH_START"
PLAY_VIDEO_MATCH
=
"PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE
=
"PLAY_VIDEO_MATCH_DONE"
GAME_STATUS
=
"GAME_STATUS"
GAME_UPDATE
=
"GAME_UPDATE"
...
...
@@ -134,9 +137,10 @@ class Message:
class
MessageBus
:
"""Central message bus for inter-thread communication"""
def
__init__
(
self
,
max_queue_size
:
int
=
1000
,
dev_message
:
bool
=
False
):
def
__init__
(
self
,
max_queue_size
:
int
=
1000
,
dev_message
:
bool
=
False
,
debug_messages
:
bool
=
False
):
self
.
max_queue_size
=
max_queue_size
self
.
dev_message
=
dev_message
self
.
debug_messages
=
debug_messages
self
.
_queues
:
Dict
[
str
,
Queue
]
=
{}
self
.
_handlers
:
Dict
[
str
,
Dict
[
MessageType
,
List
[
Callable
]]]
=
{}
self
.
_global_handlers
:
Dict
[
MessageType
,
List
[
Callable
]]
=
{}
...
...
@@ -147,6 +151,8 @@ class MessageBus:
if
dev_message
:
logger
.
info
(
"MessageBus initialized with dev_message mode enabled"
)
elif
debug_messages
:
logger
.
info
(
"MessageBus initialized with debug_messages mode enabled"
)
else
:
logger
.
info
(
"MessageBus initialized"
)
...
...
@@ -208,6 +214,16 @@ class MessageBus:
if
self
.
dev_message
:
logger
.
info
(
f
"📨 MESSAGE_BUS: {message}"
)
# Display message on screen (debug_messages mode with debug enabled)
if
self
.
debug_messages
and
self
.
dev_message
:
timestamp_str
=
datetime
.
fromtimestamp
(
message
.
timestamp
)
.
strftime
(
'
%
H:
%
M:
%
S.
%
f'
)[:
-
3
]
print
(
f
"[{timestamp_str}] 📨 {message.sender} -> {message.recipient or 'ALL'}: {message.type.value}"
)
if
message
.
data
:
# Show key data fields (truncate long values)
data_str
=
", "
.
join
([
f
"{k}: {str(v)[:50]}{'...' if len(str(v)) > 50 else ''}"
for
k
,
v
in
message
.
data
.
items
()])
print
(
f
" Data: {{{data_str}}}"
)
print
()
# Empty line for readability
if
broadcast
or
message
.
recipient
is
None
:
# Broadcast to all components
success_count
=
0
...
...
@@ -578,6 +594,18 @@ class MessageBuilder:
}
)
@
staticmethod
def
start_intro
(
sender
:
str
,
fixture_id
:
Optional
[
str
]
=
None
,
match_id
:
Optional
[
int
]
=
None
)
->
Message
:
"""Create START_INTRO message"""
return
Message
(
type
=
MessageType
.
START_INTRO
,
sender
=
sender
,
data
=
{
"fixture_id"
:
fixture_id
,
"match_id"
:
match_id
}
)
@
staticmethod
def
match_start
(
sender
:
str
,
fixture_id
:
str
,
match_id
:
int
)
->
Message
:
"""Create MATCH_START message"""
...
...
@@ -589,3 +617,29 @@ class MessageBuilder:
"match_id"
:
match_id
}
)
@
staticmethod
def
play_video_match
(
sender
:
str
,
match_id
:
int
,
video_filename
:
str
,
fixture_id
:
Optional
[
str
]
=
None
)
->
Message
:
"""Create PLAY_VIDEO_MATCH message"""
return
Message
(
type
=
MessageType
.
PLAY_VIDEO_MATCH
,
sender
=
sender
,
data
=
{
"match_id"
:
match_id
,
"video_filename"
:
video_filename
,
"fixture_id"
:
fixture_id
}
)
@
staticmethod
def
play_video_match_done
(
sender
:
str
,
match_id
:
int
,
video_filename
:
str
,
fixture_id
:
Optional
[
str
]
=
None
)
->
Message
:
"""Create PLAY_VIDEO_MATCH_DONE message"""
return
Message
(
type
=
MessageType
.
PLAY_VIDEO_MATCH_DONE
,
sender
=
sender
,
data
=
{
"match_id"
:
match_id
,
"video_filename"
:
video_filename
,
"fixture_id"
:
fixture_id
}
)
\ No newline at end of file
mbetterclient/qt_player/player.py
View file @
4d37119a
This diff is collapsed.
Click to expand it.
mbetterclient/qt_player/templates/default.html
0 → 100644
View file @
4d37119a
This diff is collapsed.
Click to expand it.
mbetterclient/qt_player/templates/match.html
0 → 100644
View file @
4d37119a
This diff is collapsed.
Click to expand it.
mbetterclient/web_dashboard/api.py
View file @
4d37119a
This diff is collapsed.
Click to expand it.
mbetterclient/web_dashboard/routes.py
View file @
4d37119a
...
...
@@ -1754,14 +1754,13 @@ def send_custom_message():
def
get_intro_templates
():
"""Get intro templates configuration"""
try
:
from
pathlib
import
Path
from
..database.models
import
GameConfigModel
import
json
import
os
from
..config.settings
import
get_user_data_dir
# Get data directory
data_dir
=
Path
(
get_user_data_dir
())
config_path
=
data_dir
/
'intro_templates.json'
session
=
api_bp
.
db_manager
.
get_session
()
try
:
# Get intro templates configuration from database
intro_config
=
session
.
query
(
GameConfigModel
)
.
filter_by
(
config_key
=
'intro_templates_config'
)
.
first
()
# Default configuration
default_config
=
{
...
...
@@ -1770,21 +1769,23 @@ def get_intro_templates():
'rotating_time'
:
'05:00'
}
if
config_path
.
exists
()
:
if
intro_config
:
try
:
with
open
(
config_path
,
'r'
)
as
f
:
config
=
json
.
load
(
f
)
config
=
json
.
loads
(
intro_config
.
config_value
)
# Merge with defaults to ensure all fields are present
for
key
,
value
in
default_config
.
items
():
if
key
not
in
config
:
config
[
key
]
=
value
return
jsonify
(
config
)
except
(
json
.
JSONDecodeError
,
FileNotFound
Error
):
logger
.
warning
(
"Failed to load intro templates config
, using defaults"
)
except
(
json
.
JSONDecodeError
,
Type
Error
):
logger
.
warning
(
"Failed to parse intro templates config from database
, using defaults"
)
return
jsonify
(
default_config
)
else
:
return
jsonify
(
default_config
)
finally
:
session
.
close
()
except
Exception
as
e
:
logger
.
error
(
f
"Error loading intro templates: {str(e)}"
)
return
jsonify
({
'templates'
:
[],
'default_show_time'
:
'00:30'
,
'rotating_time'
:
'05:00'
})
...
...
@@ -1795,11 +1796,9 @@ def get_intro_templates():
def
save_intro_templates
():
"""Save intro templates configuration"""
try
:
from
pathlib
import
Path
from
..database.models
import
GameConfigModel
import
json
import
os
import
re
from
..config.settings
import
get_user_data_dir
data
=
request
.
get_json
()
if
not
data
:
...
...
@@ -1810,6 +1809,10 @@ def save_intro_templates():
default_show_time
=
data
.
get
(
'default_show_time'
,
'00:30'
)
rotating_time
=
data
.
get
(
'rotating_time'
,
'05:00'
)
logger
.
info
(
f
"WebDashboard: Saving intro templates - received {len(templates)} templates"
)
logger
.
debug
(
f
"WebDashboard: Templates data: {templates}"
)
logger
.
debug
(
f
"WebDashboard: Default show time: {default_show_time}, Rotating time: {rotating_time}"
)
# Validate time format (MM:SS)
time_pattern
=
re
.
compile
(
r'^[0-9]{1,2}:[0-5][0-9]$'
)
...
...
@@ -1833,7 +1836,7 @@ def save_intro_templates():
if
not
time_pattern
.
match
(
template
[
'show_time'
]):
return
jsonify
({
'error'
:
f
'Template {i+1} has invalid show_time format. Use MM:SS format.'
}),
400
# Save configuration
# Save configuration
to database
config
=
{
'templates'
:
templates
,
'default_show_time'
:
default_show_time
,
...
...
@@ -1841,15 +1844,34 @@ def save_intro_templates():
'updated_at'
:
datetime
.
now
()
.
isoformat
()
}
# Get data directory and ensure it exists
data_dir
=
Path
(
get_user_data_dir
())
data_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
config_path
=
data_dir
/
'intro_templates.json'
config_json
=
json
.
dumps
(
config
)
logger
.
debug
(
f
"WebDashboard: Config JSON length: {len(config_json)}"
)
with
open
(
config_path
,
'w'
)
as
f
:
json
.
dump
(
config
,
f
,
indent
=
2
)
session
=
api_bp
.
db_manager
.
get_session
()
try
:
# Check if config already exists
existing_config
=
session
.
query
(
GameConfigModel
)
.
filter_by
(
config_key
=
'intro_templates_config'
)
.
first
()
logger
.
info
(
f
"Intro templates configuration saved with {len(templates)} templates"
)
if
existing_config
:
logger
.
debug
(
"WebDashboard: Updating existing config"
)
# Update existing config
existing_config
.
config_value
=
config_json
existing_config
.
updated_at
=
datetime
.
utcnow
()
else
:
logger
.
debug
(
"WebDashboard: Creating new config"
)
# Create new config
new_config
=
GameConfigModel
(
config_key
=
'intro_templates_config'
,
config_value
=
config_json
,
value_type
=
'json'
,
description
=
'Intro templates configuration for video player'
,
is_system
=
False
)
session
.
add
(
new_config
)
session
.
commit
()
logger
.
info
(
f
"WebDashboard: Successfully saved intro templates configuration: {len(templates)} templates"
)
return
jsonify
({
'success'
:
True
,
...
...
@@ -1859,8 +1881,11 @@ def save_intro_templates():
'rotating_time'
:
rotating_time
})
finally
:
session
.
close
()
except
Exception
as
e
:
logger
.
error
(
f
"Error saving intro templates: {str(e)}"
)
logger
.
error
(
f
"
WebDashboard:
Error saving intro templates: {str(e)}"
)
return
jsonify
({
'error'
:
'Internal server error'
}),
500
...
...
@@ -4461,3 +4486,83 @@ def get_bet_barcode_data(bet_id):
except
Exception
as
e
:
logger
.
error
(
f
"API get bet barcode data error: {e}"
)
return
jsonify
({
"error"
:
str
(
e
)}),
500
@
api_bp
.
route
(
'/templates/<template_name>'
)
def
get_template_preview
(
template_name
):
"""Serve template preview with black background - no authentication required"""
try
:
api
=
g
.
get
(
'api'
)
if
not
api
:
return
"API not available"
,
500
# Get template preview HTML
preview_html
=
api
.
get_template_preview
(
template_name
)
# Return HTML response
from
flask
import
Response
return
Response
(
preview_html
,
mimetype
=
'text/html'
)
except
Exception
as
e
:
logger
.
error
(
f
"Template preview route error: {e}"
)
return
f
"Error loading template preview: {str(e)}"
,
500
@
api_bp
.
route
(
'/upload-intro-video'
,
methods
=
[
'POST'
])
@
api_bp
.
auth_manager
.
require_auth
if
hasattr
(
api_bp
,
'auth_manager'
)
and
api_bp
.
auth_manager
else
login_required
@
api_bp
.
auth_manager
.
require_admin
if
hasattr
(
api_bp
,
'auth_manager'
)
and
api_bp
.
auth_manager
else
login_required
def
upload_intro_video
():
"""Upload default intro video file (admin only)"""
try
:
from
..config.settings
import
get_user_data_dir
from
werkzeug.utils
import
secure_filename
import
os
# Check if file was uploaded
if
'video_file'
not
in
request
.
files
:
return
jsonify
({
"error"
:
"No video file provided"
}),
400
file
=
request
.
files
[
'video_file'
]
overwrite
=
request
.
form
.
get
(
'overwrite'
,
'false'
)
.
lower
()
==
'true'
if
file
.
filename
==
''
:
return
jsonify
({
"error"
:
"No video file selected"
}),
400
# Validate file type
allowed_extensions
=
{
'mp4'
,
'avi'
,
'mov'
,
'mkv'
,
'webm'
}
if
not
(
'.'
in
file
.
filename
and
file
.
filename
.
rsplit
(
'.'
,
1
)[
1
]
.
lower
()
in
allowed_extensions
):
return
jsonify
({
"error"
:
"Invalid file type. Allowed: MP4, AVI, MOV, MKV, WebM"
}),
400
# Get persistent storage directory
user_data_dir
=
get_user_data_dir
()
videos_dir
=
user_data_dir
/
"videos"
videos_dir
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
# Set filename for intro video
filename
=
secure_filename
(
"intro_video."
+
file
.
filename
.
rsplit
(
'.'
,
1
)[
1
]
.
lower
())
filepath
=
videos_dir
/
filename
# Check if file already exists
if
filepath
.
exists
()
and
not
overwrite
:
return
jsonify
({
"error"
:
"Intro video already exists. Check 'overwrite existing' to replace it."
}),
400
# Save the file
file
.
save
(
str
(
filepath
))
# Store the path in configuration
if
api_bp
.
db_manager
:
api_bp
.
db_manager
.
set_config_value
(
'intro_video_path'
,
str
(
filepath
))
logger
.
info
(
f
"Intro video uploaded successfully: {filepath}"
)
return
jsonify
({
"success"
:
True
,
"message"
:
"Intro video uploaded successfully"
,
"filename"
:
filename
,
"path"
:
str
(
filepath
)
})
except
Exception
as
e
:
logger
.
error
(
f
"API upload intro video error: {e}"
)
return
jsonify
({
"error"
:
str
(
e
)}),
500
\ No newline at end of file
mbetterclient/web_dashboard/templates/dashboard/fixtures.html
View file @
4d37119a
...
...
@@ -115,7 +115,7 @@
<h5>
All Fixtures
</h5>
<span
id=
"filtered-count"
class=
"badge bg-secondary"
>
0 fixtures
</span>
</div>
<div
class=
"card-body p-0"
>
<div
class=
"card-body p-0"
style=
"padding-bottom: 100px !important;"
>
<div
class=
"table-responsive"
>
<table
class=
"table table-hover mb-0"
id=
"fixtures-table"
>
<thead
class=
"table-light"
>
...
...
mbetterclient/web_dashboard/templates/dashboard/index.html
View file @
4d37119a
...
...
@@ -110,7 +110,12 @@
<i
class=
"fas fa-shield-alt me-2"
></i>
Administrator Actions
</h6>
</div>
<div
class=
"col-md-6 mb-3"
>
<div
class=
"col-md-4 mb-3"
>
<button
class=
"btn btn-outline-primary w-100"
id=
"btn-upload-intro-video"
>
<i
class=
"fas fa-upload me-2"
></i>
Upload Intro Video
</button>
</div>
<div
class=
"col-md-4 mb-3"
>
<button
class=
"btn btn-outline-danger w-100"
id=
"btn-shutdown-app"
>
<i
class=
"fas fa-power-off me-2"
></i>
Shutdown Application
</button>
...
...
@@ -364,6 +369,51 @@
</div>
</div>
</div>
<!-- Upload Intro Video Modal -->
<div
class=
"modal fade"
id=
"uploadIntroVideoModal"
tabindex=
"-1"
>
<div
class=
"modal-dialog"
>
<div
class=
"modal-content"
>
<div
class=
"modal-header"
>
<h5
class=
"modal-title"
>
Upload Default Intro Video
</h5>
<button
type=
"button"
class=
"btn-close"
data-bs-dismiss=
"modal"
></button>
</div>
<div
class=
"modal-body"
>
<form
id=
"upload-intro-video-form"
enctype=
"multipart/form-data"
>
<div
class=
"mb-3"
>
<label
class=
"form-label"
>
Select Video File
</label>
<input
type=
"file"
class=
"form-control"
id=
"intro-video-file"
accept=
"video/*"
required
>
<div
class=
"form-text"
>
Supported formats: MP4, AVI, MOV, MKV, WebM (Max 500MB)
</div>
</div>
<div
class=
"mb-3"
>
<div
class=
"form-check"
>
<input
class=
"form-check-input"
type=
"checkbox"
id=
"overwrite-existing"
>
<label
class=
"form-check-label"
for=
"overwrite-existing"
>
Overwrite existing intro video
</label>
</div>
</div>
<div
id=
"upload-progress"
class=
"d-none"
>
<div
class=
"progress mb-2"
>
<div
class=
"progress-bar"
role=
"progressbar"
style=
"width: 0%"
></div>
</div>
<small
class=
"text-muted"
>
Uploading...
</small>
</div>
</form>
</div>
<div
class=
"modal-footer"
>
<button
type=
"button"
class=
"btn btn-secondary"
data-bs-dismiss=
"modal"
>
Cancel
</button>
<button
type=
"button"
class=
"btn btn-primary"
id=
"confirm-upload-intro-video"
>
<i
class=
"fas fa-upload me-1"
></i>
Upload Video
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
...
...
@@ -395,6 +445,10 @@ document.addEventListener('DOMContentLoaded', function() {
window
.
location
.
href
=
'/tokens'
;
});
document
.
getElementById
(
'btn-upload-intro-video'
).
addEventListener
(
'click'
,
function
()
{
new
bootstrap
.
Modal
(
document
.
getElementById
(
'uploadIntroVideoModal'
)).
show
();
});
// Match interval save button
document
.
getElementById
(
'btn-save-interval'
).
addEventListener
(
'click'
,
function
()
{
saveMatchInterval
();
...
...
@@ -557,6 +611,71 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
document
.
getElementById
(
'confirm-upload-intro-video'
).
addEventListener
(
'click'
,
function
()
{
const
fileInput
=
document
.
getElementById
(
'intro-video-file'
);
const
file
=
fileInput
.
files
[
0
];
const
overwrite
=
document
.
getElementById
(
'overwrite-existing'
).
checked
;
if
(
!
file
)
{
alert
(
'Please select a video file to upload'
);
return
;
}
// Validate file size (500MB limit)
const
maxSize
=
500
*
1024
*
1024
;
// 500MB in bytes
if
(
file
.
size
>
maxSize
)
{
alert
(
'File size must be less than 500MB'
);
return
;
}
// Validate file type
const
allowedTypes
=
[
'video/mp4'
,
'video/avi'
,
'video/quicktime'
,
'video/x-matroska'
,
'video/webm'
];
if
(
!
allowedTypes
.
includes
(
file
.
type
)
&&
!
file
.
name
.
match
(
/
\.(
mp4|avi|mov|mkv|webm
)
$/i
))
{
alert
(
'Please select a valid video file (MP4, AVI, MOV, MKV, or WebM)'
);
return
;
}
const
formData
=
new
FormData
();
formData
.
append
(
'video_file'
,
file
);
formData
.
append
(
'overwrite'
,
overwrite
);
const
uploadBtn
=
document
.
getElementById
(
'confirm-upload-intro-video'
);
const
originalText
=
uploadBtn
.
innerHTML
;
const
progressDiv
=
document
.
getElementById
(
'upload-progress'
);
const
progressBar
=
progressDiv
.
querySelector
(
'.progress-bar'
);
// Show progress and disable button
progressDiv
.
classList
.
remove
(
'd-none'
);
uploadBtn
.
disabled
=
true
;
uploadBtn
.
innerHTML
=
'<i class="fas fa-spinner fa-spin me-1"></i>Uploading...'
;
fetch
(
'/api/upload-intro-video'
,
{
method
:
'POST'
,
body
:
formData
})
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
if
(
data
.
success
)
{
bootstrap
.
Modal
.
getInstance
(
document
.
getElementById
(
'uploadIntroVideoModal'
)).
hide
();
// Clear the form
document
.
getElementById
(
'upload-intro-video-form'
).
reset
();
alert
(
'Intro video uploaded successfully!'
);
}
else
{
alert
(
'Failed to upload video: '
+
(
data
.
error
||
'Unknown error'
));
}
})
.
catch
(
error
=>
{
alert
(
'Error uploading video: '
+
error
.
message
);
})
.
finally
(()
=>
{
// Hide progress and restore button
progressDiv
.
classList
.
add
(
'd-none'
);
progressBar
.
style
.
width
=
'0%'
;
uploadBtn
.
disabled
=
false
;
uploadBtn
.
innerHTML
=
originalText
;
});
});
// Status update functions
function
updateSystemStatus
()
{
fetch
(
'/api/status'
)
...
...
mbetterclient/web_dashboard/templates/dashboard/templates.html
View file @
4d37119a
...
...
@@ -69,13 +69,13 @@
<div
class=
"col-md-6"
>
<label
class=
"form-label"
>
Default Show Time
</label>
<input
type=
"text"
class=
"form-control"
id=
"defaultShowTime"
placeholder=
"00:
30
"
pattern=
"[0-9]{1,2}:[0-5][0-9]"
>
placeholder=
"00:
15
"
pattern=
"[0-9]{1,2}:[0-5][0-9]"
>
<div
class=
"form-text"
>
Default display duration (MM:SS)
</div>
</div>
<div
class=
"col-md-6"
>
<label
class=
"form-label"
>
Rotating Interval
</label>
<input
type=
"text"
class=
"form-control"
id=
"rotatingTime"
placeholder=
"0
5:00
"
pattern=
"[0-9]{1,2}:[0-5][0-9]"
>
placeholder=
"0
0:15
"
pattern=
"[0-9]{1,2}:[0-5][0-9]"
>
<div
class=
"form-text"
>
Time between template rotations (MM:SS)
</div>
</div>
</div>
...
...
@@ -176,8 +176,8 @@
let
availableTemplates
=
[];
let
outcomeAssignments
=
{};
let
introTemplates
=
[];
let
defaultShowTime
=
'00:
30
'
;
let
rotatingTime
=
'0
5:00
'
;
let
defaultShowTime
=
'00:
15
'
;
let
rotatingTime
=
'0
0:15
'
;
// Define all possible outcomes
const
allOutcomes
=
[
...
...
@@ -517,8 +517,29 @@
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
introTemplates
=
data
.
templates
||
[];
defaultShowTime
=
data
.
default_show_time
||
'00:30'
;
rotatingTime
=
data
.
rotating_time
||
'05:00'
;
defaultShowTime
=
data
.
default_show_time
||
'00:15'
;
rotatingTime
=
data
.
rotating_time
||
'00:15'
;
// Set default intro templates if none exist
if
(
introTemplates
.
length
===
0
)
{
// Check if fixtures and match templates are available
const
fixturesTemplate
=
availableTemplates
.
find
(
t
=>
t
.
name
===
'fixtures'
);
const
matchTemplate
=
availableTemplates
.
find
(
t
=>
t
.
name
===
'match'
);
if
(
fixturesTemplate
)
{
introTemplates
.
push
({
name
:
'fixtures'
,
show_time
:
'00:15'
});
}
if
(
matchTemplate
)
{
introTemplates
.
push
({
name
:
'match'
,
show_time
:
'00:15'
});
}
}
document
.
getElementById
(
'defaultShowTime'
).
value
=
defaultShowTime
;
document
.
getElementById
(
'rotatingTime'
).
value
=
rotatingTime
;
...
...
@@ -526,6 +547,24 @@
.
catch
(
error
=>
{
console
.
error
(
'Error loading intro templates:'
,
error
);
introTemplates
=
[];
// Set default intro templates on error too
const
fixturesTemplate
=
availableTemplates
.
find
(
t
=>
t
.
name
===
'fixtures'
);
const
matchTemplate
=
availableTemplates
.
find
(
t
=>
t
.
name
===
'match'
);
if
(
fixturesTemplate
)
{
introTemplates
.
push
({
name
:
'fixtures'
,
show_time
:
'00:15'
});
}
if
(
matchTemplate
)
{
introTemplates
.
push
({
name
:
'match'
,
show_time
:
'00:15'
});
}
});
}
...
...
@@ -696,8 +735,8 @@
const
data
=
{
templates
:
introTemplates
,
default_show_time
:
defaultTime
||
'00:
30
'
,
rotating_time
:
rotatingInterval
||
'0
5:00
'
default_show_time
:
defaultTime
||
'00:
15
'
,
rotating_time
:
rotatingInterval
||
'0
0:15
'
};
fetch
(
'/api/intro-templates'
,
{
...
...
@@ -898,10 +937,6 @@
cursor
:
grabbing
;
}
/* Add margin at bottom for better scrolling */
.container-fluid
{
padding-bottom
:
100px
;
}
</style>
{% endblock %}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment