<?php
// scan_library.php
$mediaRoot = __DIR__ . '/media';
$indexFile = __DIR__ . '/cache/library_index.json';
function scanMediaLibrary(string $root): array {
$library = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if (!$file->isFile()) continue;
$ext = strtolower($file->getExtension());
if (!in_array($ext, ['mp3', 'ogg', 'flac', 'm4a', 'wav'])) continue;
$relPath = substr($file->getPathname(), strlen($root) + 1);
$pathParts = explode(DIRECTORY_SEPARATOR, $relPath);
// 假設結構:藝人/專輯/曲目.mp3
$artist = $pathParts[0] ?? 'Unknown Artist';
$album = $pathParts[1] ?? 'Unknown Album';
$title = basename($file->getFilename(), '.' . $ext);
$track = count($pathParts) > 2 ? (int)preg_replace('/\D/', '', $pathParts[2]) : 0;
// 讀取 ID3 標籤 (需安裝 getid3 庫)
$metadata = [];
if (class_exists('getID3')) {
$getID3 = new getID3();
$fileInfo = $getID3->analyze($file->getPathname());
$metadata = $fileInfo['tags']['id3v2'] ?? $fileInfo['tags']['id3v1'] ?? [];
$title = $metadata['title'][0] ?? $title;
$artist = $metadata['artist'][0] ?? $artist;
$album = $metadata['album'][0] ?? $album;
$track = $metadata['track'][0] ?? $track;
}
$library[$artist][$album][] = [
'title' => $title,
'artist' => $artist,
'album' => $album,
'track' => $track,
'file' => $relPath,
'url' => '/stream.php?file=' . urlencode($relPath),
'duration' => $fileInfo['playtime_seconds'] ?? 0,
'size' => $file->getSize(),
'mime' => $fileInfo['mime_type'] ?? 'audio/mpeg',
];
}
// 排序
foreach ($library as &$artistAlbums) {
ksort($artistAlbums);
foreach ($artistAlbums as &$tracks) {
usort($tracks, fn($a, $b) => $a['track'] <=> $b['track']);
}
}
return $library;
}
$library = scanMediaLibrary($mediaRoot);
file_put_contents($indexFile, json_encode($library, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
echo "媒體庫索引完成,共 " . count($library) . " 位藝人\n";
?>
<?php
// stream.php
$mediaRoot = __DIR__ . '/media';
$fileParam = $_GET['file'] ?? '';
if (!$fileParam) {
http_response_code(400);
exit('Missing file parameter');
}
// 安全:防止目錄遍歷
$requestedPath = realpath($mediaRoot . '/' . $fileParam);
$mediaRootReal = realpath($mediaRoot);
if ($requestedPath === false || strpos($requestedPath, $mediaRootReal) !== 0) {
http_response_code(403);
exit('Access denied');
}
if (!is_file($requestedPath) || !is_readable($requestedPath)) {
http_response_code(404);
exit('File not found');
}
$fileSize = filesize($requestedPath);
$mimeType = mime_content_type($requestedPath);
// 處理 Range 標頭
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
$start = 0;
$end = $fileSize - 1;
$statusCode = 200;
if ($rangeHeader) {
// Range: bytes=start-end
if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) {
$start = (int)$matches[1];
$end = $matches[2] !== '' ? (int)$matches[2] : $fileSize - 1;
if ($start >= $fileSize || $end >= $fileSize || $start > $end) {
header('Content-Range: bytes */' . $fileSize);
http_response_code(416); // Range Not Satisfiable
exit;
}
$statusCode = 206; // Partial Content
}
}
$contentLength = $end - $start + 1;
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . $contentLength);
header('Accept-Ranges: bytes');
header('Cache-Control: public, max-age=3600');
header('Content-Disposition: inline; filename="' . basename($requestedPath) . '"');
if ($statusCode === 206) {
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
http_response_code(206);
}
// 輸出檔案片段
$fp = fopen($requestedPath, 'rb');
fseek($fp, $start);
$bufferSize = 8192;
while (!feof($fp) && $contentLength > 0) {
$read = min($bufferSize, $contentLength);
echo fread($fp, $read);
$contentLength -= $read;
flush(); // 即時輸出
}
fclose($fp);
?>
<!-- player.vue -->
媒體庫
{{ artist }}
{{ album }}
-
{{ track.track }}. {{ track.title }} ({{ formatTime(track.duration) }})
<?php
// api/library_index.php
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: public, max-age=300');
readfile(__DIR__ . '/../cache/library_index.json');
?>
| 功能 | 技術要點 |
|---|---|
| 歌詞同步 (LRC) | 解析 .lrc 檔,前端依據 currentTime 顯示對應行 |
| 音頻視覺化 | Web Audio API AnalyserNode + Canvas 繪製頻譜/波形 |
| 多音質切換 | 預轉碼多版本 (128k/320k/FLAC),前端按頻寬選擇 |
| 播放歷史/收藏 | localStorage 或後端資料庫記錄 user_id + track_id |
| 離線快取 | Service Worker + Cache API 快取音頻檔案 |
| Chromecast/AirPlay | Cast SDK / WebKit 播放目標 API |
網路點播系統核心在於媒體庫索引、Range 串流支援、前端播放器狀態管理三大支柱。PHP 負責索引建立與權限控制,靜態傳輸交給 Nginx/CDN,前端採用現代框架 (Vue/React/Svelte) 配合 HTML5 Audio/Web Audio API,即可構建體驗流暢、擴展性強的音樂播放平台。