generated from dellevin/template
feat: complete local version to overwrite remote
This commit is contained in:
687
99demo/pin-img/pin-tu.py
Normal file
687
99demo/pin-img/pin-tu.py
Normal file
@@ -0,0 +1,687 @@
|
||||
# app.py
|
||||
from flask import Flask, render_template_string, send_from_directory, request
|
||||
import os
|
||||
import re
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
# --- 配置 ---
|
||||
# 请在这里修改为你存放图片的文件夹路径
|
||||
ROOT_IMAGE_FOLDER = r'\\192.168.31.92\ubuntuShare2T\学习资料' # 使用原始字符串以避免转义问题
|
||||
PORT = 80 # 你可以修改这个端口号,如果5000被占用了
|
||||
# --- 配置结束 ---
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def atoi(text):
|
||||
"""辅助函数:将文本中的数字部分转换为整数,用于排序"""
|
||||
return int(text) if text.isdigit() else text
|
||||
|
||||
|
||||
def natural_keys(text):
|
||||
"""
|
||||
辅助函数:生成用于自然排序的键。
|
||||
例如: '1002' -> ['1002'] (其中 '1002' 被转换为整数)
|
||||
'img_10' -> ['img_', 10]
|
||||
这样可以确保 '1001.jpg' 排在 '1002.png' 之前。
|
||||
"""
|
||||
return [atoi(c) for c in re.split(r'(\d+)', text)]
|
||||
|
||||
|
||||
def find_media_in_folder(folder_path):
|
||||
"""在指定文件夹中查找所有图片和视频文件"""
|
||||
allowed_image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
|
||||
allowed_video_exts = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mkv', '.mpg', '.mpeg', '.3gp',
|
||||
'.3g2'}
|
||||
|
||||
try:
|
||||
files = os.listdir(folder_path)
|
||||
except PermissionError:
|
||||
print(f"权限不足,无法访问: {folder_path}")
|
||||
return [], []
|
||||
|
||||
image_files = [f for f in files if os.path.isfile(os.path.join(folder_path, f)) and os.path.splitext(f)[
|
||||
1].lower() in allowed_image_exts]
|
||||
video_files = [f for f in files if os.path.isfile(os.path.join(folder_path, f)) and os.path.splitext(f)[
|
||||
1].lower() in allowed_video_exts]
|
||||
|
||||
return sorted(image_files, key=natural_keys), sorted(video_files, key=natural_keys)
|
||||
|
||||
|
||||
def get_subfolders(folder_path):
|
||||
"""获取指定文件夹下的所有子文件夹"""
|
||||
try:
|
||||
items = os.listdir(folder_path)
|
||||
except PermissionError:
|
||||
print(f"权限不足,无法访问: {folder_path}")
|
||||
return []
|
||||
subfolders = [item for item in items if os.path.isdir(os.path.join(folder_path, item))]
|
||||
return sorted(subfolders, key=natural_keys)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
# 默认访问根目录,subpath为空字符串
|
||||
return list_folder_content('')
|
||||
|
||||
|
||||
@app.route('/browse/')
|
||||
@app.route('/browse/<path:subpath>')
|
||||
def list_folder_content(subpath=''):
|
||||
"""
|
||||
递归浏览文件夹内容的主函数
|
||||
subpath: URL中传递的子路径
|
||||
"""
|
||||
# 从查询参数获取显示模式,默认为 'list'
|
||||
mode = request.args.get('mode', 'list')
|
||||
if mode not in ['list', 'grid', 'manga']:
|
||||
mode = 'list'
|
||||
|
||||
# 构建当前要浏览的物理路径
|
||||
# 注意:这里使用全局变量 ROOT_IMAGE_FOLDER
|
||||
current_path = os.path.normpath(os.path.join(ROOT_IMAGE_FOLDER, subpath))
|
||||
|
||||
# 检测是否为漫画路径
|
||||
is_manga_path = '漫画' in subpath
|
||||
|
||||
# 安全检查,防止路径穿越
|
||||
if not current_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
|
||||
return "Access Denied", 403
|
||||
|
||||
# 查找当前文件夹下的媒体文件和子文件夹
|
||||
image_files, video_files = find_media_in_folder(current_path)
|
||||
subfolders = get_subfolders(current_path)
|
||||
|
||||
# 构建面包屑导航
|
||||
path_parts = [p for p in subpath.split('/') if p]
|
||||
breadcrumbs = [{'name': 'Home', 'url': '/'}]
|
||||
cumulative_path = ''
|
||||
for part in path_parts:
|
||||
cumulative_path += part + '/'
|
||||
breadcrumbs.append({'name': part, 'url': f'/browse/{cumulative_path.rstrip("/")}'})
|
||||
|
||||
html_template = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>文件浏览器 - {{ current_path_name }}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
/* 顶部地址栏 - Windows风格 */
|
||||
.address-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 8px 16px;
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.address-bar a, .address-bar span.current {
|
||||
padding: 4px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
.address-bar a:hover {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
.address-bar span.current {
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
}
|
||||
.address-bar .sep {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
padding: 0 2px;
|
||||
user-select: none;
|
||||
}
|
||||
.address-bar .home-btn {
|
||||
font-size: 16px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
gap: 12px;
|
||||
}
|
||||
.mode-btn {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid #ccc;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.mode-btn:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.mode-btn.active {
|
||||
background: #0078d4;
|
||||
color: #fff;
|
||||
border-color: #0078d4;
|
||||
}
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
padding: 16px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
/* 网格模式 */
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.grid-item {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-item:hover {
|
||||
border-color: #0078d4;
|
||||
background: #f0f8ff;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.grid-item .icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1;
|
||||
}
|
||||
.grid-item img.thumb {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.grid-item .name {
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
max-height: 2.8em;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
/* 列表模式 */
|
||||
.list-container {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.list-item:last-child { border-bottom: none; }
|
||||
.list-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.list-item .icon {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
width: 28px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.list-item .thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
.list-item .info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.list-item .name {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.list-item .type {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
/* Modal / Lightbox */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.85);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.modal-content {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
.modal-content video {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
background: #000;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.modal-close:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
.modal-title {
|
||||
color: #fff;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
max-width: 80vw;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.img-trigger, .video-trigger {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 漫画模式 */
|
||||
.manga-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #1a1a1a;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.manga-item {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
line-height: 0;
|
||||
}
|
||||
.manga-item img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 地址栏 -->
|
||||
<div class="address-bar">
|
||||
<a href="/" class="home-btn" title="主页">🏠</a>
|
||||
{% for crumb in breadcrumbs %}
|
||||
{% if not loop.first %}
|
||||
<span class="sep">›</span>
|
||||
{% if loop.last %}
|
||||
<span class="current">{{ crumb.name }}</span>
|
||||
{% else %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.name }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<a href="?mode=list" class="mode-btn {% if mode == 'list' %}active{% endif %}">📋 列表</a>
|
||||
<a href="?mode=grid" class="mode-btn {% if mode == 'grid' %}active{% endif %}">⊞ 网格</a>
|
||||
{% if is_manga_path %}
|
||||
<a href="?mode=manga" class="mode-btn {% if mode == 'manga' %}active{% endif %}">📖 漫画</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 文件夹 -->
|
||||
{% if subfolders %}
|
||||
<div class="section">
|
||||
<div class="section-title"><span>📁</span> 文件夹 <span style="color:#999;font-weight:400;">({{ subfolders|length }})</span></div>
|
||||
{% if mode == 'grid' %}
|
||||
<div class="grid-container">
|
||||
{% for folder in subfolders %}
|
||||
<a href="{{ url_for('list_folder_content', subpath=(current_subpath + folder) if current_subpath else folder) }}?mode={{ mode }}" class="grid-item">
|
||||
<div class="icon">📁</div>
|
||||
<div class="name">{{ folder }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="list-container">
|
||||
{% for folder in subfolders %}
|
||||
<a href="{{ url_for('list_folder_content', subpath=(current_subpath + folder) if current_subpath else folder) }}?mode={{ mode }}" class="list-item">
|
||||
<div class="icon">📁</div>
|
||||
<div class="info">
|
||||
<div class="name">{{ folder }}</div>
|
||||
<div class="type">文件夹</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 图片 -->
|
||||
{% if images %}
|
||||
<div class="section">
|
||||
<div class="section-title"><span>📷</span> 图片 <span style="color:#999;font-weight:400;">({{ images|length }})</span></div>
|
||||
{% if mode == 'manga' %}
|
||||
<div class="manga-container">
|
||||
{% for image in images %}
|
||||
<div class="manga-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
|
||||
<img src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="{{ image }}" loading="lazy">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif mode == 'grid' %}
|
||||
<div class="grid-container">
|
||||
{% for image in images %}
|
||||
<div class="grid-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
|
||||
<img class="thumb" src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="{{ image }}" loading="lazy">
|
||||
<div class="name">{{ image }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="list-container">
|
||||
{% for image in images %}
|
||||
<div class="list-item img-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" data-name="{{ image }}">
|
||||
<img class="thumb" src="{{ url_for('serve_media', full_path=(current_subpath + image) if current_subpath else image) }}" alt="" loading="lazy">
|
||||
<div class="info">
|
||||
<div class="name">{{ image }}</div>
|
||||
<div class="type">图片文件</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 视频 -->
|
||||
{% if videos %}
|
||||
<div class="section">
|
||||
<div class="section-title"><span>🎬</span> 视频 <span style="color:#999;font-weight:400;">({{ videos|length }})</span></div>
|
||||
{% if mode == 'grid' %}
|
||||
<div class="grid-container">
|
||||
{% for video in videos %}
|
||||
<div class="grid-item video-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + video) if current_subpath else video) }}" data-name="{{ video }}">
|
||||
<div class="icon">🎬</div>
|
||||
<div class="name">{{ video }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="list-container">
|
||||
{% for video in videos %}
|
||||
<div class="list-item video-trigger" data-src="{{ url_for('serve_media', full_path=(current_subpath + video) if current_subpath else video) }}" data-name="{{ video }}">
|
||||
<div class="icon">🎬</div>
|
||||
<div class="info">
|
||||
<div class="name">{{ video }}</div>
|
||||
<div class="type">视频文件</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 图片/视频预览弹窗 -->
|
||||
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
|
||||
<div class="modal-content" onclick="event.stopPropagation()">
|
||||
<div class="modal-close" onclick="closeModal()">×</div>
|
||||
<img id="modalImg" style="display:none;" alt="">
|
||||
<video id="modalVideo" style="display:none;" controls playsinline></video>
|
||||
<div class="modal-title" id="modalTitle"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const overlay = document.getElementById('modalOverlay');
|
||||
const modalImg = document.getElementById('modalImg');
|
||||
const modalVideo = document.getElementById('modalVideo');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
|
||||
function openImage(src, name) {
|
||||
modalVideo.style.display = 'none';
|
||||
modalVideo.pause();
|
||||
modalVideo.src = '';
|
||||
modalImg.style.display = 'block';
|
||||
modalImg.src = src;
|
||||
modalTitle.textContent = name || '';
|
||||
overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function openVideo(src, name) {
|
||||
modalImg.style.display = 'none';
|
||||
modalImg.src = '';
|
||||
modalVideo.style.display = 'block';
|
||||
modalVideo.src = src;
|
||||
modalVideo.play();
|
||||
modalTitle.textContent = name || '';
|
||||
overlay.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeModal(e) {
|
||||
if (e && e.target !== overlay && !e.target.classList.contains('modal-close')) return;
|
||||
overlay.classList.remove('active');
|
||||
modalVideo.pause();
|
||||
modalVideo.src = '';
|
||||
modalImg.src = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.img-trigger').forEach(el => {
|
||||
el.addEventListener('click', () => openImage(el.dataset.src, el.dataset.name));
|
||||
});
|
||||
document.querySelectorAll('.video-trigger').forEach(el => {
|
||||
el.addEventListener('click', () => openVideo(el.dataset.src, el.dataset.name));
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
# 获取当前路径的名称用于显示
|
||||
current_path_name = os.path.basename(current_path) if current_path != ROOT_IMAGE_FOLDER else 'Root Directory'
|
||||
|
||||
return render_template_string(
|
||||
html_template,
|
||||
images=image_files,
|
||||
videos=video_files,
|
||||
subfolders=subfolders,
|
||||
current_path_name=current_path_name,
|
||||
current_subpath=subpath + '/' if subpath else '', # 传递给模板,用于构建文件路径
|
||||
breadcrumbs=breadcrumbs,
|
||||
mode=mode, # 将模式传递给模板
|
||||
is_manga_path=is_manga_path
|
||||
)
|
||||
|
||||
|
||||
# 新增:用于在新页面播放视频的路由
|
||||
@app.route('/play_video/<path:full_path>')
|
||||
def play_video(full_path):
|
||||
"""
|
||||
在新页面播放单个视频
|
||||
full_path: 视频文件的完整路径(包含子文件夹)
|
||||
"""
|
||||
# 安全检查
|
||||
full_file_path = os.path.normpath(os.path.join(ROOT_IMAGE_FOLDER, full_path))
|
||||
if not full_file_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
|
||||
return "Access Denied", 403
|
||||
|
||||
# 获取视频文件名
|
||||
video_filename = os.path.basename(full_path)
|
||||
|
||||
# 将 video_url 的生成移到模板内部
|
||||
video_player_template = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>正在播放: {{ video_filename }}</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #000; /* 黑色背景 */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh; /* 全屏高度 */
|
||||
}
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 90%; /* 最大宽度 */
|
||||
max-height: 90vh; /* 最大高度 */
|
||||
}
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain; /* 保持视频比例 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="video-container">
|
||||
<video class="video-player" controls autoplay playsinline preload="none">
|
||||
<!-- 在模板内部使用 url_for -->
|
||||
<source src="{{ url_for('serve_media', full_path=full_path) }}" type="video/{{ video_filename.split('.')[-1].lower() }}">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
# 注意:传递给 render_template_string 的变量,可以在模板内部直接使用
|
||||
return render_template_string(
|
||||
video_player_template,
|
||||
video_filename=video_filename,
|
||||
full_path=full_path # 将 full_path 也传递给模板,以便在模板内使用 url_for
|
||||
)
|
||||
|
||||
|
||||
# 统一的媒体文件服务路由,处理图片和视频
|
||||
@app.route('/media_file/<path:full_path>')
|
||||
def serve_media(full_path):
|
||||
"""
|
||||
提供媒体文件(图片/视频)服务
|
||||
full_path: 包含子文件夹路径和文件名
|
||||
"""
|
||||
# 从完整路径中分离出文件夹和文件名
|
||||
folder_path = os.path.dirname(full_path)
|
||||
filename = os.path.basename(full_path)
|
||||
|
||||
# 构建完整的物理路径
|
||||
base_dir = os.path.join(ROOT_IMAGE_FOLDER, folder_path)
|
||||
|
||||
# 安全检查
|
||||
full_file_path = os.path.normpath(os.path.join(base_dir, filename))
|
||||
if not full_file_path.startswith(os.path.normpath(ROOT_IMAGE_FOLDER)):
|
||||
return "Access Denied", 403
|
||||
|
||||
return send_from_directory(base_dir, filename)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 确保根图片文件夹存在
|
||||
if not os.path.exists(ROOT_IMAGE_FOLDER):
|
||||
print(f"错误:找不到根图片文件夹 '{ROOT_IMAGE_FOLDER}'")
|
||||
exit(1)
|
||||
|
||||
print(f"Web服务即将启动,请访问 http://127.0.0.1:{PORT}")
|
||||
print(f"根目录路径: {os.path.abspath(ROOT_IMAGE_FOLDER)}")
|
||||
app.run(debug=True, host='0.0.0.0', port=PORT)
|
||||
Reference in New Issue
Block a user