Skip to content
Merged
41 changes: 36 additions & 5 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ jobs:
tags: |
type=ref,event=pr
type=raw,value=latest,enable={{is_default_branch}}
type=match,pattern=v(.*),group=1,enable=${{ startsWith(github.ref, 'refs/tags/v') }}

- name: Get latest tag
run: |
Expand All @@ -83,7 +82,7 @@ jobs:
file: ./Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
tags: ghcr.io/${{ steps.lowercase.outputs.owner }}/moontv:${{ env.latest_tag }}
tags: ${{ github.event_name == 'pull_request_target' && format('ghcr.io/{0}/moontv:pr-{1}', steps.lowercase.outputs.owner, github.event.number) || format('ghcr.io/{0}/moontv:{1}', steps.lowercase.outputs.owner, env.latest_tag) }}
outputs: type=image,name=ghcr.io/${{ steps.lowercase.outputs.owner }}/moontv,name-canonical=true,push=true

- name: Export digest
Expand All @@ -100,10 +99,44 @@ jobs:
if-no-files-found: error
retention-days: 1

merge-pr:
runs-on: ubuntu-latest
needs:
- build
if: github.event_name == 'pull_request_target'
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set lowercase repository owner
id: lowercase
run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"

- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create -t ghcr.io/${{ steps.lowercase.outputs.owner }}/moontv:pr-${{ github.event.number }} \
$(printf 'ghcr.io/${{ steps.lowercase.outputs.owner }}/moontv@sha256:%s ' *)

merge:
runs-on: ubuntu-latest
needs:
- build
if: github.event_name != 'pull_request_target'
steps:
- name: Download digests
uses: actions/download-artifact@v4
Expand Down Expand Up @@ -132,9 +165,7 @@ jobs:
with:
images: ghcr.io/${{ steps.lowercase.outputs.owner }}/moontv
tags: |
type=ref,event=pr,prefix=pr-
type=raw,value=latest,enable={{is_default_branch}}
type=match,pattern=v(.*),group=1,enable=${{ startsWith(github.ref, 'refs/tags/v') }}

- name: Create manifest list and push
working-directory: /tmp/digests
Expand All @@ -149,7 +180,7 @@ jobs:
pr-comment:
runs-on: ubuntu-latest
needs:
- merge
- merge-pr
if: github.event_name == 'pull_request_target'
steps:
- name: Set lowercase repository owner
Expand Down
1 change: 1 addition & 0 deletions src/app/api/detail/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
});
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions src/app/api/douban/categories/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
});
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions src/app/api/douban/recommends/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export async function GET(request: NextRequest) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
});
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/douban/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
});
} catch (error) {
Expand Down Expand Up @@ -159,6 +160,7 @@ function handleTop250(pageStart: number) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
});
})
Expand Down
1 change: 1 addition & 0 deletions src/app/api/image-proxy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function GET(request: Request) {
headers.set('Cache-Control', 'public, max-age=15720000, s-maxage=15720000'); // 缓存半年
headers.set('CDN-Cache-Control', 'public, s-maxage=15720000');
headers.set('Vercel-CDN-Cache-Control', 'public, s-maxage=15720000');
headers.set('Netlify-Vary', 'query');

// 直接返回图片流
return new Response(imageResponse.body, {
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/search/one/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
}
);
Expand Down Expand Up @@ -68,6 +69,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
}
);
Expand Down
1 change: 1 addition & 0 deletions src/app/api/search/resources/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export async function GET() {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
});
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
}
);
Expand Down Expand Up @@ -63,6 +64,7 @@ export async function GET(request: Request) {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
}
);
Expand Down
137 changes: 137 additions & 0 deletions src/app/api/search/suggestions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */

import { NextRequest, NextResponse } from 'next/server';

import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { searchFromApi } from '@/lib/downstream';

export const runtime = 'edge';

export async function GET(request: NextRequest) {
try {
// 从 cookie 获取用户信息
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}

const { searchParams } = new URL(request.url);
const query = searchParams.get('q')?.trim();

if (!query) {
return NextResponse.json({ suggestions: [] });
}

// 生成建议
const suggestions = await generateSuggestions(query);

// 从配置中获取缓存时间,如果没有配置则使用默认值300秒(5分钟)
const cacheTime = config.SiteConfig.SiteInterfaceCacheTime || 300;

return NextResponse.json(
{ suggestions },
{
headers: {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
'CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Vercel-CDN-Cache-Control': `public, s-maxage=${cacheTime}`,
'Netlify-Vary': 'query',
},
}
);
} catch (error) {
console.error('获取搜索建议失败', error);
return NextResponse.json({ error: '获取搜索建议失败' }, { status: 500 });
}
}

async function generateSuggestions(query: string): Promise<
Array<{
text: string;
type: 'exact' | 'related' | 'suggestion';
score: number;
}>
> {
const queryLower = query.toLowerCase();

const config = await getConfig();
const apiSites = config.SourceConfig.filter((site: any) => !site.disabled);
let realKeywords: string[] = [];

if (apiSites.length > 0) {
// 取第一个可用的数据源进行搜索
const firstSite = apiSites[0];
const results = await searchFromApi(firstSite, query);

realKeywords = Array.from(
new Set(
results
.map((r: any) => r.title)
.filter(Boolean)
.flatMap((title: string) => title.split(/[ -::·、-]/))
.filter(
(w: string) => w.length > 1 && w.toLowerCase().includes(queryLower)
)
)
).slice(0, 8);
}

// 根据关键词与查询的匹配程度计算分数,并动态确定类型
const realSuggestions = realKeywords.map((word) => {
const wordLower = word.toLowerCase();
const queryWords = queryLower.split(/[ -::·、-]/);

// 计算匹配分数:完全匹配得分更高
let score = 1.0;
if (wordLower === queryLower) {
score = 2.0; // 完全匹配
} else if (
wordLower.startsWith(queryLower) ||
wordLower.endsWith(queryLower)
) {
score = 1.8; // 前缀或后缀匹配
} else if (queryWords.some((qw) => wordLower.includes(qw))) {
score = 1.5; // 包含查询词
}

// 根据匹配程度确定类型
let type: 'exact' | 'related' | 'suggestion' = 'related';
if (score >= 2.0) {
type = 'exact';
} else if (score >= 1.5) {
type = 'related';
} else {
type = 'suggestion';
}

return {
text: word,
type,
score,
};
});

// 按分数降序排列,相同分数按类型优先级排列
const sortedSuggestions = realSuggestions.sort((a, b) => {
if (a.score !== b.score) {
return b.score - a.score; // 分数高的在前
}
// 分数相同时,按类型优先级:exact > related > suggestion
const typePriority = { exact: 3, related: 2, suggestion: 1 };
return typePriority[b.type] - typePriority[a.type];
});

return sortedSuggestions;
}
Loading
Loading