mirror of
https://github.com/fish2018/GoComicMosaic.git
synced 2025-11-25 03:15:02 +08:00
优化TMDB搜索,支持电影,先展示结果列表页
This commit is contained in:
@@ -142,6 +142,9 @@ docker run -d --name dongman \
|
||||
---
|
||||
|
||||
# 更新日志
|
||||
-202507161526
|
||||
✅ 优化TMDB搜索,显示结果列表页,选择具体资源后再显示详情,支持电影搜索
|
||||
✅ 变更访问人数统计工具
|
||||
-202507091318
|
||||
✅ 新增贴纸功能,可以在详情页显示透明贴纸,用户自由拖拽、旋转
|
||||
-202507061001
|
||||
|
||||
@@ -530,7 +530,8 @@ onMounted(() => {
|
||||
// 添加不蒜子访问统计脚本
|
||||
const bszScript = document.createElement('script');
|
||||
bszScript.async = true;
|
||||
bszScript.src = "//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js";
|
||||
// bszScript.src = "//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js";
|
||||
bszScript.src = "//events.vercount.one/js";
|
||||
document.head.appendChild(bszScript);
|
||||
|
||||
// 加载TMDB配置
|
||||
|
||||
@@ -2298,4 +2298,244 @@
|
||||
.view-resource-link:hover {
|
||||
background-color: rgba(37, 99, 235, 0.1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 搜索结果列表样式 */
|
||||
.search-results-container {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.result-poster {
|
||||
position: relative;
|
||||
padding-top: 150%; /* 2:3 aspect ratio */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.result-card:hover .poster-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.media-type-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 5px;
|
||||
color: var(--text-color);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.result-original-title {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-year {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.result-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #f5c518; /* IMDB yellow */
|
||||
}
|
||||
|
||||
.result-rating i {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 分页控件样式 */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-numbers {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||
color: var(--dark-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 10px;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.page-number.active {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.25);
|
||||
}
|
||||
|
||||
.page-number:hover:not(.active) {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 15px rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
|
||||
/* 无搜索结果样式 */
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 40px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.no-results i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.results-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.results-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.pagination-btn, .page-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,15 @@
|
||||
<input type="number" class="form-control custom-input" id="tmdbId" v-model="editForm.tmdb_id" placeholder="输入TMDB ID">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mediaType" class="form-label">媒体类型 <small class="text-muted">(movie或tv)</small></label>
|
||||
<select class="form-control custom-input" id="mediaType" v-model="editForm.media_type">
|
||||
<option value="">未指定</option>
|
||||
<option value="movie">电影</option>
|
||||
<option value="tv">剧集</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="titleEn" class="form-label">英文标题</label>
|
||||
<input type="text" class="form-control custom-input" id="titleEn" v-model="editForm.title_en">
|
||||
@@ -441,7 +450,7 @@
|
||||
</button>
|
||||
<button class="btn-custom btn-episode" @click="toggleEpisodeExplorer"
|
||||
:class="{'active': showingEpisodeExplorer}"
|
||||
v-if="tmdbEnabled">
|
||||
v-if="tmdbEnabled && (!resource.media_type || resource.media_type === 'tv')">
|
||||
<i class="bi bi-film"></i><span class="btn-text">{{ showingEpisodeExplorer ? '返回详情' : '剧集探索' }}</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -718,11 +727,46 @@ const loadTMDBConfig = async () => {
|
||||
}
|
||||
|
||||
// 切换剧集探索显示状态
|
||||
const toggleEpisodeExplorer = () => {
|
||||
const toggleEpisodeExplorer = async () => {
|
||||
if (!resource.value?.tmdb_id && !resource.value?.title_en) {
|
||||
alert('该资源没有关联的TMDB ID或英文标题,无法查看剧集信息');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有tmdb_id但有title_en,尝试更新tmdb_id和media_type
|
||||
if (!resource.value.tmdb_id && resource.value.title_en) {
|
||||
try {
|
||||
const response = await axios.post(`/api/resources/${resource.value.id}/update-tmdb`, {
|
||||
title_en: resource.value.title_en,
|
||||
media_type: 'tv' // 剧集探索按钮只对tv类型有效
|
||||
});
|
||||
|
||||
if (response.data && response.data.tmdb_id) {
|
||||
resource.value.tmdb_id = response.data.tmdb_id;
|
||||
resource.value.media_type = response.data.media_type || 'tv';
|
||||
console.log(`已更新资源TMDB ID: ${resource.value.tmdb_id}, 媒体类型: ${resource.value.media_type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('更新TMDB ID失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有tmdb_id但没有media_type,设置默认media_type为tv
|
||||
if (resource.value.tmdb_id && !resource.value.media_type) {
|
||||
try {
|
||||
const response = await axios.put(`/api/resources/${resource.value.id}`, {
|
||||
media_type: 'tv'
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
resource.value.media_type = 'tv';
|
||||
console.log(`已更新资源媒体类型为: tv`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('更新媒体类型失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
showingEpisodeExplorer.value = !showingEpisodeExplorer.value;
|
||||
}
|
||||
|
||||
@@ -738,7 +782,8 @@ const editForm = reactive({
|
||||
poster_image: '',
|
||||
images: [], // 添加images数组存储所有图片
|
||||
tmdb_id: null, // 添加TMDB ID字段
|
||||
stickers: [] // 添加贴纸数组
|
||||
stickers: [], // 添加贴纸数组
|
||||
media_type: '' // 添加媒体类型字段
|
||||
})
|
||||
|
||||
// 链接编辑相关数据
|
||||
@@ -964,6 +1009,7 @@ const startEdit = () => {
|
||||
editForm.poster_image = resource.value.poster_image || ''
|
||||
editForm.images = [...(resource.value.images || [])] // 复制所有图片
|
||||
editForm.tmdb_id = resource.value.tmdb_id || null // 复制TMDB ID
|
||||
editForm.media_type = resource.value.media_type || '' // 复制媒体类型
|
||||
|
||||
// 确保贴纸数据为数组
|
||||
if (Array.isArray(resource.value.stickers)) {
|
||||
@@ -1209,7 +1255,8 @@ const saveChanges = async () => {
|
||||
images: editForm.images, // 提交所有图片
|
||||
links: hasLinks ? linksToSubmit : undefined, // 提交链接数据
|
||||
tmdb_id: editForm.tmdb_id === '' || editForm.tmdb_id === null ? 0 : editForm.tmdb_id, // 确保清空时传递数字0
|
||||
stickers: stickersMap // 提交贴纸数据为JsonMap格式
|
||||
stickers: stickersMap, // 提交贴纸数据为JsonMap格式
|
||||
media_type: editForm.media_type // 提交媒体类型
|
||||
})
|
||||
|
||||
// 更新本地资源数据
|
||||
|
||||
@@ -41,6 +41,83 @@
|
||||
<p>资源已成功导入!</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<div v-else-if="searchResults && !tmdbResource" class="search-results-container">
|
||||
<h2 class="results-title">
|
||||
搜索结果 <span class="results-count">(共{{ searchResults.total_results }}项)</span>
|
||||
</h2>
|
||||
|
||||
<!-- 无结果提示 -->
|
||||
<div v-if="searchResults.results && searchResults.results.length === 0" class="no-results">
|
||||
<i class="bi bi-search"></i>
|
||||
<p>未找到相关资源,请尝试其他关键词</p>
|
||||
</div>
|
||||
|
||||
<!-- 结果网格 -->
|
||||
<div v-else class="results-grid">
|
||||
<div
|
||||
v-for="item in searchResults.results"
|
||||
:key="`${item.media_type}-${item.id}`"
|
||||
class="result-card"
|
||||
@click="viewMediaDetails(item)"
|
||||
>
|
||||
<div class="result-poster">
|
||||
<img
|
||||
:src="item.poster_path ? `https://image.tmdb.org/t/p/w300${item.poster_path}` : 'https://via.placeholder.com/300x450?text=No+Image'"
|
||||
class="poster-image"
|
||||
:alt="item.title || item.name"
|
||||
>
|
||||
<div class="media-type-badge">
|
||||
{{ item.media_type === 'movie' ? '电影' : '电视剧' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-info">
|
||||
<h3 class="result-title">{{ item.title || item.name }}</h3>
|
||||
<p v-if="item.original_title && item.original_title !== item.title" class="result-original-title">
|
||||
{{ item.original_title || item.original_name }}
|
||||
</p>
|
||||
<p class="result-year" v-if="getYear(item)">{{ getYear(item) }}</p>
|
||||
<div class="result-rating" v-if="item.vote_average">
|
||||
<i class="bi bi-star-fill"></i>
|
||||
<span>{{ item.vote_average.toFixed(1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div v-if="searchResults.total_pages > 1" class="pagination-container">
|
||||
<button
|
||||
class="pagination-btn"
|
||||
@click="changePage(currentPage - 1)"
|
||||
:disabled="currentPage <= 1"
|
||||
>
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
<div class="page-numbers">
|
||||
<button
|
||||
v-for="page in displayedPages"
|
||||
:key="page"
|
||||
class="page-number"
|
||||
:class="{ active: page === currentPage }"
|
||||
@click="page !== '...' && changePage(page)"
|
||||
:disabled="page === '...'"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="pagination-btn"
|
||||
@click="changePage(currentPage + 1)"
|
||||
:disabled="currentPage >= searchResults.total_pages"
|
||||
>
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果和预览 -->
|
||||
<div v-else-if="tmdbResource">
|
||||
<div class="resource-detail">
|
||||
@@ -568,6 +645,11 @@ export default {
|
||||
showImagePreview: false,
|
||||
previewImageUrl: '',
|
||||
|
||||
// 搜索结果相关
|
||||
searchResults: null,
|
||||
currentPage: 1,
|
||||
totalPages: 0,
|
||||
|
||||
// 编辑模式相关
|
||||
isEditing: false,
|
||||
_resourceWasEdited: false, // 内部标记,用于跟踪资源是否被编辑过
|
||||
@@ -651,6 +733,31 @@ export default {
|
||||
isValidImageUrl() {
|
||||
const url = this.imageUrlInput.trim();
|
||||
return url.startsWith('http://') || url.startsWith('https://');
|
||||
},
|
||||
// 计算要显示的页码
|
||||
displayedPages() {
|
||||
const pages = [];
|
||||
if (!this.totalPages) return pages;
|
||||
|
||||
if (this.totalPages <= 5) {
|
||||
for (let i = 1; i <= this.totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// 当前页在前面
|
||||
if (this.currentPage <= 3) {
|
||||
pages.push(1, 2, 3, 4, 5, '...', this.totalPages);
|
||||
}
|
||||
// 当前页在后面
|
||||
else if (this.currentPage >= this.totalPages - 2) {
|
||||
pages.push(1, '...', this.totalPages - 4, this.totalPages - 3, this.totalPages - 2, this.totalPages - 1, this.totalPages);
|
||||
}
|
||||
// 当前页在中间
|
||||
else {
|
||||
pages.push(1, '...', this.currentPage - 1, this.currentPage, this.currentPage + 1, '...', this.totalPages);
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -660,22 +767,84 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetState();
|
||||
this.loading = true;
|
||||
|
||||
// 只有在新搜索时重置状态,分页操作时保留tmdbResource为null
|
||||
if (this.currentPage === 1) {
|
||||
this.resetState();
|
||||
} else {
|
||||
// 分页操作时只重置部分状态
|
||||
this.tmdbResource = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用TMDB搜索API
|
||||
const response = await axios.get(`/api/tmdb/search`, {
|
||||
params: { query: this.searchQuery.trim() }
|
||||
// 调用TMDB Multi Search API
|
||||
const response = await axios.get(`/api/tmdb/multi_search`, {
|
||||
params: {
|
||||
query: this.searchQuery.trim(),
|
||||
page: this.currentPage
|
||||
}
|
||||
});
|
||||
|
||||
this.tmdbResource = response.data;
|
||||
if (this.tmdbResource && this.tmdbResource.images && this.tmdbResource.images.length > 0) {
|
||||
this.currentImage = this.tmdbResource.poster_image || this.tmdbResource.images[0];
|
||||
this.searchResults = response.data;
|
||||
this.totalPages = this.searchResults.total_pages;
|
||||
this.currentPage = this.searchResults.page;
|
||||
this.hasSearched = true;
|
||||
} catch (error) {
|
||||
console.error('TMDB搜索失败:', error);
|
||||
this.error = error.response?.data?.error || '搜索失败,请稍后重试';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取年份显示
|
||||
getYear(item) {
|
||||
if (item.media_type === 'movie' && item.release_date) {
|
||||
return item.release_date.substring(0, 4);
|
||||
} else if (item.media_type === 'tv' && item.first_air_date) {
|
||||
return item.first_air_date.substring(0, 4);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
// 媒体详情点击事件
|
||||
async viewMediaDetails(item) {
|
||||
this.loading = true;
|
||||
try {
|
||||
// 保存搜索结果中的中文数据
|
||||
const chineseData = {
|
||||
title: item.title || item.name || '',
|
||||
overview: item.overview || ''
|
||||
};
|
||||
|
||||
// 调用详情API获取完整信息
|
||||
const response = await axios.get(`/api/tmdb/details/${item.media_type}/${item.id}`);
|
||||
const detailData = response.data;
|
||||
|
||||
// 合并数据,优先使用中文数据
|
||||
this.tmdbResource = {
|
||||
...detailData,
|
||||
// 使用搜索结果中的中文标题作为主标题
|
||||
title: chineseData.title,
|
||||
// 使用详情中的原始标题作为英文标题
|
||||
title_en: detailData.original_title || detailData.original_name || '',
|
||||
// 使用搜索结果中的中文简介
|
||||
description: chineseData.overview || detailData.overview || '',
|
||||
// 保留后端返回的中文分类
|
||||
resource_type: detailData.resource_type || '',
|
||||
// 保存媒体类型
|
||||
media_type: item.media_type || 'tv'
|
||||
};
|
||||
|
||||
// 设置当前图片
|
||||
if (this.tmdbResource.images && this.tmdbResource.images.length > 0) {
|
||||
this.currentImage = this.tmdbResource.poster_path || this.tmdbResource.images[0];
|
||||
}
|
||||
|
||||
// 设置默认的活动链接类别
|
||||
if (this.tmdbResource && this.tmdbResource.links) {
|
||||
if (this.tmdbResource.links) {
|
||||
const categories = Object.keys(this.tmdbResource.links);
|
||||
for (const category of categories) {
|
||||
if (this.tmdbResource.links[category] && this.tmdbResource.links[category].length > 0) {
|
||||
@@ -686,12 +855,12 @@ export default {
|
||||
}
|
||||
|
||||
// 检查资源是否已存在
|
||||
if (this.tmdbResource && this.tmdbResource.id) {
|
||||
if (this.tmdbResource.id) {
|
||||
try {
|
||||
const checkResponse = await axios.get(`/api/tmdb/check-exists`, {
|
||||
params: {
|
||||
tmdb_id: this.tmdbResource.id,
|
||||
title: this.tmdbResource.title
|
||||
title: this.tmdbResource.title || this.tmdbResource.name
|
||||
}
|
||||
});
|
||||
|
||||
@@ -704,14 +873,11 @@ export default {
|
||||
}
|
||||
} catch (checkError) {
|
||||
console.error('检查资源是否存在失败:', checkError);
|
||||
// 检查失败不影响主流程,继续显示搜索结果
|
||||
}
|
||||
}
|
||||
|
||||
this.hasSearched = true;
|
||||
} catch (error) {
|
||||
console.error('TMDB搜索失败:', error);
|
||||
this.error = error.response?.data?.error || '搜索失败,请稍后重试';
|
||||
console.error('获取媒体详情失败:', error);
|
||||
this.error = error.response?.data?.error || '获取详情失败,请稍后重试';
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -762,6 +928,8 @@ export default {
|
||||
links: this.tmdbResource.links,
|
||||
// 添加TMDB ID
|
||||
id: this.tmdbResource.id,
|
||||
// 添加媒体类型
|
||||
media_type: this.tmdbResource.media_type,
|
||||
// 检查资源是否已被编辑过
|
||||
is_custom: this.hasBeenEdited
|
||||
};
|
||||
@@ -790,11 +958,16 @@ export default {
|
||||
this.activeCategory = null;
|
||||
this.resourceExists = false;
|
||||
this.existingResource = null;
|
||||
// 不重置searchResults和分页相关状态
|
||||
},
|
||||
|
||||
resetSearch() {
|
||||
// 完全重置,包括搜索结果和分页
|
||||
this.resetState();
|
||||
this.searchQuery = '';
|
||||
this.searchResults = null;
|
||||
this.currentPage = 1;
|
||||
this.totalPages = 0;
|
||||
},
|
||||
|
||||
// 编辑模式相关方法
|
||||
@@ -1006,7 +1179,9 @@ export default {
|
||||
resource_type: this.editForm.resource_type.join(','),
|
||||
poster_image: this.editForm.poster_image,
|
||||
images: [...this.editForm.images],
|
||||
links: linksToSubmit
|
||||
links: linksToSubmit,
|
||||
// 保留原来的media_type
|
||||
media_type: this.tmdbResource.media_type
|
||||
};
|
||||
|
||||
// 标记资源已被编辑
|
||||
@@ -1120,6 +1295,11 @@ export default {
|
||||
|
||||
// 清空输入框
|
||||
this.imageUrlInput = '';
|
||||
},
|
||||
changePage(page) {
|
||||
if (page < 1 || page > this.totalPages) return;
|
||||
this.currentPage = page;
|
||||
this.searchTMDB(); // 重新搜索以更新结果
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
||||
382
gobackend/cmd/diagnostic/main.go
Normal file
382
gobackend/cmd/diagnostic/main.go
Normal file
@@ -0,0 +1,382 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dongman/internal/models"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("开始数据库诊断...")
|
||||
|
||||
// 初始化数据库
|
||||
db, err := models.InitDB()
|
||||
if err != nil {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// 检查数据库位置
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("获取工作目录失败: %v", err)
|
||||
}
|
||||
log.Printf("当前工作目录: %s", workDir)
|
||||
|
||||
// 检查表结构
|
||||
log.Println("\n===== 检查数据库表结构 =====")
|
||||
pragmaRows, err := db.Query("PRAGMA table_info(resources)")
|
||||
if err != nil {
|
||||
log.Fatalf("查询表结构失败: %v", err)
|
||||
}
|
||||
defer pragmaRows.Close()
|
||||
|
||||
fmt.Println("资源表结构:")
|
||||
fmt.Printf("%-4s %-20s %-10s %-8s %-10s\n", "序号", "字段名", "类型", "可空", "默认值")
|
||||
fmt.Println(strings.Repeat("-", 60))
|
||||
|
||||
for pragmaRows.Next() {
|
||||
var cid int
|
||||
var name, type_ string
|
||||
var notnull, dfltValue, pk interface{}
|
||||
pragmaRows.Scan(&cid, &name, &type_, ¬null, &dfltValue, &pk)
|
||||
fmt.Printf("%-4d %-20s %-10s %-8v %-10v\n", cid, name, type_, notnull, dfltValue)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 检查资源数量
|
||||
log.Println("\n===== 检查资源数量 =====")
|
||||
var totalCount int
|
||||
err = db.Get(&totalCount, "SELECT COUNT(*) FROM resources")
|
||||
if err != nil {
|
||||
log.Fatalf("查询资源总数失败: %v", err)
|
||||
}
|
||||
log.Printf("数据库中总共有 %d 条资源记录", totalCount)
|
||||
|
||||
// 检查资源状态分布
|
||||
log.Println("\n===== 检查资源状态分布 =====")
|
||||
rows, err := db.Query("SELECT status, COUNT(*) FROM resources GROUP BY status")
|
||||
if err != nil {
|
||||
log.Fatalf("查询资源状态分布失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var status string
|
||||
var count int
|
||||
rows.Scan(&status, &count)
|
||||
log.Printf("状态 [%s]: %d 条记录", status, count)
|
||||
}
|
||||
|
||||
// 检查是否存在approved且非supplement的资源
|
||||
log.Println("\n===== 检查已批准的非补充资源 =====")
|
||||
var approvedCount int
|
||||
err = db.Get(&approvedCount,
|
||||
"SELECT COUNT(*) FROM resources WHERE status = ? AND is_supplement_approval = 0",
|
||||
"APPROVED")
|
||||
if err != nil {
|
||||
log.Fatalf("查询已批准的非补充资源数量失败: %v", err)
|
||||
}
|
||||
log.Printf("已批准的非补充资源数量: %d", approvedCount)
|
||||
|
||||
// 显示几条样例资源
|
||||
log.Println("\n===== 样例资源数据 =====")
|
||||
var sampleResources []struct {
|
||||
ID int `db:"id"`
|
||||
Title string `db:"title"`
|
||||
Status string `db:"status"`
|
||||
IsSupplementApproval bool `db:"is_supplement_approval"`
|
||||
LikesCount int `db:"likes_count"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
err = db.Select(&sampleResources, "SELECT id, title, status, is_supplement_approval, likes_count, created_at FROM resources LIMIT 3")
|
||||
if err != nil {
|
||||
log.Fatalf("查询样例资源失败: %v", err)
|
||||
}
|
||||
|
||||
for i, resource := range sampleResources {
|
||||
log.Printf("样例 #%d:", i+1)
|
||||
log.Printf(" ID: %d", resource.ID)
|
||||
log.Printf(" 标题: %s", resource.Title)
|
||||
log.Printf(" 状态: %s", resource.Status)
|
||||
log.Printf(" 是否补充批准: %v", resource.IsSupplementApproval)
|
||||
log.Printf(" 点赞数: %d", resource.LikesCount)
|
||||
log.Printf(" 创建时间: %v", resource.CreatedAt)
|
||||
}
|
||||
|
||||
// 尝试执行前端请求的查询
|
||||
log.Println("\n===== 测试前端查询 =====")
|
||||
var frontendResources []struct {
|
||||
ID int `db:"id"`
|
||||
Title string `db:"title"`
|
||||
LikesCount int `db:"likes_count"`
|
||||
}
|
||||
query := `SELECT id, title, likes_count FROM resources WHERE status = ? ORDER BY likes_count DESC LIMIT ? OFFSET ?`
|
||||
args := []interface{}{"APPROVED", 4, 0}
|
||||
|
||||
err = db.Select(&frontendResources, query, args...)
|
||||
if err != nil {
|
||||
log.Fatalf("执行前端查询失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("前端查询找到 %d 条资源", len(frontendResources))
|
||||
for i, resource := range frontendResources {
|
||||
log.Printf("结果 #%d: [ID: %d] %s (点赞: %d)",
|
||||
i+1, resource.ID, resource.Title, resource.LikesCount)
|
||||
}
|
||||
|
||||
log.Println("\n===== 检查users表结构 =====")
|
||||
userPragmaRows, err := db.Query("PRAGMA table_info(users)")
|
||||
if err != nil {
|
||||
log.Fatalf("查询users表结构失败: %v", err)
|
||||
}
|
||||
defer userPragmaRows.Close()
|
||||
|
||||
fmt.Println("用户表结构:")
|
||||
fmt.Printf("%-4s %-20s %-10s %-8s %-10s\n", "序号", "字段名", "类型", "可空", "默认值")
|
||||
fmt.Println(strings.Repeat("-", 60))
|
||||
|
||||
for userPragmaRows.Next() {
|
||||
var cid int
|
||||
var name, type_ string
|
||||
var notnull, dfltValue, pk interface{}
|
||||
userPragmaRows.Scan(&cid, &name, &type_, ¬null, &dfltValue, &pk)
|
||||
fmt.Printf("%-4d %-20s %-10s %-8v %-10v\n", cid, name, type_, notnull, dfltValue)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 检查是否有用户
|
||||
log.Println("\n===== 检查用户数量 =====")
|
||||
var userCount int
|
||||
err = db.Get(&userCount, "SELECT COUNT(*) FROM users")
|
||||
if err != nil {
|
||||
log.Fatalf("查询用户总数失败: %v", err)
|
||||
}
|
||||
log.Printf("数据库中总共有 %d 个用户", userCount)
|
||||
|
||||
// 如果有用户,显示第一个用户信息(不显示密码)
|
||||
if userCount > 0 {
|
||||
var users []struct {
|
||||
ID int `db:"id"`
|
||||
Username string `db:"username"`
|
||||
IsAdmin bool `db:"is_admin"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
err = db.Select(&users, "SELECT id, username, is_admin, created_at FROM users LIMIT 3")
|
||||
if err != nil {
|
||||
log.Fatalf("查询用户信息失败: %v", err)
|
||||
}
|
||||
|
||||
for i, user := range users {
|
||||
log.Printf("用户 #%d:", i+1)
|
||||
log.Printf(" ID: %d", user.ID)
|
||||
log.Printf(" 用户名: %s", user.Username)
|
||||
log.Printf(" 是否管理员: %v", user.IsAdmin)
|
||||
log.Printf(" 创建时间: %v", user.CreatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试获取admin用户信息
|
||||
log.Println("\n===== 检查admin用户 =====")
|
||||
|
||||
// 分开获取admin用户信息和密码
|
||||
var adminUser struct {
|
||||
ID int `db:"id"`
|
||||
Username string `db:"username"`
|
||||
IsAdmin bool `db:"is_admin"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
err = db.Get(&adminUser, "SELECT id, username, is_admin, created_at FROM users WHERE username = ?", "admin")
|
||||
if err != nil {
|
||||
log.Printf("查询admin用户失败: %v", err)
|
||||
} else {
|
||||
// 单独获取密码哈希长度
|
||||
var hashedPassword string
|
||||
err = db.Get(&hashedPassword, "SELECT hashed_password FROM users WHERE username = ?", "admin")
|
||||
if err != nil {
|
||||
log.Printf("获取admin密码哈希失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("找到admin用户:")
|
||||
log.Printf(" ID: %d", adminUser.ID)
|
||||
log.Printf(" 用户名: %s", adminUser.Username)
|
||||
log.Printf(" 密码哈希长度: %d", len(hashedPassword))
|
||||
log.Printf(" 是否管理员: %v", adminUser.IsAdmin)
|
||||
log.Printf(" 创建时间: %v", adminUser.CreatedAt)
|
||||
}
|
||||
|
||||
// 查询前端请求的表单
|
||||
log.Println("\n===== 前端登录请求格式参考 =====")
|
||||
log.Printf(`请确保前端发送的登录请求格式如下:
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
登录API地址: POST /api/auth/token`)
|
||||
|
||||
// 测试登录请求
|
||||
log.Println("\n===== 测试登录API =====")
|
||||
log.Printf("现在将使用默认账号 admin/admin123 测试登录API")
|
||||
|
||||
// 创建测试HTTP客户端
|
||||
log.Printf("请确保API服务器正在运行,测试将向 http://localhost:8000/api/auth/token 发送请求")
|
||||
log.Printf("请求头: Content-Type: application/json")
|
||||
log.Printf("请求体: {\"username\":\"admin\",\"password\":\"admin123\"}")
|
||||
|
||||
// 构建请求体
|
||||
loginReq := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}{
|
||||
Username: "admin",
|
||||
Password: "admin123",
|
||||
}
|
||||
|
||||
loginReqJSON, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
log.Printf("序列化登录请求失败: %v", err)
|
||||
} else {
|
||||
// 发送HTTP请求
|
||||
resp, err := http.Post(
|
||||
"http://localhost:8000/api/auth/token",
|
||||
"application/json",
|
||||
bytes.NewBuffer(loginReqJSON),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("发送登录请求失败: %v", err)
|
||||
log.Printf("这可能是因为API服务器未运行,请确保服务器已启动")
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
var respBody map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&respBody)
|
||||
|
||||
log.Printf("HTTP状态码: %d", resp.StatusCode)
|
||||
if err != nil {
|
||||
log.Printf("解析响应失败: %v", err)
|
||||
|
||||
// 尝试读取原始响应
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(resp.Body)
|
||||
log.Printf("原始响应: %s", buf.String())
|
||||
} else {
|
||||
log.Printf("响应体: %v", respBody)
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
log.Printf("登录成功!获取到令牌")
|
||||
} else {
|
||||
log.Printf("登录失败,请检查用户名和密码")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(`要在命令行测试登录,请执行以下命令:
|
||||
curl -X POST http://localhost:8000/api/auth/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}'`)
|
||||
|
||||
log.Printf(`如果前端使用fetch或axios发送请求,请确保格式如下:
|
||||
fetch('http://localhost:8000/api/auth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin123'
|
||||
})
|
||||
})`)
|
||||
|
||||
log.Println("\n诊断完成!")
|
||||
}
|
||||
|
||||
func resetResourceData() error {
|
||||
// 删除所有现有资源
|
||||
_, err := models.DB.Exec("DELETE FROM resources")
|
||||
if err != nil {
|
||||
return fmt.Errorf("清空资源表失败: %w", err)
|
||||
}
|
||||
|
||||
// 重置自增ID
|
||||
_, err = models.DB.Exec("DELETE FROM sqlite_sequence WHERE name='resources'")
|
||||
if err != nil {
|
||||
log.Printf("重置资源表自增ID失败 (非致命错误): %v", err)
|
||||
}
|
||||
|
||||
// 创建新的示例数据
|
||||
return createSampleResources()
|
||||
}
|
||||
|
||||
func createSampleResources() error {
|
||||
sampleResources := []models.Resource{
|
||||
{
|
||||
Title: "进击的巨人",
|
||||
TitleEn: "Attack on Titan",
|
||||
Description: "人类与巨人的生存之战",
|
||||
ResourceType: "anime",
|
||||
Status: models.ResourceStatusApproved,
|
||||
Images: []string{"/assets/imgs/1/sample1.jpg"},
|
||||
Links: models.JsonMap{"官网": "https://example.com/aot"},
|
||||
LikesCount: 120,
|
||||
IsSupplementApproval: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
Title: "鬼灭之刃",
|
||||
TitleEn: "Demon Slayer",
|
||||
Description: "少年与恶魔的战斗故事",
|
||||
ResourceType: "anime",
|
||||
Status: models.ResourceStatusApproved,
|
||||
Images: []string{"/assets/imgs/2/sample2.jpg"},
|
||||
Links: models.JsonMap{"官网": "https://example.com/ds"},
|
||||
LikesCount: 150,
|
||||
IsSupplementApproval: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
{
|
||||
Title: "海贼王",
|
||||
TitleEn: "One Piece",
|
||||
Description: "寻找传说中的大秘宝「ONE PIECE」的海洋冒险故事",
|
||||
ResourceType: "anime",
|
||||
Status: models.ResourceStatusApproved,
|
||||
Images: []string{"/assets/imgs/3/sample3.jpg"},
|
||||
Links: models.JsonMap{"官网": "https://example.com/op"},
|
||||
LikesCount: 200,
|
||||
IsSupplementApproval: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
// 插入示例数据
|
||||
for _, resource := range sampleResources {
|
||||
_, err := models.DB.Exec(
|
||||
`INSERT INTO resources (
|
||||
title, title_en, description, resource_type, images, links,
|
||||
status, likes_count, is_supplement_approval, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
resource.Title, resource.TitleEn, resource.Description, resource.ResourceType,
|
||||
resource.Images, resource.Links, resource.Status, resource.LikesCount,
|
||||
resource.IsSupplementApproval, resource.CreatedAt, resource.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("插入示例资源失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
118
gobackend/cmd/test/sqlite.go
Normal file
118
gobackend/cmd/test/sqlite.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 获取当前工作目录
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("获取工作目录失败: %v", err)
|
||||
}
|
||||
log.Printf("当前工作目录: %s", wd)
|
||||
|
||||
// 构建数据库路径 - 尝试在多个位置查找
|
||||
possiblePaths := []string{
|
||||
"./resource_hub.db", // 当前目录
|
||||
"../../resource_hub.db", // 项目根目录
|
||||
filepath.Join(wd, "resource_hub.db"), // 绝对路径
|
||||
filepath.Join(wd, "../../resource_hub.db"), // 绝对路径到项目根目录
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
var dbPath string
|
||||
|
||||
// 尝试打开数据库
|
||||
for _, path := range possiblePaths {
|
||||
log.Printf("尝试打开数据库: %s", path)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
log.Printf("找到数据库文件: %s", path)
|
||||
db, err = sql.Open("sqlite3", path)
|
||||
if err == nil {
|
||||
dbPath = path
|
||||
log.Printf("成功打开数据库: %s", path)
|
||||
break
|
||||
} else {
|
||||
log.Printf("打开数据库失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("数据库文件不存在: %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
log.Fatalf("未能找到或打开任何数据库文件")
|
||||
}
|
||||
|
||||
log.Printf("使用数据库文件: %s", dbPath)
|
||||
|
||||
// 测试数据库连接
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatalf("数据库连接测试失败: %v", err)
|
||||
}
|
||||
log.Printf("数据库连接测试成功")
|
||||
|
||||
// 检查资源表结构
|
||||
log.Println("检查资源表结构:")
|
||||
rows, err := db.Query("PRAGMA table_info(resources)")
|
||||
if err != nil {
|
||||
log.Fatalf("查询表结构失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var (
|
||||
cid int
|
||||
name string
|
||||
dataType string
|
||||
notNull int
|
||||
dfltValue interface{}
|
||||
primaryKey int
|
||||
)
|
||||
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&cid, &name, &dataType, ¬Null, &dfltValue, &primaryKey); err != nil {
|
||||
log.Fatalf("扫描表结构数据失败: %v", err)
|
||||
}
|
||||
log.Printf("列: %s, 类型: %s, 非空: %d, 默认值: %v, 主键: %d", name, dataType, notNull, dfltValue, primaryKey)
|
||||
}
|
||||
|
||||
// 统计资源数量
|
||||
var count int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM resources").Scan(&count); err != nil {
|
||||
log.Fatalf("统计资源失败: %v", err)
|
||||
}
|
||||
log.Printf("资源总数: %d", count)
|
||||
|
||||
// 查询已批准的资源数量
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM resources WHERE status = 'APPROVED'").Scan(&count); err != nil {
|
||||
log.Fatalf("统计已批准资源失败: %v", err)
|
||||
}
|
||||
log.Printf("已批准资源数: %d", count)
|
||||
|
||||
// 检查ID为1的资源记录
|
||||
log.Println("查询ID为1的资源记录:")
|
||||
rows, err = db.Query("SELECT id, title, title_en, description, resource_type, status FROM resources WHERE id = 1")
|
||||
if err != nil {
|
||||
log.Fatalf("查询资源失败: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var title, titleEn, description, resourceType, status string
|
||||
if err := rows.Scan(&id, &title, &titleEn, &description, &resourceType, &status); err != nil {
|
||||
log.Fatalf("扫描资源数据失败: %v", err)
|
||||
}
|
||||
log.Printf("资源ID: %d, 标题: %s, 英文标题: %s, 类型: %s, 状态: %s", id, title, titleEn, resourceType, status)
|
||||
log.Printf("描述: %s", description)
|
||||
}
|
||||
|
||||
fmt.Println("SQLite 数据库测试完成")
|
||||
}
|
||||
85
gobackend/cmd/test/webp_tool_test.go
Normal file
85
gobackend/cmd/test/webp_tool_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dongman/internal/models"
|
||||
"dongman/internal/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化数据库连接,以确保可以正常使用utils包
|
||||
db, err := models.InitDB()
|
||||
if err != nil {
|
||||
log.Fatalf("数据库初始化失败: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
log.Println("WebP转换工具测试")
|
||||
|
||||
// 示例图片路径 - 如果该路径不存在,请替换为实际存在的图片路径
|
||||
testImgPath := "imgs/1/test.jpg"
|
||||
|
||||
// 获取工作目录,构建assets目录路径
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("获取工作目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 构建测试目录
|
||||
assetsDir := filepath.Join(workDir, "..", "..", "assets")
|
||||
testDir := filepath.Join(assetsDir, "imgs", "1")
|
||||
|
||||
// 确保测试目录存在
|
||||
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||
log.Fatalf("创建测试目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 将测试图片从实例目录复制到测试目录 - 请确保原图片存在
|
||||
srcImagePath := filepath.Join(workDir, "..", "..", "assets", "uploads")
|
||||
files, err := filepath.Glob(filepath.Join(srcImagePath, "*", "*.jpg"))
|
||||
if err != nil || len(files) == 0 {
|
||||
log.Fatalf("未找到测试图片: %v", err)
|
||||
}
|
||||
|
||||
// 使用找到的第一张图片
|
||||
testSrcPath := files[0]
|
||||
testDstPath := filepath.Join(testDir, "test.jpg")
|
||||
|
||||
// 复制测试图片
|
||||
if err := utils.CopyFile(testSrcPath, testDstPath); err != nil {
|
||||
log.Fatalf("复制测试图片失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("已复制测试图片: %s -> %s", testSrcPath, testDstPath)
|
||||
|
||||
// 测试固定尺寸的转换
|
||||
log.Println("测试1: 固定尺寸转换(600x900)")
|
||||
webpPath, err := utils.ConvertToWebP(testImgPath)
|
||||
if err != nil {
|
||||
log.Fatalf("WebP转换失败: %v", err)
|
||||
}
|
||||
log.Printf("转换成功! WebP文件路径: %s", webpPath)
|
||||
|
||||
// 测试保持宽高比的转换
|
||||
log.Println("测试2: 保持宽高比转换(最大600x900)")
|
||||
webpRatioPath, err := utils.ConvertToWebPWithRatio(testImgPath, 600, 900, true)
|
||||
if err != nil {
|
||||
log.Fatalf("保持宽高比的WebP转换失败: %v", err)
|
||||
}
|
||||
log.Printf("转换成功! 保持宽高比的WebP文件路径: %s", webpRatioPath)
|
||||
|
||||
// 测试完整路径转换
|
||||
log.Println("测试3: 完整路径转换")
|
||||
fullPath := filepath.Join(assetsDir, testImgPath)
|
||||
webpFullPath, err := utils.ConvertToWebPFromPath(fullPath)
|
||||
if err != nil {
|
||||
log.Fatalf("完整路径的WebP转换失败: %v", err)
|
||||
}
|
||||
log.Printf("转换成功! 完整路径的WebP文件: %s", webpFullPath)
|
||||
|
||||
fmt.Println("所有测试完成!")
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"path/filepath"
|
||||
"net/url"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -699,6 +701,13 @@ func UpdateResource(c *gin.Context) {
|
||||
log.Printf("更新TMDB ID: %v", resource.TmdbID)
|
||||
}
|
||||
|
||||
if resourceUpdate.MediaType != nil {
|
||||
// 处理媒体类型更新
|
||||
resource.MediaType = resourceUpdate.MediaType
|
||||
updated = true
|
||||
log.Printf("更新媒体类型: %v", *resource.MediaType)
|
||||
}
|
||||
|
||||
if resourceUpdate.Stickers != nil {
|
||||
// 检查贴纸中是否有需要移动的图片(从临时uploads目录到永久目录)
|
||||
imagesToMove := make([]string, 0)
|
||||
@@ -985,4 +994,137 @@ func UpdateResourceStickers(c *gin.Context) {
|
||||
|
||||
log.Printf("贴纸更新成功,资源ID: %d", resourceID)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "贴纸更新成功", "resource": resource})
|
||||
}
|
||||
|
||||
// UpdateResourceTMDBInfo 更新资源的TMDB ID和媒体类型 - 通过英文标题自动更新
|
||||
func UpdateResourceTMDBInfo(c *gin.Context) {
|
||||
// 获取路径参数
|
||||
resourceID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的资源ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求
|
||||
var request struct {
|
||||
TitleEn string `json:"title_en"`
|
||||
MediaType string `json:"media_type"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查英文标题是否存在
|
||||
if request.TitleEn == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "英文标题不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查媒体类型
|
||||
if request.MediaType == "" {
|
||||
request.MediaType = "tv" // 默认为电视剧
|
||||
}
|
||||
|
||||
// 验证媒体类型是否有效
|
||||
if request.MediaType != "tv" && request.MediaType != "movie" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的媒体类型,必须是 tv 或 movie"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查资源是否存在
|
||||
var resource models.Resource
|
||||
err = models.DB.Get(&resource, `SELECT * FROM resources WHERE id = ?`, resourceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源未找到"})
|
||||
return
|
||||
}
|
||||
|
||||
// 通过TMDB API查询资源ID
|
||||
tmdbResults, err := SearchTMDBByQuery(request.TitleEn, request.MediaType)
|
||||
if err != nil || len(tmdbResults) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "在TMDB中未找到匹配的资源"})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用第一个结果的ID
|
||||
tmdbID := tmdbResults[0].ID
|
||||
|
||||
// 更新资源的TMDB ID和媒体类型
|
||||
resource.TmdbID = &tmdbID
|
||||
mediaType := request.MediaType
|
||||
resource.MediaType = &mediaType
|
||||
resource.UpdatedAt = time.Now()
|
||||
|
||||
// 保存更新
|
||||
err = models.UpdateResourceWithStickers(&resource)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("更新资源失败: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回更新后的资源
|
||||
c.JSON(http.StatusOK, resource)
|
||||
}
|
||||
|
||||
// SearchTMDBByQuery 通过查询字符串搜索TMDB资源
|
||||
func SearchTMDBByQuery(query string, mediaType string) ([]TMDBSearchResult, error) {
|
||||
if !IsTMDBEnabled() {
|
||||
return nil, fmt.Errorf("TMDB未启用")
|
||||
}
|
||||
|
||||
// 构建请求URL
|
||||
baseURL := "https://api.themoviedb.org/3/search/" + mediaType
|
||||
requestURL := fmt.Sprintf("%s?api_key=%s&query=%s&language=zh-CN",
|
||||
baseURL, utils.GetTMDBAPIKey(), url.QueryEscape(query))
|
||||
|
||||
// 发送请求
|
||||
response, err := http.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// 解析响应
|
||||
var searchResponse struct {
|
||||
Results []TMDBSearchResult `json:"results"`
|
||||
}
|
||||
|
||||
err = json.NewDecoder(response.Body).Decode(&searchResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return searchResponse.Results, nil
|
||||
}
|
||||
|
||||
// TMDBSearchResult TMDB搜索结果结构
|
||||
type TMDBSearchResult struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Overview string `json:"overview"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
}
|
||||
|
||||
// IsTMDBEnabled 检查TMDB功能是否启用
|
||||
func IsTMDBEnabled() bool {
|
||||
var settings models.SiteSettings
|
||||
err := models.GetDB().Get(&settings, "SELECT * FROM site_settings WHERE setting_key = ?", "tmdb_config")
|
||||
|
||||
if err != nil {
|
||||
// 如果找不到配置,返回未启用状态
|
||||
return false
|
||||
}
|
||||
|
||||
// 从配置中提取enabled字段
|
||||
enabled := false
|
||||
if val, ok := settings.SettingValue["enabled"].(bool); ok {
|
||||
enabled = val
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
@@ -82,6 +82,12 @@ func SetupRoutes(router *gin.Engine) {
|
||||
|
||||
// 更新资源的TMDB ID
|
||||
tmdb.PUT("/update-resource-id/:id/:tmdb_id", UpdateResourceTmdbID)
|
||||
|
||||
// 添加新的多类型搜索API
|
||||
tmdb.GET("/multi_search", MultiSearchTMDB)
|
||||
|
||||
// 添加新的媒体详情API
|
||||
tmdb.GET("/details/:media_type/:media_id", GetMediaDetails)
|
||||
}
|
||||
|
||||
// 资源路由 - 需要认证
|
||||
@@ -94,7 +100,8 @@ func SetupRoutes(router *gin.Engine) {
|
||||
resources.POST("/:id/unlike", UnlikeResource)
|
||||
resources.PUT("/:id/supplement", SupplementResource)
|
||||
resources.PUT("/:id/stickers", UpdateResourceStickers)
|
||||
|
||||
resources.POST("/:id/update-tmdb", UpdateResourceTMDBInfo)
|
||||
|
||||
resources.POST("/", CreateResource)
|
||||
|
||||
// 图片上传API - 处理不同URL路径格式
|
||||
|
||||
@@ -26,6 +26,7 @@ type TMDBSearchRequest struct {
|
||||
PosterImage string `json:"poster_image"`
|
||||
Images []string `json:"images"`
|
||||
Links map[string][]map[string]string `json:"links"`
|
||||
MediaType string `json:"media_type"` // 媒体类型:movie, tv
|
||||
IsCustom bool `json:"is_custom"` // 标识是否为自定义资源
|
||||
}
|
||||
|
||||
@@ -119,6 +120,12 @@ func CreateResourceFromTMDB(c *gin.Context) {
|
||||
tmdbID = &req.ID
|
||||
}
|
||||
|
||||
// 处理媒体类型
|
||||
var mediaType *string
|
||||
if req.MediaType != "" {
|
||||
mediaType = &req.MediaType
|
||||
}
|
||||
|
||||
// 创建自定义资源对象
|
||||
resource = &models.Resource{
|
||||
Title: req.Title,
|
||||
@@ -130,35 +137,61 @@ func CreateResourceFromTMDB(c *gin.Context) {
|
||||
Links: linksMap,
|
||||
Status: defaultStatus,
|
||||
TmdbID: tmdbID,
|
||||
MediaType: mediaType,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
} else {
|
||||
// 标准TMDB资源处理逻辑
|
||||
tmdbResource, err := utils.SearchTMDB(req.Query)
|
||||
if err != nil {
|
||||
log.Printf("TMDB搜索失败: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
// 标准TMDB资源处理逻辑 - 使用前端传递的数据而不是重新搜索
|
||||
log.Printf("处理TMDB资源导入请求: ID=%d, 标题=%s, 类型=%s",
|
||||
req.ID, req.Title, req.MediaType)
|
||||
|
||||
// 验证必要字段
|
||||
if req.ID <= 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "TMDB ID不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "资源标题不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 确保PosterImage不为空
|
||||
posterImage := tmdbResource.PosterImage
|
||||
var posterImage *string
|
||||
if req.PosterImage != "" {
|
||||
posterImage = &req.PosterImage
|
||||
} else if len(req.Images) > 0 {
|
||||
posterImage = &req.Images[0]
|
||||
}
|
||||
|
||||
// 设置TMDB ID
|
||||
tmdbID := tmdbResource.ID
|
||||
// 将 req.Links 转换为 models.JsonMap 类型
|
||||
linksMap := make(models.JsonMap)
|
||||
for key, value := range req.Links {
|
||||
linksMap[key] = value
|
||||
}
|
||||
|
||||
// 创建资源对象
|
||||
// 处理TMDB ID
|
||||
tmdbID := req.ID
|
||||
|
||||
// 处理媒体类型
|
||||
var mediaType *string
|
||||
if req.MediaType != "" {
|
||||
mediaType = &req.MediaType
|
||||
}
|
||||
|
||||
// 创建资源对象 - 直接使用前端提供的数据
|
||||
resource = &models.Resource{
|
||||
Title: tmdbResource.Title,
|
||||
TitleEn: tmdbResource.TitleEn,
|
||||
Description: tmdbResource.Description,
|
||||
ResourceType: tmdbResource.ResourceType,
|
||||
PosterImage: &posterImage,
|
||||
Images: tmdbResource.Images,
|
||||
Links: models.JsonMap(tmdbResource.Links),
|
||||
Title: req.Title,
|
||||
TitleEn: req.TitleEn,
|
||||
Description: req.Description,
|
||||
ResourceType: req.ResourceType,
|
||||
PosterImage: posterImage,
|
||||
Images: req.Images,
|
||||
Links: linksMap,
|
||||
Status: defaultStatus,
|
||||
TmdbID: &tmdbID,
|
||||
MediaType: mediaType,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
@@ -168,10 +201,10 @@ func CreateResourceFromTMDB(c *gin.Context) {
|
||||
result, err := models.DB.NamedExec(`
|
||||
INSERT INTO resources (
|
||||
title, title_en, description, resource_type, poster_image,
|
||||
images, links, status, tmdb_id, created_at, updated_at
|
||||
images, links, status, tmdb_id, media_type, created_at, updated_at
|
||||
) VALUES (
|
||||
:title, :title_en, :description, :resource_type, :poster_image,
|
||||
:images, :links, :status, :tmdb_id, :created_at, :updated_at
|
||||
:images, :links, :status, :tmdb_id, :media_type, :created_at, :updated_at
|
||||
)
|
||||
`, resource)
|
||||
|
||||
|
||||
93
gobackend/internal/handlers/tmdb_multi_search_handlers.go
Normal file
93
gobackend/internal/handlers/tmdb_multi_search_handlers.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"dongman/internal/utils"
|
||||
)
|
||||
|
||||
// MultiSearchTMDB 多类型搜索TMDB API
|
||||
// @Summary 多类型搜索TMDB API
|
||||
// @Description 根据查询字符串搜索TMDB API获取电影和电视剧信息
|
||||
// @Tags TMDB
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string true "搜索关键词"
|
||||
// @Param page query int false "页码,默认为1"
|
||||
// @Success 200 {object} utils.TMDBMultiSearchResponse
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /api/tmdb/multi_search [get]
|
||||
func MultiSearchTMDB(c *gin.Context) {
|
||||
// 从URL查询参数获取查询字符串
|
||||
query := c.Query("query")
|
||||
|
||||
// 检查查询字符串是否为空
|
||||
if strings.TrimSpace(query) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "查询字符串不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取页码参数,默认为1
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
// 使用TMDB工具搜索
|
||||
response, err := utils.MultiSearch(query, page)
|
||||
if err != nil {
|
||||
log.Printf("TMDB多类型搜索失败: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "TMDB多类型搜索失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetMediaDetails 获取媒体详情
|
||||
// @Summary 获取媒体详情
|
||||
// @Description 根据媒体类型和ID获取详细信息,包括海报和剧照
|
||||
// @Tags TMDB
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param media_type path string true "媒体类型(movie或tv)"
|
||||
// @Param media_id path int true "媒体ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
// @Router /api/tmdb/details/{media_type}/{media_id} [get]
|
||||
func GetMediaDetails(c *gin.Context) {
|
||||
// 获取路径参数
|
||||
mediaType := c.Param("media_type")
|
||||
mediaIDStr := c.Param("media_id")
|
||||
|
||||
// 验证媒体类型
|
||||
if mediaType != "movie" && mediaType != "tv" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的媒体类型,必须是 movie 或 tv"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析媒体ID
|
||||
mediaID, err := strconv.Atoi(mediaIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的媒体ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取媒体详情
|
||||
details, err := utils.GetMediaDetails(mediaType, mediaID)
|
||||
if err != nil {
|
||||
log.Printf("获取媒体详情失败: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取媒体详情失败"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, details)
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func InitDB() (*sqlx.DB, error) {
|
||||
db.SetConnMaxLifetime(time.Minute * 30)
|
||||
|
||||
// 分步执行初始化表结构,避免一次性执行可能导致的错误
|
||||
// 1. 创建resources表(不包含tmdb_id字段)
|
||||
// 1. 创建resources表(不包含media_type字段,该字段将通过迁移添加)
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id INTEGER NOT NULL,
|
||||
@@ -136,7 +136,7 @@ func InitDB() (*sqlx.DB, error) {
|
||||
return nil, fmt.Errorf("创建resources表失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 创建基本索引
|
||||
// 2. 创建基本索引(不包含media_type的索引,该索引将在迁移中创建)
|
||||
_, err = db.Exec(`
|
||||
CREATE INDEX IF NOT EXISTS ix_resources_id ON resources (id);
|
||||
CREATE INDEX IF NOT EXISTS ix_resources_title ON resources (title);
|
||||
@@ -219,6 +219,7 @@ func MigrateDatabase() error {
|
||||
}{
|
||||
{"添加tmdb_id列", AddTmdbIDColumn},
|
||||
{"添加stickers列", AddStickersColumn},
|
||||
{"添加media_type列", AddMediaTypeColumn},
|
||||
// 未来可以在这里添加更多迁移
|
||||
}
|
||||
|
||||
@@ -646,6 +647,51 @@ func AddStickersColumn() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddMediaTypeColumn 添加media_type字段到resources表
|
||||
func AddMediaTypeColumn() error {
|
||||
log.Printf("检查resources表是否需要添加media_type字段...")
|
||||
|
||||
// 先检查resources表是否存在
|
||||
var tableExists int
|
||||
err := DB.Get(&tableExists, `SELECT count(*) FROM sqlite_master WHERE type='table' AND name='resources'`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查resources表是否存在失败: %w", err)
|
||||
}
|
||||
|
||||
if tableExists == 0 {
|
||||
log.Printf("resources表不存在,无需添加media_type字段")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查media_type字段是否已存在
|
||||
var count int
|
||||
err = DB.Get(&count, `SELECT COUNT(*) FROM pragma_table_info('resources') WHERE name = 'media_type'`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查media_type字段是否存在失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果字段不存在,则添加
|
||||
if count == 0 {
|
||||
log.Printf("media_type字段不存在,正在添加...")
|
||||
_, err = DB.Exec(`ALTER TABLE resources ADD COLUMN media_type VARCHAR`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("添加media_type字段失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
_, err = DB.Exec(`CREATE INDEX IF NOT EXISTS ix_resources_media_type ON resources (media_type)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建media_type索引失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("media_type字段添加成功")
|
||||
} else {
|
||||
log.Printf("media_type字段已存在,无需添加")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateResourceWithStickers 更新资源并支持贴纸数据
|
||||
func UpdateResourceWithStickers(resource *Resource) error {
|
||||
// 更新时间戳
|
||||
@@ -656,11 +702,11 @@ func UpdateResourceWithStickers(resource *Resource) error {
|
||||
`UPDATE resources SET
|
||||
title = ?, title_en = ?, description = ?, resource_type = ?,
|
||||
images = ?, poster_image = ?, links = ?, updated_at = ?,
|
||||
tmdb_id = ?, stickers = ?
|
||||
tmdb_id = ?, media_type = ?, stickers = ?
|
||||
WHERE id = ?`,
|
||||
resource.Title, resource.TitleEn, resource.Description, resource.ResourceType,
|
||||
resource.Images, resource.PosterImage, resource.Links, resource.UpdatedAt,
|
||||
resource.TmdbID, resource.Stickers, resource.ID,
|
||||
resource.TmdbID, resource.MediaType, resource.Stickers, resource.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -194,6 +194,7 @@ type Resource struct {
|
||||
IsSupplementApproval bool `db:"is_supplement_approval" json:"is_supplement_approval"`
|
||||
LikesCount int `db:"likes_count" json:"likes_count"`
|
||||
TmdbID *int `db:"tmdb_id" json:"tmdb_id"`
|
||||
MediaType *string `db:"media_type" json:"media_type"`
|
||||
Stickers JsonMap `db:"stickers" json:"stickers"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
@@ -231,6 +232,7 @@ type ResourceUpdate struct {
|
||||
PosterImage *string `json:"poster_image"`
|
||||
Links JsonMap `json:"links"`
|
||||
TmdbID *int `json:"tmdb_id"`
|
||||
MediaType *string `json:"media_type"`
|
||||
Stickers JsonMap `json:"stickers"`
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ func SetTMDBAPIKey(apiKey string) {
|
||||
|
||||
// TMDB中的类型ID映射到我们系统中的类型名称
|
||||
var GENRES = map[int]string{
|
||||
// 原有映射
|
||||
16: "幽默",
|
||||
35: "讽刺",
|
||||
10759: "冒险",
|
||||
@@ -60,13 +61,31 @@ var GENRES = map[int]string{
|
||||
80: "犯罪",
|
||||
9648: "悬疑",
|
||||
18: "浪漫",
|
||||
|
||||
// 添加电影常用分类
|
||||
12: "冒险", // Adventure
|
||||
28: "动作", // Action
|
||||
878: "科幻", // Science Fiction
|
||||
14: "奇幻", // Fantasy
|
||||
36: "历史", // History
|
||||
10751: "家庭", // Family
|
||||
10749: "爱情", // Romance
|
||||
53: "惊悚", // Thriller
|
||||
10752: "战争", // War
|
||||
37: "西部", // Western
|
||||
99: "纪录片", // Documentary
|
||||
10402: "音乐", // Music
|
||||
10770: "电视电影", // TV Movie
|
||||
10762: "儿童", // Kids
|
||||
}
|
||||
|
||||
// TMDBSearchResult TMDB搜索结果
|
||||
type TMDBSearchResult struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Name string `json:"name"` // 电视剧名称
|
||||
Title string `json:"title"` // 电影标题
|
||||
OriginalName string `json:"original_name"` // 电视剧原名
|
||||
OriginalTitle string `json:"original_title"` // 电影原标题
|
||||
Overview string `json:"overview"`
|
||||
}
|
||||
|
||||
@@ -87,8 +106,10 @@ type TMDBGenre struct {
|
||||
// TMDBDetails TMDB详情
|
||||
type TMDBDetails struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Name string `json:"name"` // 电视剧名称
|
||||
Title string `json:"title"` // 电影标题
|
||||
OriginalName string `json:"original_name"` // 电视剧原名
|
||||
OriginalTitle string `json:"original_title"` // 电影原标题
|
||||
Overview string `json:"overview"`
|
||||
Genres []TMDBGenre `json:"genres"`
|
||||
}
|
||||
@@ -109,13 +130,14 @@ type TMDBImageResponse struct {
|
||||
// TMDBResource 最终整合的TMDB结果,用于替代models.ResourceCreate
|
||||
type TMDBResource struct {
|
||||
ID int `json:"id"` // TMDB ID
|
||||
Title string `json:"title"`
|
||||
TitleEn string `json:"title_en"`
|
||||
Description string `json:"description"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
PosterImage string `json:"poster_image"`
|
||||
Images []string `json:"images"`
|
||||
Links map[string]interface{} `json:"links"`
|
||||
Title string `json:"title"` // 标题(电影Title或电视剧Name)
|
||||
TitleEn string `json:"title_en"` // 英文标题(电影OriginalTitle或电视剧OriginalName)
|
||||
Description string `json:"description"` // 描述
|
||||
ResourceType string `json:"resource_type"` // 资源类型,逗号分隔的类型名称
|
||||
PosterImage string `json:"poster_image"` // 海报图片URL
|
||||
Images []string `json:"images"` // 所有图片URL列表
|
||||
Links map[string]interface{} `json:"links"` // 链接信息
|
||||
MediaType string `json:"media_type"` // 媒体类型:movie, tv
|
||||
}
|
||||
|
||||
// SearchAnime 搜索动画,返回动画ID
|
||||
@@ -264,27 +286,213 @@ func GetImages(animeID int) (string, []string, error) {
|
||||
return posterURL, backdropURLs, nil
|
||||
}
|
||||
|
||||
// SearchMovie 搜索电影,返回电影ID
|
||||
func SearchMovie(query string, language string) (int, error) {
|
||||
if language == "" {
|
||||
language = "zh-CN"
|
||||
}
|
||||
|
||||
// URL编码查询参数
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
|
||||
// 构建URL,使用GetTMDBAPIKey()获取API密钥
|
||||
requestURL := fmt.Sprintf("%s/search/movie?api_key=%s&query=%s&language=%s", BASE_URL, GetTMDBAPIKey(), encodedQuery, language)
|
||||
|
||||
// 创建HTTP客户端并设置超时
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("搜索失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("API返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var searchResp TMDBSearchResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&searchResp)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("解析搜索结果失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查是否有结果
|
||||
if len(searchResp.Results) == 0 {
|
||||
return 0, errors.New("未找到匹配的电影")
|
||||
}
|
||||
|
||||
// 返回第一个结果的ID
|
||||
return searchResp.Results[0].ID, nil
|
||||
}
|
||||
|
||||
// 获取电影详情
|
||||
func GetMovieDetails(movieID int) (TMDBDetails, error) {
|
||||
// 构建URL,使用GetTMDBAPIKey()获取API密钥
|
||||
requestURL := fmt.Sprintf("%s/movie/%d?api_key=%s&language=zh-CN", BASE_URL, movieID, GetTMDBAPIKey())
|
||||
|
||||
// 发送请求
|
||||
resp, err := http.Get(requestURL)
|
||||
if err != nil {
|
||||
return TMDBDetails{}, fmt.Errorf("获取详情失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return TMDBDetails{}, fmt.Errorf("API返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var details TMDBDetails
|
||||
err = json.NewDecoder(resp.Body).Decode(&details)
|
||||
if err != nil {
|
||||
return TMDBDetails{}, fmt.Errorf("解析详情失败: %w", err)
|
||||
}
|
||||
|
||||
return details, nil
|
||||
}
|
||||
|
||||
// 获取电影图片
|
||||
func GetMovieImages(movieID int) (string, []string, error) {
|
||||
// 构建URL,使用GetTMDBAPIKey()获取API密钥
|
||||
requestURL := fmt.Sprintf("%s/movie/%d/images?api_key=%s", BASE_URL, movieID, GetTMDBAPIKey())
|
||||
|
||||
// 发送请求
|
||||
resp, err := http.Get(requestURL)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("获取图片失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", nil, fmt.Errorf("API返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var imageResp TMDBImageResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&imageResp)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("解析图片数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理海报图片
|
||||
var posterURL string
|
||||
if len(imageResp.Posters) > 0 {
|
||||
// 按投票数排序
|
||||
sortedPosters := imageResp.Posters
|
||||
// 简单的冒泡排序
|
||||
for i := 0; i < len(sortedPosters)-1; i++ {
|
||||
for j := 0; j < len(sortedPosters)-i-1; j++ {
|
||||
if sortedPosters[j].VoteCount < sortedPosters[j+1].VoteCount {
|
||||
sortedPosters[j], sortedPosters[j+1] = sortedPosters[j+1], sortedPosters[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
posterURL = fmt.Sprintf("%s/%s%s", IMAGE_BASE_URL, POSTER_W, sortedPosters[0].FilePath)
|
||||
}
|
||||
|
||||
// 处理背景图片
|
||||
var backdropURLs []string
|
||||
if len(imageResp.Backdrops) > 0 {
|
||||
// 按投票数排序
|
||||
sortedBackdrops := imageResp.Backdrops
|
||||
// 简单的冒泡排序
|
||||
for i := 0; i < len(sortedBackdrops)-1; i++ {
|
||||
for j := 0; j < len(sortedBackdrops)-i-1; j++ {
|
||||
if sortedBackdrops[j].VoteCount < sortedBackdrops[j+1].VoteCount {
|
||||
sortedBackdrops[j], sortedBackdrops[j+1] = sortedBackdrops[j+1], sortedBackdrops[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取前10张背景图片
|
||||
count := len(sortedBackdrops)
|
||||
if count > 10 {
|
||||
count = 10
|
||||
}
|
||||
|
||||
backdropURLs = make([]string, count+1) // +1 是为了加入海报图片
|
||||
backdropURLs[0] = posterURL // 将海报图片加入到第一位
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
backdropURLs[i+1] = fmt.Sprintf("%s/%s%s", IMAGE_BASE_URL, BACKDROP_W, sortedBackdrops[i].FilePath)
|
||||
}
|
||||
} else {
|
||||
// 如果没有背景图片,至少返回海报图片
|
||||
backdropURLs = []string{posterURL}
|
||||
}
|
||||
|
||||
return posterURL, backdropURLs, nil
|
||||
}
|
||||
|
||||
// SearchTMDB 搜索TMDB并返回适合资源表结构的结果
|
||||
func SearchTMDB(query string) (*TMDBResource, error) {
|
||||
// 1. 根据查询搜索动画ID
|
||||
animeID, err := SearchAnime(query, "zh-CN")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TMDB搜索失败: %w", err)
|
||||
// 尝试作为电影搜索
|
||||
movieID, movieErr := SearchMovie(query, "zh-CN")
|
||||
|
||||
// 如果电影搜索失败,尝试作为电视剧搜索
|
||||
if movieErr != nil {
|
||||
animeID, err := SearchAnime(query, "zh-CN")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TMDB搜索失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取电视剧详情
|
||||
details, err := GetAnimeDetails(animeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取TMDB详情失败: %w", err)
|
||||
}
|
||||
|
||||
// 获取海报和背景图片
|
||||
posterURL, imageURLs, err := GetImages(animeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取TMDB图片失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理类型
|
||||
var genres []string
|
||||
for _, genre := range details.Genres {
|
||||
if genreName, ok := GENRES[genre.ID]; ok {
|
||||
genres = append(genres, genreName)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建适合资源表的结构
|
||||
resource := &TMDBResource{
|
||||
ID: animeID,
|
||||
Title: details.Name,
|
||||
TitleEn: details.OriginalName,
|
||||
Description: details.Overview,
|
||||
ResourceType: strings.Join(genres, ","),
|
||||
PosterImage: posterURL,
|
||||
Images: imageURLs,
|
||||
Links: map[string]interface{}{},
|
||||
MediaType: "tv", // 电视剧类型
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// 2. 获取动画详情
|
||||
details, err := GetAnimeDetails(animeID)
|
||||
// 电影搜索成功,获取电影详情
|
||||
details, err := GetMovieDetails(movieID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取TMDB详情失败: %w", err)
|
||||
return nil, fmt.Errorf("获取电影详情失败: %w", err)
|
||||
}
|
||||
|
||||
// 3. 获取海报和背景图片
|
||||
posterURL, imageURLs, err := GetImages(animeID)
|
||||
// 获取海报和背景图片
|
||||
posterURL, imageURLs, err := GetMovieImages(movieID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取TMDB图片失败: %w", err)
|
||||
return nil, fmt.Errorf("获取电影图片失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 处理类型
|
||||
// 处理类型
|
||||
var genres []string
|
||||
for _, genre := range details.Genres {
|
||||
if genreName, ok := GENRES[genre.ID]; ok {
|
||||
@@ -292,16 +500,17 @@ func SearchTMDB(query string) (*TMDBResource, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 构建适合资源表的结构
|
||||
// 构建适合资源表的结构
|
||||
resource := &TMDBResource{
|
||||
ID: animeID,
|
||||
Title: details.Name,
|
||||
TitleEn: details.OriginalName,
|
||||
ID: movieID,
|
||||
Title: details.Title, // 使用电影标题
|
||||
TitleEn: details.OriginalTitle, // 使用电影原标题
|
||||
Description: details.Overview,
|
||||
ResourceType: strings.Join(genres, ","),
|
||||
PosterImage: posterURL,
|
||||
Images: imageURLs,
|
||||
Links: map[string]interface{}{},
|
||||
MediaType: "movie", // 电影类型
|
||||
}
|
||||
|
||||
return resource, nil
|
||||
|
||||
286
gobackend/internal/utils/tmdb_multi_search.go
Normal file
286
gobackend/internal/utils/tmdb_multi_search.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TMDBMultiSearchResult 表示Multi Search API的单个结果项
|
||||
type TMDBMultiSearchResult struct {
|
||||
Adult bool `json:"adult"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title,omitempty"` // 电影标题
|
||||
OriginalTitle string `json:"original_title,omitempty"` // 电影原始标题
|
||||
Name string `json:"name,omitempty"` // 电视剧标题
|
||||
OriginalName string `json:"original_name,omitempty"` // 电视剧原始标题
|
||||
Overview string `json:"overview"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
MediaType string `json:"media_type"` // movie 或 tv
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
GenreIDs []int `json:"genre_ids"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
ReleaseDate string `json:"release_date,omitempty"` // 电影发行日期
|
||||
FirstAirDate string `json:"first_air_date,omitempty"` // 电视剧首播日期
|
||||
Video bool `json:"video,omitempty"` // 仅电影有此字段
|
||||
VoteAverage float64 `json:"vote_average"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
OriginCountry []string `json:"origin_country,omitempty"` // 仅电视剧有此字段
|
||||
}
|
||||
|
||||
// TMDBMultiSearchResponse 表示Multi Search API的响应
|
||||
type TMDBMultiSearchResponse struct {
|
||||
Page int `json:"page"`
|
||||
Results []TMDBMultiSearchResult `json:"results"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
}
|
||||
|
||||
// MultiSearch 执行多类型搜索,调用TMDB API获取结果
|
||||
// 参数:
|
||||
// - query: 搜索关键词
|
||||
// - page: 页码(默认为1)
|
||||
// 返回:
|
||||
// - *TMDBMultiSearchResponse: 搜索结果
|
||||
// - error: 错误信息
|
||||
func MultiSearch(query string, page ...int) (*TMDBMultiSearchResponse, error) {
|
||||
// URL编码查询参数
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
|
||||
// 处理页码参数
|
||||
pageNum := 1
|
||||
if len(page) > 0 && page[0] > 0 {
|
||||
pageNum = page[0]
|
||||
}
|
||||
|
||||
// 构建URL,使用GetTMDBAPIKey()获取API密钥
|
||||
requestURL := fmt.Sprintf("%s/search/multi?api_key=%s&query=%s&language=zh-CN&page=%d",
|
||||
BASE_URL, GetTMDBAPIKey(), encodedQuery, pageNum)
|
||||
|
||||
// 创建HTTP客户端并设置超时
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
log.Printf("发送TMDB Multi Search请求: %s", requestURL)
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TMDB API请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("TMDB API返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var searchResp TMDBMultiSearchResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&searchResp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析TMDB API响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 过滤结果,只保留电影和电视剧
|
||||
var filteredResults []TMDBMultiSearchResult
|
||||
for _, result := range searchResp.Results {
|
||||
if result.MediaType == "movie" || result.MediaType == "tv" {
|
||||
filteredResults = append(filteredResults, result)
|
||||
}
|
||||
}
|
||||
searchResp.Results = filteredResults
|
||||
|
||||
log.Printf("TMDB Multi Search成功,找到 %d 个结果,当前页 %d,总页数 %d",
|
||||
len(searchResp.Results), searchResp.Page, searchResp.TotalPages)
|
||||
return &searchResp, nil
|
||||
}
|
||||
|
||||
// TMDBMediaDetails 表示媒体详情响应
|
||||
type TMDBMediaDetails struct {
|
||||
ID int `json:"id"`
|
||||
Images TMDBMediaImages `json:"images"`
|
||||
Genres []TMDBGenre `json:"genres"`
|
||||
// 其他字段根据需要添加
|
||||
}
|
||||
|
||||
// TMDBMediaImages 表示媒体图片集合
|
||||
type TMDBMediaImages struct {
|
||||
Backdrops []TMDBImage `json:"backdrops"`
|
||||
Posters []TMDBImage `json:"posters"`
|
||||
}
|
||||
|
||||
// GetMediaDetails 获取媒体详情
|
||||
// 参数:
|
||||
// - mediaType: 媒体类型 (movie 或 tv)
|
||||
// - mediaID: 媒体ID
|
||||
// 返回:
|
||||
// - map[string]interface{}: 详情数据
|
||||
// - error: 错误信息
|
||||
func GetMediaDetails(mediaType string, mediaID int) (map[string]interface{}, error) {
|
||||
// 构建URL,使用正确的API接口路径
|
||||
requestURL := fmt.Sprintf("%s/%s/%d?api_key=%s&append_to_response=images",
|
||||
BASE_URL, mediaType, mediaID, GetTMDBAPIKey())
|
||||
|
||||
// 创建HTTP客户端并设置超时
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
log.Printf("发送TMDB %s详情请求: %s", mediaType, requestURL)
|
||||
resp, err := client.Get(requestURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TMDB API请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("TMDB API返回错误状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var details TMDBMediaDetails
|
||||
err = json.NewDecoder(resp.Body).Decode(&details)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析TMDB API响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理图片数据
|
||||
var imageURLs []string
|
||||
if len(details.Images.Backdrops) > 0 {
|
||||
// 按照投票数排序
|
||||
for i := 0; i < len(details.Images.Backdrops); i++ {
|
||||
for j := i + 1; j < len(details.Images.Backdrops); j++ {
|
||||
if details.Images.Backdrops[i].VoteCount < details.Images.Backdrops[j].VoteCount {
|
||||
details.Images.Backdrops[i], details.Images.Backdrops[j] = details.Images.Backdrops[j], details.Images.Backdrops[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 最多取10张图片
|
||||
count := min(10, len(details.Images.Backdrops))
|
||||
for i := 0; i < count; i++ {
|
||||
imageURL := fmt.Sprintf("https://image.tmdb.org/t/p/w1280%s", details.Images.Backdrops[i].FilePath)
|
||||
imageURLs = append(imageURLs, imageURL)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取海报URL
|
||||
posterURL := ""
|
||||
if len(details.Images.Posters) > 0 {
|
||||
posterURL = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", details.Images.Posters[0].FilePath)
|
||||
}
|
||||
|
||||
// 处理类型,转换为中文分类
|
||||
var genres []string
|
||||
// 创建中文化的genres数组,替换原有的英文genres
|
||||
var chineseGenres []map[string]interface{}
|
||||
|
||||
for _, genre := range details.Genres {
|
||||
// 对于resource_type字段使用中文名称拼接
|
||||
if genreName, ok := GENRES[genre.ID]; ok {
|
||||
genres = append(genres, genreName)
|
||||
|
||||
// 创建一个新的genre对象,但使用中文名称
|
||||
chineseGenre := map[string]interface{}{
|
||||
"id": genre.ID,
|
||||
"name": genreName,
|
||||
}
|
||||
chineseGenres = append(chineseGenres, chineseGenre)
|
||||
} else {
|
||||
// 如果在映射表中找不到对应的中文名称,保留原始英文名称
|
||||
chineseGenres = append(chineseGenres, map[string]interface{}{
|
||||
"id": genre.ID,
|
||||
"name": genre.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 构建返回结果
|
||||
result := map[string]interface{}{
|
||||
"id": details.ID,
|
||||
"genres": chineseGenres, // 使用中文化的genres数组
|
||||
"images": imageURLs,
|
||||
"poster_path": posterURL,
|
||||
"media_type": mediaType,
|
||||
"resource_type": strings.Join(genres, ","), // 添加中文分类字段
|
||||
}
|
||||
|
||||
// 根据媒体类型添加不同的字段
|
||||
if mediaType == "movie" {
|
||||
// 再次发起一个multi_search请求,获取中文标题和简介
|
||||
searchResp, err := MultiSearch(fmt.Sprintf("id:%d", mediaID), 1)
|
||||
if err == nil && len(searchResp.Results) > 0 {
|
||||
for _, item := range searchResp.Results {
|
||||
if item.MediaType == "movie" && item.ID == mediaID {
|
||||
result["title"] = item.Title
|
||||
result["original_title"] = item.OriginalTitle
|
||||
result["overview"] = item.Overview
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果无法通过multi_search获取,直接从详情API解析
|
||||
var movieDetails struct {
|
||||
Title string `json:"title"`
|
||||
OriginalTitle string `json:"original_title"`
|
||||
Overview string `json:"overview"`
|
||||
}
|
||||
resp, err := client.Get(requestURL)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
json.NewDecoder(resp.Body).Decode(&movieDetails)
|
||||
result["title"] = movieDetails.Title
|
||||
result["original_title"] = movieDetails.OriginalTitle
|
||||
result["overview"] = movieDetails.Overview
|
||||
}
|
||||
}
|
||||
} else if mediaType == "tv" {
|
||||
// 再次发起一个multi_search请求,获取中文标题和简介
|
||||
searchResp, err := MultiSearch(fmt.Sprintf("id:%d", mediaID), 1)
|
||||
if err == nil && len(searchResp.Results) > 0 {
|
||||
for _, item := range searchResp.Results {
|
||||
if item.MediaType == "tv" && item.ID == mediaID {
|
||||
result["name"] = item.Name
|
||||
result["original_name"] = item.OriginalName
|
||||
result["overview"] = item.Overview
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果无法通过multi_search获取,直接从详情API解析
|
||||
var tvDetails struct {
|
||||
Name string `json:"name"`
|
||||
OriginalName string `json:"original_name"`
|
||||
Overview string `json:"overview"`
|
||||
Seasons []interface{} `json:"seasons"`
|
||||
}
|
||||
resp, err := client.Get(requestURL)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
json.NewDecoder(resp.Body).Decode(&tvDetails)
|
||||
result["name"] = tvDetails.Name
|
||||
result["original_name"] = tvDetails.OriginalName
|
||||
result["overview"] = tvDetails.Overview
|
||||
result["seasons"] = tvDetails.Seasons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// min 返回两个整数中的较小值
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user