commit 26021ab21e5771667fc0b66e651f376f645e94c2 Author: admin Date: Tue Sep 29 05:43:20 2026 +0800 AI 生成: 一个生成 M3U 播放列表文件的项目,包含多个可订阅的 M3U 文件和一个简单的 Flask Web 界面用于预览和下载。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..026e2da --- /dev/null +++ b/README.md @@ -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 diff --git a/app.py b/app.py new file mode 100644 index 0000000..1a21d67 --- /dev/null +++ b/app.py @@ -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/') +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) diff --git a/playlists/movies.m3u b/playlists/movies.m3u new file mode 100644 index 0000000..9438a85 --- /dev/null +++ b/playlists/movies.m3u @@ -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 diff --git a/playlists/music.m3u b/playlists/music.m3u new file mode 100644 index 0000000..7c074dc --- /dev/null +++ b/playlists/music.m3u @@ -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 diff --git a/playlists/news.m3u b/playlists/news.m3u new file mode 100644 index 0000000..9e2e960 --- /dev/null +++ b/playlists/news.m3u @@ -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 diff --git a/playlists/sports.m3u b/playlists/sports.m3u new file mode 100644 index 0000000..c6f657f --- /dev/null +++ b/playlists/sports.m3u @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..047e950 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask==3.0.0 diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..424e7ee --- /dev/null +++ b/static/style.css @@ -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; +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ee41cea --- /dev/null +++ b/templates/index.html @@ -0,0 +1,44 @@ + + + + + + M3U 播放列表 + + + +
+

🎵 M3U 播放列表

+

选择下方任意播放列表进行订阅或下载

+
+ +
+ {% if playlists %} +
+ {% for pl in playlists %} +
+

{{ pl.name }}

+

+ 频道数:{{ pl.channels }} + 大小:{{ pl.size }} 字节 +

+ + +
+ {% endfor %} +
+ {% else %} +

暂无播放列表,请在 playlists/ 目录下添加 .m3u 文件。

+ {% endif %} +
+ + + +