| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- {% extends "base.html" %}
- {% block title %}电视控制{% endblock %}
- {% block extra_styles %}
- .tv-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 15px;
- margin-bottom: 20px;
- justify-content: flex-start;
- }
- .tv-card {
- flex: 0 0 auto;
- width: 140px;
- background: #f8f9fa;
- border-radius: 12px;
- padding: 15px;
- text-align: center;
- border: 1px solid #eee;
- transition: all 0.2s;
- cursor: pointer;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- }
- .tv-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 8px 15px rgba(0,0,0,0.1);
- background: white;
- border-color: var(--primary-color);
- }
- .tv-icon { font-size: 3em; margin-bottom: 10px; }
- .tv-name { font-weight: bold; color: #2c3e50; margin-bottom: 5px; }
- .tv-index { font-size: 0.8em; color: #999; margin-bottom: 10px; }
- /* Modal */
- .modal {
- display: none;
- position: fixed;
- top: 0; left: 0; width: 100%; height: 100%;
- background-color: rgba(0,0,0,0.5);
- z-index: 2000;
- justify-content: center;
- align-items: center;
- }
- .modal-content {
- background-color: white;
- padding: 30px;
- border-radius: 15px;
- width: 90%;
- max-width: 500px;
- max-height: 90vh;
- overflow-y: auto;
- position: relative;
- box-shadow: 0 5px 30px rgba(0,0,0,0.3);
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- border-bottom: 1px solid #eee;
- padding-bottom: 10px;
- }
- .modal-title { font-size: 1.4em; color: #2c3e50; font-weight: bold; }
- .close-btn { font-size: 1.5em; cursor: pointer; color: #aaa; line-height: 1; }
- .close-btn:hover { color: #333; }
- .config-section {
- background: #f8f9fa;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 15px;
- border: 1px solid #eee;
- }
- {% endblock %}
- {% block content %}
- <div class="module-header">
- <h2>📺 电视控制 (Kodi)</h2>
- </div>
-
- <div class="sub-section">
- <h3>👤 控制单个电视</h3>
- <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">点击电视图标进行详细配置</p>
- <div id="tv-grid-container" class="tv-grid">
- <div style="width: 100%; text-align: center; padding: 20px;">
- 加载中...
- </div>
- </div>
- </div>
- <div class="sub-section">
- <h3>👥 控制所有电视</h3>
-
- <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-bottom: 20px;">
- <h4 style="margin-bottom: 15px; color: #ff6b6b; border-left: 4px solid #ff6b6b; padding-left: 10px;">同步播放控制</h4>
- <div class="control-row">
- <div class="control-group">
- <label for="videoSelect">选择视频 (Video ID)</label>
- <select id="videoSelect">
- <option value="">加载中...</option>
- </select>
- </div>
- <div class="control-group" style="flex: 0 0 auto;">
- <button class="btn btn-secondary" onclick="startKodi()">同步播放视频</button>
- </div>
- </div>
- </div>
- <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-bottom: 20px;">
- <h4 style="margin-bottom: 15px; color: #4ecdc4; border-left: 4px solid #4ecdc4; padding-left: 10px;">音量控制</h4>
- <div class="control-row">
- <div class="control-group">
- <label for="globalVolume">全局音量 (0-100)</label>
- <input type="number" id="globalVolume" min="0" max="100" value="65">
- </div>
- <div class="control-group" style="flex: 0 0 auto;">
- <button class="btn btn-info" onclick="setGlobalVolume()">设置音量</button>
- </div>
- </div>
- </div>
- <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee;">
- <h4 style="margin-bottom: 15px; color: #333; border-left: 4px solid #333; padding-left: 10px;">系统与模式控制</h4>
- <div class="control-row">
- <button class="btn btn-primary" style="flex: 1;" onclick="startAllKodiApps()">启动所有电视Kodi应用</button>
- <button class="btn btn-warning" style="flex: 1;" onclick="revokeIndividualState()">撤销独立控制 (恢复同步)</button>
- </div>
- <div class="control-row">
- <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnAllTvs()">唤醒所有电视</button>
- <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffAllTvs()">息屏所有电视</button>
- </div>
- </div>
- <div style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #eee; margin-top: 20px;">
- <h4 style="margin-bottom: 15px; color: #9b59b6; border-left: 4px solid #9b59b6; padding-left: 10px;">⏰ 定时闲时播放控制</h4>
- <p style="margin-bottom: 15px; color: #666; font-size: 0.9em;">
- 启用后,系统将在 07:30 - 18:00 期间自动循环播放视频。
- </p>
- <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;">
- <div>
- <strong>当前状态:</strong>
- <span id="freeTimeStatusText" style="font-weight: bold; color: #666;">加载中...</span>
- </div>
- <div class="control-row" style="margin-bottom: 0; gap: 10px; flex: 0 0 auto;">
- <button id="btnEnableFreeTime" class="btn btn-primary" onclick="controlFreeTimePlay('start')">开启功能</button>
- <button id="btnDisableFreeTime" class="btn btn-secondary" style="background-color: #95a5a6; display: none;" onclick="controlFreeTimePlay('stop')">关闭功能</button>
- </div>
- </div>
- </div>
-
- <div class="status-display" id="kodiStatusDisplay">
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
- <strong>📊 Kodi 系统状态</strong>
- <button class="btn btn-warning" style="padding: 5px 10px; font-size: 12px;" onclick="getKodiStatus()">刷新</button>
- </div>
- <div id="kodiStatusContent">加载中...</div>
- </div>
- </div>
- <!-- 弹窗 (Modal) -->
- <div id="tvConfigModal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <div class="modal-title">
- 📺 <span id="modalTvName">电视配置</span>
- </div>
- <span class="close-btn" onclick="closeTvModal()">×</span>
- </div>
-
- <!-- 电源控制 -->
- <div class="config-section">
- <div class="config-title" style="color: #ff6b6b; font-weight: bold; margin-bottom: 10px;">🔌 电源控制</div>
- <div class="control-row" style="margin-bottom: 0;">
- <button class="btn btn-secondary" style="flex: 1;" onclick="turnOnCurrentTv()">唤醒</button>
- <button class="btn" style="flex: 1; background-color: #34495e; color: white;" onclick="turnOffCurrentTv()">息屏</button>
- </div>
- </div>
- <!-- 图片播放 -->
- <div class="config-section">
- <div class="config-title" style="color: #4ecdc4; font-weight: bold; margin-bottom: 10px;">🖼️ 图片播放</div>
- <div class="control-group" style="margin-bottom: 10px;">
- <label>方式一:上传图片</label>
- <input type="file" id="modalImageFile" accept="image/*">
- </div>
- <div class="control-group" style="margin-bottom: 10px;">
- <label>方式二:图片URL</label>
- <input type="text" id="modalImageUrl" placeholder="http://example.com/image.jpg">
- </div>
- <button class="btn btn-primary" style="width: 100%;" onclick="playImageCurrentTv()">播放图片</button>
- </div>
- <!-- RTSP播放 -->
- <div class="config-section">
- <div class="config-title" style="color: #667eea; font-weight: bold; margin-bottom: 10px;">📹 RTSP视频流</div>
- <div class="control-group" style="margin-bottom: 10px;">
- <label>RTSP URL</label>
- <input type="text" id="modalRtspUrl" placeholder="rtsp://example.com/stream">
- </div>
- <div class="control-group" style="margin-bottom: 10px;">
- <label>音量 (0-100)</label>
- <input type="number" id="modalRtspVolume" min="0" max="100" value="0">
- </div>
- <button class="btn btn-info" style="width: 100%;" onclick="playRtspCurrentTv()">播放RTSP流</button>
- </div>
- </div>
- </div>
- {% endblock %}
- {% block scripts %}
- <script>
- let currentKodiStatus = null;
- let activeTvIndex = null;
- document.addEventListener('DOMContentLoaded', function() {
- getKodiStatus();
- getKodiClients();
- getVideoList();
- getFreeTimePlayStatus();
- window.onclick = function(event) {
- const modal = document.getElementById('tvConfigModal');
- if (event.target == modal) {
- closeTvModal();
- }
- }
- });
- async function getKodiStatus() {
- try {
- const response = await fetch('/api/kodi/status');
- const result = await response.json();
- if (result.success) {
- currentKodiStatus = result.data;
- document.getElementById('kodiStatusContent').innerHTML = `
- <p><strong>线程状态:</strong> ${currentKodiStatus.is_running ? '运行中' : '已停止'}</p>
- <p><strong>信息:</strong> ${currentKodiStatus.message}</p>
- `;
- } else {
- document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">获取失败: ${result.message}</span>`;
- }
- } catch (error) {
- document.getElementById('kodiStatusContent').innerHTML = `<span style="color:red">网络错误</span>`;
- }
- }
- async function getKodiClients() {
- const container = document.getElementById('tv-grid-container');
- if (!container) return;
-
- try {
- const response = await fetch('/api/kodi/clients');
- const result = await response.json();
-
- if (result.success && result.data.length > 0) {
- container.innerHTML = '';
- result.data.forEach(client => {
- const index = client.index;
- const name = client.name;
-
- const cardDiv = document.createElement('div');
- cardDiv.className = 'tv-card';
- cardDiv.onclick = () => openTvModal(index, name);
-
- cardDiv.innerHTML = `
- <div class="tv-icon">📺</div>
- <div class="tv-name">${name}</div>
- <div class="tv-index">ID: ${index}</div>
- <button class="btn btn-primary" style="padding: 5px 15px; font-size: 12px; margin-top: 5px;">
- ⚙️ 配置
- </button>
- `;
- container.appendChild(cardDiv);
- });
- } else {
- container.innerHTML = '<div style="width: 100%; text-align: center; color: #666;">未找到 Kodi 客户端</div>';
- }
- } catch (error) {
- console.error(error);
- container.innerHTML = '<div style="width: 100%; text-align: center; color: red;">加载失败</div>';
- }
- }
- async function getVideoList() {
- const selectEl = document.getElementById('videoSelect');
- try {
- const response = await fetch('/api/kodi/videos');
- const result = await response.json();
-
- if (result.success && result.data.length > 0) {
- selectEl.innerHTML = '';
- result.data.forEach(video => {
- const option = document.createElement('option');
- option.value = video.id;
- option.textContent = `${video.name} (ID: ${video.id})`;
- selectEl.appendChild(option);
- });
- } else {
- selectEl.innerHTML = '<option value="">未找到视频</option>';
- }
- } catch (error) {
- console.error(error);
- selectEl.innerHTML = '<option value="">加载失败</option>';
- }
- }
- async function startKodi() {
- const videoId = parseInt(document.getElementById('videoSelect').value);
- if (isNaN(videoId) || videoId < 0) {
- showMessage('请选择有效的视频', 'error');
- return;
- }
- try {
- showLoading(true);
- const response = await fetch('/api/kodi/start', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ video_id: videoId })
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- getKodiStatus();
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function setGlobalVolume() {
- const volume = parseInt(document.getElementById('globalVolume').value);
- if (isNaN(volume) || volume < 0 || volume > 100) {
- showMessage('音量必须是 0-100 之间的整数', 'error');
- return;
- }
- try {
- showLoading(true);
- const response = await fetch('/api/kodi/set_volume', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ volume: volume })
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function revokeIndividualState() {
- try {
- showLoading(true);
- const response = await fetch('/api/kodi/revoke_individual_state', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- getKodiStatus();
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function startAllKodiApps() {
- try {
- showLoading(true);
- const response = await fetch('/api/kodi/start_all_apps', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- getKodiStatus();
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- function openTvModal(index, name) {
- activeTvIndex = index;
- document.getElementById('modalTvName').textContent = `${name} (Index: ${index})`;
- document.getElementById('modalImageFile').value = '';
- document.getElementById('modalImageUrl').value = '';
- document.getElementById('modalRtspUrl').value = '';
- document.getElementById('modalRtspVolume').value = '0';
- document.getElementById('tvConfigModal').style.display = 'flex';
- }
- function closeTvModal() {
- document.getElementById('tvConfigModal').style.display = 'none';
- activeTvIndex = null;
- }
- async function turnOnCurrentTv() {
- if (activeTvIndex === null) return;
- try {
- showLoading(true);
- const response = await fetch('/api/mitv/turn_on', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ kodi_id: activeTvIndex })
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function turnOffCurrentTv() {
- if (activeTvIndex === null) return;
- try {
- showLoading(true);
- const response = await fetch('/api/mitv/turn_off', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ kodi_id: activeTvIndex })
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function playImageCurrentTv() {
- if (activeTvIndex === null) return;
- const fileInput = document.getElementById('modalImageFile');
- const urlInput = document.getElementById('modalImageUrl').value.trim();
- if ((!fileInput.files || fileInput.files.length === 0) && !urlInput) {
- showMessage('请选择图片文件或输入图片URL', 'error');
- return;
- }
- try {
- showLoading(true);
- let response;
- if (fileInput.files && fileInput.files.length > 0) {
- const formData = new FormData();
- formData.append('file', fileInput.files[0]);
- formData.append('kodi_client_index', activeTvIndex);
- response = await fetch('/api/kodi/play_image', {
- method: 'POST',
- body: formData
- });
- } else {
- response = await fetch('/api/kodi/play_image', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- image_url: urlInput,
- kodi_client_index: activeTvIndex
- })
- });
- }
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function playRtspCurrentTv() {
- if (activeTvIndex === null) return;
- const rtspUrl = document.getElementById('modalRtspUrl').value.trim();
- const volume = parseInt(document.getElementById('modalRtspVolume').value);
- if (!rtspUrl) {
- showMessage('请输入RTSP视频流URL', 'error');
- return;
- }
- if (isNaN(volume) || volume < 0 || volume > 100) {
- showMessage('音量必须是0-100之间的整数', 'error');
- return;
- }
- try {
- showLoading(true);
- const response = await fetch('/api/kodi/play_rtsp', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- rtsp_url: rtspUrl,
- kodi_client_index: activeTvIndex,
- volume: volume
- })
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function turnOnAllTvs() {
- if (!confirm('确定要唤醒所有电视吗?')) return;
- try {
- showLoading(true);
- const response = await fetch('/api/mitv/turn_on_all', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function turnOffAllTvs() {
- if (!confirm('确定要息屏所有电视吗?')) return;
- try {
- showLoading(true);
- const response = await fetch('/api/mitv/turn_off_all', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- });
- const result = await response.json();
- if (result.success) {
- showMessage(result.message);
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- async function getFreeTimePlayStatus() {
- const statusSpan = document.getElementById('freeTimeStatusText');
- const btnEnable = document.getElementById('btnEnableFreeTime');
- const btnDisable = document.getElementById('btnDisableFreeTime');
-
- if (!statusSpan) return;
- try {
- const response = await fetch('/api/kodi/free_time/status');
- const result = await response.json();
-
- if (result.success) {
- const isEnabled = result.data.enabled;
- if (isEnabled) {
- statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
- btnEnable.style.display = 'none';
- btnDisable.style.display = 'inline-block';
- } else {
- statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
- btnEnable.style.display = 'inline-block';
- btnDisable.style.display = 'none';
- }
- } else {
- statusSpan.textContent = '获取失败';
- }
- } catch (error) {
- console.error(error);
- statusSpan.textContent = '网络错误';
- }
- }
- async function controlFreeTimePlay(action) {
- try {
- showLoading(true);
- const response = await fetch('/api/kodi/free_time/control', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action: action })
- });
- const result = await response.json();
-
- if (result.success) {
- showMessage(result.message);
- // 更新状态
- if (result.data) {
- const isEnabled = result.data.enabled;
- const statusSpan = document.getElementById('freeTimeStatusText');
- const btnEnable = document.getElementById('btnEnableFreeTime');
- const btnDisable = document.getElementById('btnDisableFreeTime');
-
- if (isEnabled) {
- statusSpan.innerHTML = '<span style="color: #2ecc71;">✅ 已开启 (07:30-18:00 自动播放)</span>';
- btnEnable.style.display = 'none';
- btnDisable.style.display = 'inline-block';
- } else {
- statusSpan.innerHTML = '<span style="color: #95a5a6;">⛔ 已关闭</span>';
- btnEnable.style.display = 'inline-block';
- btnDisable.style.display = 'none';
- }
- } else {
- getFreeTimePlayStatus();
- }
- } else {
- showMessage(result.message, 'error');
- }
- } catch (error) {
- showMessage('网络错误: ' + error.message, 'error');
- } finally {
- showLoading(false);
- }
- }
- </script>
- {% endblock %}
|