Skip to content

我的服务器是海外的 ​

应该不让访问B站,就先不部署了,先存一下代码

代码 ​

php
<?php
error_reporting(0);
header('Content-Type: text/html; charset=utf-8');

define('VIEW_API', 'https://api.bilibili.com/x/web-interface/view?');
define('PLAY_API', 'https://api.bilibili.com/x/player/playurl?fnval=16&fnver=0&fourk=1');
define('REFERER', 'https://www.bilibili.com/');
define('UA', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36');

if (isset($_GET['proxy']) && isset($_GET['url']) && isset($_GET['filename'])) {
    $audioUrl = base64_decode($_GET['url']);
    $filename = base64_decode($_GET['filename']);
    
    $fallbackName = preg_replace('/[^\x20-\x7E]/', '', $filename);
    if (empty(trim($fallbackName))) {
        $fallbackName = 'bilibili_audio';
    }
    
    $headers =[
        'Referer: ' . REFERER,
        'Origin: https://www.bilibili.com',
        'User-Agent: ' . UA
    ];
    
    if (isset($_SERVER['HTTP_RANGE'])) {
        $headers[] = 'Range: ' . $_SERVER['HTTP_RANGE'];
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $audioUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 0);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    
    header('Content-Type: audio/mp4');
    header('Accept-Ranges: bytes');
    header('Cache-Control: public, max-age=31536000');
    header('Content-Disposition: attachment; filename="' . $fallbackName . '.m4a"; filename*=UTF-8\'\'' . rawurlencode($filename) . '.m4a');
    
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $headerLine) {
        if (preg_match('/^HTTP\/(1\.0|1\.1|2|3) (\d+) /i', $headerLine, $matches)) {
            http_response_code(intval($matches[2]));
        } else {
            $headerParts = explode(':', $headerLine, 2);
            if (count($headerParts) == 2) {
                $headerName = strtolower(trim($headerParts[0]));
                if (in_array($headerName, ['content-length', 'content-range'])) {
                    header(trim($headerLine));
                }
            }
        }
        return strlen($headerLine);
    });
    
    curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) {
        echo $data;
        if (ob_get_level() > 0) ob_flush();
        flush();
        return strlen($data);
    });
    
    curl_exec($ch);
    curl_close($ch);
    exit;
}

function fetch($url, $headers =[]) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $response = curl_exec($ch);
    curl_close($ch);
    return $response;
}

function getRedirectUrl($url) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_NOBODY, true);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if (in_array($httpCode,[301, 302, 307, 308])) {
        if (preg_match('/Location:\s*(.+?)\r?\n/i', $response, $matches)) {
            $redirectUrl = trim($matches[1]);
            if (strpos($redirectUrl, 'http') !== 0) {
                $parsed = parse_url($url);
                $redirectUrl = $parsed['scheme'] . '://' . $parsed['host'] . (strpos($redirectUrl, '/') === 0 ? '' : '/') . $redirectUrl;
            }
            return $redirectUrl;
        }
    }
    
    if ($httpCode == 200) {
        return $url;
    }
    
    return null;
}

function buildApiHeaders() {
    return[
        'Referer: ' . REFERER,
        'Origin: https://www.bilibili.com',
        'User-Agent: ' . UA
    ];
}

function extractVideoIds($input) {
    $results =[];
    
    $bvPattern = '/(?i)(BV[0-9A-Za-z]{10})/';
    $avPattern = '/(?i)(?<![0-9A-Za-z])av(\d+)(?![0-9A-Za-z])/';
    $b23Pattern = '/https?:\/\/b23\.tv\/[0-9A-Za-z]+/i';
    $biliUrlPattern = '/https?:\/\/(?:www\.)?bilibili\.com\/video\/[^\s]+/i';
    
    preg_match_all($bvPattern, $input, $bvMatches);
    foreach ($bvMatches[1] as $match) {
        $results[] = 'BV' . substr($match, 2);
    }
    
    preg_match_all($avPattern, $input, $avMatches);
    foreach ($avMatches[1] as $match) {
        $results[] = 'av' . $match;
    }
    
    preg_match_all($b23Pattern, $input, $b23Matches);
    foreach ($b23Matches[0] as $url) {
        $redirectUrl = getRedirectUrl($url);
        if ($redirectUrl) {
            $nested = extractVideoIds($redirectUrl);
            $results = array_merge($results, $nested);
        }
    }
    
    preg_match_all($biliUrlPattern, $input, $biliMatches);
    foreach ($biliMatches[0] as $url) {
        preg_match_all($bvPattern, $url, $urlBvMatches);
        foreach ($urlBvMatches[1] as $match) {
            $results[] = 'BV' . substr($match, 2);
        }
        preg_match_all($avPattern, $url, $urlAvMatches);
        foreach ($urlAvMatches[1] as $match) {
            $results[] = 'av' . $match;
        }
    }
    
    return array_unique($results);
}

