feat: 「自动获取」改用豆瓣数据源,海报自动下载入库
此前的 OMDb 数据源必须用英文片名,国产片覆盖极差。改用豆瓣公开接口后 中文片名直接可搜,冷门片/老片/剧集也能命中。 fetch_movie.php(全量重写) - 搜索走 movie.douban.com/j/subject_suggest;详情走移动端 m.douban.com/rexxar/api/v2/movie/<id>(PC 详情页有反爬 302,故用移动端接口) - 海报下载后写入 uploads/posters/,文件名沿用内容指纹规则 - 新增登录鉴权:未登录返回 401(此前该接口完全公开) - 启用 TLS 证书校验(原为 CURLOPT_SSL_VERIFYPEER=false) - 魔数兜底 + 12MB 上限:挡掉反爬返回的 HTML 页面与异常大响应 - old_poster 残留清理:重复点击时删掉上一张,且仅删「未被数据库引用」的文件 - 接口地址支持环境变量覆盖,便于用 mock 做端到端测试 lib/helpers.php - 新增 isValidStoredKey():校验表单回传的存储键,防目录穿越与越权引用 add.php - 前端改用新字段(豆瓣评分 / 地区 / 又名),并把 poster_key 放进隐藏域回传 - 提交时「手动上传的海报优先」,没有才采用自动获取已入库的海报 README.md - 同步为豆瓣数据源说明,移除已过时的 OMDb API Key 与 SSL 校验两条安全提示 验证 - 本地 mock 豆瓣端到端:33/33 通过 - 既有存储回归套件:196/196 通过 - A/B 归一化比对:除 add.php 按预期变化外,其余 45 个响应完全一致 - 线上实测:流浪地球 / 哪吒之魔童闹海 均抓取成功,海报落盘为合法 JPEG
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// 文件: add.php (支持 OMDb 自动获取 + 票根裁剪)
|
||||
// 文件: add.php (支持豆瓣自动获取 + 票根裁剪)
|
||||
// ============================================
|
||||
|
||||
require_once 'config.php';
|
||||
@@ -40,8 +40,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} elseif (empty($watch_date)) {
|
||||
$error = '请选择观影日期';
|
||||
} else {
|
||||
// ----- 海报:直接上传 -----
|
||||
// ----- 海报:优先手动上传,其次采用「自动获取」已抓取入库的海报 -----
|
||||
$poster_path = uploadImageFile('poster', POSTER_DIR, 'uploads/posters/') ?? '';
|
||||
if ($poster_path === '') {
|
||||
// 前端把「自动获取」接口返回的 poster_key 放在隐藏域回传。
|
||||
// 该键来自客户端,不可信任:需校验目录/文件名,并确认文件确实存在才采用。
|
||||
$fetched_poster = trim($_POST['fetched_poster_key'] ?? '');
|
||||
if (isValidStoredKey($fetched_poster, 'uploads/posters/') && storageExists($fetched_poster)) {
|
||||
$poster_path = $fetched_poster;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 票根:裁剪结果优先,其次原始上传 -----
|
||||
$ticket_path = '';
|
||||
@@ -417,12 +425,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
<div class="form-group">
|
||||
<label>电影海报</label>
|
||||
<input type="file" name="poster" accept="image/*" id="posterInput">
|
||||
<input type="hidden" name="fetched_poster_key" id="fetchedPosterKey" value="">
|
||||
<div id="posterPreviewWrap" style="display:none;margin-top:6px;">
|
||||
<img id="posterPreview" style="max-width:150px;max-height:200px;border-radius:4px;border:1px solid var(--border-color);">
|
||||
<br>
|
||||
<span style="font-size:12px;color:var(--text-secondary);">预览(仅供参考,仍需上传图片文件)</span>
|
||||
<span id="posterPreviewHint" style="font-size:12px;color:var(--text-secondary);"></span>
|
||||
</div>
|
||||
<div class="form-hint">推荐比例 2:3</div>
|
||||
<div class="form-hint">推荐比例 2:3(「自动获取」会连同海报一起下载;手动上传时以上传的为准)</div>
|
||||
</div>
|
||||
<!-- 票根(带裁剪) -->
|
||||
<div class="form-group">
|
||||
@@ -571,23 +580,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- OMDb 自动获取 ----------
|
||||
// ---------- 豆瓣自动获取 ----------
|
||||
document.getElementById('fetchBtn').addEventListener('click', function() {
|
||||
const title = document.getElementById('movieTitle').value.trim();
|
||||
const status = document.getElementById('fetchStatus');
|
||||
const btn = this;
|
||||
|
||||
|
||||
if (!title) {
|
||||
status.style.color = '#e74c3c';
|
||||
status.textContent = '⚠️ 请先输入电影名称';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
btn.disabled = true;
|
||||
status.style.color = 'var(--text-secondary)';
|
||||
status.textContent = '⏳ 正在获取...';
|
||||
|
||||
fetch('fetch_movie.php?q=' + encodeURIComponent(title))
|
||||
status.textContent = '⏳ 正在获取…';
|
||||
|
||||
// 上一次抓取但仍未采用的海报键,交给服务端清理,避免重复点击堆积文件
|
||||
const oldKey = document.getElementById('fetchedPosterKey').value;
|
||||
let url = 'fetch_movie.php?q=' + encodeURIComponent(title);
|
||||
if (oldKey) url += '&old_poster=' + encodeURIComponent(oldKey);
|
||||
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
@@ -596,32 +610,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充电影名称(自动补全)
|
||||
document.getElementById('movieTitle').value = data.title;
|
||||
|
||||
// 组装信息文本
|
||||
|
||||
// 片名以豆瓣的正名称为准(自动补全/纠正)
|
||||
if (data.title) document.getElementById('movieTitle').value = data.title;
|
||||
|
||||
// 组装电影信息文本
|
||||
let infoText = '';
|
||||
if (data.year) infoText += '📅 年份:' + data.year + '\n';
|
||||
if (data.country) infoText += '🌍 地区:' + data.country + '\n';
|
||||
if (data.genre) infoText += '🏷️ 类型:' + data.genre + '\n';
|
||||
if (data.runtime) infoText += '⏱️ 片长:' + data.runtime + '\n';
|
||||
if (data.director) infoText += '🎬 导演:' + data.director + '\n';
|
||||
if (data.actors) infoText += '👤 主演:' + data.actors + '\n';
|
||||
if (data.imdbRating && data.imdbRating !== 'N/A') infoText += '⭐ IMDb评分:' + data.imdbRating + '\n';
|
||||
if (data.plot && data.plot !== 'N/A') infoText += '\n📝 ' + data.plot;
|
||||
|
||||
if (data.rating) infoText += '⭐ 豆瓣评分:' + data.rating + '\n';
|
||||
if (data.alias) infoText += '🔖 又名:' + data.alias + '\n';
|
||||
if (data.plot) infoText += '\n📝 ' + data.plot;
|
||||
|
||||
document.getElementById('movieInfo').value = infoText;
|
||||
|
||||
// 显示海报预览
|
||||
if (data.poster && data.poster !== 'N/A') {
|
||||
const preview = document.getElementById('posterPreview');
|
||||
const wrap = document.getElementById('posterPreviewWrap');
|
||||
|
||||
// 海报:已由服务端下载入库。这里记录键值(提交时写入 poster_path)并预览。
|
||||
const keyInput = document.getElementById('fetchedPosterKey');
|
||||
const preview = document.getElementById('posterPreview');
|
||||
const wrap = document.getElementById('posterPreviewWrap');
|
||||
const hint = document.getElementById('posterPreviewHint');
|
||||
|
||||
if (data.poster && data.poster_key) {
|
||||
keyInput.value = data.poster_key;
|
||||
preview.src = data.poster;
|
||||
hint.textContent = '海报已自动保存,提交时一并写入(若手动上传图片,以上传的为准)';
|
||||
wrap.style.display = 'block';
|
||||
status.textContent = '✅ 已获取信息,海报仅供参考,仍需上传图片文件';
|
||||
status.textContent = '✅ 已获取信息与海报';
|
||||
} else {
|
||||
document.getElementById('posterPreviewWrap').style.display = 'none';
|
||||
status.textContent = '✅ 已获取基本信息(无海报)';
|
||||
keyInput.value = '';
|
||||
wrap.style.display = 'none';
|
||||
status.textContent = '✅ 已获取基本信息(该片暂无海报)';
|
||||
}
|
||||
status.style.color = '#27ae60';
|
||||
btn.disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user