AI 生成: 一个生成 M3U 播放列表文件的项目,包含多个可订阅的 M3U 文件和一个简单的 Flask Web 界面用于预览和下载。

This commit is contained in:
admin
2026-09-29 05:43:20 +08:00
commit 26021ab21e
9 changed files with 363 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# M3U Playlist Generator
一个简单实用的 M3U 播放列表生成器,提供多个可订阅的 M3U 文件,并附带一个基于 Flask 的 Web 界面用于浏览和下载。
## 功能特性
- 提供多个分类的 M3U 播放列表文件(新闻、音乐、电影、体育等)
- 支持通过 HTTP 订阅播放列表
- 提供 Flask Web 界面,方便浏览和下载
- 使用相对路径,便于部署
## 目录结构
```
m3u-playlist-generator/
├── README.md
├── app.py # Flask 应用入口
├── requirements.txt # Python 依赖
├── playlists/ # M3U 播放列表文件目录
│ ├── news.m3u
│ ├── music.m3u
│ ├── movies.m3u
│ └── sports.m3u
├── templates/ # HTML 模板
│ └── index.html
└── static/
└── style.css
```
## 快速开始
### 1. 安装依赖
```bash
pip install -r requirements.txt
```
### 2. 启动服务
```bash
python app.py
```
### 3. 访问
打开浏览器访问 http://127.0.0.1:5000
### 4. 订阅播放列表
在播放器(如 VLC、PotPlayer、Kodi 等)中添加以下订阅地址:
- http://127.0.0.1:5000/playlists/news.m3u
- http://127.0.0.1:5000/playlists/music.m3u
- http://127.0.0.1:5000/playlists/movies.m3u
- http://127.0.0.1:5000/playlists/sports.m3u
## 自定义
直接编辑 `playlists/` 目录下的 `.m3u` 文件即可添加或修改频道。
M3U 文件格式示例:
```
#EXTM3U
#EXTINF:-1 tvg-id="channel1" tvg-logo="https://example.com/logo.png" group-title="News",Channel 1
https://example.com/stream1.m3u8
```
## 许可证
MIT
+64
View File
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
"""
M3U Playlist Generator - Flask 应用入口
提供 M3U 播放列表的浏览、下载和订阅服务。
"""
import os
from flask import Flask, render_template, send_from_directory, abort
app = Flask(__name__)
# 播放列表目录(相对路径)
PLAYLIST_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'playlists')
@app.route('/')
def index():
"""首页:列出所有可用的播放列表。"""
playlists = []
if os.path.isdir(PLAYLIST_DIR):
for filename in sorted(os.listdir(PLAYLIST_DIR)):
if filename.endswith('.m3u'):
filepath = os.path.join(PLAYLIST_DIR, filename)
size = os.path.getsize(filepath)
# 统计频道数量(以 #EXTINF 开头的行)
channel_count = 0
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
if line.strip().startswith('#EXTINF'):
channel_count += 1
except Exception:
pass
playlists.append({
'name': filename,
'size': size,
'channels': channel_count,
'url': f'playlists/{filename}'
})
return render_template('index.html', playlists=playlists)
@app.route('/playlists/<path:filename>')
def serve_playlist(filename):
"""提供播放列表文件下载/订阅。"""
# 安全检查:防止路径穿越
if '..' in filename or filename.startswith('/'):
abort(400)
if not filename.endswith('.m3u'):
abort(404)
filepath = os.path.join(PLAYLIST_DIR, filename)
if not os.path.isfile(filepath):
abort(404)
return send_from_directory(
PLAYLIST_DIR,
filename,
mimetype='audio/x-mpegurl',
as_attachment=False
)
if __name__ == '__main__':
# 监听所有网卡,方便局域网内其他设备订阅
app.run(host='0.0.0.0', port=5000, debug=True)
+11
View File
@@ -0,0 +1,11 @@
#EXTM3U
#EXTINF:-1 tvg-id="hbo" tvg-logo="https://example.com/hbo.png" group-title="电影",HBO
https://example.com/streams/hbo.m3u8
#EXTINF:-1 tvg-id="cinemax" tvg-logo="https://example.com/cinemax.png" group-title="电影",Cinemax
https://example.com/streams/cinemax.m3u8
#EXTINF:-1 tvg-id="starz" tvg-logo="https://example.com/starz.png" group-title="电影",Starz
https://example.com/streams/starz.m3u8
#EXTINF:-1 tvg-id="showtime" tvg-logo="https://example.com/showtime.png" group-title="电影",Showtime
https://example.com/streams/showtime.m3u8
#EXTINF:-1 tvg-id="tcm" tvg-logo="https://example.com/tcm.png" group-title="电影",Turner Classic Movies
https://example.com/streams/tcm.m3u8
+11
View File
@@ -0,0 +1,11 @@
#EXTM3U
#EXTINF:-1 tvg-id="mtv" tvg-logo="https://example.com/mtv.png" group-title="音乐",MTV Live
https://example.com/streams/mtv.m3u8
#EXTINF:-1 tvg-id="vh1" tvg-logo="https://example.com/vh1.png" group-title="音乐",VH1
https://example.com/streams/vh1.m3u8
#EXTINF:-1 tvg-id="billboard" tvg-logo="https://example.com/billboard.png" group-title="音乐",Billboard Music
https://example.com/streams/billboard.m3u8
#EXTINF:-1 tvg-id="kiss-fm" tvg-logo="https://example.com/kiss.png" group-title="音乐",Kiss FM
https://example.com/streams/kiss-fm.m3u8
#EXTINF:-1 tvg-id="classic-fm" tvg-logo="https://example.com/classic.png" group-title="音乐",Classic FM
https://example.com/streams/classic-fm.m3u8
+11
View File
@@ -0,0 +1,11 @@
#EXTM3U
#EXTINF:-1 tvg-id="cctv1" tvg-logo="https://example.com/cctv1.png" group-title="新闻",CCTV-1 综合
https://example.com/streams/cctv1.m3u8
#EXTINF:-1 tvg-id="cctv13" tvg-logo="https://example.com/cctv13.png" group-title="新闻",CCTV-13 新闻
https://example.com/streams/cctv13.m3u8
#EXTINF:-1 tvg-id="bbc-world" tvg-logo="https://example.com/bbc.png" group-title="新闻",BBC World News
https://example.com/streams/bbc-world.m3u8
#EXTINF:-1 tvg-id="cnn" tvg-logo="https://example.com/cnn.png" group-title="新闻",CNN International
https://example.com/streams/cnn.m3u8
#EXTINF:-1 tvg-id="aljazeera" tvg-logo="https://example.com/aljazeera.png" group-title="新闻",Al Jazeera English
https://example.com/streams/aljazeera.m3u8
+11
View File
@@ -0,0 +1,11 @@
#EXTM3U
#EXTINF:-1 tvg-id="espn" tvg-logo="https://example.com/espn.png" group-title="体育",ESPN
https://example.com/streams/espn.m3u8
#EXTINF:-1 tvg-id="fox-sports" tvg-logo="https://example.com/foxsports.png" group-title="体育",Fox Sports
https://example.com/streams/fox-sports.m3u8
#EXTINF:-1 tvg-id="sky-sports" tvg-logo="https://example.com/skysports.png" group-title="体育",Sky Sports
https://example.com/streams/sky-sports.m3u8
#EXTINF:-1 tvg-id="nba-tv" tvg-logo="https://example.com/nba.png" group-title="体育",NBA TV
https://example.com/streams/nba-tv.m3u8
#EXTINF:-1 tvg-id="eurosport" tvg-logo="https://example.com/eurosport.png" group-title="体育",Eurosport
https://example.com/streams/eurosport.m3u8
+1
View File
@@ -0,0 +1 @@
Flask==3.0.0
+139
View File
@@ -0,0 +1,139 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f5f7fa;
color: #2c3e50;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 40px 20px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
header h1 {
font-size: 2.2rem;
margin-bottom: 8px;
}
header p {
opacity: 0.9;
}
main {
flex: 1;
max-width: 1100px;
width: 100%;
margin: 0 auto;
padding: 40px 20px;
}
.playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
.playlist-card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
}
.playlist-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.playlist-card h2 {
font-size: 1.2rem;
margin-bottom: 12px;
color: #667eea;
word-break: break-all;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #7f8c8d;
margin-bottom: 16px;
}
.actions {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.btn {
flex: 1;
text-align: center;
padding: 10px 12px;
border-radius: 8px;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
transition: background 0.2s;
}
.btn-primary {
background: #667eea;
color: #fff;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #eef0f5;
color: #2c3e50;
}
.btn-secondary:hover {
background: #dfe3ec;
}
.subscribe-hint {
font-size: 0.75rem;
color: #95a5a6;
word-break: break-all;
}
.subscribe-hint code {
background: #f0f2f7;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.72rem;
}
.empty {
text-align: center;
color: #95a5a6;
padding: 60px 20px;
font-size: 1.1rem;
}
footer {
text-align: center;
padding: 20px;
color: #95a5a6;
font-size: 0.85rem;
border-top: 1px solid #e8ecf1;
}
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M3U 播放列表</title>
<link rel="stylesheet" href="static/style.css">
</head>
<body>
<header>
<h1>🎵 M3U 播放列表</h1>
<p>选择下方任意播放列表进行订阅或下载</p>
</header>
<main>
{% if playlists %}
<div class="playlist-grid">
{% for pl in playlists %}
<div class="playlist-card">
<h2>{{ pl.name }}</h2>
<p class="meta">
<span>频道数:{{ pl.channels }}</span>
<span>大小:{{ pl.size }} 字节</span>
</p>
<div class="actions">
<a class="btn btn-primary" href="{{ pl.url }}" target="_blank">查看 / 订阅</a>
<a class="btn btn-secondary" href="{{ pl.url }}" download>下载</a>
</div>
<p class="subscribe-hint">
订阅地址:<code>{{ request.host_url }}{{ pl.url }}</code>
</p>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty">暂无播放列表,请在 playlists/ 目录下添加 .m3u 文件。</p>
{% endif %}
</main>
<footer>
<p>M3U Playlist Generator &copy; 2024</p>
</footer>
</body>
</html>