function requestView($videoId) {
    $query = strpos($videoId, 'BV') === 0 
        ? 'bvid=' . urlencode($videoId)
        : 'aid=' . urlencode(substr($videoId, 2));
    $response = fetch(VIEW_API . $query, buildApiHeaders());
    $data = json_decode($response, true);
    if ($data['code'] !== 0) {
        throw new Exception($data['message'] ?? '接口返回异常');
    }
    return $data['data'];
}

function requestPlayUrl($bvid, $cid) {
    $api = PLAY_API . '&bvid=' . urlencode($bvid) . '&cid=' . urlencode($cid);
    $response = fetch($api, buildApiHeaders());
    $data = json_decode($response, true);
    if ($data['code'] !== 0) {
        throw new Exception($data['message'] ?? '接口返回异常');
    }
    return $data['data'];
}

function pickBestAudioUrl($playData) {
    if (!$playData || !isset($playData['dash']['audio'])) return null;
    $audios = $playData['dash']['audio'];
    if (empty($audios)) return null;
    
    $bestAudio = null;
    $maxBandwidth = -1;
    foreach ($audios as $audio) {
        $bandwidth = $audio['bandwidth'] ?? 0;
        if ($bestAudio === null || $bandwidth > $maxBandwidth) {
            $bestAudio = $audio;
            $maxBandwidth = $bandwidth;
        }
    }
    
    if (!$bestAudio) return null;
    
    $url = $bestAudio['base_url'] ?? $bestAudio['baseUrl'] ?? '';
    if (!empty($url)) return $url;
    
    $backupUrls = $bestAudio['backup_url'] ?? $bestAudio['backupUrl'] ??[];
    if (!empty($backupUrls)) return $backupUrls[0];
    
    return null;
}

function formatDuration($seconds) {
    $m = floor($seconds / 60);
    $s = $seconds % 60;
    return sprintf('%d:%02d', $m, $s);
}

function formatNumber($num) {
    if ($num >= 10000) {
        return round($num / 10000, 1) . '万';
    }
    return $num;
}

function sanitizeFilename($str) {
    $str = preg_replace('/[^\x{4e00}-\x{9fa5}a-zA-Z0-9\s\-_\(\)\[\]【】]/u', '_', $str);
    if ($str === null) {
        return 'bilibili_audio_' . time();
    }
    $str = preg_replace('/_+/', '_', $str);
    $str = trim($str, '_');
    return $str ?: 'bilibili_audio_' . time();
}

function truncate($str, $length = 120, $suffix = '...') {
    if (mb_strlen($str, 'UTF-8') <= $length) return $str;
    return mb_substr($str, 0, $length, 'UTF-8') . $suffix;
}

