AI 生成: 一个生成 M3U 播放列表文件的项目,包含多个可订阅的 M3U 文件和一个简单的 Flask Web 界面用于预览和下载。
This commit is contained in:
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Flask==3.0.0
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 © 2024</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user