正妹不在家
Photography & Archive Project
]+src=["\']([^"\']+)["\']/i', $content, $m)) {
$thumb = $m[1];
}
$fileDate = str_replace('.txt', '', $file);
$date = (strlen($fileDate) >= 14 && is_numeric($fileDate))
? date('Y-m-d', strtotime($fileDate))
: date('Y-m-d', filemtime($filePath));
$allPosts[] = [
'cat' => $cat,
'file' => $file,
'title' => $title,
'views' => $views,
'thumb' => $thumb,
'date' => $date
];
}
}
}
// 1. 熱門文章(點閱 views 由大到小排序,取前 10 篇)
$popularPosts = $allPosts;
usort($popularPosts, function($a, $b) {
if ($b['views'] === $a['views']) return strcmp($b['file'], $a['file']);
return $b['views'] - $a['views'];
});
$popularTop10 = array_slice($popularPosts, 0, 10);
// 2. 關注文章(最冷門 / 點閱 views 由小到大排序,取前 10 篇)
$focusPosts = $allPosts;
usort($focusPosts, function($a, $b) {
if ($a['views'] === $b['views']) return strcmp($b['file'], $a['file']);
return $a['views'] - $b['views'];
});
$focusTop10 = array_slice($focusPosts, 0, 10);
$showcaseData = [
'generated_at' => date('Y-m-d H:i:s'),
'popular' => $popularTop10,
'focus' => $focusTop10
];
@file_put_contents($cacheFile, json_encode($showcaseData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
$popularList = $showcaseData['popular'] ?? [];
$focusList = $showcaseData['focus'] ?? [];
// 預設縮圖 (當文章內沒有任何圖片時使用)
$defaultThumb = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='300' height='200' fill='%231e1e1e'/%3E%3Cpath d='M80 140 L120 90 L160 130 L190 85 L230 140 Z' fill='%23333'/%3E%3Ccircle cx='120' cy='65' r='14' fill='%23d4af37' opacity='0.7'/%3E%3Ctext x='50%25' y='88%25' fill='%23777' font-size='13' text-anchor='middle' font-family='sans-serif'%3E正妹不在家%3C/text%3E%3C/svg%3E";
// ================= 3. 推薦相簿:讀取 dirlist.txt 並隨機挑選縮圖 =================
/**
* 解析 dirlist.txt,取得所有相簿目錄的「相對路徑」清單。
* dirlist.txt 每行格式範例:
* X:\taiwan\台中20號倉庫藝術村\文化義工
* 會被轉換為相對路徑:
* taiwan/台中20號倉庫藝術村/文化義工
*/
/**
* 把一行文字轉成 UTF-8。
*
* 真正的問題根源:dirlist.txt 是在「Windows 繁體中文版 cmd.exe」下產生的,
* cmd.exe 預設輸出編碼是 Big5(碼頁 950),而 Synology / Linux 上實際的
* 檔案系統與目錄名稱是 UTF-8。同一組中文字,Big5 與 UTF-8 的位元組完全不同,
* 所以只要路徑中含中文,逐位元組比對 (is_dir/glob) 必定失敗;只有純英文/數字
* 路徑(如 Disk3、other、200005)因為 ASCII 在兩種編碼下位元組相同才會成功,
* 這正好對應你觀察到「只有純英文路徑正常」的現象。
*
* 這裡先偵測該行是否已經是合法 UTF-8(例如日後改用 PowerShell/UTF-8 匯出
* 就不需要再轉碼),若不是,才嘗試以 Big5 轉成 UTF-8。
*/
function convertLineToUtf8($str) {
if ($str === '') return $str;
// 已經是合法 UTF-8,直接使用,不做任何轉換
if (function_exists('mb_check_encoding') && mb_check_encoding($str, 'UTF-8')) {
return $str;
}
// 優先使用 iconv 轉碼(PHP 內建擴充套件,Synology 上通常已啟用)
if (function_exists('iconv')) {
$out = @iconv('BIG5', 'UTF-8//IGNORE', $str);
if ($out !== false && $out !== '') {
return $out;
}
}
// iconv 不可用時,退而使用 mbstring 轉碼
if (function_exists('mb_convert_encoding')) {
$out = @mb_convert_encoding($str, 'UTF-8', 'BIG5');
if ($out !== false && $out !== '') {
return $out;
}
}
// 兩種轉碼方式都無法使用,只好原樣返回(極少數情況)
return $str;
}
function loadAlbumDirList($dirListFile) {
$dirs = [];
if (!is_file($dirListFile)) {
return $dirs;
}
// dirlist.txt 若是在 Windows 繁體中文 cmd.exe 下用 "dir /s /b > dirlist.txt"
// 之類方式產生,整個檔案很可能是 Big5 編碼、行尾為 CRLF。
// 這裡直接讀取原始位元組再逐行處理,而不是用 file() 依賴系統 locale 判斷。
$raw = file_get_contents($dirListFile);
if ($raw === false || $raw === '') {
return $dirs;
}
// 去除可能存在的 UTF-8 BOM
if (substr($raw, 0, 3) === "\xEF\xBB\xBF") {
$raw = substr($raw, 3);
}
// 統一換行符號後切成一行一行
$raw = str_replace("\r\n", "\n", $raw);
$raw = str_replace("\r", "\n", $raw);
$lines = explode("\n", $raw);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') continue;
// 先轉成 UTF-8,確保後續與磁碟上的實際(UTF-8)目錄名稱比對一致
$line = convertLineToUtf8($line);
// 去除開頭的磁碟機代號,如 "X:\" 或 "X:/"
$rel = preg_replace('#^[A-Za-z]:[\\\\/]#', '', $line);
// 反斜線一律轉為正斜線
$rel = str_replace('\\', '/', $rel);
// 去除頭尾多餘的斜線與空白
$rel = trim($rel, "/ \t\n\r\0\x0B");
if ($rel !== '') {
$dirs[] = $rel;
}
}
// 去除重複目錄
return array_values(array_unique($dirs));
}
/**
* 將 dirlist.txt 內的相對路徑,逐層對應到實際檔案系統上的目錄。
*
* 修正說明:先前版本只用一次 is_dir($docRoot . '/' . $relDir) 直接比對整條路徑,
* 但 dirlist.txt 是從其他裝置(如透過網路磁碟機 X: 匯出)取得的路徑字串,
* 大小寫、全形/半形、或路徑分隔習慣可能與 Synology (Linux,檔案系統區分大小寫)
* 上實際目錄不完全一致。只要路徑中有任何一層對不上,is_dir() 就會直接判定失敗,
* 而失敗的機率通常和列表順序無關,但實務上常造成「只有列表前面少數幾筆能命中」
* 的現象。這裡改成逐層比對:先嘗試完全相符,若對不上,該層改用「忽略大小寫」
* 掃描目前目錄底下的項目來找出真正對應的資料夾,大幅提高比對成功率。
*
* @return string|null 找到則回傳實際存在的完整檔案系統路徑,找不到則回傳 null
*/
function resolveActualDir($docRoot, $relDir) {
$current = rtrim($docRoot, '/');
$segments = explode('/', $relDir);
foreach ($segments as $seg) {
if ($seg === '') continue;
$candidate = $current . '/' . $seg;
if (is_dir($candidate)) {
$current = $candidate;
continue;
}
// 完全相符失敗,改用忽略大小寫方式在目前層級尋找相符的子目錄
$matched = null;
$items = @scandir($current);
if ($items !== false) {
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
if (strcasecmp($item, $seg) === 0 && is_dir($current . '/' . $item)) {
$matched = $item;
break;
}
}
}
if ($matched === null) {
return null; // 這一層完全找不到相符目錄,判定此路徑無效
}
$current = $current . '/' . $matched;
}
return $current;
}
/**
* 從指定的相簿相對路徑清單中,隨機挑選最多 $count 組「有 .jpg 圖片」的相簿。
*
* 挑選邏輯(每次都從「目前尚未嘗試過」的候選池中隨機抽一個,抽過就從池中移除,
* 確保每一輪抽選涵蓋整份清單、不會偏向列表前段):
* 1. 從候選池中隨機挑一個目錄,並將它從池中移除(不重複嘗試)。
* 2. 判斷該目錄底下是否有 .jpg 圖片:
* - 目錄不存在(含大小寫容錯後仍找不到)或裡面沒有任何 .jpg -> 略過,
* 回到步驟 1 繼續隨機挑下一個目錄。
* - 有 .jpg -> 從中隨機挑一張作為縮圖,該相簿入選。
* 3. 重複以上動作,直到湊滿 $count 筆「精選相簿」,或候選池已經抽完為止。
*
* @param array $albumDirs 相簿相對路徑清單
* @param string $docRoot 網站根目錄(用來組出實際檔案系統路徑)
* @param int $count 希望取得的相簿數量,上限固定為 10
* @return array 每筆為 ['title'=>..., 'dir'=>相對目錄, 'img'=>相對圖片路徑]
*/
function pickRandomAlbums($albumDirs, $docRoot, $count = 12) {
$count = max(0, min(12, (int)$count)); // 精選相簿數量最多 12 個
$picked = [];
if ($count === 0 || empty($albumDirs)) {
return $picked;
}
// 候選池:存放尚未嘗試過的 $albumDirs 索引
$pool = array_keys($albumDirs);
while (!empty($pool) && count($picked) < $count) {
// 從「目前剩餘」的候選池中均勻隨機抽一筆
$poolKey = array_rand($pool);
$idx = $pool[$poolKey];
// 立刻從池中移除,避免重複嘗試、也保證迴圈一定會結束
unset($pool[$poolKey]);
$pool = array_values($pool);
$relDir = $albumDirs[$idx];
$actualDir = resolveActualDir($docRoot, $relDir);
if ($actualDir === null) {
continue; // 目錄不存在,略過,繼續隨機挑下一個
}
// 掃描該目錄下的 .jpg 檔案(不分大小寫),不遞迴子目錄
$jpgFiles = glob($actualDir . '/*.[jJ][pP][gG]');
if ($jpgFiles === false || empty($jpgFiles)) {
continue; // 沒有 jpg 圖片,略過並繼續隨機挑下一個目錄
}
// 隨機挑一張圖片作為縮圖
$chosenFile = $jpgFiles[array_rand($jpgFiles)];
// 轉成相對於網站根目錄的圖片路徑
$relImg = ltrim(substr($chosenFile, strlen(rtrim($docRoot, '/'))), '/');
// 相簿標題:相簿目錄「完整路徑」去掉所有 "/",且不顯示 "taiwan" 這層根目錄名稱
// (過長由前端 CSS 裁切顯示)
$titleSegments = array_filter(explode('/', $relDir), function ($seg) {
return strcasecmp($seg, 'taiwan') !== 0;
});
$title = implode('', $titleSegments);
$picked[] = [
'title' => $title,
'dir' => $relDir,
'img' => $relImg,
];
}
return $picked;
}
$dirListFile = __DIR__ . '/dirlist.txt';
$albumRecommendCount = 12; // 首頁「推薦相簿」顯示幾組,最多 12 個
$albumDirs = loadAlbumDirList($dirListFile);
$recommendAlbums = pickRandomAlbums($albumDirs, $docRoot, $albumRecommendCount);
// 相簿瀏覽連結格式,沿用站內既有慣例:photo.php?dir=./相對路徑/
function albumLink($relDir) {
$encodedSegments = array_map('rawurlencode', explode('/', $relDir));
return 'photo.php?dir=./' . implode('/', $encodedSegments) . '/';
}
// 相簿縮圖 路徑(相對路徑直接輸出,讓瀏覽器自行處理 UTF-8 檔名)
function albumImgSrc($relImg) {
$encodedSegments = array_map('rawurlencode', explode('/', $relImg));
return implode('/', $encodedSegments);
}
?>
Photography & Archive Project