$videos = [];
$globalAudioList =[];
$error = '';
$input = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['input'])) {
    $input = $_POST['input'];
    $videoIds = extractVideoIds($input);
    
    if (empty($videoIds)) {
        $error = '未识别到有效的内容';
    } else {
        foreach ($videoIds as $vid) {
            try {
                $viewData = requestView($vid);
                $bvid = $viewData['bvid'];
                $title = $viewData['title'];
                $author = $viewData['owner']['name'] ?? '未知UP';
                $cover = $viewData['pic'];
                $duration = $viewData['duration'];
                $stat = $viewData['stat'] ??[];
                $desc = $viewData['desc'] ?? '';
                $pubdate = $viewData['pubdate'] ?? 0;
                $pages = $viewData['pages'] ?? [];
                
                $videoInfo =[
                    'bvid' => $bvid,
                    'title' => $title,
                    'author' => $author,
                    'cover' => $cover,
                    'duration' => formatDuration($duration),
                    'view' => formatNumber($stat['view'] ?? 0),
                    'like' => formatNumber($stat['like'] ?? 0),
                    'coin' => formatNumber($stat['coin'] ?? 0),
                    'favorite' => formatNumber($stat['favorite'] ?? 0),
                    'share' => formatNumber($stat['share'] ?? 0),
                    'danmaku' => formatNumber($stat['danmaku'] ?? 0),
                    'desc' => $desc,
                    'pubdate' => $pubdate,
                    'parts' =>[]
                ];
                
                if (count($pages) > 1) {
                    foreach ($pages as $index => $page) {
                        $cid = $page['cid'];
                        $partName = $page['part'] ?: ('P' . ($index + 1));
                        $pageNum = $page['page'] ?? ($index + 1);
                        $displayName = 'P' . $pageNum . ($page['part'] ? ' - ' . $page['part'] : '');
                        
                        $filename = sanitizeFilename($displayName . ' - ' . $title);
                        
                        try {
                            $playData = requestPlayUrl($bvid, $cid);
                            $audioUrl = pickBestAudioUrl($playData);
                            if ($audioUrl) {
                                $proxyUrl = '?proxy=1&url=' . urlencode(base64_encode($audioUrl)) . '&filename=' . urlencode(base64_encode($filename));
                                $part =[
                                    'name' => $displayName,
                                    'cid' => $cid,
                                    'audioUrl' => $audioUrl,
                                    'proxyUrl' => $proxyUrl,
                                    'duration' => formatDuration($page['duration']),
                                    'filename' => $filename
                                ];
                                $videoInfo['parts'][] = $part;
                                
                                $globalAudioList[] =[
                                    'name' => $displayName . ' - ' . $title,
                                    'artist' => $author,
                                    'url' => $proxyUrl,
                                    'cover' => $cover,
                                    'duration' => $part['duration'],
                                    'bvid' => $bvid,
                                    'partName' => $displayName
                                ];
                            }
                        } catch (Exception $e) {
                            continue;
                        }
                    }
                } else {
                    $cid = $viewData['cid'];
                    $displayName = '完整音频';
                    
                    $filename = sanitizeFilename($title);
                    
                    try {
                        $playData = requestPlayUrl($bvid, $cid);
                        $audioUrl = pickBestAudioUrl($playData);
                        if ($audioUrl) {
                            $proxyUrl = '?proxy=1&url=' . urlencode(base64_encode($audioUrl)) . '&filename=' . urlencode(base64_encode($filename));
                            $part =[
                                'name' => $displayName,
                                'cid' => $cid,
                                'audioUrl' => $audioUrl,
                                'proxyUrl' => $proxyUrl,
                                'duration' => formatDuration($duration),
                                'filename' => $filename
                            ];
                            $videoInfo['parts'][] = $part;
                            
                            $globalAudioList[] =[
                                'name' => $title,
                                'artist' => $author,
                                'url' => $proxyUrl,
                                'cover' => $cover,
                                'duration' => $part['duration'],
                                'bvid' => $bvid,
                                'partName' => $displayName
                            ];
                        }
                    } catch (Exception $e) {
                        continue;
                    }
                }
                
                if (!empty($videoInfo['parts'])) {
                    $videos[] = $videoInfo;
                }
            } catch (Exception $e) {
                continue;
            }
        }
        
        if (empty($videos)) {
            $error = '未能获取到任何视频信息';
        }
    }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <meta name="referrer" content="no-referrer">
    <title>哔哩哔哩音频解析</title>
    <link rel="stylesheet" href="https://jsd2.pixcat.cn/npm/aplayer@1.10.1/dist/APlayer.min.css">
    <link rel="stylesheet" href="https://jsd2.pixcat.cn/npm/@fortawesome/fontawesome-free@6.0.0/css/all.min.css">
    <style>
        :root {
            --bili-pink: #fb7299;
            --bg-light: #f4f5f7;
            --card-white: #ffffff;
            --text-dark: #1e2a3e;
            --text-gray: #5a6874;
            --border-light: #eef2f6;
            --radius-card: 20px;
            --radius-element: 12px;
        }
        
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            background: var(--bg-light);
            color: var(--text-dark);
            line-height: 1.5;
            padding: 4rem 1rem 3rem;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .container {
            max-width: 1100px;
            margin: 0 auto;
            display: flex;
            flex-direction: column;
            width: 100%;
        }
        
        .hero {
            text-align: center;
            margin-bottom: 2rem;
        }
        
        .hero h1 {
            font-size: 2rem;
            font-weight: 600;
            color: var(--bili-pink);
            margin-bottom: 0.5rem;
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
        }
        
        .hero p {
            color: var(--text-gray);
            font-size: 0.95rem;
        }
        
        .search-card {
            background: var(--card-white);
            border-radius: var(--radius-card);
            box-shadow: 0 8px 20px rgba(0,0,0,0.02), 0 2px 4px rgba(0,0,0,0.03);
            padding: 1.5rem;
            margin-bottom: 2rem;
        }
        
        textarea {
            width: 100%;
            padding: 1rem;
            border: 1px solid var(--border-light);
            border-radius: var(--radius-element);
            font-size: 0.95rem;
            font-family: inherit;
            resize: vertical;
            transition: 0.2s;
            background: #fff;
        }
        
        textarea:focus {
            outline: none;
            border-color: var(--bili-pink);
            box-shadow: 0 0 0 3px rgba(251,114,153,0.1);
        }
        
        button {
            background: var(--bili-pink);
            color: white;
            border: none;
            padding: 0.8rem 1.8rem;
            border-radius: 40px;
            font-size: 0.95rem;
            font-weight: 500;
            cursor: pointer;
            transition: 0.2s;
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
        }
        
        button:hover {
            background: #e85b8a;
            transform: translateY(-1px);
        }
        
        .hint {
            font-size: 0.8rem;
            color: var(--text-gray);
            margin-top: 0.8rem;
            display: flex;
            align-items: center;
            gap: 0.3rem;
            flex-wrap: wrap;
        }
        
        .error {
            background: #fee9e6;
            color: #d44c2f;
            padding: 1rem;
            border-radius: var(--radius-element);
            margin-bottom: 1.5rem;
            font-size: 0.9rem;
        }
        
        .global-player-card {
            background: var(--card-white);
            border-radius: var(--radius-card);
            box-shadow: 0 8px 24px rgba(0,0,0,0.05);
            margin-top: 2rem;
            overflow: hidden;
            order: 2;
        }
        
        .aplayer {
            border-radius: 0;
            box-shadow: none;
            background: #fafbfd;
        }
        
        .videos-container {
            order: 1;
        }
        
        .video-card {
            background: var(--card-white);
            border-radius: var(--radius-card);
            box-shadow: 0 4px 12px rgba(0,0,0,0.02), 0 1px 2px rgba(0,0,0,0.03);
            margin-bottom: 2rem;
            overflow: hidden;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        
        .video-card:hover {
            transform: translateY(-2px);
            box-shadow: 0 12px 24px rgba(0,0,0,0.08);
        }
        
        .video-header {
            display: flex;
            gap: 1.2rem;
            padding: 1.5rem;
            border-bottom: 1px solid var(--border-light);
            flex-wrap: wrap;
        }
        
        .cover {
            width: 160px;
            height: 90px;
            object-fit: cover;
            border-radius: var(--radius-element);
            flex-shrink: 0;
            background: #eef;
        }
        
        .video-info {
            flex: 1;
            min-width: 200px;
        }
        
        .video-title {
            font-size: 1.2rem;
            font-weight: 600;
            margin-bottom: 0.4rem;
            line-height: 1.3;
        }
        
        .video-meta {
            display: flex;
            flex-wrap: wrap;
            gap: 0.8rem;
            font-size: 0.8rem;
            color: var(--text-gray);
            margin-bottom: 0.5rem;
        }
        
        .video-meta span {
            display: inline-flex;
            align-items: center;
            gap: 0.25rem;
        }
        
        .video-stats {
            display: flex;
            flex-wrap: wrap;
            gap: 1rem;
            margin: 0.8rem 0 0.4rem;
            font-size: 0.8rem;
            color: var(--text-gray);
        }
        
        .video-stats i {
            width: 1rem;
        }
        
        .desc {
            font-size: 0.85rem;
            color: var(--text-gray);
            background: #f8fafc;
            padding: 0.8rem;
            border-radius: var(--radius-element);
            margin-top: 0.8rem;
            line-height: 1.4;
            word-break: break-word;
        }
        
        .parts {
            padding: 0 1.5rem 1rem;
        }
        
        .part {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.9rem 0;
            border-bottom: 1px solid var(--border-light);
            flex-wrap: wrap;
            gap: 0.8rem;
        }
        
        .part:last-child {
            border-bottom: none;
        }
        
        .part-name {
            font-size: 0.95rem;
            font-weight: 500;
            display: flex;
            align-items: center;
            gap: 0.5rem;
            flex-wrap: wrap;
        }
        
        .part-time {
            font-size: 0.75rem;
            color: var(--text-gray);
            background: #f0f2f5;
            padding: 0.2rem 0.5rem;
            border-radius: 20px;
        }
        
        .part-actions {
            display: flex;
            gap: 0.6rem;
        }
        
        .btn-icon {
            background: transparent;
            padding: 0.4rem 0.8rem;
            border-radius: 40px;
            font-size: 0.8rem;
            font-family: inherit;
            font-weight: 500;
            display: inline-flex;
            align-items: center;
            gap: 0.4rem;
            transition: 0.2s;
            cursor: pointer;
            border: none;
            text-decoration: none;
        }
        
        .btn-play {
            background: var(--bili-pink);
            color: white;
        }
        
        .btn-play:hover {
            background: #e85b8a;
        }
        
        .btn-dl {
            background: #f0f2f5;
            color: var(--text-dark);
        }
        
        .btn-dl:hover {
            background: #e4e8ef;
        }
        
        .empty-state {
            text-align: center;
            padding: 3rem 1rem;
            color: var(--text-gray);
            background: var(--card-white);
            border-radius: var(--radius-card);
            box-shadow: 0 4px 12px rgba(0,0,0,0.02);
        }
        
        @media (max-width: 680px) {
            body {
                padding: 3rem 1rem 2rem;
            }
            .video-header {
                flex-direction: column;
            }
            .cover {
                width: 100%;
                height: auto;
                aspect-ratio: 16/9;
            }
            .part {
                flex-direction: column;
                align-items: flex-start;
            }
            .part-actions {
                width: 100%;
            }
            .btn-icon {
                flex: 1;
                justify-content: center;
            }
            .video-stats {
                gap: 0.8rem;
            }
        }
        
        @media (max-width: 480px) {
            .hero h1 {
                font-size: 1.6rem;
            }
            .video-title {
                font-size: 1rem;
            }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="hero">
        <h1><i class="fa-brands fa-bilibili fa-bounce"></i> 哔哩哔哩音频解析</h1>
        <p>获取指定视频的音频部分,并尝试以高音质下载</p>
    </div>
    
    <div class="search-card">
        <form method="POST" id="form">
            <div class="input-group">
                <textarea name="input" placeholder="填入粘贴 BV/AV 号、b23.tv 短链、B站完整分享链接或包含它们的文本..."><?php echo htmlspecialchars($input); ?></textarea>
            </div>
            <button type="submit" id="submitBtn"><i class="fas fa-search"></i> 解析</button>
            <div class="hint"><i class="fas fa-lightbulb"></i> 支持分P与多个视频条目</div>
        </form>
    </div>
    
    <?php if ($error): ?>
        <div class="error"><i class="fas fa-exclamation-circle"></i> <?php echo $error; ?></div>
    <?php endif; ?>
    
    <?php if (!empty($globalAudioList)): ?>
        <div class="videos-container">
            <?php foreach ($videos as $video): ?>
                <div class="video-card" data-bvid="<?php echo $video['bvid']; ?>">
                    <div class="video-header">
                        <img class="cover" src="<?php echo htmlspecialchars($video['cover']); ?>" alt="封面" loading="lazy">
                        <div class="video-info">
                            <div class="video-title"><?php echo htmlspecialchars($video['title']); ?></div>
                            <div class="video-meta">
                                <span><i class="fas fa-user-circle"></i> <?php echo htmlspecialchars($video['author']); ?></span>
                                <span><i class="far fa-calendar-alt"></i> <?php echo date('Y-m-d', $video['pubdate']); ?></span>
                                <span><i class="far fa-clock"></i> <?php echo $video['duration']; ?></span>
                            </div>
                            <div class="video-stats">
                                <span><i class="fas fa-play"></i> <?php echo $video['view']; ?></span>
                                <span><i class="fas fa-thumbs-up"></i> <?php echo $video['like']; ?></span>
                                <span><i class="fas fa-coins"></i> <?php echo $video['coin']; ?></span>
                                <span><i class="fas fa-star"></i> <?php echo $video['favorite']; ?></span>
                                <span><i class="fas fa-share-alt"></i> <?php echo $video['share']; ?></span>
                                <span><i class="fas fa-comment-dots"></i> <?php echo $video['danmaku']; ?></span>
                            </div>
                            <?php if (!empty($video['desc'])): ?>
                                <div class="desc"><i class="fas fa-align-left"></i> <?php echo nl2br(htmlspecialchars(truncate($video['desc'], 180))); ?></div>
                            <?php endif; ?>
                        </div>
                    </div>
                    
                    <div class="parts">
                        <?php foreach ($video['parts'] as $index => $part): ?>
                            <div class="part" data-part-index="<?php echo $index; ?>">
                                <div class="part-name">
                                    <i class="fas fa-music"></i> <?php echo htmlspecialchars($part['name']); ?>
                                    <span class="part-time"><?php echo $part['duration']; ?></span>
                                </div>
                                <div class="part-actions">
                                    <button class="btn-icon btn-play" data-audio-name="<?php echo htmlspecialchars(count($video['parts']) > 1 ? $part['name'] . ' - ' . $video['title'] : $video['title'], ENT_QUOTES); ?>" data-artist="<?php echo htmlspecialchars($video['author'], ENT_QUOTES); ?>" data-cover="<?php echo htmlspecialchars($video['cover']); ?>" data-url="<?php echo $part['proxyUrl']; ?>"><i class="fas fa-play"></i> 播放</button>
                                    <a href="<?php echo $part['proxyUrl']; ?>" class="btn-icon btn-dl" download="<?php echo htmlspecialchars($part['filename'], ENT_QUOTES); ?>.m4a"><i class="fas fa-download"></i> 下载</a>
                                </div>
                            </div>
                        <?php endforeach; ?>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
        
        <div class="global-player-card">
            <div id="global-player"></div>
        </div>
    <?php elseif ($_SERVER['REQUEST_METHOD'] === 'POST'): ?>
        <div class="empty-state"><i class="fas fa-search"></i> 未找到有效音频,请检查输入内容</div>
    <?php else: ?>
        <div class="empty-state"><i class="fas fa-arrow-up"></i> 在输入框中粘贴 B 站链接开始解析</div>
    <?php endif; ?>

</div>

<script src="https://jsd2.pixcat.cn/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
<script>
    var globalAudioData = <?php echo json_encode($globalAudioList); ?>;
    var globalPlayer = null;

    function initGlobalPlayer() {
        if (!globalAudioData.length) return;
        globalPlayer = new APlayer({
            container: document.getElementById('global-player'),
            audio: globalAudioData.map(item => ({
                name: item.name,
                artist: item.artist,
                url: item.url,
                cover: item.cover,
                theme: '#fb7299'
            })),
            autoplay: false,
            loop: 'all',
            order: 'list',
            listFolded: false,
            listMaxHeight: 280,
            theme: '#fb7299'
        });
    }

    function playAudioInGlobal(audioName, artist, cover, url) {
        if (!globalPlayer) return;
        let index = -1;
        for (let i = 0; i < globalAudioData.length; i++) {
            if (globalAudioData[i].url === url) {
                index = i;
                break;
            }
        }
        if (index !== -1) {
            globalPlayer.list.switch(index);
            globalPlayer.play();
            document.querySelector('.global-player-card').scrollIntoView({ behavior: 'smooth', block: 'start' });
        } else {
            for (let i = 0; i < globalAudioData.length; i++) {
                if (globalAudioData[i].name === audioName && globalAudioData[i].artist === artist) {
                    globalPlayer.list.switch(i);
                    globalPlayer.play();
                    document.querySelector('.global-player-card').scrollIntoView({ behavior: 'smooth', block: 'start' });
                    break;
                }
            }
        }
    }

    function bindPlayButtons() {
        document.querySelectorAll('.btn-play').forEach(btn => {
            btn.removeEventListener('click', playHandler);
            btn.addEventListener('click', playHandler);
        });
    }

    function playHandler(e) {
        e.preventDefault();
        const name = this.dataset.audioName;
        const artist = this.dataset.artist;
        const cover = this.dataset.cover;
        const url = this.dataset.url;
        if (name && artist && url) {
            playAudioInGlobal(name, artist, cover, url);
        }
    }

    document.addEventListener('DOMContentLoaded', function() {
        if (globalAudioData.length) {
            initGlobalPlayer();
            bindPlayButtons();
        }
    });

    const form = document.getElementById('form');
    if (form) {
        form.addEventListener('submit', function() {
            const btn = document.getElementById('submitBtn');
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> 解析中...';
        });
    }
</script>
</body>
</html>