Initial commit: ap_ds Code Repository System v1.0.0

This commit is contained in:
dvs
2026-08-28 11:23:52 +08:00
commit 74a93c63f2
23 changed files with 5811 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.venv/
venv/
env/
# Repo data
data/
releases/
branches/
uploads/
feedback.json
*.db
*.sqlite3
# System files
Thumbs.db
.DS_Store
desktop.ini
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 DVS (dvs-dvsxt)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+79
View File
@@ -0,0 +1,79 @@
# 📚 ap_ds Code Repository System
> A self-hosted **code hosting & documentation platform** — code browsing, releases, version downloads, feedback, and admin management.
**ap_ds Code Repository System** is a Flask-based platform for hosting and showcasing code. It features code/file browsing with syntax highlighting, Markdown rendering, release management with version downloads (ZIP / TAR / WHL), search, user feedback, and a full admin dashboard.
---
## ✨ Features
| Feature | Description |
|---------|-------------|
| 📂 **Code Browser** | Browse repository files / directories |
| 👁️ **Code View** | Syntax highlighting (Pygments) |
| 📄 **Markdown View** | Render Markdown documents |
| 📦 **Release Downloads** | Download version archives (ZIP / TAR / WHL) |
| 🔖 **Releases Page** | List versions & release notes |
| 🔍 **Search** | Search across the repository |
| 💬 **Feedback** | Users submit feedback; admin manages it |
| 🛠️ **Admin Panel** | Login, dashboard, branches, version upload, feedback management |
---
## 🔌 Routes
| Route | Description |
|-------|-------------|
| `/` | Homepage |
| `/code` | Code browsing |
| `/browse/<dir_path>` | Browse a directory |
| `/view/<file_path>` | View a file (syntax highlight) |
| `/download/<file_path>` | Download a file |
| `/download/zip|tar|whl` | Download version archives |
| `/releases` | Releases page |
| `/search` | Search |
| `/feedback` | Submit feedback |
| `/admin/*` | Admin management |
---
## 🚀 Quick Start
### Prerequisites
- Python 3.8+
- `flask`, `markdown`, `pygments`, `markupsafe`, `werkzeug`
### Install & Run
```bash
pip install flask markdown pygments markupsafe werkzeug
python app.py
```
Then open the site in your browser.
---
## 📁 Project Structure
```
AP_DS_GIT_CODE/
├── app.py # Main Flask application
├── static/
│ ├── css.css # Stylesheet
│ └── fonts/ # Icon fonts
├── templates/ # 15 HTML templates
│ ├── index.html # Homepage
│ ├── code_browser.html # Code browser
│ ├── admin_*.html # Admin pages
│ └── ...
└── README.md # This document
```
---
## 📄 License
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
+824
View File
@@ -0,0 +1,824 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ap_ds 代码仓库系统
完整的代码托管与文档展示平台
"""
import os
import json
import shutil
import zipfile
import tarfile
import hashlib
import socket
import mimetypes
import threading
import subprocess
from datetime import datetime
from pathlib import Path
from functools import wraps
from flask import (
Flask, render_template, send_file, request,
redirect, url_for, session, jsonify, abort
)
from markupsafe import Markup
from werkzeug.utils import secure_filename
import markdown
from pygments import highlight
from pygments.lexers import get_lexer_by_name, guess_lexer
from pygments.formatters import HtmlFormatter
# ============================================
# 应用初始化
# ============================================
app = Flask(__name__)
app.secret_key = "ap_ds_code_repo_secret_key_2026"
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB
# 目录配置
BASE_DIR = Path(__file__).parent
CODE_DIR = BASE_DIR / "code"
PYPI_DIR = BASE_DIR / "pypi"
STATIC_DIR = BASE_DIR / "static"
TEMPLATES_DIR = BASE_DIR / "templates"
DATA_DIR = BASE_DIR / "data"
UPLOAD_DIR = BASE_DIR / "uploads"
# 创建必要目录
for d in [CODE_DIR, PYPI_DIR, DATA_DIR, UPLOAD_DIR]:
d.mkdir(exist_ok=True)
# 管理员密码
ADMIN_PASSWORD_HASH = ""
def get_version_announcement(branch, version):
"""获取版本的公告内容"""
ann_path = CODE_DIR / branch / version / "ANNCMNT.md"
if ann_path.exists():
with open(ann_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
return render_markdown(content)
return None
def get_version_readme(branch, version):
"""获取版本的 README 内容"""
readme_path = CODE_DIR / branch / version / "README.md"
if readme_path.exists():
with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
return render_markdown(content)
return None
# ============================================
# 数据存储辅助函数
# ============================================
def load_json(filepath, default=None):
"""加载JSON文件"""
if default is None:
default = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return default
def save_json(filepath, data):
"""保存JSON文件"""
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load_feedbacks():
"""加载反馈数据"""
return load_json(DATA_DIR / "feedbacks.json", [])
def save_feedbacks(feedbacks):
"""保存反馈数据"""
save_json(DATA_DIR / "feedbacks.json", feedbacks)
def load_branches():
"""加载分支列表"""
branches = load_json(DATA_DIR / "branches.json", {})
if not branches:
# 扫描现有目录初始化
for branch_dir in CODE_DIR.iterdir():
if branch_dir.is_dir():
versions = []
for version_dir in branch_dir.iterdir():
if version_dir.is_dir():
versions.append(version_dir.name)
branches[branch_dir.name] = sorted(versions, reverse=True)
save_json(DATA_DIR / "branches.json", branches)
return branches
def save_branches(branches):
"""保存分支列表"""
save_json(DATA_DIR / "branches.json", branches)
# ============================================
# 邮件发送函数
# ============================================
def send_feedback_email(user_email, feedback_content):
print(f"The original email sending function contains sensitive content. If you need it, please figure it out yourself.")
# ============================================
# 文件操作辅助函数
# ============================================
def get_branches_and_versions():
"""获取所有分支和版本"""
branches_data = {}
for branch_dir in CODE_DIR.iterdir():
if branch_dir.is_dir():
versions = []
for version_dir in sorted(branch_dir.iterdir(), reverse=True):
if version_dir.is_dir():
versions.append({
"name": version_dir.name,
"path": str(version_dir.relative_to(CODE_DIR)).replace('\\', '/'),
"created": datetime.fromtimestamp(version_dir.stat().st_ctime).strftime("%Y-%m-%d")
})
if versions:
branches_data[branch_dir.name] = versions
return branches_data
def get_file_tree(branch, version):
"""获取文件树结构"""
version_path = CODE_DIR / branch / version
if not version_path.exists():
return []
tree = []
for item in sorted(version_path.iterdir()):
# 使用正斜杠统一路径分隔符
path_str = str(item.relative_to(CODE_DIR)).replace('\\', '/')
tree.append({
"name": item.name,
"type": "dir" if item.is_dir() else "file",
"path": path_str,
"size": item.stat().st_size if item.is_file() else 0,
"modified": datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
})
return tree
def get_file_content(file_path):
"""获取文件内容"""
full_path = CODE_DIR / file_path
if not full_path.exists() or full_path.is_dir():
return None
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
return f.read()
def get_file_metadata(file_path):
"""获取文件元数据"""
full_path = CODE_DIR / file_path
if not full_path.exists():
return None
# 使用正斜杠统一路径分隔符
path_str = str(full_path.relative_to(CODE_DIR)).replace('\\', '/')
return {
"name": full_path.name,
"path": path_str,
"size": full_path.stat().st_size,
"modified": datetime.fromtimestamp(full_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"extension": full_path.suffix.lower()
}
def render_markdown(content):
"""渲染Markdown为HTML"""
extensions = ['extra', 'codehilite', 'toc', 'nl2br']
return markdown.markdown(content, extensions=extensions)
def syntax_highlight(code, language=None):
"""语法高亮"""
if not language:
try:
lexer = guess_lexer(code)
except:
lexer = get_lexer_by_name("text")
else:
try:
lexer = get_lexer_by_name(language)
except:
lexer = get_lexer_by_name("text")
formatter = HtmlFormatter(style="monokai", linenos=True)
return highlight(code, lexer, formatter)
def create_zip_archive(branch, version):
"""创建ZIP压缩包"""
source_dir = CODE_DIR / branch / version
if not source_dir.exists():
return None
zip_name = f"{branch}_{version}.zip"
zip_path = UPLOAD_DIR / zip_name
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for file_path in source_dir.rglob("*"):
if file_path.is_file():
arcname = file_path.relative_to(source_dir)
zf.write(file_path, arcname)
return zip_path
def create_tar_archive(branch, version):
"""创建TAR.GZ压缩包"""
source_dir = CODE_DIR / branch / version
if not source_dir.exists():
return None
tar_name = f"{branch}_{version}.tar.gz"
tar_path = UPLOAD_DIR / tar_name
with tarfile.open(tar_path, 'w:gz') as tf:
tf.add(source_dir, arcname=version)
return tar_path
def find_whl_file(version):
"""查找对应版本的whl文件"""
for whl_file in PYPI_DIR.glob(f"*{version}*.whl"):
return whl_file
return None
# ============================================
# 管理员认证装饰器
# ============================================
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not session.get('admin_logged_in'):
return redirect(url_for('admin_login'))
return f(*args, **kwargs)
return decorated_function
# ============================================
# 路由 - 首页
# ============================================
@app.route('/')
def index():
"""首页"""
branches_data = get_branches_and_versions()
# 获取最新发行版
releases = []
for branch, versions in branches_data.items():
for version in versions[:1]: # 每个分支最新版本
whl_file = find_whl_file(version['name'])
releases.append({
"branch": branch,
"version": version['name'],
"date": version['created'],
"has_whl": whl_file is not None
})
# 获取公告内容
announcement = None
for branch, versions in branches_data.items():
for version in versions:
ann_path = CODE_DIR / branch / version['name'] / "ANNCMNT.md"
if ann_path.exists():
with open(ann_path, 'r', encoding='utf-8', errors='ignore') as f:
announcement = render_markdown(f.read())
break
if announcement:
break
return render_template('index.html',
branches=branches_data,
releases=releases[:5],
announcement=announcement)
# ============================================
# 路由 - 代码浏览器
# ============================================
@app.route('/code')
def code_browser():
"""代码浏览器主页"""
branch = request.args.get('branch', '')
version = request.args.get('version', '')
branches_data = get_branches_and_versions()
# 如果没有指定分支,使用第一个
if not branch and branches_data:
branch = list(branches_data.keys())[0]
if branches_data[branch]:
version = branches_data[branch][0]['name']
file_tree = get_file_tree(branch, version) if branch and version else []
return render_template('code_browser.html',
branches=branches_data,
current_branch=branch,
current_version=version,
file_tree=file_tree)
# ============================================
# 路由 - 目录浏览
# ============================================
@app.route('/browse/<path:dir_path>')
def browse_directory(dir_path):
"""浏览子目录"""
branch = request.args.get('branch', '')
version = request.args.get('version', '')
full_dir = CODE_DIR / dir_path
if not full_dir.exists() or not full_dir.is_dir():
abort(404)
files = []
for item in sorted(full_dir.iterdir()):
files.append({
"name": item.name,
"type": "dir" if item.is_dir() else "file",
"path": str(item.relative_to(CODE_DIR)).replace('\\', '/'),
"size": item.stat().st_size if item.is_file() else 0,
"modified": datetime.fromtimestamp(item.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
})
# 面包屑导航
breadcrumbs = []
parts = Path(dir_path).parts
current = ""
for part in parts:
current = str(Path(current) / part) if current else part
breadcrumbs.append({"name": part, "path": current.replace('\\', '/')})
return render_template('directory.html',
files=files,
current_path=dir_path,
breadcrumbs=breadcrumbs,
branch=branch,
version=version)
@app.route('/view/<path:file_path>')
def view_file(file_path):
"""查看文件内容"""
branch = request.args.get('branch', '')
version = request.args.get('version', '')
# 将反斜杠替换为正斜杠
file_path = file_path.replace('\\', '/')
# 构建完整路径
if branch and version:
# 如果 file_path 已经包含分支,直接使用
if file_path.startswith(f"{branch}/"):
full_path = CODE_DIR / file_path
else:
# 否则拼接分支和版本
full_path = CODE_DIR / branch / version / file_path
else:
full_path = CODE_DIR / file_path
if not full_path.exists() or full_path.is_dir():
abort(404)
metadata = get_file_metadata(str(full_path.relative_to(CODE_DIR)))
extension = full_path.suffix.lower()
# Python文件 - 语法高亮
if extension == '.py':
content = get_file_content(str(full_path.relative_to(CODE_DIR)))
highlighted = syntax_highlight(content, 'python')
return render_template('view_code.html',
metadata=metadata,
content=content,
highlighted_code=highlighted,
language='python',
branch=branch,
version=version)
# Markdown文件 - 渲染
elif extension in ['.md', '.markdown']:
content = get_file_content(str(full_path.relative_to(CODE_DIR)))
html_content = render_markdown(content)
return render_template('view_markdown.html',
metadata=metadata,
html_content=Markup(html_content),
branch=branch,
version=version)
# 文本文件
elif extension in ['.txt', '.json', '.xml', '.yaml', '.yml', '.conf', '.cfg', '.ini']:
content = get_file_content(str(full_path.relative_to(CODE_DIR)))
return render_template('view_file.html',
metadata=metadata,
content=content,
is_text=True,
branch=branch,
version=version)
# 其他文件 - 显示下载选项
else:
return render_template('view_file.html',
metadata=metadata,
is_text=False,
branch=branch,
version=version)
# ============================================
# 路由 - 下载文件
# ============================================
@app.route('/download/<path:file_path>')
def download_file(file_path):
"""下载单个文件"""
file_path = file_path.replace('\\', '/')
full_path = CODE_DIR / file_path
if not full_path.exists() or full_path.is_dir():
abort(404)
return send_file(full_path, as_attachment=True)
# ============================================
# 路由 - 下载压缩包
# ============================================
@app.route('/download/zip/<branch>/<version>')
def download_zip(branch, version):
"""下载ZIP压缩包"""
zip_path = create_zip_archive(branch, version)
if not zip_path:
abort(404)
return send_file(zip_path, as_attachment=True, download_name=f"{branch}_{version}.zip")
@app.route('/download/tar/<branch>/<version>')
def download_tar(branch, version):
"""下载TAR.GZ压缩包"""
tar_path = create_tar_archive(branch, version)
if not tar_path:
abort(404)
return send_file(tar_path, as_attachment=True, download_name=f"{branch}_{version}.tar.gz")
@app.route('/download/whl/<version>')
def download_whl(version):
"""下载whl文件 - 支持带v和不带v的版本号"""
# 去除版本号开头的 'v' 如果存在
clean_version = version.lstrip('v')
# 先尝试用原始版本号查找
whl_file = find_whl_file(version)
# 如果没找到,尝试用去掉v的版本号查找
if not whl_file and clean_version != version:
whl_file = find_whl_file(clean_version)
# 如果还没找到,尝试模糊匹配(直接搜索包含版本号的whl文件)
if not whl_file:
for whl in PYPI_DIR.glob(f"*{clean_version}*.whl"):
whl_file = whl
break
if not whl_file or not whl_file.exists():
abort(404)
return send_file(whl_file, as_attachment=True, download_name=whl_file.name)
@app.route('/api/version-info')
def api_version_info():
"""获取版本信息"""
return jsonify({
"version": "v3.0.0 LTS",
"title": "轻量级 Python 音频革命",
"subtitle": "2.5MB 终结 160MB 臃肿时代",
"size": "2.5MB",
"description": "极轻量级 Python 音频库,完整元数据解析,非阻塞播放。首个长期支持版本,永久免费技术支持。"
})
@app.route('/api/latest-announcement')
def api_latest_announcement():
"""获取最新公告(ANNCMNT.md)"""
# 查找最新的 ANNCMNT.md
branches_data = get_branches_and_versions()
announcement_html = None
for branch, versions in branches_data.items():
for version in versions[:1]:
ann_path = CODE_DIR / branch / version['name'] / "ANNCMNT.md"
if ann_path.exists():
with open(ann_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
announcement_html = render_markdown(content)
break
if announcement_html:
break
return jsonify({"html": announcement_html})
# ============================================
# 路由 - 发行版页面
# ============================================
@app.route('/releases')
def releases():
"""发行版列表页"""
branches_data = get_branches_and_versions()
all_releases = []
for branch, versions in branches_data.items():
for version in versions:
whl_file = find_whl_file(version['name'])
# 获取该版本的公告内容
announcement = get_version_announcement(branch, version['name'])
all_releases.append({
"branch": branch,
"version": version['name'],
"date": version['created'],
"has_whl": whl_file is not None,
"announcement": announcement
})
# 按版本号排序(降序)
all_releases.sort(key=lambda x: x['version'], reverse=True)
return render_template('releases.html', releases=all_releases)
# ============================================
# 路由 - 搜索
# ============================================
@app.route('/search')
def search():
"""全目录搜索"""
query = request.args.get('q', '').strip()
branch = request.args.get('branch', '')
version = request.args.get('version', '')
results = []
if query and branch and version:
search_dir = CODE_DIR / branch / version
if search_dir.exists():
for file_path in search_dir.rglob("*"):
if file_path.is_file() and query.lower() in file_path.name.lower():
results.append({
"name": file_path.name,
"path": str(file_path.relative_to(CODE_DIR)).replace('\\', '/'),
"size": file_path.stat().st_size,
"modified": datetime.fromtimestamp(file_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
})
return render_template('code_browser.html',
branches=get_branches_and_versions(),
current_branch=branch,
current_version=version,
search_query=query,
search_results=results)
# ============================================
# 路由 - 反馈
# ============================================
@app.route('/feedback', methods=['GET', 'POST'])
def feedback():
"""反馈页面"""
if request.method == 'POST':
email = request.form.get('email', '').strip()
content = request.form.get('content', '').strip()
if not email or not content:
return render_template('feedback.html', error="请填写邮箱和反馈内容")
# 保存反馈
feedbacks = load_feedbacks()
feedbacks.append({
"id": len(feedbacks) + 1,
"email": email,
"content": content,
"status": "unread",
"created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
save_feedbacks(feedbacks)
# 发送邮件
send_feedback_email(email, content)
return render_template('feedback.html', success="反馈已提交,感谢您的建议!")
return render_template('feedback.html')
# ============================================
# 路由 - API
# ============================================
@app.route('/api/download')
def api_download():
"""API获取下载地址"""
branch = request.args.get('branch', '')
version = request.args.get('version', '')
if not branch or not version:
return jsonify({"error": "缺少 branch 或 version 参数"}), 400
# 检查版本是否存在
version_path = CODE_DIR / branch / version
if not version_path.exists():
return jsonify({"error": "版本不存在"}), 404
base_url = request.host_url.rstrip('/')
response = {
"branch": branch,
"version": version,
"downloads": {
"zip": f"{base_url}/download/zip/{branch}/{version}",
"tar": f"{base_url}/download/tar/{branch}/{version}"
}
}
whl_file = find_whl_file(version)
if whl_file:
response["downloads"]["whl"] = f"{base_url}/download/whl/{version}"
return jsonify(response)
# ============================================
# 管理员路由
# ============================================
@app.route('/admin/login', methods=['GET', 'POST'])
def admin_login():
"""管理员登录"""
if request.method == 'POST':
password = request.form.get('password', '')
password_hash = hashlib.sha256(password.encode()).hexdigest()
if password_hash == ADMIN_PASSWORD_HASH:
session['admin_logged_in'] = True
return redirect(url_for('admin_dashboard'))
else:
return render_template('admin_login.html', error="密码错误")
return render_template('admin_login.html')
@app.route('/admin/logout')
def admin_logout():
"""管理员登出"""
session.pop('admin_logged_in', None)
return redirect(url_for('admin_login'))
@app.route('/admin')
@admin_required
def admin_dashboard():
"""管理仪表板"""
branches_data = get_branches_and_versions()
feedbacks = load_feedbacks()
stats = {
"branches": len(branches_data),
"versions": sum(len(v) for v in branches_data.values()),
"feedbacks": len(feedbacks),
"unread_feedbacks": len([f for f in feedbacks if f.get('status') == 'unread'])
}
return render_template('admin_dashboard.html', stats=stats, recent_feedbacks=feedbacks[-10:])
@app.route('/admin/feedbacks')
@admin_required
def admin_feedbacks():
"""反馈管理"""
feedbacks = load_feedbacks()
return render_template('admin_feedbacks.html', feedbacks=feedbacks)
@app.route('/admin/feedback/mark_read/<int:feedback_id>')
@admin_required
def admin_feedback_mark_read(feedback_id):
"""标记反馈为已读"""
feedbacks = load_feedbacks()
for f in feedbacks:
if f.get('id') == feedback_id:
f['status'] = 'read'
break
save_feedbacks(feedbacks)
return redirect(url_for('admin_feedbacks'))
@app.route('/admin/feedback/delete/<int:feedback_id>')
@admin_required
def admin_feedback_delete(feedback_id):
"""删除反馈"""
feedbacks = load_feedbacks()
feedbacks = [f for f in feedbacks if f.get('id') != feedback_id]
save_feedbacks(feedbacks)
return redirect(url_for('admin_feedbacks'))
@app.route('/admin/branches')
@admin_required
def admin_branches():
"""分支管理"""
branches_data = get_branches_and_versions()
return render_template('admin_branches.html', branches=branches_data)
@app.route('/admin/branch/create', methods=['POST'])
@admin_required
def admin_branch_create():
"""创建分支"""
branch_name = request.form.get('branch_name', '').strip()
if not branch_name:
return jsonify({"error": "分支名不能为空"}), 400
# 安全处理分支名
branch_name = secure_filename(branch_name)
branch_path = CODE_DIR / branch_name
if branch_path.exists():
return jsonify({"error": "分支已存在"}), 400
branch_path.mkdir(parents=True)
# 更新分支数据
branches = load_branches()
branches[branch_name] = []
save_branches(branches)
return redirect(url_for('admin_branches'))
@app.route('/admin/version/upload', methods=['POST'])
@admin_required
def admin_version_upload():
"""上传新版本"""
branch = request.form.get('branch', '').strip()
version = request.form.get('version', '').strip()
if not branch or not version:
return jsonify({"error": "分支和版本不能为空"}), 400
version = secure_filename(version)
version_path = CODE_DIR / branch / version
if version_path.exists():
return jsonify({"error": "版本已存在"}), 400
version_path.mkdir(parents=True)
# 处理文件上传
if 'files' in request.files:
files = request.files.getlist('files')
for file in files:
if file and file.filename:
filename = secure_filename(file.filename)
file.save(version_path / filename)
# 更新分支数据
branches = load_branches()
if branch not in branches:
branches[branch] = []
if version not in branches[branch]:
branches[branch].append(version)
save_branches(branches)
return redirect(url_for('admin_branches'))
@app.route('/admin/version/delete/<branch>/<version>')
@admin_required
def admin_version_delete(branch, version):
"""删除版本"""
version_path = CODE_DIR / branch / version
if version_path.exists():
shutil.rmtree(version_path)
# 更新分支数据
branches = load_branches()
if branch in branches and version in branches[branch]:
branches[branch].remove(version)
save_branches(branches)
return redirect(url_for('admin_branches'))
@app.route('/admin/whl/upload', methods=['POST'])
@admin_required
def admin_whl_upload():
"""上传whl文件"""
if 'whl_file' not in request.files:
return jsonify({"error": "请选择文件"}), 400
file = request.files['whl_file']
if file.filename == '':
return jsonify({"error": "请选择文件"}), 400
if file.filename.endswith('.whl'):
filename = secure_filename(file.filename)
file.save(PYPI_DIR / filename)
return redirect(url_for('admin_dashboard'))
# ============================================
# 错误页面
# ============================================
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('500.html'), 500
# ============================================
# 主程序入口
# ============================================
if __name__ == '__main__':
print("=" * 50)
print("ap_ds 代码仓库系统启动")
print("访问地址: http://127.0.0.1:8888")
print("管理员后台: http://127.0.0.1:8888/admin/login")
print("=" * 50)
app.run(host='0.0.0.0', port=8888, debug=True)
+2312
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}404 - 页面未找到{% endblock %}
{% block content %}
<div class="repo-container repo-text-center repo-py-10">
<span style="font-size: 6rem;">🔍</span>
<h1 class="repo-text-2xl repo-font-bold repo-mt-4">404</h1>
<p class="repo-text-secondary repo-mt-2">页面未找到</p>
<a href="{{ url_for('index') }}" class="repo-btn repo-btn--primary repo-mt-6">返回首页</a>
</div>
{% endblock %}
+12
View File
@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}500 - 服务器错误{% endblock %}
{% block content %}
<div class="repo-container repo-text-center repo-py-10">
<span style="font-size: 6rem;">⚠️</span>
<h1 class="repo-text-2xl repo-font-bold repo-mt-4">500</h1>
<p class="repo-text-secondary repo-mt-2">服务器内部错误</p>
<a href="{{ url_for('index') }}" class="repo-btn repo-btn--primary repo-mt-6">返回首页</a>
</div>
{% endblock %}
+90
View File
@@ -0,0 +1,90 @@
{% extends "base.html" %}
{% block title %}分支管理 - ap_ds{% endblock %}
{% block content %}
<div class="repo-container">
<div class="repo-flex repo-justify-between repo-items-center repo-mb-6">
<h1 class="repo-text-2xl repo-font-bold">分支与版本管理</h1>
<a href="{{ url_for('admin_dashboard') }}" class="repo-btn repo-btn--ghost">返回仪表板</a>
</div>
<!-- 创建分支 -->
<div class="repo-card repo-mb-6">
<div class="repo-card__header">
<h3 class="repo-card__title">创建新分支</h3>
</div>
<div class="repo-card__body">
<form method="post" action="{{ url_for('admin_branch_create') }}" class="repo-flex repo-gap-3">
<input type="text" name="branch_name" class="repo-input" placeholder="分支名称 (如: LFV)" required>
<button type="submit" class="repo-btn repo-btn--primary">创建分支</button>
</form>
</div>
</div>
<!-- 上传新版本 -->
<div class="repo-card repo-mb-6">
<div class="repo-card__header">
<h3 class="repo-card__title">上传新版本</h3>
</div>
<div class="repo-card__body">
<form method="post" action="{{ url_for('admin_version_upload') }}" enctype="multipart/form-data">
<div class="repo-row">
<div class="repo-col">
<select name="branch" class="repo-select__trigger repo-w-full" required>
<option value="">选择分支</option>
{% for branch_name in branches.keys() %}
<option value="{{ branch_name }}">{{ branch_name }}</option>
{% endfor %}
</select>
</div>
<div class="repo-col">
<input type="text" name="version" class="repo-input" placeholder="版本号 (如: v2.5.0)" required>
</div>
<div class="repo-col">
<input type="file" name="files" class="repo-input" multiple>
</div>
<div class="repo-col">
<button type="submit" class="repo-btn repo-btn--primary">上传版本</button>
</div>
</div>
</form>
</div>
</div>
<!-- 分支列表 -->
{% for branch_name, versions in branches.items() %}
<div class="repo-card repo-mb-6">
<div class="repo-card__header">
<h3 class="repo-card__title">📁 {{ branch_name }}</h3>
</div>
<div class="repo-card__body">
<table class="repo-table">
<thead>
<tr>
<th>版本</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for version in versions %}
<tr>
<td>{{ version.name }}</td>
<td class="repo-text-secondary">{{ version.created }}</td>
<td>
<a href="{{ url_for('admin_version_delete', branch=branch_name, version=version.name) }}"
class="repo-btn repo-btn--sm repo-btn--danger"
onclick="return confirm('确定要删除版本 {{ version.name }} 吗?')">
删除
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
+117
View File
@@ -0,0 +1,117 @@
{% extends "base.html" %}
{% block title %}管理仪表板 - ap_ds{% endblock %}
{% block content %}
<div class="repo-container">
<div class="repo-flex repo-justify-between repo-items-center repo-mb-6">
<h1 class="repo-text-2xl repo-font-bold">管理仪表板</h1>
<a href="{{ url_for('admin_logout') }}" class="repo-btn repo-btn--danger">登出</a>
</div>
<!-- 统计卡片 -->
<div class="repo-row repo-mb-6">
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-text-center">
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-primary);">{{ stats.branches }}</div>
<div class="repo-text-secondary">分支</div>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-text-center">
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-success);">{{ stats.versions }}</div>
<div class="repo-text-secondary">版本</div>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-text-center">
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-warning);">{{ stats.feedbacks }}</div>
<div class="repo-text-secondary">反馈总数</div>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-text-center">
<div class="repo-text-3xl repo-font-bold" style="color: var(--repo-color-danger);">{{ stats.unread_feedbacks }}</div>
<div class="repo-text-secondary">未读反馈</div>
</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="repo-row">
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__header">
<h3 class="repo-card__title">上传 WHL 文件</h3>
</div>
<div class="repo-card__body">
<form method="post" action="{{ url_for('admin_whl_upload') }}" enctype="multipart/form-data">
<input type="file" name="whl_file" accept=".whl" class="repo-input repo-mb-3" required>
<button type="submit" class="repo-btn repo-btn--primary">上传</button>
</form>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__header">
<h3 class="repo-card__title">管理链接</h3>
</div>
<div class="repo-card__body">
<div class="repo-flex repo-gap-3">
<a href="{{ url_for('admin_branches') }}" class="repo-btn repo-btn--primary">分支管理</a>
<a href="{{ url_for('admin_feedbacks') }}" class="repo-btn repo-btn--secondary">反馈管理</a>
</div>
</div>
</div>
</div>
</div>
<!-- 最近反馈 -->
<div class="repo-card repo-mt-6">
<div class="repo-card__header">
<h3 class="repo-card__title">最近反馈</h3>
</div>
<div class="repo-card__body">
{% if recent_feedbacks %}
<table class="repo-table">
<thead>
<tr>
<th>时间</th>
<th>邮箱</th>
<th>内容</th>
<th>状态</th>
</tr>
</thead>
<tbody>
{% for f in recent_feedbacks %}
<tr>
<td class="repo-text-sm">{{ f.created }}</td>
<td class="repo-text-sm">{{ f.email }}</td>
<td class="repo-text-sm repo-truncate" style="max-width: 300px;">{{ f.content[:50] }}...</td>
<td>
{% if f.status == 'unread' %}
<span class="repo-tag repo-tag--danger">未读</span>
{% else %}
<span class="repo-tag repo-tag--default">已读</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="repo-text-secondary repo-text-center">暂无反馈</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+62
View File
@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}反馈管理 - ap_ds{% endblock %}
{% block content %}
<div class="repo-container">
<div class="repo-flex repo-justify-between repo-items-center repo-mb-6">
<h1 class="repo-text-2xl repo-font-bold">反馈管理</h1>
<a href="{{ url_for('admin_dashboard') }}" class="repo-btn repo-btn--ghost">返回仪表板</a>
</div>
<div class="repo-card">
<div class="repo-card__body">
{% if feedbacks %}
<table class="repo-table">
<thead>
<tr>
<th>ID</th>
<th>时间</th>
<th>邮箱</th>
<th>内容</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for f in feedbacks|reverse %}
<tr>
<td>{{ f.id }}</td>
<td class="repo-text-sm">{{ f.created }}</td>
<td class="repo-text-sm">{{ f.email }}</td>
<td class="repo-text-sm" style="max-width: 400px;">
<div class="repo-line-clamp-2">{{ f.content }}</div>
</td>
<td>
{% if f.status == 'unread' %}
<span class="repo-tag repo-tag--danger">未读</span>
{% else %}
<span class="repo-tag repo-tag--default">已读</span>
{% endif %}
</td>
<td>
<div class="repo-flex repo-gap-2">
{% if f.status == 'unread' %}
<a href="{{ url_for('admin_feedback_mark_read', feedback_id=f.id) }}" class="repo-btn repo-btn--sm repo-btn--primary">标记已读</a>
{% endif %}
<a href="{{ url_for('admin_feedback_delete', feedback_id=f.id) }}"
class="repo-btn repo-btn--sm repo-btn--danger"
onclick="return confirm('确定要删除这条反馈吗?')">删除</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="repo-text-secondary repo-text-center">暂无反馈</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录 - ap_ds</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css.css') }}">
</head>
<body style="background: var(--repo-color-bg-secondary); min-height: 100vh; display: flex; align-items: center; justify-content: center;">
<div class="repo-card" style="width: 100%; max-width: 400px;">
<div class="repo-card__header">
<h2 class="repo-card__title">管理员登录</h2>
</div>
<div class="repo-card__body">
{% if error %}
<div class="repo-message repo-message--error" style="position: static; transform: none; margin-bottom: 1rem;">
{{ error }}
</div>
{% endif %}
<form method="post">
<div class="repo-mb-4">
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">密码</label>
<input type="password" name="password" class="repo-input" required autofocus>
</div>
<button type="submit" class="repo-btn repo-btn--primary repo-w-full">登录</button>
</form>
</div>
</div>
</body>
</html>
+581
View File
@@ -0,0 +1,581 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}ap_ds · 轻量级Python音频库{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css.css') }}">
<!-- highlight.js 主题 (浅色) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
/* 浅色主题覆盖 - 基于 css.css 变量 */
:root {
--repo-color-primary: #3b82f6;
--repo-color-primary-hover: #2563eb;
--repo-color-text: #1f2937;
--repo-color-text-secondary: #4b5563;
--repo-color-text-tertiary: #9ca3af;
--repo-color-bg: #ffffff;
--repo-color-bg-secondary: #f9fafb;
--repo-color-bg-tertiary: #f3f4f6;
--repo-color-border: #e5e7eb;
--repo-color-border-hover: #d1d5db;
}
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--repo-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif);
font-size: var(--repo-font-size-base, 14px);
line-height: var(--repo-line-height-base, 1.5);
color: var(--repo-color-text);
background-color: var(--repo-color-bg-secondary);
}
/* 导航栏 - 使用 css.css 变量 */
.header {
background-color: var(--repo-color-bg);
border-bottom: 1px solid var(--repo-color-border);
position: sticky;
top: 0;
z-index: var(--repo-z-sticky, 100);
}
.header-inner {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--repo-spacing-6, 16px);
display: flex;
align-items: center;
justify-content: space-between;
height: var(--repo-header-height, 60px);
}
.logo-area {
display: flex;
align-items: center;
gap: var(--repo-spacing-4, 16px);
}
.logo {
font-size: var(--repo-font-size-xl, 18px);
font-weight: 600;
color: var(--repo-color-text);
text-decoration: none;
}
.logo span {
color: var(--repo-color-primary);
}
.nav-links {
display: flex;
gap: var(--repo-spacing-4, 16px);
}
.nav-links a {
color: var(--repo-color-text-secondary);
text-decoration: none;
font-size: var(--repo-font-size-base, 14px);
font-weight: 500;
padding: var(--repo-spacing-2, 8px) 0;
transition: color 0.2s ease;
}
.nav-links a:hover {
color: var(--repo-color-primary);
}
.repo-badge {
background-color: var(--repo-color-bg-tertiary);
padding: 4px 10px;
border-radius: var(--repo-radius-full, 20px);
font-size: var(--repo-font-size-xs, 12px);
font-family: var(--repo-font-family-mono, monospace);
color: var(--repo-color-text-secondary);
}
/* 主容器 */
.main-container {
max-width: 1280px;
margin: 0 auto;
padding: var(--repo-spacing-6, 24px) var(--repo-spacing-4, 16px);
}
/* 仓库头部 */
.repo-header {
margin-bottom: var(--repo-spacing-6, 24px);
}
.repo-path {
display: flex;
align-items: center;
gap: var(--repo-spacing-2, 8px);
font-size: var(--repo-font-size-xl, 18px);
font-weight: 600;
margin-bottom: var(--repo-spacing-4, 16px);
}
.repo-path a {
color: var(--repo-color-primary);
text-decoration: none;
}
.repo-path a:hover {
text-decoration: underline;
}
.repo-path .separator {
color: var(--repo-color-text-tertiary);
}
/* 两栏布局 */
.two-column {
display: flex;
gap: var(--repo-spacing-8, 32px);
}
/* 左侧边栏 */
.sidebar {
width: 260px;
flex-shrink: 0;
}
.sidebar-section {
margin-bottom: var(--repo-spacing-6, 24px);
}
.sidebar-title {
font-size: var(--repo-font-size-xs, 12px);
font-weight: 600;
color: var(--repo-color-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: var(--repo-spacing-3, 12px);
}
.branch-selector {
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-md, 6px);
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
margin-bottom: var(--repo-spacing-4, 16px);
color: var(--repo-color-text);
}
.branch-selector:hover {
background-color: var(--repo-color-bg-hover);
border-color: var(--repo-color-border-hover);
}
/* 右侧主内容 */
.main-content {
flex: 1;
min-width: 0;
}
/* 文件列表表格 */
.file-list {
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-lg, 8px);
overflow: hidden;
}
.file-list-header {
background-color: var(--repo-color-bg-secondary);
padding: var(--repo-spacing-3, 12px) var(--repo-spacing-4, 16px);
border-bottom: 1px solid var(--repo-color-border);
display: flex;
justify-content: space-between;
font-size: var(--repo-font-size-xs, 12px);
color: var(--repo-color-text-tertiary);
font-weight: 500;
}
.file-row {
display: flex;
align-items: center;
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-4, 16px);
border-bottom: 1px solid var(--repo-color-border);
transition: background 0.2s;
}
.file-row:hover {
background-color: var(--repo-color-bg-hover);
}
.file-row:last-child {
border-bottom: none;
}
.file-name {
flex: 1;
display: flex;
align-items: center;
gap: var(--repo-spacing-2, 8px);
}
.file-name a {
color: var(--repo-color-primary);
text-decoration: none;
font-family: var(--repo-font-family-mono, monospace);
font-size: var(--repo-font-size-sm, 13px);
}
.file-name a:hover {
text-decoration: underline;
}
.file-message {
flex: 2;
font-size: var(--repo-font-size-xs, 12px);
color: var(--repo-color-text-tertiary);
}
.file-time {
width: 100px;
font-size: var(--repo-font-size-xs, 12px);
color: var(--repo-color-text-tertiary);
text-align: right;
}
/* README 区域 */
.readme-section {
margin-top: var(--repo-spacing-8, 32px);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-lg, 8px);
overflow: hidden;
}
.readme-header {
background-color: var(--repo-color-bg-secondary);
padding: var(--repo-spacing-3, 12px) var(--repo-spacing-4, 16px);
border-bottom: 1px solid var(--repo-color-border);
font-weight: 600;
font-size: var(--repo-font-size-base, 14px);
color: var(--repo-color-text);
}
.readme-content {
padding: var(--repo-spacing-8, 32px);
background-color: var(--repo-color-bg);
}
.readme-content h1, .readme-content h2, .readme-content h3 {
margin-top: var(--repo-spacing-6, 24px);
margin-bottom: var(--repo-spacing-4, 16px);
font-weight: 600;
}
.readme-content h1 { font-size: 2em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
.readme-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
.readme-content h3 { font-size: 1.25em; }
.readme-content p { margin-bottom: var(--repo-spacing-4, 16px); line-height: 1.6; }
.readme-content code {
background-color: var(--repo-color-bg-tertiary);
padding: 0.2em 0.4em;
border-radius: var(--repo-radius-sm, 4px);
font-family: var(--repo-font-family-mono, monospace);
font-size: 85%;
}
.readme-content pre {
background-color: var(--repo-color-bg-tertiary);
padding: var(--repo-spacing-4, 16px);
border-radius: var(--repo-radius-md, 6px);
overflow-x: auto;
margin-bottom: var(--repo-spacing-4, 16px);
}
.readme-content pre code {
background: none;
padding: 0;
}
.readme-content ul, .readme-content ol {
padding-left: 2em;
margin-bottom: var(--repo-spacing-4, 16px);
}
.readme-content blockquote {
border-left: 4px solid var(--repo-color-border);
padding-left: var(--repo-spacing-4, 16px);
color: var(--repo-color-text-tertiary);
margin-bottom: var(--repo-spacing-4, 16px);
}
.readme-content table {
border-collapse: collapse;
margin-bottom: var(--repo-spacing-4, 16px);
width: 100%;
}
.readme-content th, .readme-content td {
border: 1px solid var(--repo-color-border);
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
}
.readme-content th {
background-color: var(--repo-color-bg-secondary);
}
/* 代码查看器 */
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--repo-spacing-3, 12px) var(--repo-spacing-4, 16px);
background-color: var(--repo-color-bg-secondary);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-lg, 8px) var(--repo-radius-lg, 8px) 0 0;
}
.code-content {
border: 1px solid var(--repo-color-border);
border-top: none;
border-radius: 0 0 var(--repo-radius-lg, 8px) var(--repo-radius-lg, 8px);
overflow: auto;
}
.code-content pre {
margin: 0;
padding: var(--repo-spacing-4, 16px);
background-color: var(--repo-color-bg);
}
.code-content code {
font-family: var(--repo-font-family-mono, "SF Mono", Monaco, monospace);
font-size: var(--repo-font-size-sm, 13px);
line-height: 1.5;
}
/* 按钮 - 使用 css.css 样式 */
.copy-btn {
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
color: var(--repo-color-text-secondary);
padding: var(--repo-spacing-1, 4px) var(--repo-spacing-3, 12px);
border-radius: var(--repo-radius-md, 6px);
font-size: var(--repo-font-size-xs, 12px);
cursor: pointer;
transition: all 0.2s ease;
}
.copy-btn:hover {
background-color: var(--repo-color-bg-hover);
border-color: var(--repo-color-border-hover);
color: var(--repo-color-text);
}
.btn {
display: inline-flex;
align-items: center;
gap: var(--repo-spacing-1, 4px);
padding: var(--repo-spacing-1, 4px) var(--repo-spacing-3, 12px);
font-size: var(--repo-font-size-xs, 12px);
font-weight: 500;
border-radius: var(--repo-radius-md, 6px);
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
}
.btn-primary {
background-color: var(--repo-color-primary);
border: 1px solid transparent;
color: white;
}
.btn-primary:hover {
background-color: var(--repo-color-primary-hover);
}
.btn-secondary {
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
color: var(--repo-color-text);
}
.btn-secondary:hover {
background-color: var(--repo-color-bg-hover);
border-color: var(--repo-color-border-hover);
}
/* 搜索框 */
.search-box {
display: flex;
gap: var(--repo-spacing-2, 8px);
margin-bottom: var(--repo-spacing-6, 24px);
}
.search-input {
flex: 1;
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-md, 6px);
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
color: var(--repo-color-text);
font-size: var(--repo-font-size-base, 14px);
transition: all 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: var(--repo-color-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* 卡片 */
.repo-card-custom {
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-lg, 8px);
overflow: hidden;
}
/* 响应式 */
@media (max-width: 768px) {
.two-column {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.file-message {
display: none;
}
.header-inner {
padding: 0 var(--repo-spacing-4, 16px);
}
.nav-links {
gap: var(--repo-spacing-3, 12px);
}
.nav-links a {
font-size: var(--repo-font-size-sm, 13px);
}
}
/* 图标占位 */
.icon {
width: 16px;
text-align: center;
font-size: 14px;
}
/* 加载动画 */
.loading {
text-align: center;
padding: var(--repo-spacing-8, 32px);
color: var(--repo-color-text-tertiary);
}
/* 提示信息 */
.info-message {
background-color: var(--repo-color-bg-secondary);
border-left: 4px solid var(--repo-color-primary);
padding: var(--repo-spacing-4, 16px);
border-radius: var(--repo-radius-md, 6px);
margin-bottom: var(--repo-spacing-4, 16px);
}
.error-message {
background-color: #fef2f2;
border-left: 4px solid var(--repo-color-danger, #ef4444);
padding: var(--repo-spacing-4, 16px);
border-radius: var(--repo-radius-md, 6px);
margin-bottom: var(--repo-spacing-4, 16px);
color: #b91c1c;
}
.success-message {
background-color: #f0fdf4;
border-left: 4px solid var(--repo-color-success, #10b981);
padding: var(--repo-spacing-4, 16px);
border-radius: var(--repo-radius-md, 6px);
margin-bottom: var(--repo-spacing-4, 16px);
color: #047857;
}
/* 分支菜单下拉 */
.branch-menu-dropdown {
position: absolute;
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-md, 6px);
width: 200px;
z-index: var(--repo-z-dropdown, 100);
box-shadow: var(--repo-shadow-lg, 0 10px 15px -3px rgba(0,0,0,0.1));
}
.branch-menu-item {
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
cursor: pointer;
transition: background 0.2s;
}
.branch-menu-item:hover {
background-color: var(--repo-color-bg-hover);
}
</style>
{% block extra_head %}{% endblock %}
</head>
<body>
<div class="header">
<div class="header-inner">
<div class="logo-area">
<a href="/" class="logo">ap_<span>ds</span></a>
<div class="nav-links">
<a href="{{ url_for('index') }}">首页</a>
<a href="{{ url_for('code_browser') }}">代码</a>
<a href="{{ url_for('feedback') }}">提问/建议</a>
<a href="{{ url_for('releases') }}">发行版</a>
</div>
</div>
<div class="repo-badge">
v3.0.0
</div>
</div>
</div>
<div class="main-container">
{% block content %}{% endblock %}
</div>
<script>
// 初始化 highlight.js
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('pre code').forEach(function(block) {
hljs.highlightElement(block);
});
});
// 复制代码功能
function copyCode(btn) {
var pre = btn.closest('.code-header')?.nextElementSibling;
var codeBlock = pre?.querySelector('code');
if (!codeBlock) return;
var code = codeBlock.innerText;
navigator.clipboard.writeText(code).then(function() {
var originalText = btn.innerText;
btn.innerText = '已复制!';
setTimeout(function() { btn.innerText = originalText; }, 2000);
});
}
// 全局分支/版本选择辅助函数
function selectBranch(branch, baseUrl) {
window.location.href = baseUrl + '?branch=' + encodeURIComponent(branch);
}
function selectVersion(version, baseUrl, branch) {
window.location.href = baseUrl + '?branch=' + encodeURIComponent(branch) + '&version=' + encodeURIComponent(version);
}
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>
+381
View File
@@ -0,0 +1,381 @@
<!-- 英文 SEO 标签 -->
<meta name="description" content="ap_ds official code repository. Browse branches (APDSLTS001), tags (v3.0.0), view README/setup.py with syntax highlighting, search files, and download ZIP/TAR.GZ/WHL. Self-hosted, never offline.">
<meta name="keywords" content="ap_ds source code, Python audio library code, browse repository, download WHL, self-hosted git, APDSLTS001, v3.0.0 source">
<meta property="og:title" content="ap_ds Code Repository – Browse & Download Source">
<meta property="og:description" content="Official self-hosted Git-style code browser for ap_ds. Syntax-highlighted Python files, Markdown rendering, multi-format downloads.">
<meta property="og:type" content="software">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="ap_ds Code Repository">
<!-- 中文 SEO 标签 -->
<meta name="description" lang="zh-CN" content="ap_ds 官方代码仓库。浏览分支(APDSLTS001)和版本(v3.0.0)下的所有源文件,语法高亮预览 README/setup.py,文件搜索,一键下载 ZIP/TAR.GZ/WHL。自主托管,永不掉线。">
<meta name="keywords" lang="zh-CN" content="ap_ds 源代码, Python音频库代码, 浏览仓库, 下载WHL, 自建Git仓库, APDSLTS001, v3.0.0源码">
<meta property="og:title" lang="zh-CN" content="ap_ds 代码仓库 – 浏览与下载源码">
<meta property="og:description" lang="zh-CN" content="ap_ds 官方自建代码仓库。支持语法高亮的 Python 文件预览、Markdown 渲染、多格式源码下载。">{% extends "base.html" %}
{% block content %}
{% block title %}ap_ds Code Repository – Official Self-Hosted Git Source Code & Downloads{% endblock %}<!-- 仓库头部 -->
<div class="repo-header">
<div class="repo-path">
<a href="/code">ap_ds</a>
<span class="separator">/</span>
<span>{{ current_branch if current_branch else '选择分支' }}</span>
{% if current_version %}
<span class="separator">/</span>
<span>{{ current_version }}</span>
{% endif %}
</div>
</div>
<!-- 搜索栏 -->
<div class="search-box">
<form action="{{ url_for('search') }}" method="get" style="display: flex; gap: 8px; width: 100%;">
<input type="hidden" name="branch" value="{{ current_branch }}">
<input type="hidden" name="version" value="{{ current_version }}">
<input type="text" name="q" class="search-input" placeholder="查找文件..." value="{{ search_query or '' }}">
<button type="submit" class="btn btn-primary">搜索</button>
</form>
</div>
<div class="two-column">
<!-- 左侧边栏 -->
<div class="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">📌 分支</div>
<div class="branch-selector" id="branch-selector">
<span>{{ current_branch if current_branch else '选择分支' }}</span>
<span>▼</span>
</div>
<div id="branch-menu" class="branch-menu-dropdown" style="display: none;">
{% for branch_name in branches.keys() %}
<div class="branch-menu-item" data-branch="{{ branch_name }}">{{ branch_name }}</div>
{% endfor %}
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">🏷️ 版本</div>
<div class="branch-selector" id="version-selector">
<span>{{ current_version if current_version else '选择版本' }}</span>
<span>▼</span>
</div>
<div id="version-menu" class="branch-menu-dropdown" style="display: none;">
{% if current_branch and branches[current_branch] %}
{% for version in branches[current_branch] %}
<div class="branch-menu-item" data-version="{{ version.name }}">{{ version.name }}</div>
{% endfor %}
{% endif %}
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">📦 下载</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<a href="{{ url_for('download_zip', branch=current_branch, version=current_version) if current_branch and current_version else '#' }}" class="btn btn-secondary {% if not current_branch or not current_version %}disabled{% endif %}" {% if not current_branch or not current_version %}style="opacity:0.5; pointer-events:none;"{% endif %}>
📥 下载 ZIP
</a>
<a href="{{ url_for('download_tar', branch=current_branch, version=current_version) if current_branch and current_version else '#' }}" class="btn btn-secondary {% if not current_branch or not current_version %}disabled{% endif %}" {% if not current_branch or not current_version %}style="opacity:0.5; pointer-events:none;"{% endif %}>
📥 下载 TAR.GZ
</a>
<a href="{{ url_for('download_whl', version=current_version) if current_version else '#' }}" class="btn btn-secondary {% if not current_version %}disabled{% endif %}" {% if not current_version %}style="opacity:0.5; pointer-events:none;"{% endif %}>
📥 下载 WHL
</a>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">🔗 快速链接</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
<a href="/releases" class="btn btn-secondary" style="text-align: center;">查看发行版</a>
<a href="/feedback" class="btn btn-secondary" style="text-align: center;">提交反馈</a>
</div>
</div>
</div>
<!-- 右侧内容 -->
<div class="main-content">
<!-- 搜索结果 -->
{% if search_results %}
<div class="file-list">
<div class="file-list-header">
<span>🔍 搜索结果 ({{ search_results|length }})</span>
<span></span>
</div>
{% for result in search_results %}
<div class="file-row">
<div class="file-name">
<span class="icon">📄</span>
{% set relative_path = result.path.replace(current_branch + '/' + current_version + '/', '') %}
<a href="{{ url_for('view_file', file_path=relative_path, branch=current_branch, version=current_version) }}">{{ result.name }}</a>
</div>
<div class="file-message">{{ result.path }}</div>
<div class="file-time">
{% if result.size < 1024 %}
{{ result.size }} B
{% elif result.size < 1024*1024 %}
{{ (result.size/1024)|round(1) }} KB
{% else %}
{{ (result.size/(1024*1024))|round(1) }} MB
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- 文件列表 -->
{% if not search_results %}
<div class="file-list">
<div class="file-list-header">
<span>📁 文件列表</span>
<span></span>
<span>大小</span>
</div>
<!-- 文件列表中的链接改为使用完整路径 -->
{% for item in file_tree %}
<div class="file-row">
<div class="file-name">
<span class="icon">{% if item.type == 'dir' %}📁{% else %}📄{% endif %}</span>
{% if item.type == 'dir' %}
<a href="{{ url_for('browse_directory', dir_path=item.path, branch=current_branch, version=current_version) }}">{{ item.name }}</a>
{% else %}
<!-- 直接使用完整路径,不加替换 -->
<a href="{{ url_for('view_file', file_path=item.path, branch=current_branch, version=current_version) }}">{{ item.name }}</a>
{% endif %}
</div>
<div class="file-message">
{% if item.type == 'dir' %}目录{% else %}{{ item.name }}{% endif %}
</div>
<div class="file-time">
{% if item.type == 'file' %}
{% if item.size < 1024 %}
{{ item.size }} B
{% elif item.size < 1024*1024 %}
{{ (item.size/1024)|round(1) }} KB
{% else %}
{{ (item.size/(1024*1024))|round(1) }} MB
{% endif %}
{% else %}
-
{% endif %}
</div>
</div>
{% else %}
<div class="file-row">
<div class="file-name" style="justify-content: center; color: var(--repo-color-text-tertiary);">
此目录为空
</div>
<div class="file-message"></div>
<div class="file-time"></div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- README 区域 -->
<div class="readme-section">
<div class="readme-header">
📖 README.md
</div>
<div class="readme-content" id="readme-content">
<div class="loading">加载中...</div>
</div>
</div>
</div>
</div>
<script>
// 获取当前 URL 基础路径
var currentUrl = "{{ url_for('code_browser') }}";
var currentBranch = "{{ current_branch }}";
var currentVersion = "{{ current_version }}";
// ========== 分支/版本选择器 ==========
var branchSelector = document.getElementById('branch-selector');
var branchMenu = document.getElementById('branch-menu');
var versionSelector = document.getElementById('version-selector');
var versionMenu = document.getElementById('version-menu');
if (branchSelector) {
branchSelector.onclick = function(e) {
e.stopPropagation();
branchMenu.style.display = branchMenu.style.display === 'none' ? 'block' : 'none';
if (versionMenu) versionMenu.style.display = 'none';
};
}
if (versionSelector) {
versionSelector.onclick = function(e) {
e.stopPropagation();
versionMenu.style.display = versionMenu.style.display === 'none' ? 'block' : 'none';
if (branchMenu) branchMenu.style.display = 'none';
};
}
document.onclick = function() {
if (branchMenu) branchMenu.style.display = 'none';
if (versionMenu) versionMenu.style.display = 'none';
};
var branchItems = document.querySelectorAll('#branch-menu .branch-menu-item');
branchItems.forEach(function(item) {
item.onclick = function(e) {
e.stopPropagation();
var branch = this.getAttribute('data-branch');
if (branch) {
window.location.href = currentUrl + '?branch=' + encodeURIComponent(branch);
}
};
});
var versionItems = document.querySelectorAll('#version-menu .branch-menu-item');
versionItems.forEach(function(item) {
item.onclick = function(e) {
e.stopPropagation();
var version = this.getAttribute('data-version');
if (version && currentBranch) {
window.location.href = currentUrl + '?branch=' + encodeURIComponent(currentBranch) + '&version=' + encodeURIComponent(version);
}
};
});
// ========== 加载 README ==========
function loadReadme() {
if (!currentBranch || !currentVersion) {
document.getElementById('readme-content').innerHTML = '<div class="info-message" style="text-align: center;">请选择分支和版本以查看 README</div>';
return;
}
var readmeUrl = '/view/' + currentBranch + '/' + currentVersion + '/README.md?branch=' + currentBranch + '&version=' + currentVersion;
fetch(readmeUrl)
.then(function(res) {
if (res.ok) {
return res.text();
}
throw new Error('README.md 不存在');
})
.then(function(html) {
var container = document.getElementById('readme-content');
if (html.indexOf('<div class="readme-content">') !== -1) {
var match = html.match(/<div class="readme-content">([\s\S]*?)<\/div>/);
if (match && match[1]) {
container.innerHTML = match[1];
} else {
container.innerHTML = html;
}
} else {
container.innerHTML = html;
}
var codeBlocks = container.querySelectorAll('pre code');
codeBlocks.forEach(function(block) {
if (typeof hljs !== 'undefined') {
hljs.highlightElement(block);
}
});
})
.catch(function(err) {
document.getElementById('readme-content').innerHTML = '<div class="info-message" style="text-align: center;">📄 暂无 README.md 文件</div>';
});
}
loadReadme();
</script>
<style>
.disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.branch-menu-dropdown {
position: absolute;
background-color: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-md, 6px);
width: 200px;
z-index: var(--repo-z-dropdown, 100);
box-shadow: var(--repo-shadow-md);
max-height: 300px;
overflow-y: auto;
}
.branch-menu-item {
padding: var(--repo-spacing-2, 8px) var(--repo-spacing-3, 12px);
cursor: pointer;
transition: background 0.2s;
color: var(--repo-color-text);
}
.branch-menu-item:hover {
background-color: var(--repo-color-bg-hover);
}
.loading {
text-align: center;
padding: var(--repo-spacing-8, 32px);
color: var(--repo-color-text-tertiary);
}
.info-message {
text-align: center;
padding: var(--repo-spacing-8, 32px);
color: var(--repo-color-text-tertiary);
}
/* README 内容样式 */
#readme-content h1, #readme-content h2, #readme-content h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
#readme-content h1 { font-size: 2em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
#readme-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
#readme-content p { margin-bottom: 16px; line-height: 1.6; }
#readme-content code {
background-color: var(--repo-color-bg-tertiary);
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: monospace;
}
#readme-content pre {
background-color: var(--repo-color-bg-tertiary);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin-bottom: 16px;
}
#readme-content pre code {
background: none;
padding: 0;
}
#readme-content ul, #readme-content ol {
padding-left: 2em;
margin-bottom: 16px;
}
#readme-content blockquote {
border-left: 4px solid var(--repo-color-border);
padding-left: 16px;
color: var(--repo-color-text-tertiary);
margin-bottom: 16px;
}
#readme-content table {
border-collapse: collapse;
margin-bottom: 16px;
width: 100%;
}
#readme-content th, #readme-content td {
border: 1px solid var(--repo-color-border);
padding: 8px 12px;
}
#readme-content th {
background-color: var(--repo-color-bg-secondary);
}
</style>
{% endblock %}
+81
View File
@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}目录浏览 - ap_ds{% endblock %}
{% block content %}
<div class="repo-container">
<!-- 面包屑导航 -->
<div class="repo-breadcrumb repo-mb-4">
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="repo-breadcrumb__link">根目录</a>
{% for crumb in breadcrumbs %}
<span class="repo-breadcrumb__separator">/</span>
<a href="{{ url_for('browse_directory', dir_path=crumb.path, branch=branch, version=version) }}" class="repo-breadcrumb__link">
{{ crumb.name }}
</a>
{% endfor %}
</div>
<div class="repo-card">
<div class="repo-card__body">
<table class="repo-table">
<thead>
<tr>
<th>名称</th>
<th>大小</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% if current_path %}
<tr>
<td>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="repo-text-link">..</a>
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
{% endif %}
{% for file in files %}
<tr>
<td>
{% if file.type == 'dir' %}
<span class="icon-folder repo-mr-2"></span>
<a href="{{ url_for('browse_directory', dir_path=file.path, branch=branch, version=version) }}" class="repo-text-link">
{{ file.name }}
</a>
{% else %}
<span class="icon-file repo-mr-2"></span>
<a href="{{ url_for('view_file', file_path=file.path, branch=branch, version=version) }}" class="repo-text-link">
{{ file.name }}
</a>
{% endif %}
</td>
<td class="repo-text-secondary repo-text-sm">
{% if file.type == 'file' %}
{% if file.size < 1024 %}
{{ file.size }} B
{% elif file.size < 1024*1024 %}
{{ (file.size/1024)|round(1) }} KB
{% else %}
{{ (file.size/(1024*1024))|round(1) }} MB
{% endif %}
{% else %}
-
{% endif %}
</td>
<td class="repo-text-secondary repo-text-sm">{{ file.modified }}</td>
<td>
{% if file.type == 'file' %}
<a href="{{ url_for('download_file', file_path=file.path) }}" class="repo-btn repo-btn--sm repo-btn--ghost">下载</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
+77
View File
@@ -0,0 +1,77 @@
<!-- 英文 SEO 标签 -->
<meta name="description" content="Official feedback channel for ap_ds. Submit bug reports, feature requests, or general inquiries via form. Includes FAQ: supported formats (MP3/FLAC/OGG/WAV), DAP playback logs. Direct email replies from maintainer.">
<meta name="keywords" content="ap_ds feedback, bug report Python audio, feature request, ask question, DvsXT contact, MP3 FLAC OGG WAV support, Python audio library help">
<meta property="og:title" content="ap_ds Feedback – Ask Questions & Report Issues">
<meta property="og:description" content="Direct line to ap_ds maintainer. Submit feedback, get answers via email. Read FAQ before posting.">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="ap_ds Feedback Form">
<!-- 中文 SEO 标签 -->
<meta name="description" lang="zh-CN" content="ap_ds 官方反馈通道。提交 Bug 报告、功能建议或一般咨询。附常见问题:支持的音频格式(MP3/FLAC/OGG/WAV)、DAP 播放记录获取方法。维护者通过邮件直接回复。">
<meta name="keywords" lang="zh-CN" content="ap_ds 反馈, 提交Bug, 功能建议, 提问, DvsXT 联系, MP3 FLAC OGG WAV支持, Python音频库帮助">
<meta property="og:title" lang="zh-CN" content="ap_ds 反馈 – 提问与建议">
<meta property="og:description" lang="zh-CN" content="直连 ap_ds 维护者。提交反馈,邮件获取回复。提交前可查阅常见问题。">{% extends "base.html" %}
{% block content %}{% block title %}ap_ds Feedback – Submit Questions, Bug Reports & Feature Requests{% endblock %}
<div class="repo-container" style="max-width: 800px;">
<div class="repo-card">
<div class="repo-card__header">
<h2 class="repo-card__title">💬 提问与建议</h2>
<p class="repo-text-secondary repo-text-sm repo-mt-2">
欢迎向我们提出任何问题或建议,我们会认真对待每一条反馈。
</p>
</div>
<div class="repo-card__body">
{% if error %}
<div class="repo-message repo-message--error" style="position: static; transform: none; margin-bottom: 1rem;">
{{ error }}
</div>
{% endif %}
{% if success %}
<div class="repo-message repo-message--success" style="position: static; transform: none; margin-bottom: 1rem;">
{{ success }}
</div>
{% endif %}
<form method="post">
<div class="repo-mb-4">
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">邮箱地址</label>
<input type="email" name="email" class="repo-input" required placeholder="your@email.com">
<p class="repo-text-xs repo-text-tertiary repo-mt-1">我们会将回复发送到此邮箱</p>
</div>
<div class="repo-mb-4">
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">反馈内容</label>
<textarea name="content" class="repo-textarea repo-input" rows="8" required placeholder="请详细描述您的问题或建议..."></textarea>
</div>
<div class="repo-flex repo-gap-3">
<button type="submit" class="repo-btn repo-btn--primary">提交反馈</button>
<button type="reset" class="repo-btn repo-btn--secondary">清空</button>
</div>
</form>
</div>
</div>
<div class="repo-card repo-mt-6">
<div class="repo-card__body">
<h3 class="repo-font-semibold repo-mb-3">常见问题</h3>
<div class="repo-space-y-3">
<div>
<div class="repo-font-medium">Q: ap_ds 支持哪些音频格式?</div>
<p class="repo-text-secondary repo-text-sm">A: 支持 MP3、FLAC、OGG、WAV 四种主流格式。</p>
</div>
<div>
<div class="repo-font-medium">Q: 遇到 SSL 证书错误怎么办?</div>
<p class="repo-text-secondary repo-text-sm">A: ap_ds 内置了多层下载策略,会自动处理 SSL 问题。如仍遇到问题,请手动下载 SDL2 库。</p>
</div>
<div>
<div class="repo-font-medium">Q: 如何获取 DAP 播放记录?</div>
<p class="repo-text-secondary repo-text-sm">A: 使用 get_dap_recordings() 方法获取,或使用 save_dap_to_json() 保存为文件。</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+679
View File
@@ -0,0 +1,679 @@
<!-- 英文 SEO 标签 -->
<meta name="description" content="ap_ds is an ultra-lightweight Python audio library (2.5MB) for high-quality playback & precise metadata parsing of MP3, FLAC, OGG, WAV. Zero external dependencies, non-blocking architecture. Created by Dvs (DvsXT), released 2026-03-01. A precise revolution in Python audio.">
<meta name="keywords" content="Python audio library, lightweight audio player, MP3, FLAC, OGG, WAV metadata, non-blocking playback, ap_ds, DvsXT, zero dependencies, 2026 Python ecosystem">
<meta property="og:title" content="ap_ds: Ultra-Lightweight Python Audio Library (2.5MB)">
<meta property="og:description" content="Zero external deps, non-blocking playback, precise metadata parsing for MP3/FLAC/OGG/WAV. Released 2026-03-01 by Dvs (DvsXT).">
<meta property="og:type" content="software">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="ap_ds - Python Audio Library, 2.5MB">
<meta name="twitter:description" content="Solve the lightweight vs completeness contradiction in Python audio. Non-blocking, zero deps, 4 major formats.">
<!-- 中文 SEO 标签 -->
<meta name="description" lang="zh-CN" content="ap_ds 是一个超轻量级 Python 音频库(仅 2.5MB),支持 MP3、FLAC、OGG、WAV 四种主流格式的高质量播放和精确元数据解析。零外部 Python 依赖,非阻塞播放架构让 GUI 应用流畅运行。由 Dvs (DvsXT) 创作,发布于 2026-03-01,是 Python 音频领域的精准革命。">
<meta name="keywords" lang="zh-CN" content="Python音频库, 轻量级音频播放, MP3, FLAC, OGG, WAV元数据解析, 非阻塞播放, ap_ds, DvsXT, 零依赖, 2026 Python生态">
<meta property="og:title" content="ap_ds:超轻量级 Python 音频库 (2.5MB)">
<meta property="og:description" lang="zh-CN" content="零外部依赖,非阻塞播放,精准解析 MP3/FLAC/OGG/WAV。2026-03-01 由 Dvs (DvsXT) 发布。终结 Python 音频库的「完整性与轻量化」矛盾。">
<!-- 新增:官方自建代码仓库说明 -->
<meta name="description" content="This domain is the official self-hosted code repository for ap_ds. Direct source, stable releases, and authoritative documentation.">
<meta name="description" lang="zh-CN" content="本域名是 ap_ds 官方自建代码仓库。提供直接源代码、稳定版本及权威文档。"> {% extends "base.html" %}
{% block title %}ap_ds · lightweight Python audio solution ap_ds · 轻量级Python音频库{% endblock %}
{% block extra_head %}
<link rel="canonical" href="https://apds.top" />
<link rel="me" href="https://gitee.com/dssxt/ap_ds" />
<link rel="me" href="https://gitcode.com/dvsxt/ap_ds" />
<link rel="nofollow" href="https://github.com/dvs-web/ap_ds" />
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css">
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"></script>
<style>
/* 首页自定义样式 - 基于 css.css 变量 */
.home-container {
max-width: 1280px;
margin: 0 auto;
padding: var(--repo-spacing-6) var(--repo-spacing-4);
}
/* Hero 区域 */
.hero-section {
text-align: center;
padding: var(--repo-spacing-10) 0 var(--repo-spacing-8);
background: linear-gradient(135deg, var(--repo-color-bg) 0%, var(--repo-color-bg-secondary) 100%);
border-radius: var(--repo-radius-xl);
margin-bottom: var(--repo-spacing-8);
}
.hero-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: var(--repo-spacing-4);
background: linear-gradient(135deg, var(--repo-color-primary) 0%, #6366f1 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero-subtitle {
font-size: var(--repo-font-size-xl);
color: var(--repo-color-text-secondary);
margin-bottom: var(--repo-spacing-6);
}
.version-badges {
display: flex;
justify-content: center;
gap: var(--repo-spacing-3);
flex-wrap: wrap;
margin-top: var(--repo-spacing-4);
}
/* 目录 */
.toc-section {
background: var(--repo-color-bg-secondary);
border-radius: var(--repo-radius-lg);
padding: var(--repo-spacing-6);
margin-bottom: var(--repo-spacing-8);
border-left: 4px solid var(--repo-color-primary);
}
.toc-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--repo-spacing-2);
margin-top: var(--repo-spacing-4);
}
.toc-link {
color: var(--repo-color-text-secondary);
text-decoration: none;
font-size: var(--repo-font-size-sm);
padding: var(--repo-spacing-1) 0;
display: inline-block;
}
.toc-link:hover {
color: var(--repo-color-primary);
text-decoration: none;
}
/* 内容区块 */
.content-card {
background: var(--repo-color-bg);
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-lg);
padding: var(--repo-spacing-8);
margin-bottom: var(--repo-spacing-8);
box-shadow: var(--repo-shadow-sm);
}
.content-card h2 {
font-size: var(--repo-font-size-2xl);
font-weight: 600;
margin-bottom: var(--repo-spacing-6);
padding-bottom: var(--repo-spacing-3);
border-bottom: 2px solid var(--repo-color-border);
color: var(--repo-color-text);
}
.content-card h3 {
font-size: var(--repo-font-size-xl);
font-weight: 600;
margin-top: var(--repo-spacing-6);
margin-bottom: var(--repo-spacing-4);
color: var(--repo-color-text);
}
.content-card h4 {
font-size: var(--repo-font-size-lg);
font-weight: 600;
margin-top: var(--repo-spacing-4);
margin-bottom: var(--repo-spacing-3);
color: var(--repo-color-text);
}
/* 特性网格 */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--repo-spacing-5);
margin-bottom: var(--repo-spacing-8);
}
.feature-card {
background: var(--repo-color-bg-secondary);
border-radius: var(--repo-radius-lg);
padding: var(--repo-spacing-5);
border: 1px solid var(--repo-color-border);
transition: all 0.2s ease;
}
.feature-card:hover {
transform: translateY(-2px);
box-shadow: var(--repo-shadow-md);
border-color: var(--repo-color-primary);
}
.feature-icon {
font-size: 2rem;
margin-bottom: var(--repo-spacing-3);
}
.feature-title {
font-weight: 600;
font-size: var(--repo-font-size-lg);
margin-bottom: var(--repo-spacing-2);
color: var(--repo-color-text);
}
.feature-desc {
color: var(--repo-color-text-secondary);
font-size: var(--repo-font-size-sm);
}
/* 对比表格 */
.comparison-table {
width: 100%;
border-collapse: collapse;
margin: var(--repo-spacing-4) 0;
}
.comparison-table th,
.comparison-table td {
border: 1px solid var(--repo-color-border);
padding: var(--repo-spacing-3) var(--repo-spacing-4);
text-align: left;
}
.comparison-table th {
background: var(--repo-color-bg-secondary);
font-weight: 600;
}
/* 提示框 */
.note-box {
background: #eef2ff;
border-left: 4px solid var(--repo-color-primary);
border-radius: var(--repo-radius-md);
padding: var(--repo-spacing-4) var(--repo-spacing-6);
margin: var(--repo-spacing-4) 0;
color: #1e40af;
}
.warning-box {
background: #fffbeb;
border-left: 4px solid #f59e0b;
border-radius: var(--repo-radius-md);
padding: var(--repo-spacing-4) var(--repo-spacing-6);
margin: var(--repo-spacing-4) 0;
color: #92400e;
}
.success-box {
background: #ecfdf5;
border-left: 4px solid #10b981;
border-radius: var(--repo-radius-md);
padding: var(--repo-spacing-4) var(--repo-spacing-6);
margin: var(--repo-spacing-4) 0;
color: #065f46;
}
/* 代码块 */
.code-block {
position: relative;
background: #0f172a;
border-radius: var(--repo-radius-lg);
margin: var(--repo-spacing-4) 0;
overflow: hidden;
}
.code-block pre {
margin: 0;
padding: var(--repo-spacing-5);
overflow-x: auto;
background: transparent !important;
}
.code-block code {
font-family: var(--repo-font-family-mono);
font-size: var(--repo-font-size-sm);
color: #f8fafc;
}
.copy-btn {
position: absolute;
top: 12px;
right: 16px;
background: #334155;
color: #f1f5f9;
border: none;
border-radius: var(--repo-radius-full);
padding: 4px 14px;
font-size: var(--repo-font-size-xs);
cursor: pointer;
transition: 0.2s;
opacity: 0.7;
}
.copy-btn:hover {
opacity: 1;
background: #475569;
}
.copy-btn.copied {
background: #059669;
}
/* 许可证区域 */
.license-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--repo-spacing-4);
margin-top: var(--repo-spacing-6);
}
.license-card {
background: var(--repo-color-bg-secondary);
border-radius: var(--repo-radius-lg);
padding: var(--repo-spacing-5);
border: 1px solid var(--repo-color-border);
}
.license-card h4 {
margin-top: 0;
margin-bottom: var(--repo-spacing-3);
}
/* 公告区域 */
.announcement-content {
line-height: 1.7;
}
.announcement-content h1,
.announcement-content h2,
.announcement-content h3 {
margin-top: var(--repo-spacing-5);
margin-bottom: var(--repo-spacing-3);
}
.announcement-content h1 { font-size: var(--repo-font-size-xl); border-bottom: 1px solid var(--repo-color-border); padding-bottom: var(--repo-spacing-2); }
.announcement-content h2 { font-size: var(--repo-font-size-lg); border-bottom: 1px solid var(--repo-color-border); padding-bottom: var(--repo-spacing-2); }
.announcement-content h3 { font-size: var(--repo-font-size-base); }
.announcement-content p { margin-bottom: var(--repo-spacing-4); }
.announcement-content ul, .announcement-content ol { padding-left: var(--repo-spacing-6); margin-bottom: var(--repo-spacing-4); }
.announcement-content code {
background: var(--repo-color-bg-tertiary);
padding: 0.2em 0.4em;
border-radius: var(--repo-radius-sm);
font-size: 0.9em;
}
.announcement-content pre {
background: #0f172a;
padding: var(--repo-spacing-4);
border-radius: var(--repo-radius-md);
overflow-x: auto;
margin: var(--repo-spacing-4) 0;
}
.announcement-content pre code {
background: none;
padding: 0;
color: #f8fafc;
}
.announcement-content blockquote {
border-left: 4px solid var(--repo-color-border);
padding-left: var(--repo-spacing-4);
margin: var(--repo-spacing-4) 0;
color: var(--repo-color-text-secondary);
}
/* 响应式 */
@media (max-width: 768px) {
.hero-title {
font-size: 2rem;
}
.content-card {
padding: var(--repo-spacing-5);
}
.feature-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<div class="home-container">
<!-- Hero 区域 -->
<div class="hero-section">
<h1 class="hero-title" id="hero-title">ap_ds: 轻量级 Python 音频革命</h1>
<p class="hero-subtitle" id="hero-subtitle">2.5MB 终结 160MB 臃肿时代</p>
<div class="version-badges" id="version-badges">
<span class="repo-tag repo-tag--primary">v3.0.0 LTS</span>
<span class="repo-tag repo-tag--success">仅 2.5MB</span>
<span class="repo-tag repo-tag--warning">跨平台 W/M/L</span>
</div>
</div>
<!-- 目录 -->
<div class="toc-section">
<h3 class="repo-text-lg repo-font-semibold repo-mb-4"><i class="fa fa-list-ul" style="margin-right: 8px; color: var(--repo-color-primary);"></i>目录</h3>
<div class="toc-grid">
<a href="#introduction" class="toc-link">1. 项目介绍</a>
<a href="#features" class="toc-link">2. 核心特性</a>
<a href="#comparison" class="toc-link">3. 技术对比</a>
<a href="#installation" class="toc-link">4. 安装与使用</a>
<a href="#license" class="toc-link">5. 开源许可证</a>
<a href="#faq" class="toc-link">6. 常见问题</a>
<a href="#contact" class="toc-link">7. 联系与支持</a>
<a href="#announcement" class="toc-link">8. 最新公告</a>
</div>
</div>
<!-- 1. 项目介绍 -->
<div class="content-card" id="introduction">
<h2>1. 项目介绍</h2>
<div class="note-box">
<p><strong>📋 官方信息</strong></p>
<p>项目名称:ap_ds</p>
<p>官方网站:<a href="https://apds.top" style="color: var(--repo-color-primary);">https://apds.top</a></p>
<p>PyPI:<a href="https://pypi.org/project/ap_ds/" style="color: var(--repo-color-primary);">https://pypi.org/project/ap_ds/</a></p>
<p>开发者:Dvs (DvsXT)</p>
</div>
<p>ap_ds 是一个极轻量级(2.5MB)的 Python 音频库,用于播放和精确解析 MP3、FLAC、OGG 和 WAV 文件。它没有外部 Python 依赖,仅使用 Python 标准库,为非阻塞播放提供平滑的 GUI 应用程序体验。</p>
<div class="success-box">
<p><strong>🎉 v3.0.0 LTS 长期支持版本</strong></p>
<p>首个长期支持版本,提供 <strong>永久免费技术支持</strong>,哈希验证下载,确定性资源清理。</p>
</div>
</div>
<!-- 2. 核心特性 -->
<div class="content-card" id="features">
<h2>2. 核心特性</h2>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">⚡</div>
<div class="feature-title">极致轻量</div>
<div class="feature-desc">Windows 2.5MB · macOS 3.36MB · Linux 就绪</div>
</div>
<div class="feature-card">
<div class="feature-icon">🎵</div>
<div class="feature-title">四种格式</div>
<div class="feature-desc">MP3 / FLAC / OGG / WAV</div>
</div>
<div class="feature-card">
<div class="feature-icon">📊</div>
<div class="feature-title">精确元数据</div>
<div class="feature-desc">WAV/FLAC 100% 准确</div>
</div>
<div class="feature-card">
<div class="feature-icon">▶️</div>
<div class="feature-title">非阻塞播放</div>
<div class="feature-desc">完美适配 GUI 应用</div>
</div>
</div>
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--repo-spacing-8);">
<div>
<h3>📦 体积对比</h3>
<ul style="list-style: none; padding-left: 0;">
<li style="margin-bottom: var(--repo-spacing-2);">🟢 <strong>ap_ds</strong> Windows: 2.5MB</li>
<li style="margin-bottom: var(--repo-spacing-2);">🍎 macOS: 3.36MB</li>
<li style="margin-bottom: var(--repo-spacing-2);">🐧 Linux: 动态链接 ~50KB</li>
<li style="margin-bottom: var(--repo-spacing-2);">🔴 FFmpeg: ≥160MB</li>
<li>🟡 Pygame + 解析库: 臃肿且功能不全</li>
</ul>
</div>
<div>
<h3>⚙️ 为什么这么小?</h3>
<ul>
<li>专注:仅播放和解析,无编辑/转码</li>
<li>基于工业级 SDL2</li>
<li>逐字节优化</li>
<li>跨平台 W/M/L</li>
</ul>
</div>
</div>
</div>
<!-- 3. 技术对比 -->
<div class="content-card" id="comparison">
<h2>3. 技术对比</h2>
<h3>第一章:Pygame —— 游戏引擎的"音频残疾"</h3>
<div class="code-block">
<pre class="language-python"><code># Pygame 不可靠的音频时长
my_sound = pygame.mixer.Sound('my_song.mp3')
total_length = my_sound.get_length() # 仅对 WAV 可靠!</code></pre>
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
</div>
<div class="warning-box">
⚠️ Pygame 的 <code>Sound.get_length()</code> 仅对 WAV 格式正确,MP3/OGG/FLAC 返回无效值。
</div>
<h3>第二章:FFmpeg 套件 —— 160MB 的"至少"之痛</h3>
<table class="comparison-table">
<thead><tr><th>组件</th><th>功能</th><th>大小</th></tr></thead>
<tbody>
<tr><td>ffplay.exe</td><td>播放核心</td><td>≈80MB</td></tr>
<tr><td>ffprobe.exe</td><td>元数据探测</td><td>≈80MB</td></tr>
<tr><td>ffmpeg.exe</td><td>转换器</td><td>≈80MB</td></tr>
</tbody>
</table>
<div class="note-box">🔍 三个独立可执行文件,无 Python 封装,总计 ≥160MB。</div>
<h3>第三章:ap_ds 革命</h3>
<div class="success-box">
<p><strong>七大技术突破</strong></p>
<ul>
<li>✓ 2.5MB 解决 160MB 问题</li>
<li>✓ 非阻塞消除 GUI 卡顿</li>
<li>✓ 精确进度查询</li>
<li>✓ 零依赖部署</li>
<li>✓ AID 多音频管理</li>
<li>✓ SDL2 性能</li>
<li>✓ 智能 C 依赖跨平台</li>
</ul>
</div>
</div>
<!-- 4. 安装与使用 -->
<div class="content-card" id="installation">
<h2>4. 安装与使用</h2>
<div class="code-block">
<pre class="language-bash"><code>pip install ap_ds</code></pre>
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
</div>
<h3>基础示例</h3>
<div class="code-block">
<pre class="language-python"><code>from ap_ds import AudioLibrary
lib = AudioLibrary()
aid = lib.play_from_file("music.mp3")
lib.pause_audio(aid)
lib.seek_audio(aid, 30.5)
duration = lib.stop_audio(aid)</code></pre>
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
</div>
<h3>DAP 播放列表 (v2.3.0+)</h3>
<div class="code-block">
<pre class="language-python"><code>aid1 = lib.play_from_file("song1.mp3")
recordings = lib.get_dap_recordings()
lib.save_dap_to_json("my_playlist.ap-ds-dap")</code></pre>
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
</div>
<h3>淡入淡出效果 (v2.4.0+)</h3>
<div class="code-block">
<pre class="language-python"><code># 2秒淡入
lib.fadein_music(aid, loops=-1, ms=2000)
# 3秒淡出
lib.fadeout_music(ms=3000)
# 检查播放状态
if lib.is_music_playing():
print("音乐播放中")</code></pre>
<button class="copy-btn" onclick="copyCode(this)"><i class="fa fa-clipboard"></i> 复制</button>
</div>
</div>
<!-- 5. 开源许可证 -->
<div class="content-card" id="license">
<h2>5. 开源许可证</h2>
<div class="license-grid">
<div class="license-card">
<h4>✅ 您可以</h4>
<ul>
<li>商业使用</li>
<li>修改代码</li>
<li>闭源集成</li>
<li>销售解决方案</li>
<li>云服务部署</li>
</ul>
</div>
<div class="license-card">
<h4>❌ 您不可以</h4>
<ul>
<li>使用原始品牌名称</li>
<li>删除版权声明</li>
<li>用于非法用途</li>
<li>专利侵权诉讼</li>
</ul>
</div>
<div class="license-card">
<h4>👍 您应该</h4>
<ul>
<li>保留版权声明</li>
<li>标注来源归属</li>
<li>报告安全问题</li>
<li>修改版本用独立品牌</li>
</ul>
</div>
</div>
<p class="repo-text-secondary repo-text-sm repo-mt-4">详细许可证请查看 <code>AP_DS_LICENSE.MD</code> 文件。</p>
</div>
<!-- 6. 常见问题 -->
<div class="content-card" id="faq">
<h2>6. 常见问题</h2>
<div><p class="repo-font-semibold">问:公司可以将其集成到商业产品中吗?</p><div class="success-box">✅ 完全可以,无需付费、无需通知、无需授权。</div></div>
<div><p class="repo-font-semibold repo-mt-4">问:我可以修改并重新分发吗?</p><div class="success-box">✅ 可以,但必须使用独立品牌、保留版权、标注来源、提供维护者声明。</div></div>
<div><p class="repo-font-semibold repo-mt-4">问:我必须开源我的修改吗?</p><div class="warning-box">❌ 不需要,您可以闭源分发。</div></div>
<div><p class="repo-font-semibold repo-mt-4">问:在云服务中使用需要付费吗?</p><div class="warning-box">❌ 不需要,但欢迎贡献代码。</div></div>
<div><p class="repo-font-semibold repo-mt-4">问:技术支持收费吗?</p><div class="success-box">✅ 永久免费技术支持,通过 GitCode Issues 或邮件联系。</div></div>
</div>
<!-- 7. 联系与支持 -->
<div class="content-card" id="contact">
<h2>7. 联系与支持</h2>
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--repo-spacing-6);">
<div style="background: var(--repo-color-bg-secondary); padding: var(--repo-spacing-5); border-radius: var(--repo-radius-lg);">
<h4>📧 授权咨询</h4>
<p>me@dvsyun.top 或 dvs6666@163.com</p>
<p class="repo-text-sm repo-text-tertiary">7个工作日内回复</p>
</div>
<div style="background: var(--repo-color-bg-secondary); padding: var(--repo-spacing-5); border-radius: var(--repo-radius-lg);">
<h4>🛠️ 技术支持</h4>
<p>GitCode Issues · 官方文档 · 邮件支持</p>
<p class="repo-text-sm repo-text-success">✅ 永久免费</p>
</div>
</div>
<div class="repo-mt-6" style="background: var(--repo-color-bg-secondary); border-radius: var(--repo-radius-lg); padding: var(--repo-spacing-5);">
<h3>🔗 官方仓库</h3>
<div class="grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--repo-spacing-4); margin-top: var(--repo-spacing-3);">
<div>
<p>✅ <strong>主仓库:</strong><a href="https://gitcode.com/dvsxt/ap_ds" class="repo-text-link">GitCode</a></p>
<p>✅ <strong>国内镜像:</strong><a href="https://gitee.com/dssxt/ap_ds" class="repo-text-link">Gitee</a></p>
</div>
<div>
<p>❌ <strong>已废弃:</strong><a href="https://github.com/dvs-web/ap_ds" class="repo-text-danger">GitHub</a></p>
<p>🔗 <strong>项目主页:</strong><a href="https://apds.top" class="repo-text-link">apds.top</a></p>
</div>
</div>
</div>
</div>
<!-- 8. 最新公告 -->
<div class="content-card" id="announcement">
<h2>8. 最新公告</h2>
<div id="announcement-content" class="announcement-content">加载中...</div>
</div>
</div>
<script>
// 复制代码函数
window.copyCode = function(btn) {
const pre = btn.parentElement.querySelector('pre');
const code = pre.innerText;
navigator.clipboard.writeText(code).then(() => {
btn.classList.add('copied');
btn.innerHTML = '<i class="fa fa-check"></i> 已复制';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = '<i class="fa fa-clipboard"></i> 复制';
}, 2000);
});
};
// 从后端获取版本信息
fetch('/api/version-info')
.then(res => res.json())
.then(data => {
if (data.title) document.getElementById('hero-title').innerHTML = data.title;
if (data.subtitle) document.getElementById('hero-subtitle').innerHTML = data.subtitle;
const badgesDiv = document.getElementById('version-badges');
if (data.version || data.size) {
badgesDiv.innerHTML = `
<span class="repo-tag repo-tag--primary">${data.version || 'v3.0.0 LTS'}</span>
<span class="repo-tag repo-tag--success">仅 ${data.size || '2.5MB'}</span>
<span class="repo-tag repo-tag--warning">跨平台 W/M/L</span>
`;
}
})
.catch(() => {});
// 加载最新公告
fetch('/api/latest-announcement')
.then(res => res.json())
.then(data => {
const container = document.getElementById('announcement-content');
if (data.html) {
container.innerHTML = data.html;
// 高亮代码块
if (typeof Prism !== 'undefined') {
Prism.highlightAllUnder(container);
}
// 为代码块添加复制按钮
container.querySelectorAll('pre').forEach(pre => {
if (!pre.parentElement.classList.contains('code-block')) {
const wrapper = document.createElement('div');
wrapper.className = 'code-block';
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
const btn = document.createElement('button');
btn.className = 'copy-btn';
btn.innerHTML = '<i class="fa fa-clipboard"></i> 复制';
btn.onclick = function() { window.copyCode(btn); };
wrapper.appendChild(btn);
}
});
} else {
container.innerHTML = '<p class="repo-text-tertiary">暂无公告</p>';
}
})
.catch(() => {
document.getElementById('announcement-content').innerHTML = '<p class="repo-text-tertiary">无法加载公告</p>';
});
</script>
{% endblock %}
+297
View File
@@ -0,0 +1,297 @@
<!-- 英文 SEO 标签 -->
<meta name="description" content="Official releases page for ap_ds. Full version list: v3.0.0 LTS (current stable), branch APDSLTS001, release date 2026-03-01. Each version includes ZIP/TAR.GZ/WHL downloads, detailed changelogs (deterministic cleanup, hash verification), upgrade guides, and version status labels (stable/withdrawn/incident).">
<meta name="keywords" content="ap_ds releases, download v3.0.0, changelog, LTS version, Python audio library stable release, APDSLTS001, hash verification, deterministic cleanup, WHL download">
<meta property="og:title" content="ap_ds Releases – v3.0.0 LTS & Version Archive">
<meta property="og:description" content="Official ap_ds release history. Download stable builds, read detailed changelogs, check version status (stable/withdrawn/incident).">
<meta property="og:type" content="software">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="ap_ds Releases & Changelogs">
<!-- 中文 SEO 标签 -->
<meta name="description" lang="zh-CN" content="ap_ds 官方发行版页面。完整版本列表:v3.0.0 LTS(当前稳定版),分支 APDSLTS001,发布日期 2026-03-01。每个版本附 ZIP/TAR.GZ/WHL 下载、详细更新日志(确定性资源清理、哈希验证)、升级指南及版本状态标注(稳定版/已撤回/开发事故)。">
<meta name="keywords" lang="zh-CN" content="ap_ds 发行版, 下载 v3.0.0, 更新日志, LTS版本, Python音频库稳定版, APDSLTS001, 哈希验证, 确定性清理, WHL下载">
<meta property="og:title" lang="zh-CN" content="ap_ds 发行版 – v3.0.0 LTS 及版本归档">
<meta property="og:description" lang="zh-CN" content="ap_ds 官方版本历史。下载稳定版,阅读详细更新日志,查看版本状态(稳定/已撤回/开发事故)。">{% extends "base.html" %}{% block content %}{% block title %}ap_ds Releases – Version History, Changelogs & Stable Downloads (v3.0.0 LTS){% endblock %}
<div class="repo-header">
<div class="repo-path">
<span>📦 发行版</span>
</div>
<p style="color: var(--repo-color-text-tertiary); margin-top: 8px;">所有 ap_ds 版本下载</p>
</div>
<!-- 版本列表 -->
<div class="file-list">
<div class="file-list-header">
<span>版本</span>
<span>分支</span>
<span>发布日期</span>
<span>下载</span>
</div>
{% for release in releases %}
<div class="file-row">
<div class="file-name" style="flex: 0.3;">
<span class="icon">🏷️</span>
<strong style="color: var(--repo-color-primary);">{{ release.version }}</strong>
</div>
<div class="file-message" style="flex: 0.3;">{{ release.branch }}</div>
<div class="file-time" style="flex: 0.2;">{{ release.date }}</div>
<div style="display: flex; gap: 8px; flex: 0.2;">
<a href="{{ url_for('download_zip', branch=release.branch, version=release.version) }}" class="btn btn-secondary" style="padding: 4px 8px;">ZIP</a>
<a href="{{ url_for('download_tar', branch=release.branch, version=release.version) }}" class="btn btn-secondary" style="padding: 4px 8px;">TAR.GZ</a>
<a href="{{ url_for('download_whl', version=release.version) }}" class="btn btn-primary" style="padding: 4px 8px;">WHL</a>
</div>
</div>
{% endfor %}
</div>
<!-- 版本详情 - 展开/收起 -->
<div class="releases-details">
{% for release in releases %}
<div class="release-card" data-version="{{ release.version }}">
<div class="release-header" onclick="toggleRelease('{{ release.version }}')">
<div class="release-header-left">
<span class="release-version">{{ release.version }}</span>
<span class="release-branch">{{ release.branch }}</span>
<span class="release-date">{{ release.date }}</span>
</div>
<div class="release-header-right">
<span class="release-status">
{% if release.has_whl %}
<span class="repo-tag repo-tag--success">PyPI</span>
{% endif %}
</span>
<span class="release-toggle" id="toggle-{{ release.version }}">▼</span>
</div>
</div>
<div class="release-body" id="release-{{ release.version }}" style="display: none;">
<div class="release-content">
{% if release.announcement %}
{{ release.announcement|safe }}
{% else %}
<div class="info-message">
📄 暂无版本说明文档。请查看 <a href="{{ url_for('view_file', file_path=release.branch + '/' + release.version + '/README.md', branch=release.branch, version=release.version) }}">README.md</a>
</div>
{% endif %}
</div>
<div class="release-downloads">
<a href="{{ url_for('download_zip', branch=release.branch, version=release.version) }}" class="btn btn-secondary">📦 下载 ZIP</a>
<a href="{{ url_for('download_tar', branch=release.branch, version=release.version) }}" class="btn btn-secondary">📦 下载 TAR.GZ</a>
<a href="{{ url_for('download_whl', version=release.version) }}" class="btn btn-primary">📦 下载 WHL</a>
<a href="{{ url_for('view_file', file_path=release.branch + '/' + release.version + '/README.md', branch=release.branch, version=release.version) }}" class="btn btn-secondary">📖 查看 README</a>
</div>
</div>
</div>
{% endfor %}
</div>
<style>
.releases-details {
margin-top: 32px;
display: flex;
flex-direction: column;
gap: 12px;
}
.release-card {
border: 1px solid var(--repo-color-border);
border-radius: var(--repo-radius-lg, 8px);
overflow: hidden;
background-color: var(--repo-color-bg);
}
.release-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: var(--repo-color-bg-secondary);
cursor: pointer;
transition: background 0.2s;
}
.release-header:hover {
background-color: var(--repo-color-bg-hover);
}
.release-header-left {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.release-version {
font-size: 18px;
font-weight: 600;
color: var(--repo-color-primary);
}
.release-branch {
font-size: 13px;
color: var(--repo-color-text-secondary);
background-color: var(--repo-color-bg-tertiary);
padding: 2px 8px;
border-radius: 20px;
}
.release-date {
font-size: 13px;
color: var(--repo-color-text-tertiary);
}
.release-header-right {
display: flex;
align-items: center;
gap: 16px;
}
.release-toggle {
font-size: 16px;
color: var(--repo-color-text-tertiary);
transition: transform 0.2s;
}
.release-card.open .release-toggle {
transform: rotate(180deg);
}
.release-body {
padding: 24px;
border-top: 1px solid var(--repo-color-border);
background-color: var(--repo-color-bg);
}
.release-content {
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid var(--repo-color-border);
}
/* README 内容样式 */
.release-content h1, .release-content h2, .release-content h3 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
.release-content h1 { font-size: 1.8em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
.release-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
.release-content h3 { font-size: 1.25em; }
.release-content h4 { font-size: 1.1em; }
.release-content p { margin-bottom: 16px; line-height: 1.6; }
.release-content code {
background-color: var(--repo-color-bg-tertiary);
padding: 0.2em 0.4em;
border-radius: 4px;
font-family: var(--repo-font-family-mono, monospace);
font-size: 0.9em;
}
.release-content pre {
background-color: var(--repo-color-bg-tertiary);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin-bottom: 16px;
}
.release-content pre code {
background: none;
padding: 0;
}
.release-content ul, .release-content ol {
padding-left: 2em;
margin-bottom: 16px;
}
.release-content blockquote {
border-left: 4px solid var(--repo-color-border);
padding-left: 16px;
color: var(--repo-color-text-tertiary);
margin-bottom: 16px;
}
.release-content table {
border-collapse: collapse;
margin-bottom: 16px;
width: 100%;
}
.release-content th, .release-content td {
border: 1px solid var(--repo-color-border);
padding: 8px 12px;
}
.release-content th {
background-color: var(--repo-color-bg-secondary);
}
.release-content hr {
border: none;
border-top: 1px solid var(--repo-color-border);
margin: 24px 0;
}
.release-content img {
max-width: 100%;
}
.release-downloads {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.info-message {
padding: 16px;
background-color: var(--repo-color-bg-secondary);
border-radius: var(--repo-radius-md, 6px);
color: var(--repo-color-text-secondary);
text-align: center;
}
.repo-tag--success {
background-color: rgba(16, 185, 129, 0.1);
color: #10b981;
padding: 2px 8px;
border-radius: 20px;
font-size: 12px;
}
@media (max-width: 768px) {
.release-header-left {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.release-downloads {
flex-direction: column;
}
.release-downloads .btn {
text-align: center;
}
}
</style>
<script>
function toggleRelease(version) {
var body = document.getElementById('release-' + version);
var card = document.querySelector('.release-card[data-version="' + version + '"]');
if (body.style.display === 'none') {
body.style.display = 'block';
card.classList.add('open');
} else {
body.style.display = 'none';
card.classList.remove('open');
}
}
// 默认展开第一个版本
document.addEventListener('DOMContentLoaded', function() {
var firstRelease = document.querySelector('.release-card');
if (firstRelease) {
var version = firstRelease.getAttribute('data-version');
if (version) {
var body = document.getElementById('release-' + version);
if (body) {
body.style.display = 'block';
firstRelease.classList.add('open');
}
}
}
});
</script>
{% endblock %}
+59
View File
@@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block title %}{{ metadata.name }} · ap_ds{% endblock %}
{% block content %}
<div class="repo-header">
<div class="repo-path">
<a href="/code">ap_ds</a>
<span class="separator">/</span>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}">{{ branch }}</a>
<span class="separator">/</span>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}">{{ version }}</a>
{% if branch and version %}
{# 将反斜杠替换为正斜杠 #}
{% set normalized_path = metadata.path.replace('\\', '/') %}
{% set relative_path = normalized_path.replace(branch + '/' + version + '/', '') %}
{% if relative_path and '/' in relative_path %}
{% set parts = relative_path.split('/') %}
{% for part in parts[:-1] %}
<span class="separator">/</span>
<a href="#">{{ part }}</a>
{% endfor %}
{% endif %}
{% endif %}
<span class="separator">/</span>
<span>{{ metadata.name }}</span>
</div>
</div>
<div class="code-header">
<div>
<span style="font-family: monospace;">{{ metadata.name }}</span>
<span style="margin-left: 12px; font-size: 12px; color: #848d97;">{{ metadata.size }} 字节</span>
</div>
<div style="display: flex; gap: 8px;">
<button class="copy-btn" onclick="copyCode(this)">复制代码</button>
<a href="{{ url_for('download_file', file_path=metadata.path.replace('\\', '/')) }}" class="copy-btn" style="text-decoration: none;">下载文件</a>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="copy-btn" style="text-decoration: none;">返回</a>
</div>
</div>
<div class="code-content">
<pre><code class="language-python" id="code-content">{{ content|escape }}</code></pre>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
hljs.highlightElement(document.querySelector('#code-content'));
});
function copyCode(btn) {
var code = document.getElementById('code-content').innerText;
navigator.clipboard.writeText(code).then(function() {
var originalText = btn.innerText;
btn.innerText = '已复制!';
setTimeout(function() { btn.innerText = originalText; }, 2000);
});
}
</script>
{% endblock %}
+40
View File
@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}{{ metadata.name }} - ap_ds{% endblock %}
{% block content %}
<div class="repo-container">
<div class="repo-card repo-mb-4">
<div class="repo-card__body">
<div class="repo-flex repo-justify-between repo-items-center">
<div>
<h2 class="repo-text-lg repo-font-semibold">{{ metadata.name }}</h2>
<div class="repo-text-sm repo-text-tertiary repo-mt-1">
{{ metadata.size }} 字节 · 最后修改 {{ metadata.modified }} · 类型: {{ metadata.extension }}
</div>
</div>
<div class="repo-flex repo-gap-2">
<a href="{{ url_for('download_file', file_path=metadata.path) }}" class="repo-btn repo-btn--sm repo-btn--primary">下载文件</a>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="repo-btn repo-btn--sm repo-btn--ghost">返回</a>
</div>
</div>
</div>
</div>
{% if is_text %}
<div class="repo-card">
<div class="repo-card__body">
<pre style="background: var(--repo-color-bg-tertiary); padding: 1rem; border-radius: 8px; overflow-x: auto; font-family: monospace; font-size: 13px;">{{ content }}</pre>
</div>
</div>
{% else %}
<div class="repo-card repo-text-center repo-py-8">
<div class="repo-card__body">
<span class="icon-file" style="font-size: 4rem; color: var(--repo-color-text-tertiary);"></span>
<p class="repo-text-secondary repo-mt-4">此文件类型无法在线预览</p>
<a href="{{ url_for('download_file', file_path=metadata.path) }}" class="repo-btn repo-btn--primary repo-mt-4">下载文件</a>
</div>
</div>
{% endif %}
</div>
{% endblock %}
+34
View File
@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}{{ metadata.name }} · ap_ds{% endblock %}
{% block content %}
<div class="repo-header">
<div class="repo-path">
<a href="/code">ap_ds</a>
<span class="separator">/</span>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}">{{ branch }}</a>
<span class="separator">/</span>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}">{{ version }}</a>
<span class="separator">/</span>
<span>{{ metadata.name }}</span>
</div>
</div>
<div class="code-header">
<div>
<span style="font-family: monospace;">{{ metadata.name }}</span>
<span style="margin-left: 12px; font-size: 12px; color: var(--repo-color-text-tertiary);">{{ metadata.size }} 字节</span>
<span style="margin-left: 12px; font-size: 12px; color: var(--repo-color-text-tertiary);">最后修改 {{ metadata.modified }}</span>
</div>
<div style="display: flex; gap: 8px;">
<a href="{{ url_for('download_file', file_path=metadata.path) }}" class="copy-btn" style="text-decoration: none;">下载文件</a>
<a href="{{ url_for('code_browser') }}?branch={{ branch }}&version={{ version }}" class="copy-btn" style="text-decoration: none;">返回</a>
</div>
</div>
<!-- 关键修改:使用 |safe 过滤器 -->
<div class="readme-content" style="border: 1px solid var(--repo-color-border); border-top: none; border-radius: 0 0 var(--repo-radius-lg, 8px) var(--repo-radius-lg, 8px);">
{{ html_content|safe }}
</div>
{% endblock %}