Showing preview only (2,068K chars total). Download the full file or copy to clipboard to get everything.
Repository: fish2018/pansou
Branch: main
Commit: 8da4e6a10ee7
Files: 198
Total size: 1.9 MB
Directory structure:
gitextract_lptfo9vf/
├── .dockerignore
├── .github/
│ └── workflows/
│ └── docker_ci.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│ ├── auth_handler.go
│ ├── filter.go
│ ├── handler.go
│ ├── middleware.go
│ └── router.go
├── config/
│ └── config.go
├── docker-compose.yml
├── docs/
│ ├── MCP-SERVICE.md
│ ├── 插件开发指南.md
│ └── 系统开发设计文档.md
├── go.mod
├── go.sum
├── main.go
├── mcp-config.json
├── model/
│ ├── plugin_result.go
│ ├── request.go
│ └── response.go
├── package.json
├── plugin/
│ ├── ahhhhfs/
│ │ ├── ahhhhfs.go
│ │ └── html结构分析.md
│ ├── aikanzy/
│ │ └── aikanzy.go
│ ├── alupan/
│ │ ├── alupan.go
│ │ └── html结构分析.md
│ ├── ash/
│ │ ├── ash.go
│ │ └── html结构分析.md
│ ├── bixin/
│ │ ├── bixin.go
│ │ └── json结构分析.md
│ ├── cldi/
│ │ ├── cldi.go
│ │ └── html结构分析.md
│ ├── clmao/
│ │ ├── clmao.go
│ │ └── html结构分析.md
│ ├── clxiong/
│ │ ├── clxiong.go
│ │ └── html结构分析.md
│ ├── cyg/
│ │ └── cyg.go
│ ├── daishudj/
│ │ ├── daishudj.go
│ │ └── html结构分析.md
│ ├── ddys/
│ │ ├── ddys.go
│ │ └── html结构分析.md
│ ├── discourse/
│ │ ├── discourse.go
│ │ └── json结构分析.md
│ ├── djgou/
│ │ └── djgou.go
│ ├── duoduo/
│ │ ├── duoduo.go
│ │ └── html结构分析.md
│ ├── dyyj/
│ │ ├── dyyj.go
│ │ └── html结构分析.md
│ ├── erxiao/
│ │ ├── erxiao.go
│ │ └── html结构分析.md
│ ├── feikuai/
│ │ ├── feikuai.go
│ │ ├── html结构分析.md
│ │ └── json结构分析.md
│ ├── fox4k/
│ │ ├── fox4k.go
│ │ └── html结构分析.md
│ ├── gying/
│ │ ├── README.md
│ │ ├── gying.go
│ │ └── html结构分析.md
│ ├── haisou/
│ │ ├── haisou.go
│ │ └── json结构分析.md
│ ├── hdmoli/
│ │ ├── hdmoli.go
│ │ └── html结构分析.md
│ ├── hdr4k/
│ │ ├── hdr4k.go
│ │ ├── html结构分析.md
│ │ └── 设计文档.md
│ ├── huban/
│ │ ├── html结构分析.md
│ │ ├── huban.go
│ │ └── json结构分析.md
│ ├── hunhepan/
│ │ └── hunhepan.go
│ ├── javdb/
│ │ └── javdb.go
│ ├── jikepan/
│ │ └── jikepan.go
│ ├── jsnoteclub/
│ │ └── jsnoteclub.go
│ ├── jutoushe/
│ │ ├── html结构分析.md
│ │ └── jutoushe.go
│ ├── kkmao/
│ │ ├── html结构分析.md
│ │ └── kkmao.go
│ ├── kkv/
│ │ ├── html结构分析.md
│ │ └── kkv.go
│ ├── labi/
│ │ ├── html结构分析.md
│ │ └── labi.go
│ ├── leijing/
│ │ ├── html结构分析.md
│ │ └── leijing.go
│ ├── libvio/
│ │ ├── html结构分析.md
│ │ └── libvio.go
│ ├── lou1/
│ │ └── lou1.go
│ ├── meitizy/
│ │ ├── json结构分析.md
│ │ └── meitizy.go
│ ├── miaoso/
│ │ ├── json结构分析.md
│ │ └── miaoso.go
│ ├── mikuclub/
│ │ ├── html结构分析.md
│ │ ├── json结构分析.md
│ │ └── mikuclub.go
│ ├── mizixing/
│ │ └── mizixing.go
│ ├── muou/
│ │ ├── html结构分析.md
│ │ └── muou.go
│ ├── nsgame/
│ │ ├── json结构分析.md
│ │ └── nsgame.go
│ ├── nyaa/
│ │ ├── html结构分析.md
│ │ └── nyaa.go
│ ├── ouge/
│ │ ├── json结构分析.md
│ │ └── ouge.go
│ ├── pan666/
│ │ └── pan666.go
│ ├── pansearch/
│ │ └── pansearch.go
│ ├── panta/
│ │ ├── panta.go
│ │ └── panta插件设计文档.md
│ ├── panwiki/
│ │ └── panwiki.go
│ ├── panyq/
│ │ └── panyq.go
│ ├── pianku/
│ │ ├── html结构分析.md
│ │ └── pianku.go
│ ├── plugin.go
│ ├── qingying/
│ │ ├── html结构分析.md
│ │ └── qingying.go
│ ├── qqpd/
│ │ ├── README.md
│ │ └── qqpd.go
│ ├── quark4k/
│ │ ├── json结构分析.md
│ │ └── quark4k.go
│ ├── quarksoo/
│ │ ├── html结构分析.md
│ │ └── quarksoo.go
│ ├── qupanshe/
│ │ └── qupanshe.go
│ ├── qupansou/
│ │ └── qupansou.go
│ ├── sdso/
│ │ └── sdso.go
│ ├── shandian/
│ │ ├── html结构分析.md
│ │ └── shandian.go
│ ├── sousou/
│ │ ├── json结构分析.md
│ │ └── sousou.go
│ ├── susu/
│ │ ├── html结构分析.md
│ │ ├── susu.go
│ │ └── susu插件设计文档.md
│ ├── thepiratebay/
│ │ ├── html结构分析.md
│ │ └── thepiratebay.go
│ ├── u3c3/
│ │ └── u3c3.go
│ ├── wanou/
│ │ ├── json结构分析.md
│ │ └── wanou.go
│ ├── weibo/
│ │ ├── README.md
│ │ ├── weibo.go
│ │ └── 微博用户搜索API文档.md
│ ├── wuji/
│ │ ├── html结构分析.md
│ │ └── wuji.go
│ ├── xb6v/
│ │ └── xb6v.go
│ ├── xdpan/
│ │ ├── html结构分析.md
│ │ └── xdpan.go
│ ├── xdyh/
│ │ ├── json结构分析.md
│ │ └── xdyh.go
│ ├── xiaoji/
│ │ ├── html结构分析.md
│ │ └── xiaoji.go
│ ├── xiaozhang/
│ │ ├── html结构分析.md
│ │ └── xiaozhang.go
│ ├── xinjuc/
│ │ ├── html结构分析.md
│ │ └── xinjuc.go
│ ├── xuexizhinan/
│ │ ├── html结构分析.md
│ │ └── xuexizhinan.go
│ ├── xys/
│ │ ├── html结构分析.md
│ │ └── xys.go
│ ├── yiove/
│ │ └── yiove.go
│ ├── ypfxw/
│ │ ├── html结构分析.md
│ │ └── ypfxw.go
│ ├── yuhuage/
│ │ └── yuhuage.go
│ ├── yunsou/
│ │ ├── html结构分析.md
│ │ └── yunsou.go
│ ├── zhizhen/
│ │ ├── json结构分析.md
│ │ └── zhizhen.go
│ └── zxzj/
│ ├── html结构分析.md
│ └── zxzj.go
├── service/
│ ├── cache_integration.go
│ └── search_service.go
├── typescript/
│ ├── package.json
│ ├── src/
│ │ ├── index.ts
│ │ ├── tools/
│ │ │ ├── health.ts
│ │ │ ├── search.ts
│ │ │ └── start-backend.ts
│ │ └── utils/
│ │ ├── activity-monitor.ts
│ │ ├── backend-manager.ts
│ │ ├── config.ts
│ │ ├── http-client.ts
│ │ └── validators.ts
│ └── tsconfig.json
└── util/
├── cache/
│ ├── cache_key.go
│ ├── delayed_batch_write_manager.go
│ ├── disk_cache.go
│ ├── enhanced_two_level_cache.go
│ ├── global_buffer_manager.go
│ ├── memory_cache.go
│ ├── serializer.go
│ ├── sharded_disk_cache.go
│ ├── sharded_memory_cache.go
│ └── utils.go
├── compression.go
├── convert.go
├── http_util.go
├── json/
│ └── json.go
├── jwt.go
├── parser_util.go
├── pool/
│ ├── object_pool.go
│ └── worker_pool.go
└── regex_util.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# Git相关
.git
.gitignore
.github
# 文档和其他非必要文件
README.md
docs/
*.md
LICENSE
# 开发和测试相关
*_test.go
*.test
*.out
*.prof
# 构建产物
pansou
pansou_*
*.exe
*.exe~
*.dll
*.so
*.dylib
# 缓存和临时文件
.DS_Store
cache/
tmp/
.idea/
.vscode/
# 其他
Dockerfile
.dockerignore
================================================
FILE: .github/workflows/docker_ci.yml
================================================
name: 构建并发布Docker镜像
on:
push:
branches:
- "main"
paths-ignore:
- "README.md"
- "docs/**"
pull_request:
branches:
- "main"
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 设置QEMU
uses: docker/setup-qemu-action@v3
- name: 设置Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
- name: 登录到GitHub容器注册表
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKER }}
- name: 提取Docker元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/pansou
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
type=raw,value=latest,enable={{is_default_branch}}
- name: 构建并推送Docker镜像
uses: docker/build-push-action@v5
with:
context: .
# 这是关键修改点
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_DATE=${{ github.event.repository.updated_at }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
================================================
FILE: .gitignore
================================================
.DS_Store
================================================
FILE: Dockerfile
================================================
# 构建阶段
# 使用 --platform=$BUILDPLATFORM 确保构建器始终在运行 Actions 的机器的原生架构上运行 (通常是 linux/amd64)
# $BUILDPLATFORM 是 buildx 自动提供的变量
FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder
# 安装构建依赖
RUN apk add --no-cache git ca-certificates tzdata
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY go.mod go.sum ./
# 下载依赖
RUN go mod download
# 复制源代码
COPY . .
# 构建参数
ARG VERSION=dev
ARG BUILD_DATE=unknown
ARG VCS_REF=unknown
# 这是 buildx 自动传入的目标平台架构参数,例如 amd64, arm64
ARG TARGETARCH
# 构建应用
# Go 语言原生支持交叉编译,这里会根据传入的 TARGETARCH 编译出对应平台的可执行文件
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w -extldflags '-static'" -o pansou .
# 运行阶段
# 这一阶段会根据 buildx 的 --platform 参数选择正确的基础镜像 (例如 linux/arm64 会拉取 arm64/alpine)
FROM alpine:3.19
# 添加运行时依赖
RUN apk add --no-cache ca-certificates tzdata
# 创建缓存目录
RUN mkdir -p /app/cache
# 从构建阶段复制可执行文件
# buildx 会智能地从对应平台的 builder 中复制正确的可执行文件
COPY --from=builder /app/pansou /app/pansou
# 设置工作目录
WORKDIR /app
# 暴露端口
EXPOSE 8888
# 设置环境变量
# ENABLED_PLUGINS: 必须指定启用的插件,多个插件用逗号分隔
# AUTH_ENABLED: 认证功能默认关闭,可通过环境变量启用
ENV CACHE_PATH=/app/cache \
CACHE_ENABLED=true \
TZ=Asia/Shanghai \
ASYNC_PLUGIN_ENABLED=true \
ASYNC_RESPONSE_TIMEOUT=4 \
ASYNC_MAX_BACKGROUND_WORKERS=20 \
ASYNC_MAX_BACKGROUND_TASKS=100 \
ASYNC_CACHE_TTL_HOURS=1 \
CHANNELS=tgsearchers4,Aliyun_4K_Movies,bdbdndn11,yunpanx,bsbdbfjfjff,yp123pan,sbsbsnsqq,yunpanxunlei,tianyifc,BaiduCloudDisk,txtyzy,peccxinpd,gotopan,PanjClub,kkxlzy,baicaoZY,MCPH01,MCPH02,MCPH03,bdwpzhpd,ysxb48,jdjdn1111,yggpan,MCPH086,zaihuayun,Q66Share,ucwpzy,shareAliyun,alyp_1,dianyingshare,Quark_Movies,XiangxiuNBB,ydypzyfx,ucquark,xx123pan,yingshifenxiang123,zyfb123,tyypzhpd,tianyirigeng,cloudtianyi,hdhhd21,Lsp115,oneonefivewpfx,qixingzhenren,taoxgzy,Channel_Shares_115,tyysypzypd,vip115hot,wp123zy,yunpan139,yunpan189,yunpanuc,yydf_hzl,leoziyuan,pikpakpan,Q_dongman,yoyokuakeduanju,TG654TG,WFYSFX02,QukanMovie,yeqingjie_GJG666,movielover8888_film3,Baidu_netdisk,D_wusun,FLMdongtianfudi,KaiPanshare,QQZYDAPP,rjyxfx,PikPak_Share_Channel,btzhi,newproductsourcing,cctv1211,duan_ju,QuarkFree,yunpanNB,kkdj001,xxzlzn,pxyunpanxunlei,jxwpzy,kuakedongman,liangxingzhinan,xiangnikanj,solidsexydoll,guoman4K,zdqxm,kduanju,cilidianying,CBduanju,SharePanFilms,dzsgx,BooksRealm,Oscar_4Kmovies,douerpan,baidu_yppan,Q_jilupian,Netdisk_Movies,yunpanquark,ammmziyuan,ciliziyuanku,cili8888,jzmm_123pan \
ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou,hunhepan,jikepan,panwiki,pansearch,panta,qupansou,hdr4k,pan666,susu,thepiratebay,xuexizhinan,panyq,ouge,huban,cyg,erxiao,miaoso,fox4k,pianku,clmao,wuji,cldi,xiaozhang,libvio,leijing,xb6v,xys,ddys,hdmoli,yuhuage,u3c3,javdb,clxiong,jutoushe,sdso,xiaoji,xdyh,haisou,bixin,djgou,nyaa,xinjuc,aikanzy,qupanshe,xdpan,discourse,yunsou,qqpd,ahhhhfs,nsgame,gying,quark4k,quarksoo,sousou,ash \
AUTH_ENABLED=false \
AUTH_TOKEN_EXPIRY=24
# 构建参数
ARG VERSION=dev
ARG BUILD_DATE=unknown
ARG VCS_REF=unknown
# 添加镜像标签
LABEL org.opencontainers.image.title="PanSou" \
org.opencontainers.image.description="高性能网盘资源搜索API服务" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
org.opencontainers.image.url="https://github.com/fish2018/pansou" \
org.opencontainers.image.source="https://github.com/fish2018/pansou" \
maintainer="fish2018"
# 运行应用
CMD ["/app/pansou"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 fish2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# PanSou 网盘搜索API
PanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定义插件搜索。系统设计以性能和可扩展性为核心,支持并发搜索、结果智能排序和网盘类型分类。
[//]: # (MCP服务文档: [MCP-SERVICE.md](docs/MCP-SERVICE.md))
## 特性([详见系统设计文档](docs/%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3.md))
- **高性能搜索**:并发执行多个TG频道及异步插件搜索,显著提升搜索速度;工作池设计,高效管理并发任务
- **网盘类型分类**:自动识别多种网盘链接,按类型归类展示
- **智能排序**:基于插件等级、时间新鲜度和优先关键词的多维度综合排序算法
- **异步插件系统**:支持通过插件扩展搜索来源,支持"尽快响应,持续处理"的异步搜索模式,解决了某些搜索源响应时间长的问题。详情参考[**插件开发指南**](docs/插件开发指南.md)
- **二级缓存**:分片内存+分片磁盘缓存机制,大幅提升重复查询速度和并发性能
## MCP 服务
PanSou 还提供了一个基于 [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 的服务,可以将搜索功能集成到 Claude Desktop 等支持 MCP 的应用中。详情请参阅 [MCP 服务文档](docs/MCP-SERVICE.md)。
## 支持的网盘类型
百度网盘 (`baidu`)、阿里云盘 (`aliyun`)、夸克网盘 (`quark`)、天翼云盘 (`tianyi`)、UC网盘 (`uc`)、移动云盘 (`mobile`)、115网盘 (`115`)、PikPak (`pikpak`)、迅雷网盘 (`xunlei`)、123网盘 (`123`)、磁力链接 (`magnet`)、电驴链接 (`ed2k`)、其他 (`others`)
## 快速开始
在 Github 上先[](https://github.com/fish2018/pansou/fork)
本项目,并点上 Star !!!
### 使用Docker部署
[qqpd搜索插件文档](plugin/qqpd/README.md)
[gying搜索插件文档](plugin/gying/README.md)
[weibo搜索插件文档](plugin/weibo/README.md)
[常见问题总结](https://github.com/fish2018/pansou/issues/46)
[TG/QQ频道/插件/微博](https://github.com/fish2018/pansou/issues/4)
#### **1、前后端集成版**
##### 直接使用Docker命令
一键启动,开箱即用
```
docker run -d --name pansou -p 80:80 ghcr.io/fish2018/pansou-web
```
##### 使用Docker Compose(推荐)
```
# 下载配置文件
curl -o docker-compose.yml https://raw.githubusercontent.com/fish2018/pansou-web/refs/heads/main/docker-compose.yml
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
```
#### **2、纯后端API版**
##### 直接使用Docker命令
```bash
docker run -d --name pansou -p 8888:8888 ghcr.io/fish2018/pansou:latest
```
##### 使用Docker Compose(推荐)
```bash
# 下载配置文件
curl -o docker-compose.yml https://raw.githubusercontent.com/fish2018/pansou/refs/heads/main/docker-compose.yml
# 启动服务
docker-compose up -d
# 访问服务
http://localhost:8888
```
### 从源码安装
#### 环境要求
- Go 1.18+
- 可选:SOCKS5代理(用于访问受限地区的Telegram站点)
1. 克隆仓库
```bash
git clone https://github.com/fish2018/pansou.git
cd pansou
```
2. 配置环境变量(可选)
#### 基础配置
| 环境变量 | 描述 | 默认值 | 说明 |
|----------|------|--------|------|
| **PORT** | 服务端口 | `8888` | 修改服务监听端口 |
| **PROXY** | SOCKS5代理 | 无 | 如:`PROXY=socks5://127.0.0.1:1080` |
| **HTTPS_PROXY/HTTP_PROXY** | HTTPS/HTTP代理 | 无 | 如:`HTTPS_PROXY=http://127.0.0.1:1080`,`HTTP_PROXY=http://127.0.0.1:1080` |
| **CHANNELS** | 默认搜索的TG频道 | `tgsearchers3` | 多个频道用逗号分隔 |
| **ENABLED_PLUGINS** | 指定启用插件,多个插件用逗号分隔 | 无 | 必须显式指定 |
#### 认证配置(可选)
PanSou支持可选的安全认证功能,默认关闭。开启后,所有API接口(除登录接口外)都需要提供有效的JWT Token。详见[认证系统设计文档](docs/认证系统设计.md)。
| 环境变量 | 描述 | 默认值 | 说明 |
|----------|------|--------|------|
| **AUTH_ENABLED** | 是否启用认证 | `false` | 设置为`true`启用认证功能 |
| **AUTH_USERS** | 用户账号配置 | 无 | 格式:`user1:pass1,user2:pass2` |
| **AUTH_TOKEN_EXPIRY** | Token有效期(小时) | `24` | JWT Token的有效时长 |
| **AUTH_JWT_SECRET** | JWT签名密钥 | 自动生成 | 用于签名Token,建议手动设置 |
**认证配置示例:**
```bash
# 启用认证并配置单个用户
docker run -d --name pansou -p 8888:8888 \
-e AUTH_ENABLED=true \
-e AUTH_USERS=admin:admin123 \
-e AUTH_TOKEN_EXPIRY=24 \
ghcr.io/fish2018/pansou:latest
# 配置多个用户
docker run -d --name pansou -p 8888:8888 \
-e AUTH_ENABLED=true \
-e AUTH_USERS=admin:pass123,user1:pass456,user2:pass789 \
ghcr.io/fish2018/pansou:latest
```
**认证API接口:**
- `POST /api/auth/login` - 用户登录,获取Token
- `POST /api/auth/verify` - 验证Token有效性
- `POST /api/auth/logout` - 退出登录(客户端删除Token)
**使用Token调用API:**
```bash
# 1. 登录获取Token
curl -X POST http://localhost:8888/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# 响应:{"token":"eyJhbGc...","expires_at":1234567890,"username":"admin"}
# 2. 使用Token调用搜索API
curl -X POST http://localhost:8888/api/search \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{"kw":"速度与激情"}'
```
#### 高级配置(默认值即可)
<details>
<summary>点击展开高级配置选项(通常不需要修改)</summary>
| 环境变量 | 描述 | 默认值 |
|----------|------|--------|
| CONCURRENCY | 并发搜索数 | 自动计算 |
| CACHE_TTL | 缓存有效期(分钟) | `60` |
| CACHE_MAX_SIZE | 最大缓存大小(MB) | `100` |
| PLUGIN_TIMEOUT | 插件超时时间(秒) | `30` |
| ASYNC_RESPONSE_TIMEOUT | 快速响应超时(秒) | `4` |
| ASYNC_LOG_ENABLED | 异步插件详细日志 | `true` |
| CACHE_PATH | 缓存文件路径 | `./cache` |
| SHARD_COUNT | 缓存分片数量 | `8` |
| CACHE_WRITE_STRATEGY | 缓存写入策略(immediate/hybrid) | `hybrid` |
| ENABLE_COMPRESSION | 是否启用压缩 | `false` |
| MIN_SIZE_TO_COMPRESS | 最小压缩阈值(字节) | `1024` |
| GC_PERCENT | Go GC触发百分比 | `50` |
| ASYNC_MAX_BACKGROUND_WORKERS | 最大后台工作者数量 | CPU核心数×5 |
| ASYNC_MAX_BACKGROUND_TASKS | 最大后台任务数量 | 工作者数×5 |
| ASYNC_CACHE_TTL_HOURS | 异步缓存有效期(小时) | `1` |
| ASYNC_PLUGIN_ENABLED | 异步插件是否启用 | `true` |
| HTTP_READ_TIMEOUT | HTTP读取超时(秒) | 自动计算 |
| HTTP_WRITE_TIMEOUT | HTTP写入超时(秒) | 自动计算 |
| HTTP_IDLE_TIMEOUT | HTTP空闲超时(秒) | `120` |
| HTTP_MAX_CONNS | HTTP最大连接数 | 自动计算 |
</details>
3. 构建
```linux
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -extldflags '-static'" -o pansou .
```
4. 运行
```bash
./pansou
```
### 其他配置参考
<details>
<summary>点击展开 supervisor 配置参考</summary>
```
[program:pansou]
environment=PORT=8888,CHANNELS="tgsearchers4,Aliyun_4K_Movies,bdbdndn11,yunpanx,bsbdbfjfjff,yp123pan,sbsbsnsqq,yunpanxunlei,tianyifc,BaiduCloudDisk,txtyzy,peccxinpd,gotopan,PanjClub,kkxlzy,baicaoZY,MCPH01,bdwpzhpd,ysxb48,jdjdn1111,yggpan,MCPH086,zaihuayun,Q66Share,Oscar_4Kmovies,ucwpzy,shareAliyun,alyp_1,dianyingshare,Quark_Movies,XiangxiuNBB,ydypzyfx,ucquark,xx123pan,yingshifenxiang123,zyfb123,tyypzhpd,tianyirigeng,cloudtianyi,hdhhd21,Lsp115,oneonefivewpfx,qixingzhenren,taoxgzy,Channel_Shares_115,tyysypzypd,vip115hot,wp123zy,yunpan139,yunpan189,yunpanuc,yydf_hzl,leoziyuan,pikpakpan,Q_dongman,yoyokuakeduanju",ENABLED_PLUGINS="labi,zhizhen,shandian,duoduo,muou"
command=/home/work/pansou/pansou
directory=/home/work/pansou
autostart=true
autorestart=true
startsecs=5
startretries=3
exitcodes=0
stopwaitsecs=10
stopasgroup=true
killasgroup=true
```
</details>
<details>
<summary>点击展开 nginx 配置参考</summary>
```
server {
listen 80;
server_name pansou.252035.xyz;
# 将 HTTP 重定向到 HTTPS
return 301 https://$host$request_uri;
}
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=60r/m;
server {
listen 443 ssl http2;
server_name pansou.252035.xyz;
access_log /home/work/logs/pansou.log;
# 证书和密钥路径
ssl_certificate /etc/letsencrypt/live/252035.xyz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/252035.xyz/privkey.pem;
# 增强 SSL 安全性
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
ssl_prefer_server_ciphers on;
# 后端代理,应用限流
location / {
# 应用限流规则
limit_req zone=api_limit burst=10 nodelay;
# 当超过限制时返回 429 状态码
limit_req_status 429;
proxy_pass http://127.0.0.1:8888;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
</details>
## API文档
### 认证说明
当启用认证功能(`AUTH_ENABLED=true`)时,除登录和健康检测接口外的所有API接口都需要提供有效的JWT Token。
**请求头格式**:
```
Authorization: Bearer <your-jwt-token>
```
**获取Token**:
1. 调用登录接口获取Token(详见下方[认证API](#认证API))
2. 在后续所有API请求的Header中添加`Authorization: Bearer <token>`
3. Token过期后需要重新登录获取新Token
**示例**:
```bash
# 未启用认证时
curl -X POST http://localhost:8888/api/search \
-H "Content-Type: application/json" \
-d '{"kw":"速度与激情"}'
# 启用认证时
curl -X POST http://localhost:8888/api/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGc..." \
-d '{"kw":"速度与激情"}'
```
### 认证API
#### 用户登录
获取JWT Token用于后续API调用。
**接口地址**:`/api/auth/login`
**请求方法**:`POST`
**Content-Type**:`application/json`
**是否需要认证**:否
**请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| username | string | 是 | 用户名 |
| password | string | 是 | 密码 |
**请求示例**:
```bash
curl -X POST http://localhost:8888/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
```
**成功响应**:
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": 1234567890,
"username": "admin"
}
```
**错误响应**:
```json
{
"error": "用户名或密码错误"
}
```
#### 验证Token
验证当前Token是否有效。
**接口地址**:`/api/auth/verify`
**请求方法**:`POST`
**是否需要认证**:是
**请求示例**:
```bash
curl -X POST http://localhost:8888/api/auth/verify \
-H "Authorization: Bearer eyJhbGc..."
```
**成功响应**:
```json
{
"valid": true,
"username": "admin"
}
```
#### 退出登录
退出当前登录(客户端删除Token即可)。
**接口地址**:`/api/auth/logout`
**请求方法**:`POST`
**是否需要认证**:否
**请求示例**:
```bash
curl -X POST http://localhost:8888/api/auth/logout
```
**成功响应**:
```json
{
"message": "退出成功"
}
```
### 搜索API
搜索网盘资源。
**接口地址**:`/api/search`
**请求方法**:`POST` 或 `GET`
**Content-Type**:`application/json`(POST方法)
**是否需要认证**:取决于`AUTH_ENABLED`配置
**POST请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| kw | string | 是 | 搜索关键词 |
| channels | string[] | 否 | 搜索的频道列表,不提供则使用默认配置 |
| conc | number | 否 | 并发搜索数量,不提供则自动设置为频道数+插件数+10 |
| refresh | boolean | 否 | 强制刷新,不使用缓存,便于调试和获取最新数据 |
| res | string | 否 | 结果类型:all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type),默认为merge |
| src | string | 否 | 数据来源类型:all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) |
| plugins | string[] | 否 | 指定搜索的插件列表,不指定则搜索全部插件 |
| cloud_types | string[] | 否 | 指定返回的网盘类型列表,支持:baidu、aliyun、quark、tianyi、uc、mobile、115、pikpak、xunlei、123、magnet、ed2k,不指定则返回所有类型 |
| ext | object | 否 | 扩展参数,用于传递给插件的自定义参数,如{"title_en":"English Title", "is_all":true} |
| filter | object | 否 | 过滤配置,用于过滤返回结果。格式:{"include":["关键词1","关键词2"],"exclude":["排除词1","排除词2"]}。include为包含关键词列表(OR关系),exclude为排除关键词列表(OR关系) |
**GET请求参数**:
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| kw | string | 是 | 搜索关键词 |
| channels | string | 否 | 搜索的频道列表,使用英文逗号分隔多个频道,不提供则使用默认配置 |
| conc | number | 否 | 并发搜索数量,不提供则自动设置为频道数+插件数+10 |
| refresh | boolean | 否 | 强制刷新,设置为"true"表示不使用缓存 |
| res | string | 否 | 结果类型:all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type),默认为merge |
| src | string | 否 | 数据来源类型:all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件) |
| plugins | string | 否 | 指定搜索的插件列表,使用英文逗号分隔多个插件名,不指定则搜索全部插件 |
| cloud_types | string | 否 | 指定返回的网盘类型列表,使用英文逗号分隔多个类型,支持:baidu、aliyun、quark、tianyi、uc、mobile、115、pikpak、xunlei、123、magnet、ed2k,不指定则返回所有类型 |
| ext | string | 否 | JSON格式的扩展参数,用于传递给插件的自定义参数,如{"title_en":"English Title", "is_all":true} |
| filter | string | 否 | JSON格式的过滤配置,用于过滤返回结果。格式:{"include":["关键词1","关键词2"],"exclude":["排除词1","排除词2"]} |
**POST请求示例**:
```bash
# 未启用认证
curl -X POST http://localhost:8888/api/search \
-H "Content-Type: application/json" \
-d '{
"kw": "速度与激情",
"channels": ["tgsearchers3", "xxx"],
"conc": 2,
"refresh": true,
"res": "merge",
"src": "all",
"plugins": ["jikepan"],
"cloud_types": ["baidu", "quark"],
"ext": {
"title_en": "Fast and Furious",
"is_all": true
}
}'
# 启用认证时(需要添加Authorization头)
curl -X POST http://localhost:8888/api/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-d '{
"kw": "速度与激情",
"res": "merge"
}'
# 使用过滤器(只返回包含“合集”或“全集”,且不包含“预告”或“花絮”的结果)
curl -X POST http://localhost:8888/api/search \
-H "Content-Type: application/json" \
-d '{
"kw": "唐朝诡事录",
"filter": {
"include": ["合集", "全集"],
"exclude": ["预告", "花絮"]
}
}'
```
**GET请求示例**:
```bash
# 未启用认证
curl "http://localhost:8888/api/search?kw=速度与激情&res=merge&src=tg"
# 启用认证时(需要添加Authorization头)
curl "http://localhost:8888/api/search?kw=速度与激情&res=merge" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# 使用过滤器(GET方式需要URL编码JSON)
curl "http://localhost:8888/api/search?kw=唐朝诡事录&filter=%7B%22include%22%3A%5B%22合集%22%2C%22全集%22%5D%2C%22exclude%22%3A%5B%22预告%22%5D%7D"
```
**成功响应**:
```json
{
"total": 15,
"results": [
{
"message_id": "12345",
"unique_id": "channel-12345",
"channel": "tgsearchers3",
"datetime": "2023-06-10T14:23:45Z",
"title": "速度与激情全集1-10",
"content": "速度与激情系列全集,1080P高清...",
"links": [
{
"type": "baidu",
"url": "https://pan.baidu.com/s/1abcdef",
"password": "1234",
"datetime": "2023-06-10T14:23:45Z",
"work_title": "速度与激情全集1-10"
}
],
"tags": ["电影", "合集"],
"images": [
"https://cdn1.cdn-telegram.org/file/xxx.jpg"
]
},
// 更多结果...
],
"merged_by_type": {
"baidu": [
{
"url": "https://pan.baidu.com/s/1abcdef",
"password": "1234",
"note": "速度与激情全集1-10",
"datetime": "2023-06-10T14:23:45Z",
"source": "tg:频道名称",
"images": [
"https://cdn1.cdn-telegram.org/file/xxx.jpg"
]
},
// 更多百度网盘链接...
],
"quark": [
{
"url": "https://pan.quark.cn/s/xxxx",
"password": "",
"note": "凡人修仙传",
"datetime": "2023-06-10T15:30:22Z",
"source": "plugin:插件名",
"images": []
}
],
"aliyun": [
// 阿里云盘链接...
]
// 更多网盘类型...
}
}
```
**字段说明**:
**SearchResult对象**:
- `message_id`: 消息ID
- `unique_id`: 全局唯一标识符
- `channel`: 来源频道名称
- `datetime`: 消息发布时间
- `title`: 消息标题
- `content`: 消息内容
- `links`: 网盘链接数组
- `tags`: 标签数组(可选)
- `images`: TG消息中的图片链接数组(可选)
**Link对象**:
- `type`: 网盘类型(baidu、quark、aliyun等)
- `url`: 网盘链接地址
- `password`: 提取码/密码
- `datetime`: 链接更新时间(可选)
- `work_title`: 作品标题(可选)
- 用于区分同一消息中多个作品的链接
- 当一条消息包含≤4个链接时,所有链接使用相同的work_title
- 当一条消息包含>4个链接时,系统会智能识别每个链接对应的作品标题
**MergedLink对象**:
- `url`: 网盘链接地址
- `password`: 提取码/密码
- `note`: 资源说明/标题
- `datetime`: 链接更新时间
- `source`: 数据来源标识
- `tg:频道名称`: 来自Telegram频道
- `plugin:插件名`: 来自指定插件
- `unknown`: 未知来源
- `images`: TG消息中的图片链接数组(可选)
- 仅在来源为Telegram频道且消息包含图片时出现
**错误响应**:
```json
// 参数错误
{
"code": 400,
"message": "关键词不能为空"
}
// 未授权(启用认证但未提供Token)
{
"error": "未授权:缺少认证令牌",
"code": "AUTH_TOKEN_MISSING"
}
// Token无效或过期
{
"error": "未授权:令牌无效或已过期",
"code": "AUTH_TOKEN_INVALID"
}
```
### 健康检查
检查API服务是否正常运行。
**接口地址**:`/api/health`
**请求方法**:`GET`
**是否需要认证**:否(公开接口)
**请求示例**:
```bash
curl http://localhost:8888/api/health
```
**成功响应**:
```json
{
"status": "ok",
"auth_enabled": true,
"plugins_enabled": true,
"plugin_count": 16,
"plugins": [
"pansearch",
"panta",
"qupansou",
"hunhepan",
"jikepan",
"pan666",
"panyq",
"susu",
"xuexizhinan",
"hdr4k",
"labi",
"shandian",
"duoduo",
"muou",
"wanou",
"ouge",
"zhizhen",
"huban"
],
"channels_count": 1,
"channels": [
"tgsearchers3"
]
}
```
**字段说明**:
- `status`: 服务状态,"ok"表示正常
- `auth_enabled`: 是否启用认证功能
- `plugins_enabled`: 是否启用插件
- `plugin_count`: 已启用的插件数量
- `plugins`: 已启用的插件列表
- `channels_count`: 配置的频道数量
- `channels`: 配置的频道列表
## 📄 许可证
本项目采用 MIT 许可证。详情请见 [LICENSE](LICENSE) 文件。
## ⭐ Star 历史
[](https://star-history.com/#fish2018/pansou&Date)
================================================
FILE: api/auth_handler.go
================================================
package api
import (
"time"
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/util"
)
// LoginRequest 登录请求结构
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应结构
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
Username string `json:"username"`
}
// LoginHandler 处理用户登录
func LoginHandler(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "参数错误:用户名和密码不能为空"})
return
}
// 验证认证系统是否启用
if !config.AppConfig.AuthEnabled {
c.JSON(403, gin.H{"error": "认证功能未启用"})
return
}
// 验证用户配置是否存在
if config.AppConfig.AuthUsers == nil || len(config.AppConfig.AuthUsers) == 0 {
c.JSON(500, gin.H{"error": "认证系统未正确配置"})
return
}
// 验证用户名和密码
storedPassword, exists := config.AppConfig.AuthUsers[req.Username]
if !exists || storedPassword != req.Password {
c.JSON(401, gin.H{"error": "用户名或密码错误"})
return
}
// 生成JWT token
token, err := util.GenerateToken(
req.Username,
config.AppConfig.AuthJWTSecret,
config.AppConfig.AuthTokenExpiry,
)
if err != nil {
c.JSON(500, gin.H{"error": "生成令牌失败"})
return
}
// 返回token和过期时间
expiresAt := time.Now().Add(config.AppConfig.AuthTokenExpiry).Unix()
c.JSON(200, LoginResponse{
Token: token,
ExpiresAt: expiresAt,
Username: req.Username,
})
}
// VerifyHandler 验证token有效性
func VerifyHandler(c *gin.Context) {
// 如果未启用认证,直接返回有效
if !config.AppConfig.AuthEnabled {
c.JSON(200, gin.H{
"valid": true,
"message": "认证功能未启用",
})
return
}
// 如果能到达这里,说明中间件已经验证通过
username, exists := c.Get("username")
if !exists {
c.JSON(401, gin.H{"error": "未授权"})
return
}
c.JSON(200, gin.H{
"valid": true,
"username": username,
})
}
// LogoutHandler 退出登录(客户端删除token即可)
func LogoutHandler(c *gin.Context) {
// JWT是无状态的,服务端不需要处理注销
// 客户端删除存储的token即可
c.JSON(200, gin.H{"message": "退出成功"})
}
================================================
FILE: api/filter.go
================================================
package api
import (
"pansou/model"
"strings"
)
// applyResultFilter 应用过滤器到搜索响应
func applyResultFilter(response model.SearchResponse, filter *model.FilterConfig, resultType string) model.SearchResponse {
if filter == nil || (len(filter.Include) == 0 && len(filter.Exclude) == 0) {
return response
}
// 预处理关键词(转小写)
includeKeywords := make([]string, len(filter.Include))
for i, kw := range filter.Include {
includeKeywords[i] = strings.ToLower(kw)
}
excludeKeywords := make([]string, len(filter.Exclude))
for i, kw := range filter.Exclude {
excludeKeywords[i] = strings.ToLower(kw)
}
// 根据结果类型决定过滤策略
if resultType == "merged_by_type" || resultType == "" {
// 过滤 merged_by_type 的 note 字段
response.MergedByType = filterMergedByType(response.MergedByType, includeKeywords, excludeKeywords)
// 重新计算 total
total := 0
for _, links := range response.MergedByType {
total += len(links)
}
response.Total = total
} else if resultType == "all" || resultType == "results" {
// 过滤 results 的 title 和 links 的 work_title
response.Results = filterResults(response.Results, includeKeywords, excludeKeywords)
response.Total = len(response.Results)
// 如果是 all 类型,也需要过滤 merged_by_type
if resultType == "all" {
response.MergedByType = filterMergedByType(response.MergedByType, includeKeywords, excludeKeywords)
}
}
return response
}
// filterMergedByType 过滤 merged_by_type 中的链接
func filterMergedByType(mergedLinks model.MergedLinks, includeKeywords, excludeKeywords []string) model.MergedLinks {
if mergedLinks == nil {
return nil
}
filtered := make(model.MergedLinks)
for linkType, links := range mergedLinks {
filteredLinks := make([]model.MergedLink, 0)
for _, link := range links {
if matchFilter(link.Note, includeKeywords, excludeKeywords) {
filteredLinks = append(filteredLinks, link)
}
}
// 只添加非空的类型
if len(filteredLinks) > 0 {
filtered[linkType] = filteredLinks
}
}
return filtered
}
// filterResults 过滤 results 数组
func filterResults(results []model.SearchResult, includeKeywords, excludeKeywords []string) []model.SearchResult {
if results == nil {
return nil
}
filtered := make([]model.SearchResult, 0)
for _, result := range results {
// 先检查 title 是否匹配
if !matchFilter(result.Title, includeKeywords, excludeKeywords) {
continue
}
// title 匹配后,过滤 links 中的 work_title
filteredLinks := make([]model.Link, 0)
for _, link := range result.Links {
// 如果 link 有 work_title,检查它;否则使用 result.Title
checkText := link.WorkTitle
if checkText == "" {
checkText = result.Title
}
if matchFilter(checkText, includeKeywords, excludeKeywords) {
filteredLinks = append(filteredLinks, link)
}
}
// 只有有链接的结果才添加
if len(filteredLinks) > 0 {
result.Links = filteredLinks
filtered = append(filtered, result)
}
}
return filtered
}
// matchFilter 检查文本是否匹配过滤条件
func matchFilter(text string, includeKeywords, excludeKeywords []string) bool {
lowerText := strings.ToLower(text)
// 检查 exclude(任一匹配则排除)
for _, kw := range excludeKeywords {
if strings.Contains(lowerText, kw) {
return false
}
}
// 检查 include(如果有 include 列表,必须至少匹配一个)
if len(includeKeywords) > 0 {
matched := false
for _, kw := range includeKeywords {
if strings.Contains(lowerText, kw) {
matched = true
break
}
}
if !matched {
return false
}
}
return true
}
================================================
FILE: api/handler.go
================================================
package api
import (
// "fmt"
"net/http"
// "os"
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/model"
"pansou/service"
jsonutil "pansou/util/json"
"pansou/util"
"strings"
)
// 保存搜索服务的实例
var searchService *service.SearchService
// SetSearchService 设置搜索服务实例
func SetSearchService(service *service.SearchService) {
searchService = service
}
// SearchHandler 搜索处理函数
func SearchHandler(c *gin.Context) {
var req model.SearchRequest
var err error
// 根据请求方法不同处理参数
if c.Request.Method == http.MethodGet {
// GET方式:从URL参数获取
// 获取keyword,必填参数
keyword := c.Query("kw")
// 处理channels参数,支持逗号分隔
channelsStr := c.Query("channels")
var channels []string
// 只有当参数非空时才处理
if channelsStr != "" && channelsStr != " " {
parts := strings.Split(channelsStr, ",")
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
channels = append(channels, trimmed)
}
}
}
// 处理并发数
concurrency := 0
concStr := c.Query("conc")
if concStr != "" && concStr != " " {
concurrency = util.StringToInt(concStr)
}
// 处理强制刷新
forceRefresh := false
refreshStr := c.Query("refresh")
if refreshStr != "" && refreshStr != " " && refreshStr == "true" {
forceRefresh = true
}
// 处理结果类型和来源类型
resultType := c.Query("res")
if resultType == "" || resultType == " " {
resultType = "merge" // 直接设置为默认值merge
}
sourceType := c.Query("src")
if sourceType == "" || sourceType == " " {
sourceType = "all" // 直接设置为默认值all
}
// 处理plugins参数,支持逗号分隔
var plugins []string
// 检查请求中是否存在plugins参数
if c.Request.URL.Query().Has("plugins") {
pluginsStr := c.Query("plugins")
// 判断参数是否非空
if pluginsStr != "" && pluginsStr != " " {
parts := strings.Split(pluginsStr, ",")
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
plugins = append(plugins, trimmed)
}
}
}
} else {
// 如果请求中不存在plugins参数,设置为nil
plugins = nil
}
// 处理cloud_types参数,支持逗号分隔
var cloudTypes []string
// 检查请求中是否存在cloud_types参数
if c.Request.URL.Query().Has("cloud_types") {
cloudTypesStr := c.Query("cloud_types")
// 判断参数是否非空
if cloudTypesStr != "" && cloudTypesStr != " " {
parts := strings.Split(cloudTypesStr, ",")
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
cloudTypes = append(cloudTypes, trimmed)
}
}
}
} else {
// 如果请求中不存在cloud_types参数,设置为nil
cloudTypes = nil
}
// 处理ext参数,JSON格式
var ext map[string]interface{}
extStr := c.Query("ext")
if extStr != "" && extStr != " " {
// 处理特殊情况:ext={}
if extStr == "{}" {
ext = make(map[string]interface{})
} else {
if err := jsonutil.Unmarshal([]byte(extStr), &ext); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的ext参数格式: "+err.Error()))
return
}
}
}
// 确保ext不为nil
if ext == nil {
ext = make(map[string]interface{})
}
// 处理filter参数,JSON格式
var filter *model.FilterConfig
filterStr := c.Query("filter")
if filterStr != "" && filterStr != " " {
filter = &model.FilterConfig{}
if err := jsonutil.Unmarshal([]byte(filterStr), filter); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的filter参数格式: "+err.Error()))
return
}
}
req = model.SearchRequest{
Keyword: keyword,
Channels: channels,
Concurrency: concurrency,
ForceRefresh: forceRefresh,
ResultType: resultType,
SourceType: sourceType,
Plugins: plugins,
CloudTypes: cloudTypes, // 添加cloud_types到请求中
Ext: ext,
Filter: filter,
}
} else {
// POST方式:从请求体获取
data, err := c.GetRawData()
if err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "读取请求数据失败: "+err.Error()))
return
}
if err := jsonutil.Unmarshal(data, &req); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorResponse(400, "无效的请求参数: "+err.Error()))
return
}
}
// 检查并设置默认值
if len(req.Channels) == 0 {
req.Channels = config.AppConfig.DefaultChannels
}
// 如果未指定结果类型,默认返回merge并转换为merged_by_type
if req.ResultType == "" {
req.ResultType = "merged_by_type"
} else if req.ResultType == "merge" {
// 将merge转换为merged_by_type,以兼容内部处理
req.ResultType = "merged_by_type"
}
// 如果未指定数据来源类型,默认为全部
if req.SourceType == "" {
req.SourceType = "all"
}
// 参数互斥逻辑:当src=tg时忽略plugins参数,当src=plugin时忽略channels参数
if req.SourceType == "tg" {
req.Plugins = nil // 忽略plugins参数
} else if req.SourceType == "plugin" {
req.Channels = nil // 忽略channels参数
} else if req.SourceType == "all" {
// 对于all类型,如果plugins为空或不存在,统一设为nil
if req.Plugins == nil || len(req.Plugins) == 0 {
req.Plugins = nil
}
}
// 可选:启用调试输出(生产环境建议注释掉)
// fmt.Printf("🔧 [调试] 搜索参数: keyword=%s, channels=%v, concurrency=%d, refresh=%v, resultType=%s, sourceType=%s, plugins=%v, cloudTypes=%v, ext=%v\n",
// req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins, req.CloudTypes, req.Ext)
// 执行搜索
result, err := searchService.Search(req.Keyword, req.Channels, req.Concurrency, req.ForceRefresh, req.ResultType, req.SourceType, req.Plugins, req.CloudTypes, req.Ext)
if err != nil {
response := model.NewErrorResponse(500, "搜索失败: "+err.Error())
jsonData, _ := jsonutil.Marshal(response)
c.Data(http.StatusInternalServerError, "application/json", jsonData)
return
}
// 应用过滤器
if req.Filter != nil {
result = applyResultFilter(result, req.Filter, req.ResultType)
}
// 包装SearchResponse到标准响应格式中
response := model.NewSuccessResponse(result)
jsonData, _ := jsonutil.Marshal(response)
c.Data(http.StatusOK, "application/json", jsonData)
}
================================================
FILE: api/middleware.go
================================================
package api
import (
"fmt"
"net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/util"
)
// CORSMiddleware 跨域中间件
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
// LoggerMiddleware 日志中间件
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqURI := c.Request.RequestURI
// 对于搜索API,尝试解码关键词以便更好地显示
displayURI := reqURI
if strings.Contains(reqURI, "/api/search") && strings.Contains(reqURI, "kw=") {
if parsedURL, err := url.Parse(reqURI); err == nil {
if keyword := parsedURL.Query().Get("kw"); keyword != "" {
if decodedKeyword, err := url.QueryUnescape(keyword); err == nil {
// 替换原始URI中的编码关键词为解码后的关键词
displayURI = strings.Replace(reqURI, "kw="+keyword, "kw="+decodedKeyword, 1)
}
}
}
}
// 状态码
statusCode := c.Writer.Status()
// 请求IP
clientIP := c.ClientIP()
// 日志格式
gin.DefaultWriter.Write([]byte(
fmt.Sprintf("| %s | %s | %s | %d | %s\n",
clientIP, reqMethod, displayURI, statusCode, latencyTime.String())))
}
}
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 如果未启用认证,直接放行
if !config.AppConfig.AuthEnabled {
c.Next()
return
}
// 定义公开接口(不需要认证)
publicPaths := []string{
"/api/auth/login",
"/api/auth/logout",
"/api/health", // 健康检查接口可选择是否需要认证
}
// 检查当前路径是否是公开接口
path := c.Request.URL.Path
for _, p := range publicPaths {
if strings.HasPrefix(path, p) {
c.Next()
return
}
}
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{
"error": "未授权:缺少认证令牌",
"code": "AUTH_TOKEN_MISSING",
})
c.Abort()
return
}
// 解析Bearer token
const bearerPrefix = "Bearer "
if !strings.HasPrefix(authHeader, bearerPrefix) {
c.JSON(401, gin.H{
"error": "未授权:令牌格式错误",
"code": "AUTH_TOKEN_INVALID_FORMAT",
})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, bearerPrefix)
// 验证token
claims, err := util.ValidateToken(tokenString, config.AppConfig.AuthJWTSecret)
if err != nil {
c.JSON(401, gin.H{
"error": "未授权:令牌无效或已过期",
"code": "AUTH_TOKEN_INVALID",
})
c.Abort()
return
}
// 将用户信息存入上下文,供后续处理使用
c.Set("username", claims.Username)
c.Next()
}
}
================================================
FILE: api/router.go
================================================
package api
import (
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/plugin"
"pansou/service"
"pansou/util"
)
// SetupRouter 设置路由
func SetupRouter(searchService *service.SearchService) *gin.Engine {
// 设置搜索服务
SetSearchService(searchService)
// 设置为生产模式
gin.SetMode(gin.ReleaseMode)
// 创建默认路由
r := gin.Default()
// 添加中间件
r.Use(CORSMiddleware())
r.Use(LoggerMiddleware())
r.Use(util.GzipMiddleware()) // 添加压缩中间件
r.Use(AuthMiddleware()) // 添加认证中间件
// 定义API路由组
api := r.Group("/api")
{
// 认证接口(不需要认证,由中间件公开路径处理)
auth := api.Group("/auth")
{
auth.POST("/login", LoginHandler)
auth.POST("/verify", VerifyHandler)
auth.POST("/logout", LogoutHandler)
}
// 搜索接口 - 支持POST和GET两种方式
api.POST("/search", SearchHandler)
api.GET("/search", SearchHandler) // 添加GET方式支持
// 健康检查接口
api.GET("/health", func(c *gin.Context) {
// 根据配置决定是否返回插件信息
pluginCount := 0
pluginNames := []string{}
pluginsEnabled := config.AppConfig.AsyncPluginEnabled
if pluginsEnabled && searchService != nil && searchService.GetPluginManager() != nil {
plugins := searchService.GetPluginManager().GetPlugins()
pluginCount = len(plugins)
for _, p := range plugins {
pluginNames = append(pluginNames, p.Name())
}
}
// 获取频道信息
channels := config.AppConfig.DefaultChannels
channelsCount := len(channels)
response := gin.H{
"status": "ok",
"auth_enabled": config.AppConfig.AuthEnabled, // 添加认证状态
"plugins_enabled": pluginsEnabled,
"channels": channels,
"channels_count": channelsCount,
}
// 只有当插件启用时才返回插件相关信息
if pluginsEnabled {
response["plugin_count"] = pluginCount
response["plugins"] = pluginNames
}
c.JSON(200, response)
})
}
// 注册插件的Web路由(如果插件实现了PluginWithWebHandler接口)
// 只有当插件功能启用且插件在启用列表中时才注册路由
if config.AppConfig.AsyncPluginEnabled && searchService != nil && searchService.GetPluginManager() != nil {
enabledPlugins := searchService.GetPluginManager().GetPlugins()
for _, p := range enabledPlugins {
if webPlugin, ok := p.(plugin.PluginWithWebHandler); ok {
webPlugin.RegisterWebRoutes(r.Group(""))
}
}
}
return r
}
================================================
FILE: config/config.go
================================================
package config
import (
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
)
// Config 应用配置结构
type Config struct {
DefaultChannels []string
DefaultConcurrency int
Port string
ProxyURL string
UseProxy bool
HTTPProxyURL string
HTTPSProxyURL string
// 缓存相关配置
CacheEnabled bool
CachePath string
CacheMaxSizeMB int
CacheTTLMinutes int
// 压缩相关配置
EnableCompression bool
MinSizeToCompress int // 最小压缩大小(字节)
// GC相关配置
GCPercent int // GC触发阈值百分比
OptimizeMemory bool // 是否启用内存优化
// 插件相关配置
PluginTimeoutSeconds int // 插件超时时间(秒)
PluginTimeout time.Duration // 插件超时时间(Duration)
// 异步插件相关配置
AsyncPluginEnabled bool // 是否启用异步插件
EnabledPlugins []string // 启用的具体插件列表(空表示启用所有)
AsyncResponseTimeout int // 响应超时时间(秒)
AsyncResponseTimeoutDur time.Duration // 响应超时时间(Duration)
AsyncMaxBackgroundWorkers int // 最大后台工作者数量
AsyncMaxBackgroundTasks int // 最大后台任务数量
AsyncCacheTTLHours int // 异步缓存有效期(小时)
AsyncLogEnabled bool // 是否启用异步插件详细日志
// HTTP服务器配置
HTTPReadTimeout time.Duration // 读取超时
HTTPWriteTimeout time.Duration // 写入超时
HTTPIdleTimeout time.Duration // 空闲超时
HTTPMaxConns int // 最大连接数
// 认证相关配置
AuthEnabled bool // 是否启用认证
AuthUsers map[string]string // 用户名:密码映射
AuthTokenExpiry time.Duration // Token有效期
AuthJWTSecret string // JWT签名密钥
}
// 全局配置实例
var AppConfig *Config
// 初始化配置
func Init() {
proxyURL := getProxyURL()
pluginTimeoutSeconds := getPluginTimeout()
asyncResponseTimeoutSeconds := getAsyncResponseTimeout()
AppConfig = &Config{
DefaultChannels: getDefaultChannels(),
DefaultConcurrency: getDefaultConcurrency(),
Port: getPort(),
ProxyURL: proxyURL,
UseProxy: proxyURL != "",
HTTPProxyURL: getHTTPProxyURL(),
HTTPSProxyURL: getHTTPSProxyURL(),
// 缓存相关配置
CacheEnabled: getCacheEnabled(),
CachePath: getCachePath(),
CacheMaxSizeMB: getCacheMaxSize(),
CacheTTLMinutes: getCacheTTL(),
// 压缩相关配置
EnableCompression: getEnableCompression(),
MinSizeToCompress: getMinSizeToCompress(),
// GC相关配置
GCPercent: getGCPercent(),
OptimizeMemory: getOptimizeMemory(),
// 插件相关配置
PluginTimeoutSeconds: pluginTimeoutSeconds,
PluginTimeout: time.Duration(pluginTimeoutSeconds) * time.Second,
// 异步插件相关配置
AsyncPluginEnabled: getAsyncPluginEnabled(),
EnabledPlugins: getEnabledPlugins(),
AsyncResponseTimeout: asyncResponseTimeoutSeconds,
AsyncResponseTimeoutDur: time.Duration(asyncResponseTimeoutSeconds) * time.Second,
AsyncMaxBackgroundWorkers: getAsyncMaxBackgroundWorkers(),
AsyncMaxBackgroundTasks: getAsyncMaxBackgroundTasks(),
AsyncCacheTTLHours: getAsyncCacheTTLHours(),
AsyncLogEnabled: getAsyncLogEnabled(),
// HTTP服务器配置
HTTPReadTimeout: getHTTPReadTimeout(),
HTTPWriteTimeout: getHTTPWriteTimeout(),
HTTPIdleTimeout: getHTTPIdleTimeout(),
HTTPMaxConns: getHTTPMaxConns(),
// 认证相关配置
AuthEnabled: getAuthEnabled(),
AuthUsers: getAuthUsers(),
AuthTokenExpiry: getAuthTokenExpiry(),
AuthJWTSecret: getAuthJWTSecret(),
}
// 应用GC配置
applyGCSettings()
}
// 从环境变量获取默认频道列表,如果未设置则使用默认值
func getDefaultChannels() []string {
channelsEnv := os.Getenv("CHANNELS")
if channelsEnv == "" {
return []string{"tgsearchers4"}
}
return strings.Split(channelsEnv, ",")
}
// 从环境变量获取默认并发数,如果未设置则使用基于环境变量的简单计算
func getDefaultConcurrency() int {
concurrencyEnv := os.Getenv("CONCURRENCY")
if concurrencyEnv != "" {
concurrency, err := strconv.Atoi(concurrencyEnv)
if err == nil && concurrency > 0 {
return concurrency
}
}
// 环境变量未设置或无效,使用基于环境变量的简单计算
// 计算频道数
channelCount := len(getDefaultChannels())
// 估计插件数(从环境变量或默认值,实际在应用启动后会根据真实插件数调整)
pluginCountEnv := os.Getenv("PLUGIN_COUNT")
pluginCount := 0
if pluginCountEnv != "" {
count, err := strconv.Atoi(pluginCountEnv)
if err == nil && count > 0 {
pluginCount = count
}
}
// 如果没有指定插件数,默认使用7个(当前已知的插件数)
if pluginCount == 0 {
pluginCount = 7
}
// 计算并发数 = 频道数 + 插件数 + 10
concurrency := channelCount + pluginCount + 10
if concurrency < 1 {
concurrency = 1 // 确保至少为1
}
return concurrency
}
// 更新默认并发数(根据实际插件数或0调用)
// pluginCount: 如果插件被禁用则为0,否则为实际插件数
func UpdateDefaultConcurrency(pluginCount int) {
if AppConfig == nil {
return
}
// 只有当未通过环境变量指定并发数时才进行调整
concurrencyEnv := os.Getenv("CONCURRENCY")
if concurrencyEnv != "" {
return
}
// 计算频道数
channelCount := len(AppConfig.DefaultChannels)
// 计算并发数 = 频道数 + 插件数(插件禁用时为0)+ 10
concurrency := channelCount + pluginCount + 10
if concurrency < 1 {
concurrency = 1 // 确保至少为1
}
// 更新配置
AppConfig.DefaultConcurrency = concurrency
}
// 从环境变量获取服务端口,如果未设置则使用默认值
func getPort() string {
port := os.Getenv("PORT")
if port == "" {
return "8888"
}
return port
}
func getProxyURL() string {
return os.Getenv("PROXY")
}
func getHTTPProxyURL() string {
if proxyURL := os.Getenv("HTTP_PROXY"); proxyURL != "" {
return proxyURL
}
return os.Getenv("http_proxy")
}
func getHTTPSProxyURL() string {
if proxyURL := os.Getenv("HTTPS_PROXY"); proxyURL != "" {
return proxyURL
}
return os.Getenv("https_proxy")
}
// 从环境变量获取是否启用缓存,如果未设置则默认启用
func getCacheEnabled() bool {
enabled := os.Getenv("CACHE_ENABLED")
if enabled == "" {
return true
}
return enabled != "false" && enabled != "0"
}
// 从环境变量获取缓存路径,如果未设置则使用默认路径
func getCachePath() string {
path := os.Getenv("CACHE_PATH")
if path == "" {
// 默认在当前目录下创建cache文件夹
defaultPath, err := filepath.Abs("./cache")
if err != nil {
return "./cache"
}
return defaultPath
}
return path
}
// 从环境变量获取缓存最大大小(MB),如果未设置则使用默认值
func getCacheMaxSize() int {
sizeEnv := os.Getenv("CACHE_MAX_SIZE")
if sizeEnv == "" {
return 100 // 默认100MB
}
size, err := strconv.Atoi(sizeEnv)
if err != nil || size <= 0 {
return 100
}
return size
}
// 从环境变量获取缓存TTL(分钟),如果未设置则使用默认值
func getCacheTTL() int {
ttlEnv := os.Getenv("CACHE_TTL")
if ttlEnv == "" {
return 60 // 默认60分钟
}
ttl, err := strconv.Atoi(ttlEnv)
if err != nil || ttl <= 0 {
return 60
}
return ttl
}
// 从环境变量获取是否启用压缩,如果未设置则默认禁用
func getEnableCompression() bool {
enabled := os.Getenv("ENABLE_COMPRESSION")
if enabled == "" {
return false // 默认禁用,因为通常由Nginx等处理
}
return enabled == "true" || enabled == "1"
}
// 从环境变量获取最小压缩大小,如果未设置则使用默认值
func getMinSizeToCompress() int {
sizeEnv := os.Getenv("MIN_SIZE_TO_COMPRESS")
if sizeEnv == "" {
return 1024 // 默认1KB
}
size, err := strconv.Atoi(sizeEnv)
if err != nil || size <= 0 {
return 1024
}
return size
}
// 从环境变量获取GC百分比,如果未设置则使用默认值
func getGCPercent() int {
percentEnv := os.Getenv("GC_PERCENT")
if percentEnv == "" {
return 50 // 默认50% - 优化内存管理,更频繁的GC避免内存暴涨
}
percent, err := strconv.Atoi(percentEnv)
if err != nil || percent <= 0 {
return 50 // 错误时也使用优化后的默认值
}
return percent
}
// 从环境变量获取是否优化内存,如果未设置则默认启用
func getOptimizeMemory() bool {
enabled := os.Getenv("OPTIMIZE_MEMORY")
if enabled == "" {
return true // 默认启用
}
return enabled != "false" && enabled != "0"
}
// 从环境变量获取插件超时时间(秒),如果未设置则使用默认值
func getPluginTimeout() int {
timeoutEnv := os.Getenv("PLUGIN_TIMEOUT")
if timeoutEnv == "" {
return 30 // 默认30秒
}
timeout, err := strconv.Atoi(timeoutEnv)
if err != nil || timeout <= 0 {
return 30
}
return timeout
}
// 从环境变量获取是否启用异步插件,如果未设置则默认启用
func getAsyncPluginEnabled() bool {
enabled := os.Getenv("ASYNC_PLUGIN_ENABLED")
if enabled == "" {
return true // 默认启用
}
return enabled != "false" && enabled != "0"
}
// 从环境变量获取启用的插件列表
// 返回nil表示未设置环境变量(不启用任何插件)
// 返回[]string{}表示设置为空(不启用任何插件)
// 返回具体列表表示启用指定插件
func getEnabledPlugins() []string {
plugins, exists := os.LookupEnv("ENABLED_PLUGINS")
if !exists {
// 未设置环境变量时返回nil,表示不启用任何插件
return nil
}
if plugins == "" {
// 设置为空字符串,也表示不启用任何插件
return []string{}
}
// 按逗号分割插件名
result := make([]string, 0)
for _, plugin := range strings.Split(plugins, ",") {
plugin = strings.TrimSpace(plugin)
if plugin != "" {
result = append(result, plugin)
}
}
return result
}
// 从环境变量获取异步响应超时时间(秒),如果未设置则使用默认值
func getAsyncResponseTimeout() int {
timeoutEnv := os.Getenv("ASYNC_RESPONSE_TIMEOUT")
if timeoutEnv == "" {
return 4 // 默认4秒
}
timeout, err := strconv.Atoi(timeoutEnv)
if err != nil || timeout <= 0 {
return 4
}
return timeout
}
// 从环境变量获取最大后台工作者数量,如果未设置则自动计算
func getAsyncMaxBackgroundWorkers() int {
sizeEnv := os.Getenv("ASYNC_MAX_BACKGROUND_WORKERS")
if sizeEnv != "" {
size, err := strconv.Atoi(sizeEnv)
if err == nil && size > 0 {
return size
}
}
// 自动计算:根据CPU核心数计算
// 每个CPU核心分配5个工作者,最小20个
cpuCount := runtime.NumCPU()
workers := cpuCount * 5
// 确保至少有20个工作者
if workers < 20 {
workers = 20
}
return workers
}
// 从环境变量获取最大后台任务数量,如果未设置则自动计算
func getAsyncMaxBackgroundTasks() int {
sizeEnv := os.Getenv("ASYNC_MAX_BACKGROUND_TASKS")
if sizeEnv != "" {
size, err := strconv.Atoi(sizeEnv)
if err == nil && size > 0 {
return size
}
}
// 自动计算:工作者数量的5倍,最小100个
workers := getAsyncMaxBackgroundWorkers()
tasks := workers * 5
// 确保至少有100个任务
if tasks < 100 {
tasks = 100
}
return tasks
}
// 从环境变量获取异步缓存有效期(小时),如果未设置则使用默认值
func getAsyncCacheTTLHours() int {
ttlEnv := os.Getenv("ASYNC_CACHE_TTL_HOURS")
if ttlEnv == "" {
return 1 // 默认1小时
}
ttl, err := strconv.Atoi(ttlEnv)
if err != nil || ttl <= 0 {
return 1
}
return ttl
}
// 从环境变量获取HTTP读取超时,如果未设置则自动计算
func getHTTPReadTimeout() time.Duration {
timeoutEnv := os.Getenv("HTTP_READ_TIMEOUT")
if timeoutEnv != "" {
timeout, err := strconv.Atoi(timeoutEnv)
if err == nil && timeout > 0 {
return time.Duration(timeout) * time.Second
}
}
// 自动计算:默认30秒,异步模式下根据异步响应超时调整
timeout := 30 * time.Second
// 如果启用了异步插件,确保读取超时足够长
if getAsyncPluginEnabled() {
// 读取超时应该至少是异步响应超时的3倍,确保有足够时间完成异步操作
asyncTimeoutSecs := getAsyncResponseTimeout()
asyncTimeoutExtended := time.Duration(asyncTimeoutSecs * 3) * time.Second
if asyncTimeoutExtended > timeout {
timeout = asyncTimeoutExtended
}
}
return timeout
}
// 从环境变量获取HTTP写入超时,如果未设置则自动计算
func getHTTPWriteTimeout() time.Duration {
timeoutEnv := os.Getenv("HTTP_WRITE_TIMEOUT")
if timeoutEnv != "" {
timeout, err := strconv.Atoi(timeoutEnv)
if err == nil && timeout > 0 {
return time.Duration(timeout) * time.Second
}
}
// 自动计算:默认60秒,但根据插件超时和异步处理时间调整
timeout := 60 * time.Second
// 如果启用了异步插件,确保写入超时足够长
pluginTimeoutSecs := getPluginTimeout()
// 计算1.5倍的插件超时时间(使用整数运算:乘以3再除以2)
pluginTimeoutExtended := time.Duration(pluginTimeoutSecs * 3 / 2) * time.Second
if pluginTimeoutExtended > timeout {
timeout = pluginTimeoutExtended
}
return timeout
}
// 从环境变量获取HTTP空闲超时,如果未设置则自动计算
func getHTTPIdleTimeout() time.Duration {
timeoutEnv := os.Getenv("HTTP_IDLE_TIMEOUT")
if timeoutEnv != "" {
timeout, err := strconv.Atoi(timeoutEnv)
if err == nil && timeout > 0 {
return time.Duration(timeout) * time.Second
}
}
// 自动计算:默认120秒,考虑到保持连接的效益
return 120 * time.Second
}
// 从环境变量获取HTTP最大连接数,如果未设置则自动计算
func getHTTPMaxConns() int {
maxConnsEnv := os.Getenv("HTTP_MAX_CONNS")
if maxConnsEnv != "" {
maxConns, err := strconv.Atoi(maxConnsEnv)
if err == nil && maxConns > 0 {
return maxConns
}
}
// 自动计算:根据CPU核心数计算
// 每个CPU核心分配200个连接,最小1000个
cpuCount := runtime.NumCPU()
maxConns := cpuCount * 200
// 确保至少有1000个连接
if maxConns < 1000 {
maxConns = 1000
}
return maxConns
}
// 从环境变量获取异步插件日志开关,如果未设置则使用默认值
func getAsyncLogEnabled() bool {
logEnv := os.Getenv("ASYNC_LOG_ENABLED")
if logEnv == "" {
return true // 默认启用日志
}
enabled, err := strconv.ParseBool(logEnv)
if err != nil {
return true // 解析失败时默认启用
}
return enabled
}
// 从环境变量获取认证开关,如果未设置则默认关闭
func getAuthEnabled() bool {
enabled := os.Getenv("AUTH_ENABLED")
return enabled == "true" || enabled == "1"
}
// 从环境变量获取用户配置,格式:user1:pass1,user2:pass2
func getAuthUsers() map[string]string {
usersEnv := os.Getenv("AUTH_USERS")
if usersEnv == "" {
return nil
}
users := make(map[string]string)
pairs := strings.Split(usersEnv, ",")
for _, pair := range pairs {
parts := strings.SplitN(pair, ":", 2)
if len(parts) == 2 {
username := strings.TrimSpace(parts[0])
password := strings.TrimSpace(parts[1])
if username != "" && password != "" {
users[username] = password
}
}
}
return users
}
// 从环境变量获取Token有效期(小时),如果未设置则使用默认值
func getAuthTokenExpiry() time.Duration {
expiryEnv := os.Getenv("AUTH_TOKEN_EXPIRY")
if expiryEnv == "" {
return 24 * time.Hour // 默认24小时
}
expiry, err := strconv.Atoi(expiryEnv)
if err != nil || expiry <= 0 {
return 24 * time.Hour
}
return time.Duration(expiry) * time.Hour
}
// 从环境变量获取JWT密钥,如果未设置则生成随机密钥
func getAuthJWTSecret() string {
secret := os.Getenv("AUTH_JWT_SECRET")
if secret == "" {
// 生成随机密钥(32字节)
import_crypto := "crypto/rand"
import_encoding := "encoding/base64"
_ = import_crypto
_ = import_encoding
// 注意:实际使用时应该使用crypto/rand生成随机密钥
// 这里为了简化,使用时间戳作为临时密钥
secret = "pansou-default-secret-" + strconv.FormatInt(time.Now().Unix(), 10)
}
return secret
}
// 应用GC设置
func applyGCSettings() {
// 设置GC百分比
debug.SetGCPercent(AppConfig.GCPercent)
// 如果启用内存优化
if AppConfig.OptimizeMemory {
// 释放操作系统内存
debug.FreeOSMemory()
}
}
================================================
FILE: docker-compose.yml
================================================
version: '3.8'
services:
pansou:
image: ghcr.io/fish2018/pansou:latest
container_name: pansou
restart: unless-stopped
ports:
- "8888:8888"
environment:
- PORT=8888
- CHANNELS=tgsearchers4,Aliyun_4K_Movies,bdbdndn11,yunpanx,bsbdbfjfjff,yp123pan,sbsbsnsqq,yunpanxunlei,tianyifc,BaiduCloudDisk,txtyzy,peccxinpd,gotopan,PanjClub,kkxlzy,baicaoZY,MCPH01,MCPH02,MCPH03,bdwpzhpd,ysxb48,jdjdn1111,yggpan,MCPH086,zaihuayun,Q66Share,ucwpzy,shareAliyun,alyp_1,dianyingshare,Quark_Movies,XiangxiuNBB,ydypzyfx,ucquark,xx123pan,yingshifenxiang123,zyfb123,tyypzhpd,tianyirigeng,cloudtianyi,hdhhd21,Lsp115,oneonefivewpfx,qixingzhenren,taoxgzy,Channel_Shares_115,tyysypzypd,vip115hot,wp123zy,yunpan139,yunpan189,yunpanuc,yydf_hzl,leoziyuan,pikpakpan,Q_dongman,yoyokuakeduanju,TG654TG,WFYSFX02,QukanMovie,yeqingjie_GJG666,movielover8888_film3,Baidu_netdisk,D_wusun,FLMdongtianfudi,KaiPanshare,QQZYDAPP,rjyxfx,PikPak_Share_Channel,btzhi,newproductsourcing,cctv1211,duan_ju,QuarkFree,yunpanNB,kkdj001,xxzlzn,pxyunpanxunlei,jxwpzy,kuakedongman,liangxingzhinan,xiangnikanj,solidsexydoll,guoman4K,zdqxm,kduanju,cilidianying,CBduanju,SharePanFilms,dzsgx,BooksRealm,Oscar_4Kmovies,douerpan,baidu_yppan,Q_jilupian,Netdisk_Movies,yunpanquark,ammmziyuan,ciliziyuanku,cili8888,jzmm_123pan
# 必须指定启用的插件,多个插件用逗号分隔
- ENABLED_PLUGINS=labi,zhizhen,shandian,duoduo,muou,wanou,hunhepan,jikepan,panwiki,pansearch,panta,qupansou,hdr4k,pan666,susu,thepiratebay,xuexizhinan,panyq,ouge,huban,cyg,erxiao,miaoso,fox4k,pianku,clmao,wuji,cldi,xiaozhang,libvio,leijing,xb6v,xys,ddys,hdmoli,yuhuage,u3c3,javdb,clxiong,jutoushe,sdso,xiaoji,xdyh,haisou,bixin,djgou,nyaa,xinjuc,aikanzy,qupanshe,xdpan,discourse,yunsou,qqpd,ahhhhfs,nsgame,gying,quark4k,quarksoo,sousou,ash
- CACHE_ENABLED=true
- CACHE_PATH=/app/cache
- CACHE_MAX_SIZE=100
- CACHE_TTL=60
- ASYNC_PLUGIN_ENABLED=true
- ASYNC_RESPONSE_TIMEOUT=4
- ASYNC_MAX_BACKGROUND_WORKERS=20
- ASYNC_MAX_BACKGROUND_TASKS=100
- ASYNC_CACHE_TTL_HOURS=1
# 认证配置(可选)
# - AUTH_ENABLED=true
# - AUTH_USERS=admin:admin123,user:pass456
# - AUTH_TOKEN_EXPIRY=24
# - AUTH_JWT_SECRET=your-secret-key-here
# 如果需要代理,取消下面的注释并设置代理地址
# - PROXY=socks5://proxy:7897
volumes:
- pansou-cache:/app/cache
networks:
- pansou-network
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8888/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# 如果需要代理,取消下面的注释
# proxy:
# image: ghcr.io/snail007/goproxy:latest
# container_name: pansou-proxy
# restart: unless-stopped
# command: /proxy socks -p :7897
# networks:
# - pansou-network
volumes:
pansou-cache:
name: pansou-cache
networks:
pansou-network:
name: pansou-network
================================================
FILE: docs/MCP-SERVICE.md
================================================
# PanSou MCP 服务文档
## 功能介绍
PanSou MCP 服务是一个基于 [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 的工具服务,它将 PanSou 网盘搜索 API 的功能封装为可在支持 MCP 的客户端(如 Cherry Studio)中直接调用的工具。
通过 PanSou MCP 服务,可以直接在 Claude 等 AI 助手中搜索网盘资源,极大地提升了获取网盘资源的便捷性。
### 核心功能
1. **搜索网盘资源 (`search_netdisk`)**:
- 支持通过 `keyword` 参数搜索网盘资源。
- 可通过 `source_type` 参数指定搜索来源:Telegram 频道、插件或两者结合。
- 可通过 `cloud_types` 参数过滤结果,显示特定类型的网盘链接。
- 支持通过 `force_refresh` 参数请求后端刷新缓存。
- 支持通过 `ext_params` 参数向后端插件传递扩展参数。
- 支持通过 `result_type` 参数控制后端返回的结果格式。
- 支持通过 `concurrency` 参数指定并发搜索数量。
2. **检查服务健康状态 (`check_service_health`)**:
- 检查所连接的 PanSou 后端服务是否正常运行。
- 获取后端服务的配置信息,如可用的 Telegram 频道列表和插件列表。
3. **启动后端服务 (`start_backend`)**:
- 自动启动本地的 PanSou Go 后端服务(如果尚未运行)。
- 等待服务完全启动并可用后才开始处理其他请求。
- 支持参数:`force_restart`(可选,布尔值,是否强制重启后端服务,默认为false)。
4. **获取静态资源信息 (`pansou://` URI scheme)**:
- 提供可用插件列表、可用频道列表和支持的网盘类型列表等静态信息资源。
- 支持资源URI:`pansou://plugins`(插件列表)、`pansou://channels`(频道列表)、`pansou://cloud-types`(网盘类型列表)。
### 架构与部署方式
PanSou MCP 服务设计为与 PanSou Go 后端服务分离,通过 HTTP API 进行通信。支持以下部署方式:
- **Node.js 部署 (TypeScript)**: MCP 服务基于 TypeScript 开发,编译后通过 `node` 命令运行编译后的 JavaScript 文件。它会自动连接到指定的 PanSou 后端服务。
- **Docker 部署**: 使用 Docker 容器运行 PanSou 后端服务,MCP 服务通过 HTTP API 连接到容器化的后端。
---
## 安装与部署
### 前提条件
1. **Node.js**: 确保您的系统已安装 Node.js (版本 >= 18.0.0)。您可以通过在终端运行 `node -v` 来检查版本。
2. **Go**: 确保您的系统已安装 Go (版本 >= 1.18)。您可以通过在终端运行 `go version` 来检查版本。
### 部署步骤
PanSou 后端服务通常运行在 `http://localhost:8888` (默认地址)。支持以下两种后端部署方式:
## 后端服务部署
### 方式一:源码部署后端服务
- 确保系统已安装 Go 1.23.0 或更高版本。
- 克隆或确保已有 PanSou Go 项目源码。
- 在项目根目录下,打开终端并执行以下命令进行构建:
```bash
# Windows (PowerShell/CMD)
go build -o pansou.exe .
```
- 构建完成后,运行生成的可执行文件以启动后端服务:
```bash
# Windows
.\pansou.exe
```
服务默认将在 `http://localhost:8888` 启动。
### 方式二:Docker 部署后端服务
Docker 部署方式更加简单,无需手动构建 Go 后端服务,直接使用预构建的 Docker 镜像。
**前提条件**:确保您的系统已安装 Docker 和 Docker Compose。
在 PanSou 项目根目录下,使用 Docker Compose 启动后端服务:
```bash
# 启动 Docker 容器
docker-compose up -d
# 检查容器状态
docker ps
# 验证服务是否正常运行
curl http://localhost:8888/api/health
```
### 验证后端服务
无论使用哪种方式启动后端服务,您都可以通过访问 `http://localhost:8888/api/health` 来检查服务状态,应该能看到类似以下的 JSON 响应:
```json
{
"status": "ok",
"plugins_enabled": true,
"channels_count": 1,
"channels": ["tgsearchers3"],
"plugin_count": 38,
"plugins": ["ddys", "erxiao", "..."]
}
```
---
## MCP 服务配置与使用
### 1. 构建 MCP 服务
- 确保系统已安装 Node.js (版本 >= 18.0.0)。
- 在 `typescript` 目录下,打开终端并执行以下命令来安装依赖并构建项目:
```bash
cd typescript
npm install
npm run build
```
构建完成后,编译后的 JavaScript 文件将位于 `typescript/dist` 目录下。
### 2. MCP 服务运行方式
构建完成后,可以通过以下方式之一运行 MCP 服务:
- **在MCP调用时自动启动** (推荐):
直接配置MCP客户端,调用时会自动启动后端服务器。
- **使用 `node` 直接运行** (手动启动):
在 PanSou 项目根目录下(包含 `typescript` 文件夹),运行:
```bash
# Windows (CMD/PowerShell)
node .\typescript\dist\index.js
```
服务启动后,将默认尝试连接到 `http://localhost:8888` 的 PanSou 后端服务。
如果想要后端服务运行在不同的地址或端口上,需要通过环境变量指定:
```bash
# Windows (CMD)
set PANSOU_SERVER_URL=http://your-backend-address:port
node .\typescript\dist\index.js
# Windows (PowerShell)
$env:PANSOU_SERVER_URL='http://your-backend-address:port'
node .\typescript\dist\index.js
```
### 3. MCP 客户端配置
#### 示例配置 Cherry Studio(版本1.5.7)
要在 Cherry Studio 中使用 PanSou MCP 服务,需要将其添加到 Cherry Studio MCP 的配置文件中。
- 找到 设置中的MCP。
- 选择 `添加服务器` 、 `从JSON导入` 。
- 加入服务配置(可以直接复制项目根目录下的 `mcp-config.json` 内容):
```json
{
"mcpServers": {
"pansou": {
"command": "node",
"args": [
"C:\\full\\path\\to\\your\\project\\typescript\\dist\\index.js"
],
"env": {
"PANSOU_SERVER_URL": "http://localhost:8888",
"REQUEST_TIMEOUT": "30",
"MAX_RESULTS": "50",
"DEFAULT_CLOUD_TYPES": "baidu,aliyun,quark,tianyi,uc,mobile,115,pikpak,xunlei,123,magnet,ed2k,others",
"AUTO_START_BACKEND": "true",
"DOCKER_MODE": "false",
"BACKEND_SHUTDOWN_DELAY": "5000",
"BACKEND_STARTUP_TIMEOUT": "30000",
"IDLE_TIMEOUT": "300000",
"ENABLE_IDLE_SHUTDOWN": "true",
"PROJECT_ROOT_PATH": "C:\\full\\path\\to\\your\\project",
"ENABLED_PLUGINS": "labi,zhizhen,shandian,duoduo,muou,wanou"
}
}
}
}
```
**注意**:
- 请将 `C:\\full\\path\\to\\your\\project` 替换为您项目实际的完整路径
- 如需强制指定部署模式,可修改 `DOCKER_MODE` 和 `AUTO_START_BACKEND` 参数
- **重要**:从当前版本开始,必须通过 `ENABLED_PLUGINS` 显式指定要启用的插件,否则不会启用任何插件
### 4. 启动 MCP 服务并开始使用
配置完成后,在对话界面启用 PanSou MCP 服务,即可开始尝试搜索。
<img width="495" height="649" alt="image" src="https://github.com/user-attachments/assets/b8c72649-03e8-4f52-86ba-aa16c4cc3b7e" />
---
## 配置说明与高级选项
### 智能检测机制
当 `DOCKER_MODE` 设置为 `"false"` 或未设置时,MCP 服务将自动检测部署模式:
1. **Docker 容器检测**:检查是否有运行中的 Docker 容器(名称包含 "pansou")
2. **源码部署检测**:检查是否存在 Go 可执行文件(pansou.exe/main.exe)
3. **服务运行检测**:检查后端服务是否已在运行
### 配置模式
- **自动模式**(推荐):使用默认配置,让服务自动检测部署方式
- **强制 Docker 模式**:设置 `"DOCKER_MODE": "true"`
- **强制源码模式**:设置 `"DOCKER_MODE": "false"` 且 `"AUTO_START_BACKEND": "true"`
- **仅连接模式**:设置 `"AUTO_START_BACKEND": "false"`(适用于手动启动的后端)
### 统一配置文件
无论使用哪种后端部署方式,都可以使用统一的 `mcp-config.json` 配置文件。MCP 服务会根据配置自动检测和适配不同的部署模式。
### 常见问题排查
#### 后端服务连接问题
1. **检查服务状态**:
```bash
# 检查健康状态
curl http://localhost:8888/api/health
# 或使用 PowerShell
Invoke-WebRequest -Uri "http://localhost:8888/api/health"
```
2. **Docker 部署问题**:
```bash
# 检查容器状态
docker ps
# 查看容器日志
docker-compose logs
# 重启容器
docker-compose restart
```
3. **源码部署问题**:
- 确认 Go 版本 >= 1.25.0
- 检查端口 8888 是否被占用
- 确认防火墙设置
4. **MCP 服务问题**:
- 确认 Node.js 版本 >= 18.0.0
- 检查 `typescript/dist` 目录是否存在
- 验证配置文件中的路径是否正确
---
## 支持的参数
MCP 服务通过工具调用接收参数。以下是主要工具及其支持的参数:
### `search_netdisk` 工具
用于搜索网盘资源。
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
| :-------------- | :-------------- | :--- | :------------------- | :----------------------------------------------------------- |
| `keyword` | string | 是 | - | 搜索关键词,例如 "速度与激情"、"Python教程"。 |
| `channels` | array of string | 否 | 配置默认值 | 要搜索的 Telegram 频道列表,例如 `["tgsearchers3", "another_channel"]`。 |
| `plugins` | array of string | 否 | 配置默认值或所有插件 | 要使用的搜索插件列表,例如 `["pansearch", "panta"]`。 |
| `cloud_types` | array of string | 否 | 无过滤 | 过滤结果,仅返回指定类型的网盘链接。支持的类型有:`baidu`, `aliyun`, `quark`, `tianyi`, `uc`, `mobile`, `115`, `pikpak`, `xunlei`, `123`, `magnet`, `ed2k`, `others`。 |
| `source_type` | string | 否 | `"all"` | 数据来源类型。可选值:`"all"` (全部来源), `"tg"` (仅 Telegram), `"plugin"` (仅插件)。 |
| `force_refresh` | boolean | 否 | `false` | 是否强制刷新缓存,以获取最新数据。 |
| `result_type` | string | 否 | `"merge"` | 返回结果的类型。可选值:`"all"` (返回所有结果), `"results"` (仅返回详细结果), `"merge"` (仅返回按网盘类型分组的结果)。 |
| `concurrency` | number | 否 | 自动计算 | 并发搜索的数量,0或不指定则自动计算。 |
| `ext_params` | object | 否 | `{}` | 传递给后端插件的自定义扩展参数,例如 `{"title_en": "Fast and Furious", "is_all": true}`。 |
---
### `check_service_health` 工具
用于检查后端服务健康状态。
- **参数**: 无
---
### `start_backend` 工具
用于启动本地 PanSou 后端服务。
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
| :-------------- | :------ | :--- | :------ | :----------------------------------------- |
| `force_restart` | boolean | 否 | `false` | 是否强制重启后端服务(即使它已经在运行)。 |
---
### 环境变量配置
您可以通过设置环境变量来配置 MCP 服务的行为:
| 环境变量 | 描述 | 默认值 |
| :--------------------- | :--------------------------------------------------------- | :------------------------ |
| `PANSOU_SERVER_URL` | PanSou 后端服务的 URL 地址。 | `http://localhost:8888` |
| `REQUEST_TIMEOUT` | HTTP 请求超时时间(秒)。 | `30` |
| `MAX_RESULTS` | (内部使用,限制处理结果数量) | `100` |
| `DEFAULT_CHANNELS` | 默认搜索的 Telegram 频道列表(逗号分隔)。 | `""` (使用后端默认) |
| `DEFAULT_PLUGINS` | 默认使用的搜索插件列表(逗号分隔)。 | `""` (使用后端默认或所有) |
| `ENABLED_PLUGINS` | 指定后端启用的插件列表(逗号分隔),必须显式指定。 | `""` (需要显式设置) |
| `DEFAULT_CLOUD_TYPES` | 默认的网盘类型过滤器(逗号分隔)。 | `""` (无过滤) |
| `AUTO_START_BACKEND` | 是否在 MCP 服务启动时自动尝试启动后端服务。 | `true` |
| `DOCKER_MODE` | 部署模式控制。设置为 `true` 强制使用 Docker 模式;设置为 `false` 或未设置时启用智能检测。智能检测将自动识别 Docker 容器、源码部署或运行中的服务。 | `false` (智能检测) |
| `PROJECT_ROOT_PATH` | PanSou 后端可执行文件所在的目录路径(用于自动启动)。 | 无 |
| `IDLE_TIMEOUT` | 空闲超时时间(毫秒),超过此时间无活动则可能关闭后端服务。 | `300000` (5分钟) |
| `ENABLE_IDLE_SHUTDOWN` | 是否启用空闲超时自动关闭后端服务。 | `true` |
================================================
FILE: docs/插件开发指南.md
================================================
# PanSou 插件开发指南
## 概述
PanSou 采用异步插件架构,支持通过插件扩展搜索来源。插件系统基于 Go 接口设计,提供高性能的并发搜索能力和智能缓存机制。
## 系统架构
### 核心组件
- **插件管理器 (PluginManager)**: 管理所有插件的注册和调度
- **异步插件 (AsyncSearchPlugin)**: 实现异步搜索接口的插件
- **基础插件 (BaseAsyncPlugin)**: 提供通用功能的基础结构
- **工作池**: 管理并发请求和资源限制
- **缓存系统**: 二级缓存提供高性能数据存储
### 异步处理机制
1. **双级超时控制**:
- 短超时 (4秒): 确保快速响应用户
- 长超时 (30秒): 允许完整数据处理
2. **渐进式结果返回**:
- `isFinal=false`: 部分结果,继续后台处理
- `isFinal=true`: 完整结果,停止处理
3. **智能缓存更新**:
- 实时更新主缓存 (内存+磁盘)
- 结果合并去重
- 用户无感知数据更新
## 插件接口规范
### AsyncSearchPlugin 接口
```go
type AsyncSearchPlugin interface {
// Name 返回插件名称 (必须唯一)
Name() string
// Priority 返回插件优先级 (1-4,数字越小优先级越高,影响搜索结果排序)
Priority() int
// AsyncSearch 异步搜索方法 (核心方法)
AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error), mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error)
// SetMainCacheKey 设置主缓存键 (由系统调用)
SetMainCacheKey(key string)
// SetCurrentKeyword 设置当前搜索关键词 (用于日志显示)
SetCurrentKeyword(keyword string)
// Search 同步搜索方法 (兼容性方法)
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
// SkipServiceFilter 返回是否跳过Service层的关键词过滤 (新增功能)
// 对于磁力搜索等需要宽泛结果的插件,应返回true
SkipServiceFilter() bool
}
```
### 参数说明
- **keyword**: 搜索关键词
- **searchFunc**: HTTP搜索函数,处理实际的网络请求
- **mainCacheKey**: 主缓存键,用于缓存管理
- **ext**: 扩展参数,支持自定义搜索选项
### Service层过滤控制 (新功能)
PanSou支持插件级别的Service层过滤控制,允许插件自主决定是否在Service层进行关键词过滤:
#### 过滤机制说明
1. **插件层过滤**: 在插件内部使用 `FilterResultsByKeyword()` 进行精确过滤
2. **Service层过滤**: 在 `search_service.go` 的 `mergeResultsByType()` 中进行二次过滤
3. **双层过滤问题**: 某些插件(如磁力搜索)需要更宽泛的搜索结果,二次过滤会误删有效结果
#### 适用场景
**应该跳过Service层过滤的插件类型**:
- ✅ **磁力搜索插件**: 如 thepiratebay,标题格式特殊(点号分隔),需要宽泛匹配
- ✅ **国外资源插件**: 英文资源标题与中文关键词匹配度低
- ✅ **特殊格式插件**: 标题包含大量符号或编码,标准过滤可能失效
- ✅ **聚合搜索插件**: 需要保留所有相关结果供用户筛选
**应该保持Service层过滤的插件类型**:
- ⚠️ **网盘搜索插件**: 标准中文资源,过滤有助于提高精确度
- ⚠️ **API接口插件**: 结构化数据,关键词匹配准确
- ⚠️ **论坛爬取插件**: 标题格式标准,过滤效果良好
## 插件优先级系统
### 优先级等级
PanSou 采用4级插件优先级系统,直接影响搜索结果的排序权重:
| 等级 | 得分 | 适用场景 | 示例插件 |
|------|------|----------|----------|
| **等级1** | **1000分** | 高质量、稳定可靠的数据源 | panta, zhizhen, labi |
| **等级2** | **500分** | 质量良好、响应稳定的数据源 | huban, shandian, duoduo |
| **等级3** | **0分** | 普通质量的数据源 | pansearch, hunhepan, pan666 |
| **等级4** | **-200分** | 质量较低或不稳定的数据源 | - |
### 排序算法影响
插件优先级在PanSou的多维度排序算法中占据主导地位:
```
总得分 = 插件得分(1000/500/0/-200) + 时间得分(最高500) + 关键词得分(最高420)
```
**权重分配**:
- 🥇 **插件等级**: ~52% (主导因素)
- 🥈 **关键词匹配**: ~22% (重要因素)
- 🥉 **时间新鲜度**: ~26% (重要因素)
**实际效果**:
- 等级1插件的结果通常排在前列
- 即使是较旧的等级1插件结果,也会优于新的等级3插件结果
- 包含优先关键词的等级2插件可能超越等级1插件
### 如何选择优先级
在开发新插件时,应根据以下标准选择合适的优先级:
#### 选择等级1的条件
- ✅ 数据源质量极高,很少出现无效链接
- ✅ 服务稳定性好,响应时间短
- ✅ 数据更新频率高,内容新颖
- ✅ 链接有效性高(>90%)
#### 选择等级2的条件
- ✅ 数据源质量良好,偶有无效链接
- ✅ 服务相对稳定,响应时间适中
- ✅ 数据更新较为及时
- ✅ 链接有效性中等(70-90%)
#### 选择等级3的条件
- ⚠️ 数据源质量一般,存在一定比例无效链接
- ⚠️ 服务稳定性一般,可能偶有超时
- ⚠️ 数据更新不够及时
- ⚠️ 链接有效性较低(50-70%)
#### 选择等级4的条件
- ❌ 数据源质量较差,大量无效链接
- ❌ 服务不稳定,经常超时或失败
- ❌ 数据更新缓慢或过时
- ❌ 链接有效性很低(<50%)
### 启动时显示
系统启动时会按优先级排序显示所有已加载的插件:
```
已加载插件:
- panta (优先级: 1)
- zhizhen (优先级: 1)
- labi (优先级: 1)
- huban (优先级: 2)
- duoduo (优先级: 2)
- pansearch (优先级: 3)
- hunhepan (优先级: 3)
```
## 开发新插件
### 1. 基础结构
```go
package myplugin
import (
"context"
"io"
"net/http"
"time"
"pansou/model"
"pansou/plugin"
"pansou/util/json" // 使用项目统一的高性能JSON工具
)
type MyPlugin struct {
*plugin.BaseAsyncPlugin
}
func init() {
p := &MyPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3), // 优先级3 = 普通质量数据源
}
plugin.RegisterGlobalPlugin(p)
}
// 对于需要跳过Service层过滤的插件(如磁力搜索插件)
func init() {
p := &MyMagnetPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("mymagnet", 4, true), // 跳过Service层过滤
}
plugin.RegisterGlobalPlugin(p)
}
// Search 执行搜索并返回结果(兼容性方法)
func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
result, err := p.SearchWithResult(keyword, ext)
if err != nil {
return nil, err
}
return result.Results, nil
}
// SearchWithResult 执行搜索并返回包含IsFinal标记的结果(推荐方法)
func (p *MyPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
}
```
### 2. 实现搜索逻辑(⭐ 推荐实现模式)
```go
func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 1. 构建请求URL
searchURL := fmt.Sprintf("https://api.example.com/search?q=%s", url.QueryEscape(keyword))
// 2. 处理扩展参数
if titleEn, ok := ext["title_en"].(string); ok && titleEn != "" {
searchURL += "&title_en=" + url.QueryEscape(titleEn)
}
// 3. 创建带超时的上下文 ⭐ 重要:避免请求超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 4. 创建请求对象 ⭐ 重要:使用context控制超时
req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
if err != nil {
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
}
// 5. 设置完整请求头 ⭐ 重要:避免反爬虫检测
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", "https://api.example.com/")
// 6. 发送HTTP请求(带重试机制)⭐ 重要:提高稳定性
resp, err := p.doRequestWithRetry(req, client)
if err != nil {
return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
}
defer resp.Body.Close()
// 7. 检查状态码
if resp.StatusCode != 200 {
return nil, fmt.Errorf("[%s] 请求返回状态码: %d", p.Name(), resp.StatusCode)
}
// 8. 解析响应
var apiResp APIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err)
}
// 9. 转换为标准格式
results := make([]model.SearchResult, 0, len(apiResp.Data))
for _, item := range apiResp.Data {
result := model.SearchResult{
UniqueID: fmt.Sprintf("%s-%s", p.Name(), item.ID),
Title: item.Title,
Content: item.Description,
Datetime: item.CreateTime,
Tags: item.Tags,
Links: convertLinks(item.Links), // 转换链接格式
}
results = append(results, result)
}
// 10. 关键词过滤
return plugin.FilterResultsByKeyword(results, keyword), nil
}
// doRequestWithRetry 带重试机制的HTTP请求 ⭐ 重要:提高稳定性
func (p *MyPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
maxRetries := 3
var lastErr error
for i := 0; i < maxRetries; i++ {
if i > 0 {
// 指数退避重试
backoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond
time.Sleep(backoff)
}
// 克隆请求避免并发问题
reqClone := req.Clone(req.Context())
resp, err := client.Do(reqClone)
if err == nil && resp.StatusCode == 200 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
}
```
### 3. 链接转换与 work_title 字段
#### Link 结构定义
```go
type Link struct {
Type string `json:"type"` // 网盘类型
URL string `json:"url"` // 链接地址
Password string `json:"password"` // 提取码/密码
Datetime time.Time `json:"datetime,omitempty"` // 链接更新时间(可选)
WorkTitle string `json:"work_title,omitempty"` // 作品标题(重要:用于区分同一消息中多个作品的链接)
}
```
#### work_title 字段详解
**字段作用**:
- 用于区分**同一条消息/结果中包含的多个不同作品**的链接
- 特别适用于论坛帖子、TG频道消息等一次性发布多部影视资源的场景
**使用场景示例**:
```
📺 TG频道消息示例:
【今日更新】多部热门剧集
1. 凡人修仙传 第30集
夸克:https://pan.quark.cn/s/abc123
2. 唐朝诡事录 第20集
夸克:https://pan.quark.cn/s/def456
3. 庆余年2 全集
百度:https://pan.baidu.com/s/xyz789?pwd=abcd
```
**不使用 work_title 的问题**:
- 所有链接的标题都是 "【今日更新】多部热门剧集"
- 用户无法区分哪个链接对应哪部剧集
**使用 work_title 后的效果**:
```go
links := []model.Link{
{
Type: "quark",
URL: "https://pan.quark.cn/s/abc123",
WorkTitle: "凡人修仙传 第30集", // 独立作品标题
},
{
Type: "quark",
URL: "https://pan.quark.cn/s/def456",
WorkTitle: "唐朝诡事录 第20集", // 独立作品标题
},
{
Type: "baidu",
URL: "https://pan.baidu.com/s/xyz789?pwd=abcd",
Password: "abcd",
WorkTitle: "庆余年2 全集", // 独立作品标题
},
}
```
**PanSou系统的智能处理**:
PanSou 会根据消息中的链接数量自动决定是否提取 work_title:
1. **链接数量 ≤ 4**:所有链接使用相同的 work_title(即消息标题)
```go
// 示例:一条消息只包含同一部剧的不同网盘链接
// 消息标题:"凡人修仙传 第30集"
// 链接1(夸克)、链接2(百度) → work_title 都是 "凡人修仙传 第30集"
```
2. **链接数量 > 4**:系统智能识别每个链接对应的作品标题
```go
// 示例:一条消息包含5个不同作品的链接
// 系统会分析消息文本,为每个链接提取独立的 work_title
```
**插件实现 work_title 的两种方式**:
**方式1: 依赖系统自动提取**(适用于TG频道、论坛等)
```go
// 直接返回链接,系统会自动调用 extractWorkTitlesForLinks 进行处理
links := []model.Link{
{Type: "quark", URL: "https://pan.quark.cn/s/abc123"},
{Type: "baidu", URL: "https://pan.baidu.com/s/xyz789"},
}
// PanSou会根据消息文本自动为每个链接提取work_title
```
**方式2: 插件手动设置**(适用于API插件、磁力搜索等)
```go
// 插件直接设置 work_title(如feikuai、thepiratebay等)
links := []model.Link{
{
Type: "magnet",
URL: magnetURL,
WorkTitle: buildWorkTitle(keyword, fileName), // 插件自己构建
Datetime: publishedTime,
},
}
```
**插件开发建议**:
- **网盘API插件**: 如果API直接返回单一作品,可以不设置 work_title(留空)
- **磁力搜索插件**: 建议设置 work_title,特别是文件名不含中文时需要拼接关键词
- **爬虫插件**: 如果能从页面提取每个链接的独立标题,建议设置 work_title
#### 支持的网盘类型
PanSou系统支持以下网盘类型的自动识别(完整列表):
| 网盘类型 | 类型标识 | 域名特征 | 说明 |
|---------|---------|----------|------|
| **夸克网盘** | `quark` | `pan.quark.cn` | 主流网盘 |
| **UC网盘** | `uc` | `drive.uc.cn` | 主流网盘 |
| **百度网盘** | `baidu` | `pan.baidu.com` | 主流网盘 |
| **阿里云盘** | `aliyun` | `aliyundrive.com`, `alipan.com` | 主流网盘 |
| **迅雷网盘** | `xunlei` | `pan.xunlei.com` | 主流网盘 |
| **天翼云盘** | `tianyi` | `cloud.189.cn` | 主流网盘 |
| **115网盘** | `115` | `115.com`,`115cdn.com`,`anxia.com` | 主流网盘 |
| **123网盘** | `123` | `123pan.com`,`123684.com`,`123685.com`,`123912.com`,`123pan.cn`,`123592.com` | 主流网盘 |
| **移动云盘** | `mobile` | `caiyun.139.com` | 其他网盘 |
| **PikPak** | `pikpak` | `mypikpak.com` | 其他网盘 |
| **磁力链接** | `magnet` | `magnet:?xt=urn:btih:` | 磁力链接 |
| **ED2K链接** | `ed2k` | `ed2k://` | 磁力链接 |
```go
func convertLinks(apiLinks []APILink) []model.Link {
links := make([]model.Link, 0, len(apiLinks))
for _, apiLink := range apiLinks {
link := model.Link{
Type: determineCloudType(apiLink.URL), // 自动识别网盘类型
URL: apiLink.URL,
Password: apiLink.Password,
}
links = append(links, link)
}
return links
}
func determineCloudType(url string) string {
switch {
case strings.Contains(url, "pan.quark.cn"):
return "quark"
case strings.Contains(url, "drive.uc.cn"):
return "uc"
case strings.Contains(url, "pan.baidu.com"):
return "baidu"
case strings.Contains(url, "aliyundrive.com") || strings.Contains(url, "alipan.com"):
return "aliyun"
case strings.Contains(url, "pan.xunlei.com"):
return "xunlei"
case strings.Contains(url, "cloud.189.cn"):
return "tianyi"
case strings.Contains(url, "115.com") || strings.Contains(url, "115cdn.com") || strings.Contains(url, "anxia.com"):
return "115"
case strings.Contains(url, "123684.com") || strings.Contains(url, "123685.com") ||
strings.Contains(url, "123912.com") || strings.Contains(url, "123pan.com") ||
strings.Contains(url, "123pan.cn") || strings.Contains(url, "123592.com"):
return "123"
case strings.Contains(url, "caiyun.139.com"):
return "mobile"
case strings.Contains(url, "mypikpak.com"):
return "pikpak"
case strings.Contains(url, "magnet:"):
return "magnet"
case strings.Contains(url, "ed2k://"):
return "ed2k"
default:
return "others"
}
}
// 使用示例
func convertAPILinks(apiLinks []APILink) []model.Link {
links := make([]model.Link, 0, len(apiLinks))
for _, apiLink := range apiLinks {
// 自动识别网盘类型
cloudType := determineCloudType(apiLink.URL)
// 只添加识别成功的链接
if cloudType != "others" || strings.HasPrefix(apiLink.URL, "http") {
link := model.Link{
Type: cloudType,
URL: apiLink.URL,
Password: apiLink.Password,
}
links = append(links, link)
}
}
return links
}
```
## 高级特性
### 1. 插件Web路由注册(自定义HTTP接口)
#### 概述
PanSou 支持插件注册自定义的 HTTP 路由,用于实现插件专属的管理页面、配置接口或其他Web功能。
**典型应用场景**:
- 插件配置管理界面(如 QQPD 的用户登录和频道管理)
- 插件数据查询接口
- 插件状态监控页面
- OAuth回调接口
#### 接口定义
```go
// PluginWithWebHandler 支持Web路由的插件接口
// 插件可以选择实现此接口来注册自定义的HTTP路由
type PluginWithWebHandler interface {
AsyncSearchPlugin // 继承搜索插件接口
// RegisterWebRoutes 注册Web路由
// router: gin的路由组,插件可以在此注册自己的路由
RegisterWebRoutes(router *gin.RouterGroup)
}
```
#### 实现步骤
**步骤1: 插件结构实现接口**
```go
package myplugin
import (
"github.com/gin-gonic/gin"
"pansou/plugin"
"pansou/model"
)
type MyPlugin struct {
*plugin.BaseAsyncPlugin
// ... 其他字段
}
// 确保插件实现了 PluginWithWebHandler 接口
var _ plugin.PluginWithWebHandler = (*MyPlugin)(nil)
```
**步骤2: 实现 RegisterWebRoutes 方法**
```go
// RegisterWebRoutes 注册Web路由
func (p *MyPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
// 创建插件专属的路由组
myGroup := router.Group("/myplugin")
// 注册GET路由
myGroup.GET("/status", p.handleGetStatus)
// 注册POST路由
myGroup.POST("/config", p.handleUpdateConfig)
// 支持动态路径参数
myGroup.GET("/:id", p.handleGetByID)
myGroup.POST("/:id/action", p.handleAction)
}
```
**步骤3: 实现路由处理函数**
```go
// handleGetStatus 获取插件状态
func (p *MyPlugin) handleGetStatus(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"plugin": p.Name(),
"version": "1.0.0",
})
}
// handleUpdateConfig 更新插件配置
func (p *MyPlugin) handleUpdateConfig(c *gin.Context) {
var config map[string]interface{}
if err := c.BindJSON(&config); err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
// 处理配置更新逻辑
// ...
c.JSON(200, gin.H{
"success": true,
"message": "配置已更新",
})
}
// handleGetByID 根据ID获取数据
func (p *MyPlugin) handleGetByID(c *gin.Context) {
id := c.Param("id")
// 根据ID查询数据
// ...
c.JSON(200, gin.H{
"id": id,
"data": "...",
})
}
```
#### 实际案例: QQPD 插件
QQPD 插件实现了完整的用户管理和频道配置功能:
```go
// RegisterWebRoutes 注册Web路由
func (p *QQPDPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
qqpd := router.Group("/qqpd")
// GET /:param - 显示管理页面(HTML)
qqpd.GET("/:param", p.handleManagePage)
// POST /:param - 处理管理操作(JSON API)
qqpd.POST("/:param", p.handleManagePagePOST)
}
// handleManagePage 渲染管理页面
func (p *QQPDPlugin) handleManagePage(c *gin.Context) {
param := c.Param("param")
// 生成用户专属的管理页面
html := strings.ReplaceAll(HTMLTemplate, "HASH_PLACEHOLDER", param)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, html)
}
// handleManagePagePOST 处理管理操作
func (p *QQPDPlugin) handleManagePagePOST(c *gin.Context) {
param := c.Param("param")
var req struct {
Action string `json:"action"`
Channels []string `json:"channels,omitempty"`
Keyword string `json:"keyword,omitempty"`
}
if err := c.BindJSON(&req); err != nil {
respondError(c, "无效的请求格式")
return
}
// 根据不同的 action 执行不同的操作
switch req.Action {
case "get_status":
p.handleGetStatus(c, param)
case "set_channels":
p.handleSetChannels(c, param, req.Channels)
case "test_search":
p.handleTestSearch(c, param, req.Keyword)
case "logout":
p.handleLogout(c, param)
default:
respondError(c, "未知的操作")
}
}
```
#### 实际案例: Gying 插件
```go
// RegisterWebRoutes 注册Web路由
func (p *GyingPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
gying := router.Group("/gying")
gying.GET("/:param", p.handleManagePage)
gying.POST("/:param", p.handleManagePagePOST)
}
```
#### 路由访问示例
插件注册的路由可以通过以下方式访问:
```bash
# QQPD 插件管理页面
GET http://localhost:8888/qqpd/user123
# QQPD 插件配置接口
POST http://localhost:8888/qqpd/user123
Content-Type: application/json
{
"action": "set_channels",
"channels": ["pd97631607", "kuake12345"]
}
# 自定义插件接口
GET http://localhost:8888/myplugin/status
POST http://localhost:8888/myplugin/config
GET http://localhost:8888/myplugin/resource123
```
#### 系统集成
PanSou 在启动时会自动扫描并注册所有实现了 `PluginWithWebHandler` 接口的插件路由:
```go
// api/router.go 中的自动注册逻辑
func SetupRouter(searchService *service.SearchService) *gin.Engine {
r := gin.Default()
// ... 其他路由配置 ...
// 注册插件的Web路由(如果插件实现了PluginWithWebHandler接口)
allPlugins := plugin.GetRegisteredPlugins()
for _, p := range allPlugins {
if webPlugin, ok := p.(plugin.PluginWithWebHandler); ok {
webPlugin.RegisterWebRoutes(r.Group(""))
}
}
return r
}
```
#### 开发建议
1. **路由命名规范**: 使用插件名作为路由前缀,避免与其他插件冲突
```go
// ✅ 推荐
router.Group("/myplugin")
// ❌ 避免
router.Group("/config") // 可能与其他插件冲突
```
2. **安全考虑**:
- 对敏感操作进行身份验证
- 验证用户输入,防止注入攻击
- 使用哈希或加密保护敏感参数
3. **错误处理**: 统一错误响应格式
```go
func respondError(c *gin.Context, message string) {
c.JSON(400, gin.H{
"success": false,
"message": message,
})
}
func respondSuccess(c *gin.Context, message string, data interface{}) {
c.JSON(200, gin.H{
"success": true,
"message": message,
"data": data,
})
}
```
4. **HTML模板**: 可以内嵌HTML模板提供管理界面
```go
const HTMLTemplate = `<!DOCTYPE html>
<html>
<head>
<title>插件管理</title>
</head>
<body>
<h1>{{ .PluginName }} 管理界面</h1>
<!-- ... -->
</body>
</html>`
```
5. **可选实现**: Web路由是**可选功能**,只有需要自定义HTTP接口的插件才需要实现
### 2. Service层过滤控制详解
#### 构造函数选择
```go
// 标准插件构造函数(默认启用Service层过滤)
func NewStandardPlugin() *StandardPlugin {
return &StandardPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("standard", 3), // 默认skipServiceFilter=false
}
}
// 磁力搜索插件构造函数(跳过Service层过滤)
func NewMagnetPlugin() *MagnetPlugin {
return &MagnetPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("magnet", 4, true), // skipServiceFilter=true
}
}
```
#### 实际应用示例
**ThePirateBay插件示例**:
```go
// thepiratebay插件的实际实现
func NewThePirateBayPlugin() *ThePirateBayPlugin {
return &ThePirateBayPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("thepiratebay", 4, true), // 跳过Service层过滤
optimizedClient: createOptimizedHTTPClient(),
}
}
func (p *ThePirateBayPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 支持英文搜索优化
searchKeyword := keyword
if ext != nil {
if titleEn, exists := ext["title_en"]; exists {
if titleEnStr, ok := titleEn.(string); ok && titleEnStr != "" {
searchKeyword = titleEnStr
}
}
}
// 获取搜索结果
allResults := p.fetchAllResults(client, searchKeyword)
// 标题格式优化:将'.'替换为空格,提高关键词匹配准确度
for i := range allResults {
allResults[i].Title = strings.ReplaceAll(allResults[i].Title, ".", " ")
}
// 插件层过滤(使用处理后的搜索关键词)
filteredResults := plugin.FilterResultsByKeyword(allResults, searchKeyword)
return filteredResults, nil
// 注意:Service层会通过SkipServiceFilter()方法跳过二次过滤
}
```
#### 过滤策略对比
| 过滤类型 | 标准插件 | 磁力搜索插件 |
|----------|----------|--------------|
| **插件层过滤** | ✅ 使用原始关键词 | ✅ 使用searchKeyword(支持title_en) |
| **Service层过滤** | ✅ 再次过滤 | ❌ 跳过过滤 |
| **结果特点** | 精确匹配 | 宽泛搜索 |
| **适用场景** | 中文网盘资源 | 英文磁力资源 |
#### 动态过滤检测机制
Service层通过以下机制动态判断是否需要过滤:
```go
// service/search_service.go 中的实现
func mergeResultsByType(...) {
// 检查插件是否需要跳过Service层过滤
var skipKeywordFilter bool = false
if result.UniqueID != "" && strings.Contains(result.UniqueID, "-") {
parts := strings.SplitN(result.UniqueID, "-", 2)
if len(parts) >= 1 {
pluginName := parts[0]
// 通过插件注册表动态获取过滤设置
if pluginInstance, exists := plugin.GetPluginByName(pluginName); exists {
skipKeywordFilter = pluginInstance.SkipServiceFilter()
}
}
}
// 根据插件设置决定是否过滤
if !skipKeywordFilter && keyword != "" && !strings.Contains(strings.ToLower(title), lowerKeyword) {
continue // 过滤掉不匹配的结果
}
}
```
### 2. 扩展参数处理
```go
// 支持的扩展参数示例
ext := map[string]interface{}{
"title_en": "English Title", // 英文标题
"is_all": true, // 全量搜索标志
"year": 2023, // 年份限制
"type": "movie", // 内容类型
}
// 在插件中处理
func (p *MyPlugin) handleExtParams(ext map[string]interface{}) searchOptions {
opts := searchOptions{}
if titleEn, ok := ext["title_en"].(string); ok {
opts.TitleEn = titleEn
}
if isAll, ok := ext["is_all"].(bool); ok {
opts.IsAll = isAll
}
return opts
}
```
### 2. 缓存策略
```go
// 设置缓存TTL
p.SetCacheTTL(2 * time.Hour)
// 手动缓存更新
p.UpdateMainCache(cacheKey, results, ttl, true, keyword)
```
### 3. 错误处理
```go
func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 网络错误处理
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("[%s] 网络请求失败: %w", p.Name(), err)
}
// HTTP状态码检查
if resp.StatusCode != 200 {
return nil, fmt.Errorf("[%s] HTTP错误: %d", p.Name(), resp.StatusCode)
}
// JSON解析错误 - 推荐使用项目统一的JSON工具
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 读取响应失败: %w", p.Name(), err)
}
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err)
}
// 业务逻辑错误
if apiResp.Code != 0 {
return nil, fmt.Errorf("[%s] API错误: %s", p.Name(), apiResp.Message)
}
return results, nil
}
```
## 性能优化
### 1. HTTP客户端优化
```go
// 使用连接池
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
```
### 2. 内存优化
```go
// 预分配切片容量
results := make([]model.SearchResult, 0, expectedCount)
// 及时释放大对象
defer func() {
apiResp = APIResponse{}
}()
```
### 3. 并发控制
```go
// 使用插件内置的工作池,避免创建过多goroutine
// BaseAsyncPlugin 已经提供了工作池管理
```
## 测试和调试
### 1. 单元测试
```go
func TestMyPlugin_Search(t *testing.T) {
plugin := &MyPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("test", 3),
}
results, err := plugin.Search("测试关键词", nil)
assert.NoError(t, err)
assert.NotEmpty(t, results)
}
```
### 2. 集成测试
```bash
# 使用API测试插件
curl "http://localhost:8888/api/search?kw=测试&plugins=myplugin"
```
### 3. 性能测试
```bash
# 使用压力测试脚本
python3 stress_test.py
```
## 部署和配置
### 1. 插件注册
确保在 `init()` 函数中注册插件:
```go
func init() {
p := &MyPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3),
}
plugin.RegisterGlobalPlugin(p)
}
```
### 2. 环境配置
```bash
# 异步插件配置
export ASYNC_PLUGIN_ENABLED=true
export ASYNC_RESPONSE_TIMEOUT=4
export ASYNC_MAX_BACKGROUND_WORKERS=40
export ASYNC_MAX_BACKGROUND_TASKS=200
```
### 3. 生产部署注意事项
1. **资源限制**: 根据服务器配置调整工作池大小
2. **监控告警**: 监控插件响应时间和错误率
3. **日志管理**: 合理设置日志级别,避免日志过多
4. **缓存配置**: 根据数据更新频率调整缓存TTL
## 现有插件参考
### 标准网盘搜索插件
- **jikepan** - 标准网盘插件,启用Service层过滤
- **pan666** - 标准网盘插件,启用Service层过滤
- **hunhepan** - 标准网盘插件,启用Service层过滤
- **pansearch** - 标准网盘插件,启用Service层过滤
- **qupansou** - 标准网盘插件,启用Service层过滤
- **panta** - 高质量网盘插件,启用Service层过滤
### 特殊搜索插件
- **thepiratebay** - 磁力搜索插件,跳过Service层过滤,支持title_en参数,标题格式化处理
## 插件开发最佳实践 ⭐
### 核心原则
1. **命名规范**: 插件名使用小写字母和数字
2. **优先级设置**: 1-2为高优先级,3为标准,4-5为低优先级
3. **关键词过滤**: 使用 `FilterResultsByKeyword` 提高结果相关性
4. **缓存友好**: 合理设置缓存TTL,避免频繁请求
5. **资源清理**: 及时关闭连接和释放资源
6. **过滤策略**: 根据插件类型选择合适的Service层过滤策略
### 必须实现的优化点
#### 1. Service层过滤策略选择 ⭐ 新功能
```go
// ✅ 磁力搜索插件 - 跳过Service层过滤
func NewMagnetSearchPlugin() *MagnetSearchPlugin {
return &MagnetSearchPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPluginWithFilter("magnet", 4, true), // skipServiceFilter=true
}
}
// ✅ 标准网盘插件 - 启用Service层过滤
func NewPanSearchPlugin() *PanSearchPlugin {
return &PanSearchPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("pansearch", 3), // 默认skipServiceFilter=false
}
}
```
**选择指南**:
- **跳过过滤** (`true`): 磁力搜索、英文资源、特殊格式标题、聚合搜索
- **启用过滤** (`false`): 网盘搜索、中文资源、API接口、标准格式标题
**注意事项**:
- 跳过Service层过滤的插件**必须**在插件内部进行`FilterResultsByKeyword`过滤
- 插件层过滤使用的关键词应与实际搜索关键词一致(支持`title_en`等参数)
- 标题格式化处理应在过滤之前进行(如将`"."` 替换为`" "`)
#### 2. SearchResult字段设置规范 ⭐ 重要
```go
// ✅ 正确的SearchResult设置
result := model.SearchResult{
UniqueID: fmt.Sprintf("%s-%s", p.Name(), itemID), // 插件名-资源ID
Title: title, // 资源标题
Content: description, // 资源描述
Links: downloadLinks, // 下载链接列表
Tags: tags, // 分类标签
Channel: "", // ⭐ 重要:插件搜索结果必须为空字符串
Datetime: time.Now(), // 发布时间
}
// ❌ 错误的Channel设置
result.Channel = p.Name() // 不要设置为插件名!
```
**Channel字段使用规则**:
- **插件搜索结果**: `Channel` 必须为空字符串 `""`
- **Telegram频道**: `Channel` 才设置为频道名称
- **目的**: 区分搜索来源,便于前端展示和后端统计
**Links字段处理规则** ⭐ 重要:
- **必须有链接**: 系统会自动过滤掉 `Links` 为空或长度为0的结果
- **链接质量**: 确保返回的链接都是有效的网盘链接,避免返回无效链接
- **链接验证**: 建议使用 `isValidNetworkDriveURL()` 函数预先验证链接有效性
#### 2. HTTP请求最佳实践 ⭐ 重要
```go
// ✅ 正确的请求实现
func (p *MyPlugin) makeRequest(url string, client *http.Client) (*http.Response, error) {
// 使用context控制超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// 设置完整的请求头(避免反爬虫)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", "https://example.com/")
// 使用重试机制
return p.doRequestWithRetry(req, client)
}
// ❌ 错误的简单实现
func (p *MyPlugin) badRequest(url string, client *http.Client) (*http.Response, error) {
return client.Get(url) // 没有超时控制、没有请求头、没有重试
}
```
#### 2. 实现高级搜索接口 ⭐ 推荐
```go
// ✅ 推荐:实现两个方法
func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
result, err := p.SearchWithResult(keyword, ext)
if err != nil {
return nil, err
}
return result.Results, nil
}
func (p *MyPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
}
```
#### 3. 错误处理增强 ⭐ 重要
```go
// ✅ 详细的错误信息
if resp.StatusCode != 200 {
return nil, fmt.Errorf("[%s] 请求失败,状态码: %d", p.Name(), resp.StatusCode)
}
// ✅ 包装外部错误
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, fmt.Errorf("[%s] JSON解析失败: %w", p.Name(), err)
}
```
#### 4. 重试机制模板 ⭐ 复制可用
```go
func (p *MyPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
maxRetries := 3
var lastErr error
for i := 0; i < maxRetries; i++ {
if i > 0 {
backoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond
time.Sleep(backoff)
}
reqClone := req.Clone(req.Context())
resp, err := client.Do(reqClone)
if err == nil && resp.StatusCode == 200 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
}
```
#### 5. 请求头模板 ⭐ 复制可用
```go
// HTML页面请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("Cache-Control", "max-age=0")
req.Header.Set("Referer", "https://example.com/")
// JSON API请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Referer", "https://example.com/")
```
### 常见问题避免
1. **不要使用 `client.Get(url)`** - 缺少超时控制和请求头
2. **不要忘记设置 User-Agent** - 很多网站会阻止空UA请求
3. **不要忘记错误上下文** - 使用 `fmt.Errorf("[%s] 错误描述: %w", p.Name(), err)`
4. **不要忘记关闭响应体** - `defer resp.Body.Close()`
5. **不要在循环中创建大量goroutine** - 使用信号量控制并发数
6. **Service层过滤常见问题**:
- ❌ **跳过Service层过滤但不在插件内过滤** - 会返回大量无关结果
- ❌ **磁力搜索插件使用默认构造函数** - 会被Service层误过滤
- ❌ **过滤关键词不一致** - 插件用`title_en`搜索但用原`keyword`过滤
- ❌ **标题格式化在过滤之后** - 格式化不会改善过滤效果
================================================
FILE: docs/系统开发设计文档.md
================================================
# PanSou 网盘搜索系统开发设计文档
## 📋 文档目录
- [1. 项目概述](#1-项目概述)
- [2. 系统架构设计](#2-系统架构设计)
- [3. 异步插件系统](#3-异步插件系统)
- [4. 二级缓存系统](#4-二级缓存系统)
- [5. 核心组件实现](#5-核心组件实现)
- [6. 智能排序算法详解](#6-智能排序算法详解)
- [7. API接口设计](#7-api接口设计)
- [8. 认证系统设计](#8-认证系统设计)
- [9. 插件开发框架](#9-插件开发框架)
- [10. 性能优化实现](#10-性能优化实现)
- [11. 技术选型说明](#11-技术选型说明)
---
## 1. 项目概述
### 1.1 项目定位
PanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定义插件搜索。系统采用异步插件架构,具备二级缓存机制和并发控制能力,在MacBook Pro 8GB上能够支持500用户并发访问。
### 1.2 核心特性
- **异步插件系统**: 双级超时控制(4秒/30秒),渐进式结果返回
- **二级缓存系统**: 分片内存缓存+分片磁盘缓存,GOB序列化
- **工作池管理**: 基于`util/pool`的并发控制
- **智能结果合并**: `mergeSearchResults`函数实现去重合并
- **多维度排序**: 插件等级+时间新鲜度+优先关键词综合评分
- **多网盘类型支持**: 自动识别12种网盘类型
---
## 2. 系统架构设计
### 2.1 整体架构流程
```mermaid
graph TB
A[用户请求] --> B[API Gateway<br/>Gin Handler]
B --> C[参数解析与验证<br/>GET/POST处理]
C --> D[参数预处理<br/>规范化处理]
D --> E[SearchService<br/>主搜索服务]
E --> F{源类型判断<br/>sourceType}
F -->|TG| G[并行TG搜索]
F -->|Plugin| H[并行插件搜索]
F -->|All| I[TG+插件并行搜索]
I --> G
I --> H
%% TG搜索分支
G --> G1[生成TG缓存键<br/>GenerateTGCacheKey]
G1 --> G2{强制刷新?<br/>forceRefresh}
G2 -->|否| G3[检查二级缓存<br/>EnhancedTwoLevelCache]
G2 -->|是| G6[跳过缓存检查]
G3 --> G4{缓存命中?}
G4 -->|是| G5[缓存反序列化<br/>直接返回结果]
G4 -->|否| G6[执行TG频道搜索<br/>多频道并行]
G6 --> G7[HTML解析<br/>链接提取]
G7 --> G8[结果标准化]
G8 --> G9[更新缓存<br/>SetBothLevels]
%% 插件搜索分支 - 详细的异步处理
H --> H1[生成插件缓存键<br/>GeneratePluginCacheKey]
H1 --> H2{强制刷新?<br/>forceRefresh}
H2 -->|否| H3[检查二级缓存<br/>EnhancedTwoLevelCache]
H2 -->|是| H6[跳过缓存检查]
H3 --> H4{缓存命中?}
H4 -->|是| H5[缓存反序列化<br/>直接返回结果]
H4 -->|否| H6[插件管理器调度<br/>PluginManager]
%% 异步插件详细流程
H6 --> H7[异步插件初始化<br/>SetMainCacheKey]
H7 --> H8[工作池任务提交<br/>WorkerPool]
%% 双级超时机制的并行处理
H8 --> H9{异步并行处理}
%% 快速响应分支 (4秒)
H9 --> H10[短超时处理<br/>4秒快速响应]
H10 --> H11[HTTP请求<br/>短超时模式]
H11 --> H12[部分结果解析<br/>快速过滤]
H12 --> H13[部分结果缓存<br/>isFinal=false]
H13 --> H14[立即返回<br/>部分结果给用户]
%% 持续处理分支 (30秒)
H9 --> H15[长超时后台处理<br/>最长30秒持续]
H15 --> H16[HTTP请求<br/>长超时模式]
H16 --> H17[完整结果解析<br/>深度过滤]
H17 --> H18[结果去重合并<br/>最终处理]
H18 --> H19[完整结果缓存<br/>isFinal=true]
H19 --> H20[主缓存异步更新<br/>DelayedBatchWrite]
%% 结果合并处理
G5 --> J[结果合并<br/>mergeSearchResults]
G9 --> J
H5 --> J
H14 --> J
J --> K[智能排序算法<br/>时间+关键词+插件等级]
K --> L[结果过滤<br/>高质量结果筛选]
L --> M[网盘类型分组<br/>mergeResultsByType]
M --> N{结果类型<br/>resultType}
N -->|merged_by_type| O[返回分组结果]
N -->|results| P[返回原始结果]
N -->|all| Q[返回完整结果]
O --> R[JSON响应]
P --> R
Q --> R
R --> S[用户]
%% 后台持续更新(不影响用户响应)
H20 --> T[后台缓存完善<br/>下次请求更完整]
T -.-> U[持续优化<br/>用户体验]
%% 缓存系统
subgraph Cache[二级缓存系统]
CA[分片内存缓存<br/>LRU + 原子操作]
CB[分片磁盘缓存<br/>GOB序列化]
CC[智能缓存写入管理器<br/>DelayedBatchWriteManager]
CD[全局缓冲区管理器<br/>BufferByPlugin策略]
end
G3 -.-> CA
H3 -.-> CA
CA -.-> CB
G9 -.-> CC
H13 -.-> CC
H20 -.-> CC
CC -.-> CD
%% 样式定义
classDef cacheNode fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef pluginNode fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
classDef searchNode fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
classDef fastResponse fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef slowProcess fill:#fce4ec,stroke:#880e4f,stroke-width:2px
classDef processNode fill:#f5f5f5,stroke:#424242,stroke-width:2px
class G3,H3,G5,H5,G9,H13,H20,CA,CB,CC,CD cacheNode
class H6,H7,H8 pluginNode
class G6,G7,G8 searchNode
class H10,H11,H12,H13,H14 fastResponse
class H15,H16,H17,H18,H19,H20,T slowProcess
class D,J,K,L,M processNode
```
### 2.2 异步插件工作流程
```mermaid
sequenceDiagram
participant U as 用户
participant API as API Handler
participant S as SearchService
participant SP as searchPlugins函数
participant C as 二级缓存系统
participant PM as PluginManager
participant P as AsyncPlugin
participant WP as WorkerPool
participant BWM as BatchWriteManager
participant EXT as 外部API
%% 请求处理阶段
U->>API: 🔍 搜索请求 (kw=关键词)
API->>API: 参数解析与验证
API->>API: 参数预处理规范化
API->>S: Search(req.Keyword, ...)
%% 并行搜索启动
Note over S: 🚀 并行启动TG和插件搜索
S->>SP: searchPlugins(keyword, plugins, ...)
%% 缓存检查阶段
SP->>SP: 生成插件缓存键
SP->>SP: 检查forceRefresh标志
alt forceRefresh = false
SP->>C: 🔍 Get(cacheKey)
alt 缓存命中
C-->>SP: ✅ 返回缓存数据
SP->>SP: 反序列化结果
SP-->>S: 🎯 返回缓存结果 (<10ms)
S-->>U: ⚡ 极速响应
else 缓存未命中
Note over SP: 🚨 执行异步插件搜索
SP->>PM: 获取可用插件列表
SP->>PM: 过滤指定插件
end
else forceRefresh = true
Note over SP: 🔄 跳过缓存,强制搜索
SP->>PM: 获取可用插件列表
SP->>PM: 过滤指定插件
end
%% 异步搜索初始化
PM->>P: 🎯 设置关键词和缓存键
P->>P: SetMainCacheKey(cacheKey)
P->>P: SetCurrentKeyword(keyword)
P->>P: 注入缓存更新函数
%% 🚀 异步插件的精髓:双级超时并行机制
Note over P,EXT: 🔥 异步插件精髓:快速响应 + 持续处理
P->>WP: 🚀 提交异步任务到工作池
%% 快速响应路径 (4秒)
par 🚀 快速响应路径 (4秒)
Note over WP,EXT: ⚡ 第一阶段:快速响应用户
WP->>EXT: HTTP请求 (短超时 4秒)
EXT-->>WP: 部分响应数据
WP->>P: 🔍 解析部分结果
P->>P: 快速过滤和标准化
P->>P: 📝 记录日志: 初始缓存创建
%% 部分结果立即缓存和返回
P->>BWM: 🗄️ 异步缓存更新 (isFinal=false)
Note over BWM: 部分结果缓存,不等待写入完成
P-->>SP: 📤 部分结果立即返回
SP-->>S: 🎯 部分结果 (isFinal=false)
S->>S: 与TG结果合并
S-->>U: ⚡ 快速响应 (~4秒)
and 🔄 持续处理路径 (最长30秒)
Note over WP,EXT: 🔄 第二阶段:后台持续完善
WP->>EXT: 继续HTTP请求 (长超时 30秒)
EXT-->>WP: 完整响应数据
WP->>P: 🔍 解析完整结果
P->>P: 深度过滤和去重
P->>P: 结果质量评估
P->>P: 📝 记录日志: 缓存更新完成
%% 完整结果的主缓存更新
P->>BWM: 🗄️ 主缓存更新 (isFinal=true)
Note over BWM: 完整结果写入,高优先级
BWM->>BWM: 🧠 智能缓存写入策略
BWM->>BWM: 🗂️ 全局缓冲区管理
BWM->>C: 📀 批量写入磁盘缓存
Note over C: 🎯 下次同样请求将获得完整结果
end
%% 缓存系统内部处理
C->>C: ⚡ 立即更新内存缓存
C->>C: 📀 延迟批量更新磁盘缓存
C->>C: 🧹 自动清理过期缓存
%% 持续优化标注
Note over U,EXT: 💡 异步插件核心价值
Note over U,EXT: ✅ 用户获得快速响应 (4秒内)
Note over U,EXT: ✅ 系统持续完善结果 (30秒内)
Note over U,EXT: ✅ 下次访问获得完整数据 (<100ms)
Note over U,EXT: 🔄 完美平衡:速度 vs 完整性
```
### 2.3 核心组件
#### 2.3.1 HTTP服务层 (`api/`)
- **router.go**: 路由配置
- **handler.go**: 请求处理逻辑
- **middleware.go**: 中间件(日志、CORS等)
#### 2.3.2 搜索服务层 (`service/`)
- **search_service.go**: 核心搜索逻辑,结果合并
#### 2.3.3 插件系统层 (`plugin/`)
- **plugin.go**: 插件接口定义
- **baseasyncplugin.go**: 异步插件基类
- **各插件目录**: jikepan、pan666、hunhepan等
#### 2.3.4 工具层 (`util/`)
- **cache/**: 二级缓存系统实现
- **pool/**: 工作池实现
- **其他工具**: HTTP客户端、解析工具等
---
## 3. 异步插件系统
### 3.1 设计理念
异步插件系统解决传统同步搜索响应慢的问题,采用"尽快响应,持续处理"策略:
- **4秒短超时**: 快速返回部分结果(`isFinal=false`)
- **30秒长超时**: 后台继续处理,获得完整结果(`isFinal=true`)
- **主动缓存更新**: 完整结果自动更新主缓存,下次访问更快
### 3.2 插件接口实现
基于`plugin/plugin.go`的实际接口:
```go
type AsyncSearchPlugin interface {
Name() string
Priority() int
AsyncSearch(keyword string, searchFunc func(*http.Client, string, map[string]interface{}) ([]model.SearchResult, error),
mainCacheKey string, ext map[string]interface{}) ([]model.SearchResult, error)
SetMainCacheKey(key string)
SetCurrentKeyword(keyword string)
Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error)
}
```
### 3.3 基础插件类
`plugin/baseasyncplugin.go`提供通用功能:
```go
type BaseAsyncPlugin struct {
name string
priority int
cacheTTL time.Duration
mainCacheKey string
currentKeyword string // 用于日志显示
httpClient *http.Client
mainCacheUpdater func(string, []model.SearchResult, time.Duration, bool, string) error
}
```
### 3.4 已实现插件列表
当前系统包含以下插件(基于`main.go`的导入):
- **hdr4k**
- **hunhepan**
- **jikepan**
- **pan666**
- **pansearch**
- **panta**
- **qupansou**
- **susu**
- **panyq**
- **xuexizhinan**
### 3.5 插件注册机制
```go
// 全局插件注册表(plugin/plugin.go)
var globalRegistry = make(map[string]AsyncSearchPlugin)
// 插件通过init()函数自动注册
func init() {
p := &MyPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3),
}
plugin.RegisterGlobalPlugin(p)
}
```
---
## 4. 二级缓存系统
### 4.1 实现架构
基于`util/cache/`目录的实际实现:
- **enhanced_two_level_cache.go**: 二级缓存主入口
- **sharded_memory_cache.go**: 分片内存缓存(LRU+原子操作)
- **sharded_disk_cache.go**: 分片磁盘缓存
- **serializer.go**: GOB序列化器
- **cache_key.go**: 缓存键生成和管理
### 4.2 分片缓存设计
#### 4.2.1 内存缓存分片
```go
// 基于CPU核心数的动态分片
type ShardedMemoryCache struct {
shards []*MemoryCacheShard
shardMask uint32
}
// 每个分片独立锁,减少竞争
type MemoryCacheShard struct {
data map[string]*CacheItem
lock sync.RWMutex
}
```
#### 4.2.2 磁盘缓存分片
```go
// 磁盘缓存同样采用分片设计
type ShardedDiskCache struct {
shards []*DiskCacheShard
shardMask uint32
basePath string
}
```
### 4.3 缓存读写策略
#### 4.3.1 读取流程
1. **内存优先**: 先检查分片内存缓存
2. **磁盘回源**: 内存未命中时读取磁盘缓存
3. **异步加载**: 磁盘命中后异步加载到内存
#### 4.3.2 写入流程
1. **智能写入策略**: 立即更新内存缓存,延迟批量写入磁盘
2. **DelayedBatchWriteManager**: 智能缓存写入管理器,支持immediate和hybrid两种策略
3. **原子操作**: 内存缓存使用原子操作
4. **GOB序列化**: 磁盘存储使用GOB格式
5. **数据安全保障**: 程序终止时自动保存所有待写入数据,防止数据丢失
### 4.4 缓存键策略
`cache_key.go`实现了智能缓存键生成:
```go
// TG搜索和插件搜索使用不同的缓存键前缀
func GenerateTGCacheKey(keyword string, channels []string) string
func GeneratePluginCacheKey(keyword string, plugins []string) string
```
**优势**:
- 独立更新:TG和插件缓存互不影响
- 提高命中率:精确的键匹配
- 并发安全:分片设计减少锁竞争
### 4.5 序列化性能
使用GOB序列化(`serializer.go`)的实际优势:
- **性能**: 比JSON序列化快约30%
- **体积**: 比JSON小约20%
- **兼容**: Go原生支持,无外部依赖
---
## 5. 核心组件实现
### 5.1 工作池系统 (`util/pool/`)
#### 5.1.1 worker_pool.go 实现
- **批量任务处理**: `ExecuteBatchWithTimeout`方法
- **超时控制**: 支持任务级别的超时设置
- **并发限制**: 控制最大工作者数量
#### 5.1.2 object_pool.go 实现
- **对象复用**: 减少内存分配和GC压力
- **线程安全**: 支持并发访问
### 5.2 HTTP服务配置
#### 5.2.1 服务器优化(基于config/config.go)
```go
// 自动计算HTTP连接数,防止资源耗尽
func getHTTPMaxConns() int {
cpuCount := runtime.NumCPU()
maxConns := cpuCount * 25 // 保守配置
if maxConns < 100 {
maxConns = 100
}
if maxConns > 500 {
maxConns = 500 // 限制最大值
}
return maxConns
}
```
#### 5.2.2 连接池配置(基于util/http_util.go)
```go
// HTTP客户端优化配置
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
```
### 5.3 结果处理系统
#### 5.3.1 智能排序算法(service/search_service.go)
PanSou 采用多维度综合评分排序算法,确保高质量结果优先展示:
**评分公式**:
```
总得分 = 插件得分(1000/500/0/-200) + 时间得分(最高500) + 关键词得分(最高420)
```
**权重分配**:
- 🥇 **插件等级**: ~52% (主导因素) - 等级1(1000分) > 等级2(500分) > 等级3(0分)
- 🥈 **关键词匹配**: ~22% (重要因素) - "合集"(420分) > "系列"(350分) > "全"(280分)
- 🥉 **时间新鲜度**: ~26% (重要因素) - 1天内(500分) > 3天内(400分) > 1周内(300分)
**关键优化**:
- **缓存性能**: 跳过空结果和重复数据的缓存更新,减少70%无效操作
- **排序稳定性**: 修复map遍历随机性问题,确保merged_by_type保持排序
- **插件管理**: 启动时按优先级排序显示已加载插件,便于监控
#### 5.3.2 结果合并(mergeSearchResults函数)
- **去重合并**: 基于UniqueID去重
- **完整性选择**: 选择更完整的结果保留
- **增量更新**: 新结果与缓存结果智能合并
### 5.4 网盘类型识别
支持自动识别的网盘类型(共12种):
- 百度网盘、阿里云盘、夸克网盘、天翼云盘
- UC网盘、移动云盘、115网盘、PikPak
- 迅雷网盘、123网盘、磁力链接、电驴链接
---
## 6. 智能排序算法详解
### 6.1 算法概述
PanSou 搜索引擎采用多维度综合评分排序算法,确保用户能够优先看到最相关、最新、最高质量的搜索结果。
#### 6.1.1 核心设计理念
1. **质量优先**:高等级插件的结果优先展示
2. **时效性重要**:新发布的资源获得更高权重
3. **相关性保证**:关键词匹配度影响排序
4. **用户体验**:最终排序结果保持稳定性
#### 6.1.2 排序流程
```mermaid
graph TD
A[搜索请求] --> B[获取搜索结果 allResults]
B --> C[sortResultsByTimeAndKeywords]
C --> D[为每个结果计算得分]
D --> E[时间得分<br/>最高500分]
D --> F[关键词得分<br/>最高420分]
D --> G[插件得分<br/>等级1=1000分<br/>等级2=500分<br/>等级3=0分]
E --> H[总得分 = 时间得分 + 关键词得分 + 插件得分]
F --> H
G --> H
H --> I[按总得分降序排序]
I --> J[mergeResultsByType]
J --> K[按原始顺序收集唯一链接<br/>保持排序不被破坏]
K --> L[按类型分组<br/>生成merged_by_type]
L --> M[返回最终结果]
```
### 6.2 评分算法详解
#### 6.2.1 核心公式
```
总得分 = 时间得分 + 关键词得分 + 插件得分
```
#### 6.2.2 时间得分 (Time Score)
时间得分反映资源的新鲜度,**最高 500 分**:
| 时间范围 | 得分 | 说明 |
|---------|------|------|
| ≤ 1天 | 500 | 最新资源,最高优先级 |
| ≤ 3天 | 400 | 非常新的资源 |
| ≤ 1周 | 300 | 较新资源 |
| ≤ 1月 | 200 | 相对较新 |
| ≤ 3月 | 100 | 中等新鲜度 |
| ≤ 1年 | 50 | 较旧资源 |
| > 1年 | 20 | 旧资源 |
| 无日期 | 0 | 未知时间 |
#### 6.2.3 关键词得分 (Keyword Score)
关键词得分基于搜索词在标题中的匹配情况,**最高 420 分**:
| 优先关键词 | 得分 | 说明 |
|-----------|------|------|
| "合集" | 420 | 最高优先级 |
| "系列" | 350 | 高优先级 |
| "全" | 280 | 中高优先级 |
| "完" | 210 | 中等优先级 |
| "最新" | 140 | 较低优先级 |
| "附" | 70 | 低优先级 |
| 无匹配 | 0 | 无加分 |
#### 6.2.4 插件得分 (Plugin Score)
插件得分基于数据源的质量等级,体现资源可靠性:
| 插件等级 | 得分 | 说明 |
|---------|------|------|
| 等级1 | 1000 | 顶级数据源 |
| 等级2 | 500 | 优质数据源 |
| 等级3 | 0 | 普通数据源 |
| 等级4 | -200 | 低质量数据源 |
### 6.3 权重分析与实际效果
#### 6.3.1 权重分配
| 维度 | 最高分值 | 权重占比 | 影响说明 |
|------|---------|---------|----------|
| 插件等级 | 1000 | ~52% | **主导因素**,决定基础排序 |
| 关键词匹配 | 420 | ~22% | **重要因素**,优先关键词显著加分 |
| 时间新鲜度 | 500 | ~26% | **重要因素**,同等级内排序关键 |
#### 6.3.2 实际排序示例
| 场景 | 插件等级 | 时间 | 关键词 | 总分 | 排序 |
|------|---------|------|--------|------|------|
| 等级1 + 1天内 + "合集" | 1000 | 500 | 420 | **1920** | 🥇 第1 |
| 等级1 + 1天内 + "系列" | 1000 | 500 | 350 | **1850** | 🥈 第2 |
| 等级1 + 1月内 + "合集" | 1000 | 200 | 420 | **1620** | 🥉 第3 |
| 等级2 + 1天内 + "合集" | 500 | 500 | 420 | **1420** | 第4 |
| 等级1 + 1天内 + 无关键词 | 1000 | 500 | 0 | **1500** | 第5 |
---
## 7. API接口设计
### 7.1 核心接口实现(基于api/handler.go)
#### 7.1.1 搜索接口
```
POST /api/search
GET /api/search
```
**核心参数**:
- `kw`: 搜索关键词(必填)
- `channels`: TG频道列表
- `plugins`: 插件列表
- `cloud_types`: 网盘类型过滤
- `ext`: 扩展参数(JSON格式)
- `refresh`: 强制刷新缓存
- `res`: 返回格式(merge/all/results)
- `src`: 数据源(all/tg/plugin)
#### 7.1.2 健康检查接口
```
GET /api/health
```
返回系统状态和已注册插件信息。
### 6.2 中间件系统(api/middleware.go)
- **日志中间件**: 记录请求响应,支持URL解码显示
- **CORS中间件**: 跨域请求支持
- **错误处理**: 统一错误响应格式
### 6.3 扩展参数系统
通过`ext`参数支持插件特定选项:
```json
{
"title_en": "English Title",
"is_all": true,
"year": 2023
}
```
---
## 8. 认证系统设计
### 8.1 系统概述
PanSou认证系统是一个可选的安全访问控制模块,基于JWT(JSON Web Token)标准实现。该系统设计目标是在不影响现有用户的前提下,为需要私有部署的用户提供灵活的认证功能。
#### 8.1.1 核心特性
- **可选性**: 默认关闭,通过环境变量`AUTH_ENABLED`启用
- **无状态**: 基于JWT,无需session存储
- **标准化**: 采用RFC 7519 JWT标准
- **灵活性**: 支持多用户配置
- **安全性**: Token自动过期,防止长期有效性风险
### 8.2 认证架构
#### 8.2.1 认证流程
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端
participant M as 认证中间件
participant A as 认证接口
participant S as 搜索服务
Note over U,S: 初始访问阶段
U->>F: 访问应用
F->>F: 检查localStorage中的token
alt token不存在或无效
F->>U: 显示登录窗口
U->>F: 输入账号密码
F->>A: POST /api/auth/login
A->>A: 验证账号密码
A->>A: 生成JWT Token
A-->>F: 返回Token
F->>F: 存储Token到localStorage
F->>U: 关闭登录窗口
end
Note over U,S: API调用阶段
U->>F: 发起搜索请求
F->>F: axios拦截器添加Authorization头
F->>M: GET/POST /api/search + Token
M->>M: 验证Token有效性
alt Token有效
M->>S: 转发请求
S-->>M: 返回搜索结果
M-->>F: 返回响应
F-->>U: 显示结果
else Token无效/过期
M-->>F: 返回401 Unauthorized
F->>F: 响应拦截器捕获401
F->>U: 显示登录窗口
end
```
#### 8.2.2 组件架构
```
┌─────────────────────────────────────────────────────────────┐
│ 前端层 (Vue 3) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ LoginDialog │ │ HTTP拦截器 │ │ Token管理工具 │ │
│ │ 登录组件 │ │ 自动添加Token │ │ LocalStorage │ │
│ └─────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ HTTP (Authorization: Bearer)
┌─────────────────────────────────────────────────────────────┐
│ 后端层 (Go + Gin) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────┐ │
│ │ AuthMiddleware 认证中间件 │ │
│ │ • 检查AUTH_ENABLED配置 │ │
│ │ • 排除公开接口(/api/auth/login, /api/health) │ │
│ │ • 验证JWT Token有效性 │ │
│ │ • 提取用户信息到Context │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ 认证接口 │ │ JWT工具 │ │ 配置管理 │ │
│ │ /auth/login │ │ util/jwt.go │ │ config/config.go │ │
│ │ /auth/verify│ │ GenerateToken│ │ AuthEnabled │ │
│ │ /auth/logout│ │ ValidateToken│ │ AuthUsers │ │
│ └─────────────┘ └─────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 8.3 后端实现细节
#### 8.3.1 配置模块 (config/config.go)
```go
type Config struct {
// ... 现有配置 ...
// 认证相关配置
AuthEnabled bool // 是否启用认证
AuthUsers map[string]string // 用户名:密码哈希映射
AuthTokenExpiry time.Duration // Token有效期
AuthJWTSecret string // JWT签名密钥
}
// 从环境变量读取认证配置
func getAuthEnabled() bool {
enabled := os.Getenv("AUTH_ENABLED")
return enabled == "true" || enabled == "1"
}
func getAuthUsers() map[string]string {
usersEnv := os.Getenv("AUTH_USERS")
if usersEnv == "" {
return nil
}
users := make(map[string]string)
pairs := strings.Split(usersEnv, ",")
for _, pair := range pairs {
parts := strings.SplitN(pair, ":", 2)
if len(parts) == 2 {
username := strings.TrimSpace(parts[0])
password := strings.TrimSpace(parts[1])
// 实际使用时应该对密码进行哈希处理
users[username] = password
}
}
return users
}
```
#### 8.3.2 JWT工具模块 (util/jwt.go)
```go
package util
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"time"
)
// Claims JWT载荷结构
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT token
func GenerateToken(username string, secret string, expiry time.Duration) (string, error) {
expirationTime := time.Now().Add(expiry)
claims := &Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "pansou",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// ValidateToken 验证JWT token
func ValidateToken(tokenString string, secret string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
```
#### 8.3.3 认证中间件 (api/middleware.go)
```go
// AuthMiddleware JWT认证中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 如果未启用认证,直接放行
if !config.AppConfig.AuthEnabled {
c.Next()
return
}
// 定义公开接口(不需要认证)
publicPaths := []string{
"/api/auth/login",
"/api/auth/verify",
"/api/auth/logout",
"/api/health", // 可选:健康检查是否需要认证
}
// 检查当前路径是否是公开接口
path := c.Request.URL.Path
for _, p := range publicPaths {
if strings.HasPrefix(path, p) {
c.Next()
return
}
}
// 获取Authorization头
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{
"error": "未授权:缺少认证令牌",
"code": "AUTH_TOKEN_MISSING",
})
c.Abort()
return
}
// 解析Bearer token
const bearerPrefix = "Bearer "
if !strings.HasPrefix(authHeader, bearerPrefix) {
c.JSON(401, gin.H{
"error": "未授权:令牌格式错误",
"code": "AUTH_TOKEN_INVALID_FORMAT",
})
c.Abort()
return
}
tokenString := strings.TrimPrefix(authHeader, bearerPrefix)
// 验证token
claims, err := util.ValidateToken(tokenString, config.AppConfig.AuthJWTSecret)
if err != nil {
c.JSON(401, gin.H{
"error": "未授权:令牌无效或已过期",
"code": "AUTH_TOKEN_INVALID",
})
c.Abort()
return
}
// 将用户信息存入上下文,供后续处理使用
c.Set("username", claims.Username)
c.Next()
}
}
```
#### 8.3.4 认证接口 (api/auth_handler.go)
```go
package api
import (
"github.com/gin-gonic/gin"
"pansou/config"
"pansou/util"
"time"
)
// LoginRequest 登录请求结构
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应结构
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
Username string `json:"username"`
}
// LoginHandler 处理用户登录
func LoginHandler(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
// 验证用户名和密码
if config.AppConfig.AuthUsers == nil {
c.JSON(500, gin.H{"error": "认证系统未正确配置"})
return
}
storedPassword, exists := config.AppConfig.AuthUsers[req.Username]
if !exists || storedPassword != req.Password {
c.JSON(401, gin.H{"error": "用户名或密码错误"})
return
}
// 生成JWT token
token, err := util.GenerateToken(
req.Username,
config.AppConfig.AuthJWTSecret,
config.AppConfig.AuthTokenExpiry,
)
if err != nil {
c.JSON(500, gin.H{"error": "生成令牌失败"})
return
}
// 返回token和过期时间
expiresAt := time.Now().Add(config.AppConfig.AuthTokenExpiry).Unix()
c.JSON(200, LoginResponse{
Token: token,
ExpiresAt: expiresAt,
Username: req.Username,
})
}
// VerifyHandler 验证token有效性
func VerifyHandler(c *gin.Context) {
// 如果能到达这里,说明中间件已经验证通过
username, exists := c.Get("username")
if !exists {
c.JSON(401, gin.H{"error": "未授权"})
return
}
c.JSON(200, gin.H{
"valid": true,
"username": username,
})
}
// LogoutHandler 退出登录(客户端删除token即可)
func LogoutHandler(c *gin.Context) {
c.JSON(200, gin.H{"message": "退出成功"})
}
```
### 8.4 前端实现细节
#### 8.4.1 API模块扩展 (src/api/index.ts)
```typescript
// 登录接口
export interface LoginParams {
username: string;
password: string;
}
export interface LoginResponse {
token: string;
expires_at: number;
username: string;
}
export const login = async (params: LoginParams): Promise<LoginResponse> => {
const response = await api.post<LoginResponse>('/auth/login', params);
return response.data;
};
// 验证token
export const verifyToken = async (): Promise<boolean> => {
try {
await api.post('/auth/verify');
return true;
} catch {
return false;
}
};
// 退出登录
export const logout = async (): Promise<void> => {
try {
await api.post('/auth/logout');
} finally {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_username');
}
};
```
#### 8.4.2 HTTP拦截器配置
```typescript
// 请求拦截器 - 自动添加token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器 - 处理401
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 清除token
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_username');
// 触发显示登录窗口
window.dispatchEvent(new CustomEvent('auth:required'));
}
return Promise.reject(error);
}
);
```
### 8.5 API文档组件集成
在 `ApiDocs.vue` 组件中,需要确保在线调试功能自动携带token:
```typescript
// 生成请求预览时包含Authorization头
const generateSearchRequest = () => {
const token = localStorage.getItem('auth_token');
let headers = 'Content-Type: application/json\n';
if (token) {
headers += `Authorization: Bearer ${token}\n`;
}
if (searchMethod.value === 'POST') {
return `POST /api/search
${headers}
${JSON.stringify(payload, null, 2)}`;
}
// ... GET请求类似处理
};
```
### 8.6 健康检查接口扩展
`/api/health` 接口需要返回认证状态信息:
```go
func HealthHandler(c *gin.Context) {
// ... 现有逻辑 ...
response := gin.H{
"status": "ok",
"auth_enabled": config.AppConfig.AuthEnabled, // 新增
"plugins_enabled": pluginsEnabled,
"plugin_count": pluginCount,
"plugins": pluginNames,
"channels": channels,
"channels_count": channelsCount,
}
c.JSON(200, response)
}
```
### 8.7 环境变量配置
| 变量名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `AUTH_ENABLED` | boolean | `false` | 是否启用认证功能 |
| `AUTH_USERS` | string | - | 用户配置,格式:`user1:pass1,user2:pass2` |
| `AUTH_TOKEN_EXPIRY` | int | `24` | Token有效期(小时) |
| `AUTH_JWT_SECRET` | string | 随机生成 | JWT签名密钥 |
### 8.8 安全考虑
1. **密码存储**: 生产环境应使用bcrypt等算法对密码进行哈希
2. **HTTPS传输**: 生产环境必须使用HTTPS保护token传输
3. **Token过期**: 合理设置token有效期,避免长期有效
4. **限流保护**: 对登录接口实施限流,防止暴力破解
5. **密钥管理**: JWT_SECRET应随机生成并妥善保管
### 8.9 性能影响
- **未启用认证**: 零性能影响,中间件直接放行
- **启用认证**: 每个请求增加约0.1-0.5ms的token验证时间
- **并发性能**: JWT无状态特性,对高并发无影响
- **缓存友好**: 认证不影响现有缓存机制
---
## 9. 插件开发框架
### 9.1 基础开发模板
```go
package myplugin
import (
"net/http"
"pansou/model"
"pansou/plugin"
)
type MyPlugin struct {
*plugin.BaseAsyncPlugin
}
func init() {
p := &MyPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("myplugin", 3),
}
plugin.RegisterGlobalPlugin(p)
}
func (p *MyPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
return p.AsyncSearch(keyword, p.searchImpl, p.GetMainCacheKey(), ext)
}
func (p *MyPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 实现具体搜索逻辑
// 1. 构建请求URL
// 2. 发送HTTP请求
// 3. 解析响应数据
// 4. 转换为标准格式
// 5. 关键词过滤
return plugin.FilterResultsByKeyword(results, keyword), nil
}
```
### 8.2 插件注册流程
1. **自动注册**: 通过`init()`函数自动注册到全局注册表
2. **管理器加载**: `PluginManager`统一管理所有插件
3. **导入触发**: 在`main.go`中通过空导入触发注册
### 8.3 开发最佳实践
- **命名规范**: 插件名使用小写字母
- **优先级设置**: 1-5,数字越小优先级越高
- **错误处理**: 详细错误信息,便于调试
- **资源管理**: 及时释放HTTP连接
---
## 10. 性能优化实现
### 10.1 环境配置优化
基于实际性能测试结果的配置方案:
#### 10.1.1 macOS优化配置
```bash
export HTTP_MAX_CONNS=200
export ASYNC_MAX_BACKGROUND_WORKERS=15
export ASYNC_MAX_BACKGROUND_TASKS=75
export CONCURRENCY=30
```
#### 9.1.2 服务器优化配置
```bash
export HTTP_MAX_CONNS=500
export ASYNC_MAX_BACKGROUND_WORKERS=40
export ASYNC_MAX_BACKGROUND_TASKS=200
export CONCURRENCY=50
```
### 9.2 日志控制系统
基于`config.go`的日志控制:
```bash
export ASYNC_LOG_ENABLED=false # 控制异步插件详细日志
```
异步插件缓存更新日志可通过环境变量开关,避免生产环境日志过多。
---
## 11. 技术选型说明
### 11.1 Go语言优势
- **并发支持**: 原生goroutine,适合高并发场景
- **性能优秀**: 编译型语言,接近C的性能
- **部署简单**: 单一可执行文件,无外部依赖
- **标准库丰富**: HTTP、JSON、并发原语完备
### 10.2 GIN框架选择
- **高性能**: 路由和中间件处理效率高
- **简洁易用**: API设计简洁,学习成本低
- **中间件生态**: 丰富的中间件支持
- **社区活跃**: 文档完善,问题解决快
### 10.3 GOB序列化选择
- **性能优势**: 比JSON快约30%
- **体积优势**: 比JSON小约20%
- **Go原生**: 无需第三方依赖
- **类型安全**: 保持Go类型信息
### 10.4 Sonic JSON库选择
- **高性能**: 比标准库encoding/json快3-5倍
- **统一处理**: 全局统一JSON序列化/反序列化
- **兼容性好**: 完全兼容标准JSON格式
- **内存优化**: 更高效的内存使用
### 10.5 无数据库架构
- **简化部署**: 无需数据库安装配置
- **降低复杂度**: 减少组件依赖
- **提升性能**: 避免数据库IO瓶颈
- **易于扩展**: 无状态设计,支持水平扩展
================================================
FILE: go.mod
================================================
module pansou
go 1.24.1
toolchain go1.24.9
require (
github.com/Advik-B/cloudscraper v0.0.0-20250623142001-d5e0e43555db
github.com/PuerkitoBio/goquery v1.8.1
github.com/bytedance/sonic v1.14.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
golang.org/x/net v0.41.0
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/robertkrimen/otto v0.5.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/Advik-B/cloudscraper v0.0.0-20250623142001-d5e0e43555db h1:r1hesdkYWgm4Bf7abv6UsIUlrCdFxRdKy+DuVypOpw4=
github.com/Advik-B/cloudscraper v0.0.0-20250623142001-d5e0e43555db/go.mod h1:X4xeBaRgq6YCNFrPNd/AXnzGLWq2c46oJfIBh0iLOpI=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
================================================
FILE: main.go
================================================
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"sort"
"strings"
"syscall"
"time"
"golang.org/x/net/netutil"
"pansou/api"
"pansou/config"
"pansou/plugin"
"pansou/service"
"pansou/util"
"pansou/util/cache"
// 以下是插件的空导入,用于触发各插件的init函数,实现自动注册
// 添加新插件时,只需在此处添加对应的导入语句即可
_ "pansou/plugin/hdr4k"
_ "pansou/plugin/gying"
_ "pansou/plugin/pan666"
_ "pansou/plugin/hunhepan"
_ "pansou/plugin/jikepan"
_ "pansou/plugin/panwiki"
_ "pansou/plugin/pansearch"
_ "pansou/plugin/panta"
_ "pansou/plugin/qupansou"
_ "pansou/plugin/susu"
_ "pansou/plugin/thepiratebay"
_ "pansou/plugin/wanou"
_ "pansou/plugin/xuexizhinan"
_ "pansou/plugin/panyq"
_ "pansou/plugin/zhizhen"
_ "pansou/plugin/labi"
_ "pansou/plugin/muou"
_ "pansou/plugin/ouge"
_ "pansou/plugin/shandian"
_ "pansou/plugin/duoduo"
_ "pansou/plugin/huban"
_ "pansou/plugin/cyg"
_ "pansou/plugin/erxiao"
_ "pansou/plugin/miaoso"
_ "pansou/plugin/fox4k"
_ "pansou/plugin/pianku"
_ "pansou/plugin/clmao"
_ "pansou/plugin/wuji"
_ "pansou/plugin/cldi"
_ "pansou/plugin/xiaozhang"
_ "pansou/plugin/libvio"
_ "pansou/plugin/leijing"
_ "pansou/plugin/xb6v"
_ "pansou/plugin/xys"
_ "pansou/plugin/ddys"
_ "pansou/plugin/hdmoli"
_ "pansou/plugin/yuhuage"
_ "pansou/plugin/u3c3"
_ "pansou/plugin/javdb"
_ "pansou/plugin/clxiong"
_ "pansou/plugin/jutoushe"
_ "pansou/plugin/sdso"
_ "pansou/plugin/xiaoji"
_ "pansou/plugin/xdyh"
_ "pansou/plugin/haisou"
_ "pansou/plugin/bixin"
_ "pansou/plugin/nyaa"
_ "pansou/plugin/djgou"
_ "pansou/plugin/xinjuc"
_ "pansou/plugin/aikanzy"
_ "pansou/plugin/qupanshe"
_ "pansou/plugin/xdpan"
_ "pansou/plugin/discourse"
_ "pansou/plugin/yunsou"
_ "pansou/plugin/ahhhhfs"
_ "pansou/plugin/nsgame"
_ "pansou/plugin/quark4k"
_ "pansou/plugin/quarksoo"
_ "pansou/plugin/sousou"
_ "pansou/plugin/ash"
_ "pansou/plugin/qqpd"
_ "pansou/plugin/weibo"
_ "pansou/plugin/feikuai"
_ "pansou/plugin/kkmao"
_ "pansou/plugin/alupan"
_ "pansou/plugin/ypfxw"
_ "pansou/plugin/mikuclub"
_ "pansou/plugin/daishudj"
_ "pansou/plugin/dyyj"
_ "pansou/plugin/meitizy"
_ "pansou/plugin/jsnoteclub"
_ "pansou/plugin/mizixing"
_ "pansou/plugin/lou1"
_ "pansou/plugin/yiove"
_ "pansou/plugin/zxzj"
_ "pansou/plugin/qingying"
_ "pansou/plugin/kkv"
)
// 全局缓存写入管理器
var globalCacheWriteManager *cache.DelayedBatchWriteManager
func main() {
// 初始化应用
initApp()
// 启动服务器
startServer()
}
// initApp 初始化应用程序
func initApp() {
// 初始化配置
config.Init()
// 初始化HTTP客户端
util.InitHTTPClient()
// 初始化缓存写入管理器
var err error
globalCacheWriteManager, err = cache.NewDelayedBatchWriteManager()
if err != nil {
log.Fatalf("缓存写入管理器创建失败: %v", err)
}
if err := globalCacheWriteManager.Initialize(); err != nil {
log.Fatalf("缓存写入管理器初始化失败: %v", err)
}
// 将缓存写入管理器注入到service包
service.SetGlobalCacheWriteManager(globalCacheWriteManager)
// 延迟设置主缓存更新函数,确保service初始化完成
go func() {
// 等待一小段时间确保service包完全初始化
time.Sleep(100 * time.Millisecond)
if mainCache := service.GetEnhancedTwoLevelCache(); mainCache != nil {
globalCacheWriteManager.SetMainCacheUpdater(func(key string, data []byte, ttl time.Duration) error {
return mainCache.SetBothLevels(key, data, ttl)
})
}
}()
// 确保异步插件系统初始化
plugin.InitAsyncPluginSystem()
}
// startServer 启动Web服务器
func startServer() {
// 初始化插件管理器
pluginManager := plugin.NewPluginManager()
// 注册全局插件(根据配置过滤)
if config.AppConfig.AsyncPluginEnabled {
pluginManager.RegisterGlobalPluginsWithFilter(config.AppConfig.EnabledPlugins)
}
// 更新默认并发数(如果插件被禁用则使用0)
pluginCount := 0
if config.AppConfig.AsyncPluginEnabled {
pluginCount = len(pluginManager.GetPlugins())
}
config.UpdateDefaultConcurrency(pluginCount)
// 初始化搜索服务
searchService := service.NewSearchService(pluginManager)
// 设置路由
router := api.SetupRouter(searchService)
// 获取端口配置
port := config.AppConfig.Port
// 输出服务信息
printServiceInfo(port, pluginManager)
// 创建HTTP服务器
srv := &http.Server{
Addr: ":" + port,
Handler: router,
ReadTimeout: config.AppConfig.HTTPReadTimeout,
WriteTimeout: config.AppConfig.HTTPWriteTimeout,
IdleTimeout: config.AppConfig.HTTPIdleTimeout,
}
// 创建通道来接收操作系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 在单独的goroutine中启动服务器
go func() {
// 如果设置了最大连接数,使用限制监听器
if config.AppConfig.HTTPMaxConns > 0 {
// 创建监听器
listener, err := net.Listen("tcp", srv.Addr)
if err != nil {
log.Fatalf("创建监听器失败: %v", err)
}
// 创建限制连接数的监听器
limitListener := netutil.LimitListener(listener, config.AppConfig.HTTPMaxConns)
// 使用限制监听器启动服务器
if err := srv.Serve(limitListener); err != nil && err != http.ErrServerClosed {
log.Fatalf("启动服务器失败: %v", err)
}
} else {
// 使用默认方式启动服务器(不限制连接数)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("启动服务器失败: %v", err)
}
}
}()
// 等待中断信号
<-quit
fmt.Println("正在关闭服务器...")
// 优先保存缓存数据到磁盘(数据安全第一)
// 增加关闭超时时间,确保数据有足够时间保存
shutdownTimeout := 10 * time.Second
if globalCacheWriteManager != nil {
if err := globalCacheWriteManager.Shutdown(shutdownTimeout); err != nil {
log.Printf("缓存数据保存失败: %v", err)
}
}
// 额外确保内存缓存也被保存(双重保障)
if mainCache := service.GetEnhancedTwoLevelCache(); mainCache != nil {
if err := mainCache.FlushMemoryToDisk(); err != nil {
log.Printf("内存缓存同步失败: %v", err)
}
}
// 设置关闭超时时间
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("服务器关闭异常: %v", err)
}
fmt.Println("服务器已安全关闭")
}
// printServiceInfo 打印服务信息
func printServiceInfo(port string, pluginManager *plugin.PluginManager) {
// 启动服务器
fmt.Printf("服务器启动在 http://localhost:%s\n", port)
// 输出代理信息
hasProxy := false
if config.AppConfig.ProxyURL != "" {
proxyType := "代理"
if strings.HasPrefix(config.AppConfig.ProxyURL, "socks5://") {
proxyType = "SOCKS5代理"
} else if strings.HasPrefix(config.AppConfig.ProxyURL, "http://") {
proxyType = "HTTP代理"
} else if strings.HasPrefix(config.AppConfig.ProxyURL, "https://") {
proxyType = "HTTPS代理"
}
fmt.Printf("使用%s (PROXY): %s\n", proxyType, config.AppConfig.ProxyURL)
hasProxy = true
}
if config.AppConfig.HTTPProxyURL != "" {
fmt.Printf("使用HTTP代理 (HTTP_PROXY/http_proxy): %s\n", config.AppConfig.HTTPProxyURL)
hasProxy = true
}
if config.AppConfig.HTTPSProxyURL != "" {
fmt.Printf("使用HTTPS代理 (HTTPS_PROXY/https_proxy): %s\n", config.AppConfig.HTTPSProxyURL)
hasProxy = true
}
if !hasProxy {
fmt.Println("未使用代理")
}
// 输出并发信息
if os.Getenv("CONCURRENCY") != "" {
fmt.Printf("默认并发数: %d (由环境变量CONCURRENCY指定)\n", config.AppConfig.DefaultConcurrency)
} else {
channelCount := len(config.AppConfig.DefaultChannels)
pluginCount := 0
// 只有插件启用时才计算插件数
if config.AppConfig.AsyncPluginEnabled && pluginManager != nil {
pluginCount = len(pluginManager.GetPlugins())
}
fmt.Printf("默认并发数: %d (= 频道数%d + 插件数%d + 10)\n",
config.AppConfig.DefaultConcurrency, channelCount, pluginCount)
}
// 输出缓存信息
if config.AppConfig.CacheEnabled {
fmt.Printf("缓存已启用: 路径=%s, 最大大小=%dMB, TTL=%d分钟\n",
config.AppConfig.CachePath,
config.AppConfig.CacheMaxSizeMB,
config.AppConfig.CacheTTLMinutes)
} else {
fmt.Println("缓存已禁用")
}
// 输出压缩信息
if config.AppConfig.EnableCompression {
fmt.Printf("响应压缩已启用: 最小压缩大小=%d字节\n",
config.AppConfig.MinSizeToCompress)
}
// 输出GC配置信息
fmt.Printf("GC配置: 触发阈值=%d%%, 内存优化=%v\n",
config.AppConfig.GCPercent,
config.AppConfig.OptimizeMemory)
// 输出HTTP服务器配置信息
readTimeoutMsg := ""
if os.Getenv("HTTP_READ_TIMEOUT") != "" {
readTimeoutMsg = "(由环境变量指定)"
} else {
readTimeoutMsg = "(自动计算)"
}
writeTimeoutMsg := ""
if os.Getenv("HTTP_WRITE_TIMEOUT") != "" {
writeTimeoutMsg = "(由环境变量指定)"
} else {
writeTimeoutMsg = "(自动计算)"
}
maxConnsMsg := ""
if os.Getenv("HTTP_MAX_CONNS") != "" {
maxConnsMsg = "(由环境变量指定)"
} else {
cpuCount := runtime.NumCPU()
maxConnsMsg = fmt.Sprintf("(自动计算: CPU核心数%d × 200)", cpuCount)
}
fmt.Printf("HTTP服务器配置: 读取超时=%v %s, 写入超时=%v %s, 空闲超时=%v, 最大连接数=%d %s\n",
config.AppConfig.HTTPReadTimeout, readTimeoutMsg,
config.AppConfig.HTTPWriteTimeout, writeTimeoutMsg,
config.AppConfig.HTTPIdleTimeout,
config.AppConfig.HTTPMaxConns, maxConnsMsg)
// 输出异步插件配置信息
if config.AppConfig.AsyncPluginEnabled {
// 检查工作者数量是否由环境变量指定
workersMsg := ""
if os.Getenv("ASYNC_MAX_BACKGROUND_WORKERS") != "" {
workersMsg = "(由环境变量指定)"
} else {
cpuCount := runtime.NumCPU()
workersMsg = fmt.Sprintf("(自动计算: CPU核心数%d × 5)", cpuCount)
}
// 检查任务数量是否由环境变量指定
tasksMsg := ""
if os.Getenv("ASYNC_MAX_BACKGROUND_TASKS") != "" {
tasksMsg = "(由环境变量指定)"
} else {
tasksMsg = "(自动计算: 工作者数量 × 5)"
}
fmt.Printf("异步插件已启用: 响应超时=%d秒, 最大工作者=%d %s, 最大任务=%d %s, 缓存TTL=%d小时\n",
config.AppConfig.AsyncResponseTimeout,
config.AppConfig.AsyncMaxBackgroundWorkers, workersMsg,
config.AppConfig.AsyncMaxBackgroundTasks, tasksMsg,
config.AppConfig.AsyncCacheTTLHours)
} else {
fmt.Println("异步插件已禁用")
}
// 只有当插件功能启用时才输出插件信息
if config.AppConfig.AsyncPluginEnabled {
plugins := pluginManager.GetPlugins()
if len(plugins) > 0 {
// 根据新逻辑,只有指定了具体插件才会加载插件
fmt.Printf("已启用指定插件 (%d个):\n", len(plugins))
// 按优先级排序(优先级数字越小越靠前)
sort.Slice(plugins, func(i, j int) bool {
// 优先级相同时按名称排序
if plugins[i].Priority() == plugins[j].Priority() {
return plugins[i].Name() < plugins[j].Name()
}
return plugins[i].Priority() < plugins[j].Priority()
})
for _, p := range plugins {
fmt.Printf(" - %s (优先级: %d)\n", p.Name(), p.Priority())
}
} else {
// 区分不同的情况
if config.AppConfig.EnabledPlugins == nil {
fmt.Println("未设置插件列表 (ENABLED_PLUGINS),未加载任何插件")
} else if len(config.AppConfig.EnabledPlugins) > 0 {
fmt.Printf("未找到指定的插件: %s\n", strings.Join(config.AppConfig.EnabledPlugins, ", "))
} else {
fmt.Println("插件列表为空 (ENABLED_PLUGINS=\"\"),未加载任何插件")
}
}
}
}
================================================
FILE: mcp-config.json
================================================
{
"mcpServers": {
"pansou": {
"command": "node",
"args": [
"C:\\full\\path\\to\\your\\project\\typescript\\dist\\index.js"
],
"env": {
"PANSOU_SERVER_URL": "http://localhost:8888",
"REQUEST_TIMEOUT": "60",
"MAX_RESULTS": "50",
"DEFAULT_CLOUD_TYPES": "baidu,aliyun,quark,tianyi,uc,mobile,115,pikpak,xunlei,123,magnet,ed2k,others",
"AUTO_START_BACKEND": "true",
"DOCKER_MODE": "true",
"BACKEND_SHUTDOWN_DELAY": "5000",
"BACKEND_STARTUP_TIMEOUT": "30000",
"IDLE_TIMEOUT": "300000",
"ENABLE_IDLE_SHUTDOWN": "true",
"PROJECT_ROOT_PATH": "C:\\full\\path\\to\\your\\project",
"ENABLED_PLUGINS": "labi,zhizhen,shandian,duoduo,muou,wanou"
}
}
},
"_comments": {
"description": "PanSou MCP服务统一配置文件",
"version": "2.0",
"智能模式说明": {
"自动检测": "如果DOCKER_MODE未设置或为false,服务将自动检测部署模式",
"检测优先级": [
"1. 检查是否有运行中的Docker容器(名称包含pansou)",
"2. 检查是否存在Go可执行文件(pansou.exe/main.exe)",
"3. 检查后端服务是否已在运行"
],
"环境变量覆盖": "可通过环境变量强制指定模式,如 DOCKER_MODE=true"
},
"配置说明": {
"PANSOU_SERVER_URL": "后端服务地址,默认http://localhost:8888",
"REQUEST_TIMEOUT": "请求超时时间(秒),默认30",
"MAX_RESULTS": "最大搜索结果数,默认50",
"DEFAULT_CLOUD_TYPES": "默认搜索的网盘类型,逗号分隔",
"AUTO_START_BACKEND": "是否自动启动后端服务(源码模式),默认true",
"DOCKER_MODE": "是否强制使用Docker模式,默认false(自动检测)",
"BACKEND_SHUTDOWN_DELAY": "后端服务关闭延迟(毫秒),默认5000",
"BACKEND_STARTUP_TIMEOUT": "后端服务启动超时(毫秒),默认30000",
"IDLE_TIMEOUT": "空闲超时时间(毫秒),默认300000(5分钟)",
"ENABLE_IDLE_SHUTDOWN": "是否启用空闲自动关闭,默认true",
"PROJECT_ROOT_PATH": "项目根目录路径,用于查找Go可执行文件",
"ENABLED_PLUGINS": "指定启用的插件列表,多个插件用逗号分隔,必须显式指定"
},
"使用示例": {
"自动模式": "默认配置,自动检测部署方式",
"强制Docker模式": "设置 DOCKER_MODE=true",
"强制源码模式": "设置 DOCKER_MODE=false 且 AUTO_START_BACKEND=true",
"仅连接模式": "设置 AUTO_START_BACKEND=false(适用于手动启动的后端)"
}
}
}
================================================
FILE: model/plugin_result.go
================================================
package model
import (
"time"
)
// PluginSearchResult 插件搜索结果
type PluginSearchResult struct {
Results []SearchResult `json:"results"` // 搜索结果
IsFinal bool `json:"is_final"` // 是否为最终完整结果
Timestamp time.Time `json:"timestamp"` // 结果时间戳
Source string `json:"source"` // 插件来源
Message string `json:"message"` // 状态描述(可选)
}
// IsEmpty 检查结果是否为空
func (p *PluginSearchResult) IsEmpty() bool {
return len(p.Results) == 0
}
// Count 返回结果数量
func (p *PluginSearchResult) Count() int {
return len(p.Results)
}
// GetResults 获取搜索结果列表
func (p *PluginSearchResult) GetResults() []SearchResult {
if p.Results == nil {
return []SearchResult{}
}
return p.Results
}
================================================
FILE: model/request.go
================================================
package model
// FilterConfig 过滤配置
type FilterConfig struct {
Include []string `json:"include,omitempty"` // 包含关键词列表(OR关系)
Exclude []string `json:"exclude,omitempty"` // 排除关键词列表(AND关系)
}
// SearchRequest 搜索请求参数
type SearchRequest struct {
Keyword string `json:"kw" binding:"required"` // 搜索关键词
Channels []string `json:"channels"` // 搜索的频道列表
Concurrency int `json:"conc"` // 并发搜索数量
ForceRefresh bool `json:"refresh"` // 强制刷新,不使用缓存
ResultType string `json:"res"` // 结果类型:all(返回所有结果)、results(仅返回results)、merge(仅返回merged_by_type)
SourceType string `json:"src"` // 数据来源类型:all(默认,全部来源)、tg(仅Telegram)、plugin(仅插件)
Plugins []string `json:"plugins"` // 指定搜索的插件列表,不指定则搜索全部插件
Ext map[string]interface{} `json:"ext"` // 扩展参数,用于传递给插件的自定义参数
CloudTypes []string `json:"cloud_types"` // 指定返回的网盘类型列表,不指定则返回所有类型
Filter *FilterConfig `json:"filter,omitempty"` // 过滤配置,用于过滤返回结果
}
================================================
FILE: model/response.go
================================================
package model
import "time"
// Link 网盘链接
type Link struct {
Type string `json:"type" sonic:"type"`
URL string `json:"url" sonic:"url"`
Password string `json:"password" sonic:"password"`
Datetime time.Time `json:"datetime,omitempty" sonic:"datetime,omitempty"` // 链接更新时间(可选)
WorkTitle string `json:"work_title,omitempty" sonic:"work_title,omitempty"` // 作品标题(用于区分同一消息中多个作品的链接)
}
// SearchResult 搜索结果
type SearchResult struct {
MessageID string `json:"message_id" sonic:"message_id"`
UniqueID string `json:"unique_id" sonic:"unique_id"` // 全局唯一ID
Channel string `json:"channel" sonic:"channel"`
Datetime time.Time `json:"datetime" sonic:"datetime"`
Title string `json:"title" sonic:"title"`
Content string `json:"content" sonic:"content"`
Links []Link `json:"links" sonic:"links"`
Tags []string `json:"tags,omitempty" sonic:"tags,omitempty"`
Images []string `json:"images,omitempty" sonic:"images,omitempty"` // TG消息中的图片链接
}
// MergedLink 合并后的网盘链接
type MergedLink struct {
URL string `json:"url" sonic:"url"`
Password string `json:"password" sonic:"password"`
Note string `json:"note" sonic:"note"`
Datetime time.Time `json:"datetime" sonic:"datetime"`
Source string `json:"source,omitempty" sonic:"source,omitempty"` // 数据来源:tg:频道名 或 plugin:插件名
Images []string `json:"images,omitempty" sonic:"images,omitempty"` // TG消息中的图片链接
}
// MergedLinks 按网盘类型分组的合并链接
type MergedLinks map[string][]MergedLink
// SearchResponse 搜索响应
type SearchResponse struct {
Total int `json:"total" sonic:"total"`
Results []SearchResult `json:"results,omitempty" sonic:"results,omitempty"`
MergedByType MergedLinks `json:"merged_by_type,omitempty" sonic:"merged_by_type,omitempty"`
}
// Response API通用响应
type Response struct {
Code int `json:"code" sonic:"code"`
Message string `json:"message" sonic:"message"`
Data interface{} `json:"data,omitempty" sonic:"data,omitempty"`
}
// NewSuccessResponse 创建成功响应
func NewSuccessResponse(data interface{}) Response {
return Response{
Code: 0,
Message: "success",
Data: data,
}
}
// NewErrorResponse 创建错误响应
func NewErrorResponse(code int, message string) Response {
return Response{
Code: code,
Message: message,
}
}
================================================
FILE: package.json
================================================
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4"
}
}
================================================
FILE: plugin/ahhhhfs/ahhhhfs.go
================================================
package ahhhhfs
import (
"context"
"fmt"
"net/http"
"net/url"
"pansou/model"
"pansou/plugin"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/PuerkitoBio/goquery"
)
// 预编译的正则表达式
var (
// 从详情页URL中提取文章ID的正则表达式
articleIDRegex = regexp.MustCompile(`/(\d+)/?$`)
// 常见网盘链接的正则表达式
quarkLinkRegex = regexp.MustCompile(`https?://pan\.quark\.cn/s/[0-9a-zA-Z]+`)
baiduLinkRegex = regexp.MustCompile(`https?://pan\.baidu\.com/s/[0-9a-zA-Z_\-]+`)
aliyunLinkRegex = regexp.MustCompile(`https?://(www\.)?(aliyundrive\.com|alipan\.com)/s/[0-9a-zA-Z]+`)
ucLinkRegex = regexp.MustCompile(`https?://drive\.uc\.cn/s/[0-9a-zA-Z]+`)
xunleiLinkRegex = regexp.MustCompile(`https?://pan\.xunlei\.com/s/[0-9a-zA-Z_\-]+`)
tianyiLinkRegex = regexp.MustCompile(`https?://cloud\.189\.cn/(t|web)/[0-9a-zA-Z]+`)
link115Regex = regexp.MustCompile(`https?://115\.com/s/[0-9a-zA-Z]+`)
link123Regex = regexp.MustCompile(`https?://123pan\.com/s/[0-9a-zA-Z]+`)
pikpakLinkRegex = regexp.MustCompile(`https?://mypikpak\.com/s/[0-9a-zA-Z]+`)
// 提取码匹配模式
pwdPatterns = []*regexp.Regexp{
regexp.MustCompile(`提取码[::]\s*([0-9a-zA-Z]+)`),
regexp.MustCompile(`密码[::]\s*([0-9a-zA-Z]+)`),
regexp.MustCompile(`pwd[=::]\s*([0-9a-zA-Z]+)`),
regexp.MustCompile(`code[=::]\s*([0-9a-zA-Z]+)`),
}
// 缓存相关
detailCache = sync.Map{} // 缓存详情页解析结果
lastCleanupTime = time.Now()
cacheTTL = 1 * time.Hour
)
const (
// 插件名称
pluginName = "ahhhhfs"
// 优先级
defaultPriority = 2
// 超时时间
DefaultTimeout = 10 * time.Second
DetailTimeout = 8 * time.Second
// 并发数限制
MaxConcurrency = 15
// HTTP连接池配置
MaxIdleConns = 100
MaxIdleConnsPerHost = 30
MaxConnsPerHost = 50
IdleConnTimeout = 90 * time.Second
)
// 性能统计
var (
searchRequests int64 = 0
detailPageRequests int64 = 0
cacheHits int64 = 0
cacheMisses int64 = 0
)
// AhhhhfsAsyncPlugin ahhhhfs异步插件
type AhhhhfsAsyncPlugin struct {
*plugin.BaseAsyncPlugin
optimizedClient *http.Client
}
// 在init函数中注册插件
func init() {
plugin.RegisterGlobalPlugin(NewAhhhhfsPlugin())
// 启动缓存清理goroutine
go startCacheCleaner()
}
// startCacheCleaner 启动一个定期清理缓存的goroutine
func startCacheCleaner() {
ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
for range ticker.C {
// 清空所有缓存
detailCache = sync.Map{}
lastCleanupTime = time.Now()
}
}
// createOptimizedHTTPClient 创建优化的HTTP客户端
func createOptimizedHTTPClient() *http.Client {
transport := &http.Transport{
MaxIdleConns: MaxIdleConns,
MaxIdleConnsPerHost: MaxIdleConnsPerHost,
MaxConnsPerHost: MaxConnsPerHost,
IdleConnTimeout: IdleConnTimeout,
DisableKeepAlives: false,
}
return &http.Client{
Transport: transport,
Timeout: DefaultTimeout,
}
}
// NewAhhhhfsPlugin 创建新的ahhhhfs异步插件
func NewAhhhhfsPlugin() *AhhhhfsAsyncPlugin {
return &AhhhhfsAsyncPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin(pluginName, defaultPriority),
optimizedClient: createOptimizedHTTPClient(),
}
}
// Search 执行搜索并返回结果(兼容性方法)
func (p *AhhhhfsAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
result, err := p.SearchWithResult(keyword, ext)
if err != nil {
return nil, err
}
return result.Results, nil
}
// SearchWithResult 执行搜索并返回包含IsFinal标记的结果
func (p *AhhhhfsAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
}
// searchImpl 实现具体的搜索逻辑
func (p *AhhhhfsAsyncPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 性能统计
start := time.Now()
atomic.AddInt64(&searchRequests, 1)
defer func() {
fmt.Printf("[%s] 搜索耗时: %v\n", p.Name(), time.Since(start))
}()
// 使用优化的客户端
if p.optimizedClient != nil {
client = p.optimizedClient
}
// 1. 构建搜索URL
searchURL := fmt.Sprintf("https://www.ahhhhfs.com/?cat=&s=%s", url.QueryEscape(keyword))
// 2. 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
// 3. 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
if err != nil {
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
}
// 4. 设置完整的请求头(避免反爬虫)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("Cache-Control", "max-age=0")
req.Header.Set("Referer", "https://www.ahhhhfs.com/")
// 5. 发送请求(带重试机制)
resp, err := p.doRequestWithRetry(req, client)
if err != nil {
return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("[%s] 搜索请求返回状态码: %d", p.Name(), resp.StatusCode)
}
// 6. 解析搜索结果页面
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 解析搜索页面失败: %w", p.Name(), err)
}
// 7. 提取搜索结果
var results []model.SearchResult
var wg sync.WaitGroup
var mu sync.Mutex
semaphore := make(chan struct{}, MaxConcurrency)
doc.Find("article.post-item.item-list").Each(func(i int, s *goquery.Selection) {
// 解析基本信息
titleElem := s.Find(".entry-title a")
title := strings.TrimSpace(titleElem.Text())
if title == "" {
title = strings.TrimSpace(titleElem.AttrOr("title", ""))
}
detailURL, exists := titleElem.Attr("href")
if !exists || detailURL == "" || title == "" {
return
}
// 提取文章ID
articleID := p.extractArticleID(detailURL)
if articleID == "" {
return
}
// 提取分类标签
var tags []string
s.Find(".entry-cat-dot a").Each(func(j int, tag *goquery.Selection) {
tagText := strings.TrimSpace(tag.Text())
if tagText != "" {
tags = append(tags, tagText)
}
})
// 提取描述
content := strings.TrimSpace(s.Find(".entry-desc").Text())
// 提取时间
datetime := ""
timeElem := s.Find(".entry-meta .meta-date time")
if dt, exists := timeElem.Attr("datetime"); exists {
datetime = dt
} else {
datetime = strings.TrimSpace(timeElem.Text())
}
// 解析时间
publishTime := p.parseDateTime(datetime)
// 异步获取详情页的网盘链接
wg.Add(1)
semaphore <- struct{}{} // 获取信号量
go func(title, detailURL, articleID, content string, tags []string, publishTime time.Time) {
defer wg.Done()
defer func() { <-semaphore }() // 释放信号量
// 获取网盘链接
links := p.fetchDetailLinks(client, detailURL, articleID)
if len(links) > 0 {
result := model.SearchResult{
UniqueID: fmt.Sprintf("%s-%s", p.Name(), articleID),
Title: title,
Content: content,
Links: links,
Tags: tags,
Channel: "", // 插件搜索结果 Channel 必须为空
Datetime: publishTime,
}
mu.Lock()
results = append(results, result)
mu.Unlock()
}
}(title, detailURL, articleID, content, tags, publishTime)
})
// 等待所有详情页请求完成
wg.Wait()
fmt.Printf("[%s] 搜索结果: %d 条\n", p.Name(), len(results))
// 关键词过滤
return plugin.FilterResultsByKeyword(results, keyword), nil
}
// extractArticleID 从URL中提取文章ID
func (p *AhhhhfsAsyncPlugin) extractArticleID(detailURL string) string {
matches := articleIDRegex.FindStringSubmatch(detailURL)
if len(matches) >= 2 {
return matches[1]
}
return ""
}
// parseDateTime 解析时间字符串
func (p *AhhhhfsAsyncPlugin) parseDateTime(datetime string) time.Time {
datetime = strings.TrimSpace(datetime)
// 尝试解析 ISO 格式
if t, err := time.Parse(time.RFC3339, datetime); err == nil {
return t
}
// 尝试解析标准日期格式
layouts := []string{
"2006-01-02",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05",
"2006-01-02T15:04:05Z07:00",
}
for _, layout := range layouts {
if t, err := time.Parse(layout, datetime); err == nil {
return t
}
}
// 处理相对时间(如"1 周前"、"2 天前")
now := time.Now()
if strings.Contains(datetime, "小时前") || strings.Contains(datetime, "hours ago") {
// 简单处理,返回当天
return now
}
if strings.Contains(datetime, "天前") || strings.Contains(datetime, "days ago") {
// 简单处理,返回近期
return now.AddDate(0, 0, -7)
}
if strings.Contains(datetime, "周前") || strings.Contains(datetime, "weeks ago") {
// 简单处理,返回一个月前
return now.AddDate(0, -1, 0)
}
// 默认返回当前时间
return now
}
// fetchDetailLinks 获取详情页的网盘链接
func (p *AhhhhfsAsyncPlugin) fetchDetailLinks(client *http.Client, detailURL, articleID string) []model.Link {
atomic.AddInt64(&detailPageRequests, 1)
// 检查缓存
if cached, ok := detailCache.Load(articleID); ok {
atomic.AddInt64(&cacheHits, 1)
return cached.([]model.Link)
}
atomic.AddInt64(&cacheMisses, 1)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), DetailTimeout)
defer cancel()
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", detailURL, nil)
if err != nil {
fmt.Printf("[%s] 创建详情页请求失败: %v\n", p.Name(), err)
return nil
}
// 设置请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Referer", "https://www.ahhhhfs.com/")
// 发送请求
resp, err := client.Do(req)
if err != nil {
fmt.Printf("[%s] 详情页请求失败: %v\n", p.Name(), err)
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("[%s] 详情页返回状态码: %d\n", p.Name(), resp.StatusCode)
return nil
}
// 解析详情页
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
fmt.Printf("[%s] 解析详情页失败: %v\n", p.Name(), err)
return nil
}
// 提取网盘链接
links := p.extractNetDiskLinks(doc)
// 缓存结果
if len(links) > 0 {
detailCache.Store(articleID, links)
}
return links
}
// extractNetDiskLinks 从详情页提取网盘链接
func (p *AhhhhfsAsyncPlugin) extractNetDiskLinks(doc *goquery.Document) []model.Link {
var links []model.Link
linkMap := make(map[string]model.Link) // 用于去重
// 在文章内容中查找所有链接
doc.Find(".post-content a").Each(func(i int, s *goquery.Selection) {
href, exists := s.Attr("href")
if !exists || href == "" {
return
}
// 判断是否为网盘链接
cloudType := p.determineCloudType(href)
if cloudType == "others" {
return
}
// 提取提取码
password := p.extractPassword(s, href)
// 添加到结果(去重)
if _, exists := linkMap[href]; !exists {
link := model.Link{
Type: cloudType,
URL: href,
Password: password,
}
linkMap[href] = link
links = append(links, link)
}
})
return links
}
// determineCloudType 判断链接类型
func (p *AhhhhfsAsyncPlugin) determineCloudType(url string) string {
switch {
case strings.Contains(url, "pan.quark.cn"):
return "quark"
case strings.Contains(url, "drive.uc.cn"):
return "uc"
case strings.Contains(url, "pan.baidu.com"):
return "baidu"
case strings.Contains(url, "aliyundrive.com") || strings.Contains(url, "alipan.com"):
return "aliyun"
case strings.Contains(url, "pan.xunlei.com"):
return "xunlei"
case strings.Contains(url, "cloud.189.cn"):
return "tianyi"
case strings.Contains(url, "115.com"):
return "115"
case strings.Contains(url, "123pan.com"):
return "123"
case strings.Contains(url, "mypikpak.com"):
return "pikpak"
default:
return "others"
}
}
// extractPassword 提取提取码
func (p *AhhhhfsAsyncPlugin) extractPassword(linkElem *goquery.Selection, url string) string {
// 1. 从链接的 title 属性中提取
if title, exists := linkElem.Attr("title"); exists {
for _, pattern := range pwdPatterns {
if matches := pattern.FindStringSubmatch(title); len(matches) >= 2 {
return matches[1]
}
}
}
// 2. 从链接文本中提取
linkText := linkElem.Text()
for _, pattern := range pwdPatterns {
if matches := pattern.FindStringSubmatch(linkText); len(matches) >= 2 {
return matches[1]
}
}
// 3. 从链接后面的兄弟节点或父节点的文本中提取
parent := linkElem.Parent()
parentText := parent.Text()
// 获取链接在父元素文本中的位置
linkIndex := strings.Index(parentText, linkText)
if linkIndex >= 0 {
// 获取链接后面的文本
afterText := parentText[linkIndex+len(linkText):]
for _, pattern := range pwdPatterns {
if matches := pattern.FindStringSubmatch(afterText); len(matches) >= 2 {
return matches[1]
}
}
}
// 4. 从 URL 参数中提取
if strings.Contains(url, "pwd=") {
parts := strings.Split(url, "pwd=")
if len(parts) >= 2 {
pwd := parts[1]
// 只取密码部分(去除其他参数)
if idx := strings.IndexAny(pwd, "&?#"); idx >= 0 {
pwd = pwd[:idx]
}
return pwd
}
}
return ""
}
// doRequestWithRetry 带重试机制的HTTP请求
func (p *AhhhhfsAsyncPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
maxRetries := 3
var lastErr error
for i := 0; i < maxRetries; i++ {
if i > 0 {
// 指数退避重试
backoff := time.Duration(1<<uint(i-1)) * 200 * time.Millisecond
time.Sleep(backoff)
}
// 克隆请求避免并发问题
reqClone := req.Clone(req.Context())
resp, err := client.Do(reqClone)
if err == nil && resp.StatusCode == 200 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, lastErr)
}
================================================
FILE: plugin/ahhhhfs/html结构分析.md
================================================
# ahhhhfs (A姐分享) HTML结构分析
## 网站信息
- **网站名称**: ahhhhfs (A姐分享)
- **域名**: www.ahhhhfs.com
- **类型**: 资源分享网站(WordPress 站点)
- **特点**: 分享各类学习资源、软件、教程等
## 搜索页面结构
### 1. 搜索URL模式
```
https://www.ahhhhfs.com/search/{关键词}
或
https://www.ahhhhfs.com/?s={关键词}
示例:
https://www.ahhhhfs.com/search/小红书
https://www.ahhhhfs.com/?s=小红书
参数说明:
- 关键词: 直接使用中文或URL编码都可以
```
### 2. 搜索结果容器
- **父容器**: `.row` (结果列表容器)
- **结果项**: `<article class="post-item item-list">` (每个搜索结果)
### 3. 单个搜索结果结构
#### 标题区域 (.entry-title)
```html
<h2 class="entry-title">
<a target="_blank" href="https://www.ahhhhfs.com/76567/"
title="AI小红书虚拟电商全链路实战课:从选品到变现的AI爆款打法">
AI小红书虚拟电商全链路实战课:从选品到变现的AI爆款打法
</a>
</h2>
提取要素:
- 标题: a 的文本内容或 title 属性
- 详情页链接: a 的 href 属性
```
#### 分类标签 (.entry-cat-dot)
```html
<div class="entry-cat-dot">
<a href="https://www.ahhhhfs.com/recourse/%e7%9f%ad%e8%a7%86%e9%a2%91/">短视频</a>
<a href="https://www.ahhhhfs.com/recourse/">资源</a>
</div>
提取要素:
- 分类: 所有 a 标签的文本内容
```
#### 描述区域 (.entry-desc)
```html
<div class="entry-desc">
AI小红书虚拟电商全链路实战课程概览 《AI小红书虚拟电商5.0实战课》是一门聚焦AI与小红书生态融合的系统课程,围绕AI赋能选品、创作、运营与变现四大环节展开...
</div>
提取要素:
- 描述: div 的文本内容
```
#### 元数据栏 (.entry-meta)
```html
<div class="entry-meta">
<span class="meta-date">
<i class="far fa-clock me-1"></i>
<time class="pub-date" datetime="2025-10-18T13:43:10+08:00">1 周前</time>
</span>
<span class="meta-likes d-none d-md-inline-block"><i class="far fa-heart me-1"></i>0</span>
<span class="meta-fav d-none d-md-inline-block"><i class="far fa-star me-1"></i>1</span>
</div>
提取要素:
- 发布时间: time 标签的 datetime 属性或文本内容
```
## 详情页面结构
### 1. 详情页URL模式
```
https://www.ahhhhfs.com/{文章ID}/
示例:
https://www.ahhhhfs.com/76567/
```
### 2. 下载链接位置
下载链接在文章正文内容中 `.post-content` 里面,通常在文章末尾部分。
#### 下载链接格式示例
```html
<p>
学习地址:
<a title="..."
href="https://pan.quark.cn/s/c16a5ae18ea0"
target="_blank"
rel="nofollow noopener noreferrer">夸克</a>
</p>
或者
<p>
下载地址:
<a href="https://pan.baidu.com/s/xxxxx"
target="_blank"
rel="nofollow noopener noreferrer">百度网盘</a>
提取码: xxxx
</p>
或者多个网盘链接:
<p>
阿里云盘:<a href="...">链接</a><br>
夸克网盘:<a href="...">链接</a><br>
百度网盘:<a href="...">链接</a> 提取码: xxxx
</p>
提取要素:
- 网盘链接: .post-content 中包含网盘域名的 a 标签的 href 属性
- 提取码/密码: 链接附近的文本内容,可能包含 "提取码"、"密码"、"pwd" 等关键词
```
## CSS选择器总结
| 数据项 | CSS选择器 | 提取方式 |
|--------|-----------|----------|
| 搜索结果列表 | `article.post-item.item-list` | 遍历所有结果项 |
| 标题 | `.entry-title a` | 文本内容或 title 属性 |
| 详情页链接 | `.entry-title a` | href 属性 |
| 分类标签 | `.entry-cat-dot a` | 所有 a 标签的文本内容 |
| 描述 | `.entry-desc` | 文本内容 |
| 发布时间 | `.entry-meta .meta-date time` | datetime 属性或文本内容 |
| 文章内容 | `.post-content` | HTML 内容 |
| 网盘链接 | `.post-content a[href*="pan"]` 或匹配网盘域名 | href 属性 |
## 实现要点
### 1. 支持的网盘类型
- 夸克网盘: `pan.quark.cn`
- 阿里云盘: `aliyundrive.com`, `alipan.com`
- 百度网盘: `pan.baidu.com`
- UC网盘: `drive.uc.cn`
- 迅雷网盘: `pan.xunlei.com`
- 天翼云盘: `cloud.189.cn`
- 115网盘: `115.com`
- 123网盘: `123pan.com`
### 2. 提取码识别
提取码可能出现在以下位置:
- 链接后面的文本: `提取码: xxxx` 或 `密码: xxxx`
- 链接的 title 属性中
- `<br>` 标签分隔的下一行
- 括号内: `(提取码: xxxx)`
常见关键词:
- 提取码
- 密码
- pwd
- code
- 取码
### 3. 链接提取策略
1. 先从搜索结果页获取文章列表
2. 访问每篇文章的详情页
3. 在详情页的 `.post-content` 中查找包含网盘域名的链接
4. 提取链接和相应的提取码
5. 如果文章没有网盘链接,则跳过
### 4. 时间格式处理
- 相对时间: "1 周前"、"2 天前" 需要转换为具体日期
- 绝对时间: "2025-10-18" 可以直接使用
- datetime 属性: "2025-10-18T13:43:10+08:00" 标准ISO格式
### 5. 去重标识
- 使用文章ID作为唯一标识: 从详情页URL中提取 `/76567/`
## 注意事项
1. **搜索结果可能为空**: 如果关键词没有匹配结果,页面会显示"没有找到相关内容"
2. **分页**: 搜索结果可能有多页,但通常只抓取第一页即可
3. **网盘链接位置不固定**: 链接可能在文章开头、中间或结尾,需要遍历整个 `.post-content`
4. **广告干扰**: 页面包含广告,需要准确定位到实际内容区域
5. **需要访问详情页**: 搜索结果页不包含下载链接,必须访问详情页才能获取
6. **请求频率**: 需要访问详情页,建议控制请求频率避免被封
## 示例数据流
```
1. 搜索请求: https://www.ahhhhfs.com/search/小红书
↓
2. 解析搜索结果页,提取文章列表
- 标题: "AI小红书虚拟电商全链路实战课:从选品到变现的AI爆款打法"
- 详情页URL: https://www.ahhhhfs.com/76567/
- 分类: ["短视频", "资源"]
- 发布时间: 2025-10-18
↓
3. 访问详情页: https://www.ahhhhfs.com/76567/
↓
4. 解析详情页 .post-content,提取网盘链接
- 夸克网盘: https://pan.quark.cn/s/c16a5ae18ea0
- 提取码: (如果有)
↓
5. 构建最终结果
- UniqueID: ahhhhfs-76567
- Title: "AI小红书虚拟电商全链路实战课:从选品到变现的AI爆款打法"
- Content: 文章描述
- Links: [{Type: "quark", URL: "...", Password: ""}]
- Tags: ["短视频", "资源"]
- Datetime: 2025-10-18T13:43:10+08:00
```
================================================
FILE: plugin/aikanzy/aikanzy.go
================================================
package aikanzy
import (
"context"
"fmt"
"net/http"
"net/url"
"pansou/model"
"pansou/plugin"
"regexp"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
)
// 预编译的正则表达式
var (
// 夸克网盘链接
quarkLinkRegex = regexp.MustCompile(`https?://pan\.quark\.cn/s/[0-9a-zA-Z]+`)
// UC网盘链接
ucLinkRegex = regexp.MustCompile(`https?://drive\.uc\.cn/s/[0-9a-zA-Z]+(\?[^"'\s]*)?`)
// 百度网盘链接
baiduLinkRegex = regexp.MustCompile(`https?://pan\.baidu\.com/s/[0-9a-zA-Z_-]+`)
// 迅雷网盘链接
xunleiLinkRegex = regexp.MustCompile(`https?://pan\.xunlei\.com/s/[0-9a-zA-Z_-]+`)
// 从URL中提取文章ID
articleIDRegex = regexp.MustCompile(`/([a-z]+)/(\d+)\.html`)
// 提取阅读数
viewCountRegex = regexp.MustCompile(`(\d+)\s*阅读`)
)
// 常量定义
const (
// 插件名称
pluginName = "aikanzy"
// 搜索URL模板
searchURLTemplate = "https://www.aikanzy.com/search?word=%s&molds=article"
// 默认优先级
defaultPriority = 3
// 默认超时时间(秒)
defaultTimeout = 15
// 详情页超时时间(秒)
detailTimeout = 8
// 最大重试次数
maxRetries = 3
// 详情页并发数
detailConcurrency = 15
// 指数退避基数(毫秒)
backoffBase = 200
)
// AikanzyAsyncPlugin 是AikanZY网站的异步搜索插件实现
type AikanzyAsyncPlugin struct {
*plugin.BaseAsyncPlugin
optimizedClient *http.Client
}
// 确保AikanzyAsyncPlugin实现了AsyncSearchPlugin接口
var _ plugin.AsyncSearchPlugin = (*AikanzyAsyncPlugin)(nil)
// 在包初始化时注册插件
func init() {
plugin.RegisterGlobalPlugin(NewAikanzyAsyncPlugin())
}
// createOptimizedHTTPClient 创建优化的HTTP客户端
func createOptimizedHTTPClient() *http.Client {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false,
}
return &http.Client{
Transport: transport,
Timeout: defaultTimeout * time.Second,
}
}
// NewAikanzyAsyncPlugin 创建一个新的AikanZY异步插件实例
func NewAikanzyAsyncPlugin() *AikanzyAsyncPlugin {
return &AikanzyAsyncPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin("aikanzy", defaultPriority),
optimizedClient: createOptimizedHTTPClient(),
}
}
// Name 返回插件名称
func (p *AikanzyAsyncPlugin) Name() string {
return pluginName
}
// Priority 返回插件优先级
func (p *AikanzyAsyncPlugin) Priority() int {
return defaultPriority
}
// Search 执行搜索并返回结果(兼容性方法)
func (p *AikanzyAsyncPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
result, err := p.SearchWithResult(keyword, ext)
if err != nil {
return nil, err
}
return result.Results, nil
}
// SearchWithResult 执行搜索并返回包含IsFinal标记的结果
func (p *AikanzyAsyncPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
return p.AsyncSearchWithResult(keyword, p.doSearch, p.MainCacheKey, ext)
}
// doSearch 执行具体的搜索逻辑
func (p *AikanzyAsyncPlugin) doSearch(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
// 使用优化的客户端
if p.optimizedClient != nil {
client = p.optimizedClient
}
// 对关键词进行URL编码
encodedKeyword := url.QueryEscape(keyword)
// 构建搜索URL
searchURL := fmt.Sprintf(searchURLTemplate, encodedKeyword)
// 创建一个带有超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout*time.Second)
defer cancel()
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil)
if err != nil {
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
}
// 设置完整的请求头(避免反爬虫)
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", "https://www.aikanzy.com/")
req.Header.Set("Upgrade-Insecure-Requests", "1")
req.Header.Set("Cache-Control", "max-age=0")
// 使用带重试的请求方法发送HTTP请求
resp, err := p.doRequestWithRetry(req, client)
if err != nil {
return nil, fmt.Errorf("[%s] 请求搜索页面失败: %w", p.Name(), err)
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[%s] 请求搜索页面失败,状态码: %d", p.Name(), resp.StatusCode)
}
// 使用goquery解析HTML
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 解析HTML失败: %w", p.Name(), err)
}
// 解析搜索结果列表
articleItems := p.parseArticleList(doc)
if len(articleItems) == 0 {
return []model.SearchResult{}, nil
}
// 并发抓取详情页获取网盘链接
results := p.fetchDetailsWithLinks(articleItems, client, keyword)
// 使用过滤功能过滤结果
filteredResults := plugin.FilterResultsByKeyword(results, keyword)
return filteredResults, nil
}
// ArticleItem 文章基本信息
type ArticleItem struct {
ID string
Title string
DetailURL string
Category string
PublishDate string
ViewCount int
Summary string
ImageURL string
}
// parseArticleList 解析文章列表
func (p *AikanzyAsyncPlugin) parseArticleList(doc *goquery.Document) []ArticleItem {
var items []ArticleItem
// 查找所有文章项
doc.Find("article.post-list.contt.blockimg").Each(func(i int, s *goquery.Selection) {
// 提取详情页链接
detailLink := s.Find("a[href]").First()
detailURL, exists := detailLink.Attr("href")
if !exists || detailURL == "" {
return
}
// 提取文章ID
articleID := p.extractArticleID(detailURL)
if articleID == "" {
return
}
// 提取标题
title := strings.TrimSpace(s.Find("header.entry-header span.entry-title a").Text())
// 移除标题中的HTML标签(如<b>)
title = p.cleanHTMLTags(title)
if title == "" {
return
}
// 提取分类
category := strings.TrimSpace(s.Find("div.entry-meta > a").First().Text())
// 提取发布日期
publishDate := strings.TrimSpace(s.Find("time").First().Text())
// 提取阅读数
metaText := s.Find("div.entry-meta").Text()
viewCount := p.extractViewCount(metaText)
// 提取摘要
summary := strings.TrimSpace(s.Find("div.entry-summary.ss p").Text())
summary = p.cleanHTMLTags(summary)
// 提取缩略图
imageURL, _ := s.Find("img.block-fea").Attr("data-src")
items = append(items, ArticleItem{
ID: articleID,
Title: title,
DetailURL: detailURL,
Category: category,
PublishDate: publishDate,
ViewCount: viewCount,
Summary: summary,
ImageURL: imageURL,
})
})
return items
}
// fetchDetailsWithLinks 并发抓取详情页获取网盘链接
func (p *AikanzyAsyncPlugin) fetchDetailsWithLinks(items []ArticleItem, client *http.Client, keyword string) []model.SearchResult {
// 创建结果通道和等待组
resultChan := make(chan model.SearchResult, len(items))
var wg sync.WaitGroup
// 创建信号量控制并发数
semaphore := make(chan struct{}, detailConcurrency)
// 并发处理每个文章项
for _, item := range items {
wg.Add(1)
go func(item ArticleItem) {
defer wg.Done()
// 获取信号量
semaphore <- struct{}{}
defer func() { <-semaphore }()
// 抓取详情页
links := p.fetchDetailPageLinks(item.DetailURL, client)
// 只有包含链接的结果才添加
if len(links) > 0 {
// 解析发布时间
publishTime := p.parsePublishTime(item.PublishDate)
// 组装内容
var contentParts []string
if item.Summary != "" {
contentParts = append(contentParts, item.Summary)
}
if item.Category != "" {
contentParts = append(contentParts, item.Category)
}
if item.PublishDate != "" {
contentParts = append(contentParts, item.PublishDate)
}
if item.ViewCount > 0 {
contentParts = append(contentParts, fmt.Sprintf("%d阅读", item.ViewCount))
}
content := strings.Join(contentParts, " | ")
// 组装标签
var tags []string
if item.Category != "" {
tags = append(tags, item.Category)
}
result := model.SearchResult{
UniqueID: fmt.Sprintf("aikanzy-%s", item.ID),
Title: item.Title,
Content: content,
Links: links,
Tags: tags,
Channel: "", // 插件搜索结果Channel为空
Datetime: publishTime,
}
resultChan <- result
}
}(item)
}
// 等待所有goroutine完成
go func() {
wg.Wait()
close(resultChan)
}()
// 收集所有结果
var results []model.SearchResult
for result := range resultChan {
results = append(results, result)
}
return results
}
// fetchDetailPageLinks 抓取详情页的网盘链接
func (p *AikanzyAsyncPlugin) fetchDetailPageLinks(detailURL string, client *http.Client) []model.Link {
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), detailTimeout*time.Second)
defer cancel()
// 创建请求
req, err := http.NewRequestWithContext(ctx, "GET", detailURL, nil)
if err != nil {
return nil
}
// 设置请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", "https://www.aikanzy.com/")
req.Header.Set("Upgrade-Insecure-Requests", "1")
// 发送请求(带重试)
resp, err := p.doRequestWithRetry(req, client)
if err != nil {
return nil
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != http.StatusOK {
return nil
}
// 解析HTML
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil
}
// 提取网盘链接
return p.extractNetDiskLinks(doc)
}
// extractNetDiskLinks 从详情页提取网盘链接
func (p *AikanzyAsyncPlugin) extractNetDiskLinks(doc *goquery.Document) []model.Link {
var links []model.Link
foundURLs := make(map[string]bool) // 用于去重
// 方法1: 从<a>标签的href属性提取
doc.Find("a[href*='pan.quark.cn'], a[href*='drive.uc.cn'], a[href*='pan.baidu.com'], a[href*='pan.xunlei.com']").Each(func(i int, s *goquery.Selection) {
href, exists := s.Attr("href")
if !exists || href == "" {
return
}
// 去重
if foundURLs[href] {
return
}
foundURLs[href] = true
// 确定链接类型
linkType := p.determineLinkType(href)
if linkType == "" {
return
}
links = append(links, model.Link{
Type: linkType,
URL: href,
Password: p.extractPassword(href),
})
})
// 方法2: 从页面HTML文本中提取(正则表达式)
if len(links) == 0 {
html, _ := doc.Html()
// 提取夸克网盘链接
quarkLinks := quarkLinkRegex.FindAllString(html, -1)
for _, link := range quarkLinks {
if !foundURLs[link] {
foundURLs[link] = true
links = append(links, model.Link{
Type: "quark",
URL: link,
Password: p.extractPassword(link),
})
}
}
// 提取UC网盘链接
ucLinks := ucLinkRegex.FindAllString(html, -1)
for _, link := range ucLinks {
if !foundURLs[link] {
foundURLs[link] = true
links = append(links, model.Link{
Type: "uc",
URL: link,
Password: p.extractPassword(link),
})
}
}
// 提取百度网盘链接
baiduLinks := baiduLinkRegex.FindAllString(html, -1)
for _, link := range baiduLinks {
if !foundURLs[link] {
foundURLs[link] = true
links = append(links, model.Link{
Type: "baidu",
URL: link,
Password: p.extractPassword(link),
})
}
}
// 提取迅雷网盘链接
xunleiLinks := xunleiLinkRegex.FindAllString(html, -1)
for _, link := range xunleiLinks {
if !foundURLs[link] {
foundURLs[link] = true
links = append(links, model.Link{
Type: "xunlei",
URL: link,
Password: p.extractPassword(link),
})
}
}
}
return links
}
// determineLinkType 根据URL确定链接类型
func (p *AikanzyAsyncPlugin) determineLinkType(urlStr string) string {
lowerURL := strings.ToLower(urlStr)
switch {
case strings.Contains(lowerURL, "pan.quark.cn"):
return "quark"
case strings.Contains(lowerURL, "drive.uc.cn"):
return "uc"
case strings.Contains(lowerURL, "pan.baidu.com"):
return "baidu"
case strings.Contains(lowerURL, "pan.xunlei.com"):
return "xunlei"
default:
return ""
}
}
// extractArticleID 从URL中提取文章ID
func (p *AikanzyAsyncPlugin) extractArticleID(urlStr string) string {
matches := articleIDRegex.FindStringSubmatch(urlStr)
if len(matches) >= 3 {
return matches[2] // 返回数字ID
}
return ""
}
// extractViewCount 提取阅读数
func (p *AikanzyAsyncPlugin) extractViewCount(text string) int {
matches := viewCountRegex.FindStringSubmatch(text)
if len(matches) >= 2 {
var count int
fmt.Sscanf(matches[1], "%d", &count)
return count
}
return 0
}
// cleanHTMLTags 清除HTML标签
func (p *AikanzyAsyncPlugin) cleanHTMLTags(text string) string {
// 移除<b>标签
text = regexp.MustCompile(`<b[^>]*>`).ReplaceAllString(text, "")
text = regexp.MustCompile(`</b>`).ReplaceAllString(text, "")
// 移除其他常见HTML标签
text = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(text, "")
return strings.TrimSpace(text)
}
// parsePublishTime 解析发布时间
func (p *AikanzyAsyncPlugin) parsePublishTime(dateStr string) time.Time {
dateStr = strings.TrimSpace(dateStr)
if dateStr == "" {
return time.Time{}
}
// 尝试多种日期格式
formats := []string{
"2006-01-02",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05+08:00",
"2006-01-02T15:04:05-07:00",
}
for _, format := range formats {
if t, err := time.Parse(format, dateStr); err == nil {
return t
}
}
// 如果以上格式都不匹配,尝试使用time.RFC3339格式(处理<time>标签的datetime属性)
if t, err := time.Parse(time.RFC3339, dateStr); err == nil {
return t
}
return time.Time{}
}
// extractPassword 从网盘链接中提取密码
func (p *AikanzyAsyncPlugin) extractPassword(urlStr string) string {
// 从URL中提取pwd=后面的四位密码(不包含#)
pwdRegex := regexp.MustCompile(`pwd=([^#&]{4})`)
matches := pwdRegex.FindStringSubmatch(urlStr)
if len(matches) >= 2 {
return matches[1]
}
return ""
}
// doRequestWithRetry 发送HTTP请求,带重试机制
func (p *AikanzyAsyncPlugin) doRequestWithRetry(req *http.Request, client *http.Client) (*http.Response, error) {
var resp *http.Response
var err error
for retry := 0; retry <= maxRetries; retry++ {
if retry > 0 {
// 指数退避
backoffTime := time.Duration(1<<uint(retry-1)) * backoffBase * time.Millisecond
time.Sleep(backoffTime)
// 克隆请求
req = req.Clone(req.Context())
}
resp, err = client.Do(req)
if err == nil && resp.StatusCode == 200 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
}
return nil, fmt.Errorf("重试 %d 次后仍然失败: %w", maxRetries, err)
}
================================================
FILE: plugin/alupan/alupan.go
================================================
package alupan
import (
"context"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"pansou/model"
"pansou/plugin"
)
var (
articleIDRegex = regexp.MustCompile(`\?p=(\d+)`)
linkPatterns = []struct {
reg *regexp.Regexp
typ string
}{
{regexp.MustCompile(`https?://pan\.quark\.cn/s/[0-9A-Za-z]+`), "quark"},
{regexp.MustCompile(`https?://www\.aliyundrive\.com/s/[0-9A-Za-z]+`), "aliyun"},
{regexp.MustCompile(`https?://www\.aliyundrive\.com/drive/folder/[0-9A-Za-z]+`), "aliyun"},
}
pwdPatterns = []*regexp.Regexp{
regexp.MustCompile(`提取码[::]?\s*([0-9A-Za-z]+)`),
regexp.MustCompile(`密码[::]?\s*([0-9A-Za-z]+)`),
regexp.MustCompile(`pwd\s*[=::]\s*([0-9A-Za-z]+)`),
regexp.MustCompile(`code\s*[=::]\s*([0-9A-Za-z]+)`),
}
detailCache = sync.Map{}
cacheTTL = 1 * time.Hour
cacheCleanupInterval = 30 * time.Minute
)
type cacheEntry struct {
links []model.Link
expiresAt time.Time
}
const (
pluginName = "alupan"
defaultPriority = 2
searchTimeout = 12 * time.Second
detailTimeout = 10 * time.Second
maxConcurrency = 12
maxIdleConns = 64
maxIdlePerHost = 16
maxConnsPerHost = 32
idleConnLifetime = 90 * time.Second
tlsHandshakeTimeout = 10 * time.Second
expectContinueTimeout = 1 * time.Second
searchMaxRetries = 3
detailMaxRetries = 2
retryBaseDelay = 200 * time.Millisecond
)
// AlupanPlugin 搜索插件
type AlupanPlugin struct {
*plugin.BaseAsyncPlugin
client *http.Client
}
func init() {
plugin.RegisterGlobalPlugin(NewAlupanPlugin())
go startCacheCleaner()
}
// NewAlupanPlugin 创建插件
func NewAlupanPlugin() *AlupanPlugin {
return &AlupanPlugin{
BaseAsyncPlugin: plugin.NewBaseAsyncPlugin(pluginName, defaultPriority),
client: newHTTPClient(),
}
}
// Search 兼容方法
func (p *AlupanPlugin) Search(keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
result, err := p.SearchWithResult(keyword, ext)
if err != nil {
return nil, err
}
return result.Results, nil
}
// SearchWithResult 主搜索方法
func (p *AlupanPlugin) SearchWithResult(keyword string, ext map[string]interface{}) (model.PluginSearchResult, error) {
return p.AsyncSearchWithResult(keyword, p.searchImpl, p.MainCacheKey, ext)
}
func newHTTPClient() *http.Client {
transport := &http.Transport{
MaxIdleConns: maxIdleConns,
MaxIdleConnsPerHost: maxIdlePerHost,
MaxConnsPerHost: maxConnsPerHost,
IdleConnTimeout: idleConnLifetime,
TLSHandshakeTimeout: tlsHandshakeTimeout,
ExpectContinueTimeout: expectContinueTimeout,
ForceAttemptHTTP2: true,
}
return &http.Client{
Transport: transport,
Timeout: searchTimeout,
}
}
func (p *AlupanPlugin) searchImpl(client *http.Client, keyword string, ext map[string]interface{}) ([]model.SearchResult, error) {
if p.client != nil {
client = p.client
}
searchURL := fmt.Sprintf("https://www.aliupan.com/?s=%s", url.QueryEscape(keyword))
ctx, cancel := context.WithTimeout(context.Background(), searchTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
if err != nil {
return nil, fmt.Errorf("[%s] 创建请求失败: %w", p.Name(), err)
}
setCommonHeaders(req, "https://www.aliupan.com/")
resp, err := p.doRequestWithRetry(req, client, searchMaxRetries)
if err != nil {
return nil, fmt.Errorf("[%s] 搜索请求失败: %w", p.Name(), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[%s] 搜索返回状态码: %d", p.Name(), resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("[%s] 解析搜索页面失败: %w", p.Name(), err)
}
var (
results []model.SearchResult
wg sync.WaitGroup
mu sync.Mutex
sem = make(chan struct{}, maxConcurrency)
)
doc.Find("article.excerpt").Each(func(_ int, item *goquery.Selection) {
titleSel := item.Find("header h2 a")
title := strings.TrimSpace(titleSel.Text())
detailURL, ok := titleSel.Attr("href")
if !ok || title == "" || detailURL == "" {
return
}
articleID := extractArticleID(detailURL)
if articleID == "" {
return
}
category := strings.TrimSpace(item.Find("header .label").First().Text())
var tags []string
if category != "" {
tags = append(tags, category)
}
summary := strings.TrimSpace(item.Find("p.note").Text())
timeText := strings.TrimSpace(item.Find("p .icon-time").Parent().Text())
publishTime := parsePublishTime(timeText)
wg.Add(1)
sem <- struct{}{}
go func(title, detailURL, summary string, tags []string, publish time.Time, articleID string) {
defer wg.Done()
defer func() { <-sem }()
links := p.fetchDetailLinks(client, detailURL, articleID)
if len(links) == 0 {
return
}
result := model.SearchResult{
UniqueID: fmt.Sprintf("%s-%s", p.Name(), articleID),
Title: title,
Content: summary,
Links: links,
Tags: tags,
Channel: "",
Datetime: publish,
}
mu.Lock()
results = append(results, result)
mu.Unlock()
}(title, detailURL, summary, tags, publishTime, articleID)
})
wg.Wait()
return plugin.FilterResultsByKeyword(results, keyword), nil
}
func extractArticleID(detailURL string) string {
if matches := articleIDRegex.FindStringSubmatch(detailURL); len(matches) >= 2 {
return matches[1]
}
return ""
}
func parsePublishTime(value string) time.Time {
value = strings.TrimSpace(value)
if value == "" {
return time.Now()
}
if idx := strings.Index(value, "("); idx >= 0 && strings.HasSuffix(value, ")") {
value = value[idx+1 : len(value)-1]
value = strings.TrimSpace(value)
}
layouts := []string{
"2006-01-02",
"2006/01/02",
"2006年01月02日",
time.RFC3339,
}
for _, layout := range layouts {
if t, err := time.Parse(layout, value); err == nil {
return t
}
}
return time.Now()
}
func (p *AlupanPlugin) fetchDetailLinks(client *http.Client, detailURL, articleID string) []model.Link {
if cached, ok := detailCache.Load(articleID); ok {
if entry, valid := cached.(cacheEntry); valid {
if time.Now().Before(entry.expiresAt) && len(entry.links) > 0 {
return entry.links
}
detailCache.Delete(articleID)
}
}
ctx, cancel := context.WithTimeout(context.Background(), detailTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, detailURL, nil)
if err != nil {
return nil
}
setCommonHeaders(req, detailURL)
resp, err := p.doRequestWithRetry(req, client, detailMaxRetries)
if err != nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil
}
links := extractNetDiskLinks(doc)
if len(links) > 0 {
detailCache.Store(articleID, cacheEntry{
links: links,
expiresAt: time.Now().Add(cacheTTL),
})
}
return links
}
func extractNetDiskLinks(doc *goquery.Document) []model.Link {
var (
results []model.Link
seen = make(map[string]struct{})
)
doc.Find(".article-content a[href]").Each(func(_ int, node *goquery.Selection) {
href, exists := node.Attr("href")
if !exists {
return
}
href = strings.TrimSpace(href)
if href == "" {
return
}
linkType, normalized := classifyLink(href)
if linkType == "" {
return
}
if _, found := seen[normalized]; found {
return
}
password := extractPassword(node)
results = append(results, model.Link{
Type: linkType,
URL: normalized,
Password: password,
})
seen[normalized] = struct{}{}
})
return results
}
func classifyLink(raw string) (string, string) {
for _, pattern := range linkPatterns {
if loc := pattern.reg.FindString(raw); loc != "" {
return pattern.typ, loc
}
}
return "", ""
}
func extractPassword(link *goquery.Selection) string {
candidates := []string{
link.Text(),
}
if title, ok := link.Attr("title"); ok {
candidates = append(candidates, title)
}
if parent := link.Parent(); parent != nil && parent.Length() > 0 {
candidates = append(candidates, parent.Text())
if next := parent.Next(); next.Length() > 0 {
candidates = append(candidates, next.Text())
}
}
if next := link.Next(); next.Length() > 0 {
candidates = append(candidates, next.Text())
}
for _, text := range candidates {
if pwd := matchPassword(text); pwd != "" {
return pwd
}
}
return ""
}
func matchPassword(text string) string {
text = strings.TrimSpace(text)
if text == "" {
return ""
}
for _, pattern := range pwdPatterns {
if matches := pattern.FindStringSubmatch(text); len(matches) >= 2 {
return strings.TrimSpace(matches[1])
}
}
return ""
}
func setCommonHeaders(req *http.Request, referer string) {
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", referer)
}
func (p *AlupanPlugin) doRequestWithRetry(req *http.Request, client *http.Client, maxRetries int) (*http.Response, error) {
var lastErr error
for attempt := 0; attempt < maxRetries; attempt++ {
resp, err := client.Do(req.Clone(req.Context()))
if err == nil && resp.StatusCode == http.StatusOK {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
if attempt < maxRetries-1 {
backoff := retryBaseDelay * time.Duration(1<<attempt)
time.Sleep(backoff)
}
}
return nil, fmt.Errorf("重试 %d 次后失败: %w", maxRetries, lastErr)
}
func startCacheCleaner() {
ticker := time.NewTicker(cacheCleanupInterval)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
detailCache.Range(func(key, value interface{}) bool {
entry, ok := value.(cacheEntry)
if !ok || now.After(entry.expiresAt) {
detailCache.Delete(key)
}
return true
})
}
}
================================================
FILE: plugin/alupan/html结构分析.md
================================================
# alupan (阿里U盘) HTML结构分析
## 网站信息
- **站点名称**: 阿里U盘
- **域名**: `www.aliupan.com`
- **类型**: 影视/图书等资源聚合站(WordPress D8 主题)
- **特点**: 搜索结果页按文章列表展示,详情页正文直接给出阿里云盘/夸克网盘链接,文章数量大、分类细
## 搜索/列表页
### 1. 请求入口
```
https://www.aliupan.com/?s={关键词}
```
- 关键词直接 UTF-8;无需额外参数
- 返回 WordPress 搜索结果页(带 `archive-header`)
### 2. 结果容器
- 外层:`section.container > .content-wrap > .content`
- 列表项:`article.excerpt`(常见类名 `excerpt-titletype`)
### 3. 单条记录
```html
<article class="excerpt excerpt-titletype">
<div class="focus">
<a href="https://www.aliupan.com/?p=7078" class="thumbnail">
<img src="..." alt="[阿里云盘][夸克网盘]《遮天》(2023年)" />
</a>
</div>
<header>
<a class="label label-important" href="https://www.aliupan.com/?cat=19">中国内地电视剧<i class="label-arrow"></i></a>
<h2>
<a href="https://www.aliupan.com/?p=7078" title="...">[阿里云盘][夸克网盘]《遮天》(2023年)</a>
</h2>
</header>
<p>
<span class="muted"><i class="icon-user"></i><a href="...">阿里U盘</a></span>
<span class="muted"><i class="icon-time"></i> 1年前 (2024-07-27)</span>
<span class="muted"><i class="icon-eye-open"></i> 745浏览</span>
<span class="muted"><i class="icon-comment"></i><a href="...">0评论</a></span>
</p>
<p class="note">……摘要文本……</p>
</article>
```
#### 需要提取的字段
- **标题**: `h2 a` 文本
- **详情链接**: `h2 a[href]`
- **分类**: `.label.label-important` 文本(可作为 `Tags` 之一)
- **发布日期**: `p > span .icon-time` 所在 `<span>`,格式通常为 `1年前 (2024-07-27)`;取括号内日期
- **摘要**: `p.note`
- **封面**: `div.focus img[src]`(仅用于调试,不需要在结果中返回)
### 4. 分页
- 搜索页默认返回全部匹配列表,可根据需要继续解析分页链接(一般抓取第一页即可)。
## 详情页
### 1. URL 规则
```
https://www.aliupan.com/?p={文章ID}
```
- `文章ID` 来自列表页 URL,可直接作为唯一标识。
### 2. 主体定位
- 标题:`.article-header .article-title a`
- 元信息:`.meta`(含分类、作者、时间、阅读)
- 正文:`article.article-content`
### 3. 下载链接形态
正文中使用普通段落给出下载地址:
```html
<p>阿里云盘丨遮天:<a href="https://www.aliyundrive.com/s/xxxx" target="_blank" rel="nofollow">https://www.aliyundrive.com/s/xxxx</a></p>
<p>夸克网盘丨遮天:<a href="https://pan.quark.cn/s/5ad996dc0725" target="_blank" rel="noreferrer noopener nofollow">https://pan.quark.cn/s/5ad996dc0725</a></p>
```
- 个别文章会出现“待补”等文字;只返回真正包含链接的 `<a>`。
- 可能同文提供多个链接(夸克 / 阿里云盘 / 其他),需要全部收集。
- 提取码通常写在同一段落文本里,形如 `提取码:xxxx`、`密码:xxxx` 等。
### 4. 支持的网盘域名
- **阿里云盘**: `https://www.al
gitextract_lptfo9vf/
├── .dockerignore
├── .github/
│ └── workflows/
│ └── docker_ci.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── api/
│ ├── auth_handler.go
│ ├── filter.go
│ ├── handler.go
│ ├── middleware.go
│ └── router.go
├── config/
│ └── config.go
├── docker-compose.yml
├── docs/
│ ├── MCP-SERVICE.md
│ ├── 插件开发指南.md
│ └── 系统开发设计文档.md
├── go.mod
├── go.sum
├── main.go
├── mcp-config.json
├── model/
│ ├── plugin_result.go
│ ├── request.go
│ └── response.go
├── package.json
├── plugin/
│ ├── ahhhhfs/
│ │ ├── ahhhhfs.go
│ │ └── html结构分析.md
│ ├── aikanzy/
│ │ └── aikanzy.go
│ ├── alupan/
│ │ ├── alupan.go
│ │ └── html结构分析.md
│ ├── ash/
│ │ ├── ash.go
│ │ └── html结构分析.md
│ ├── bixin/
│ │ ├── bixin.go
│ │ └── json结构分析.md
│ ├── cldi/
│ │ ├── cldi.go
│ │ └── html结构分析.md
│ ├── clmao/
│ │ ├── clmao.go
│ │ └── html结构分析.md
│ ├── clxiong/
│ │ ├── clxiong.go
│ │ └── html结构分析.md
│ ├── cyg/
│ │ └── cyg.go
│ ├── daishudj/
│ │ ├── daishudj.go
│ │ └── html结构分析.md
│ ├── ddys/
│ │ ├── ddys.go
│ │ └── html结构分析.md
│ ├── discourse/
│ │ ├── discourse.go
│ │ └── json结构分析.md
│ ├── djgou/
│ │ └── djgou.go
│ ├── duoduo/
│ │ ├── duoduo.go
│ │ └── html结构分析.md
│ ├── dyyj/
│ │ ├── dyyj.go
│ │ └── html结构分析.md
│ ├── erxiao/
│ │ ├── erxiao.go
│ │ └── html结构分析.md
│ ├── feikuai/
│ │ ├── feikuai.go
│ │ ├── html结构分析.md
│ │ └── json结构分析.md
│ ├── fox4k/
│ │ ├── fox4k.go
│ │ └── html结构分析.md
│ ├── gying/
│ │ ├── README.md
│ │ ├── gying.go
│ │ └── html结构分析.md
│ ├── haisou/
│ │ ├── haisou.go
│ │ └── json结构分析.md
│ ├── hdmoli/
│ │ ├── hdmoli.go
│ │ └── html结构分析.md
│ ├── hdr4k/
│ │ ├── hdr4k.go
│ │ ├── html结构分析.md
│ │ └── 设计文档.md
│ ├── huban/
│ │ ├── html结构分析.md
│ │ ├── huban.go
│ │ └── json结构分析.md
│ ├── hunhepan/
│ │ └── hunhepan.go
│ ├── javdb/
│ │ └── javdb.go
│ ├── jikepan/
│ │ └── jikepan.go
│ ├── jsnoteclub/
│ │ └── jsnoteclub.go
│ ├── jutoushe/
│ │ ├── html结构分析.md
│ │ └── jutoushe.go
│ ├── kkmao/
│ │ ├── html结构分析.md
│ │ └── kkmao.go
│ ├── kkv/
│ │ ├── html结构分析.md
│ │ └── kkv.go
│ ├── labi/
│ │ ├── html结构分析.md
│ │ └── labi.go
│ ├── leijing/
│ │ ├── html结构分析.md
│ │ └── leijing.go
│ ├── libvio/
│ │ ├── html结构分析.md
│ │ └── libvio.go
│ ├── lou1/
│ │ └── lou1.go
│ ├── meitizy/
│ │ ├── json结构分析.md
│ │ └── meitizy.go
│ ├── miaoso/
│ │ ├── json结构分析.md
│ │ └── miaoso.go
│ ├── mikuclub/
│ │ ├── html结构分析.md
│ │ ├── json结构分析.md
│ │ └── mikuclub.go
│ ├── mizixing/
│ │ └── mizixing.go
│ ├── muou/
│ │ ├── html结构分析.md
│ │ └── muou.go
│ ├── nsgame/
│ │ ├── json结构分析.md
│ │ └── nsgame.go
│ ├── nyaa/
│ │ ├── html结构分析.md
│ │ └── nyaa.go
│ ├── ouge/
│ │ ├── json结构分析.md
│ │ └── ouge.go
│ ├── pan666/
│ │ └── pan666.go
│ ├── pansearch/
│ │ └── pansearch.go
│ ├── panta/
│ │ ├── panta.go
│ │ └── panta插件设计文档.md
│ ├── panwiki/
│ │ └── panwiki.go
│ ├── panyq/
│ │ └── panyq.go
│ ├── pianku/
│ │ ├── html结构分析.md
│ │ └── pianku.go
│ ├── plugin.go
│ ├── qingying/
│ │ ├── html结构分析.md
│ │ └── qingying.go
│ ├── qqpd/
│ │ ├── README.md
│ │ └── qqpd.go
│ ├── quark4k/
│ │ ├── json结构分析.md
│ │ └── quark4k.go
│ ├── quarksoo/
│ │ ├── html结构分析.md
│ │ └── quarksoo.go
│ ├── qupanshe/
│ │ └── qupanshe.go
│ ├── qupansou/
│ │ └── qupansou.go
│ ├── sdso/
│ │ └── sdso.go
│ ├── shandian/
│ │ ├── html结构分析.md
│ │ └── shandian.go
│ ├── sousou/
│ │ ├── json结构分析.md
│ │ └── sousou.go
│ ├── susu/
│ │ ├── html结构分析.md
│ │ ├── susu.go
│ │ └── susu插件设计文档.md
│ ├── thepiratebay/
│ │ ├── html结构分析.md
│ │ └── thepiratebay.go
│ ├── u3c3/
│ │ └── u3c3.go
│ ├── wanou/
│ │ ├── json结构分析.md
│ │ └── wanou.go
│ ├── weibo/
│ │ ├── README.md
│ │ ├── weibo.go
│ │ └── 微博用户搜索API文档.md
│ ├── wuji/
│ │ ├── html结构分析.md
│ │ └── wuji.go
│ ├── xb6v/
│ │ └── xb6v.go
│ ├── xdpan/
│ │ ├── html结构分析.md
│ │ └── xdpan.go
│ ├── xdyh/
│ │ ├── json结构分析.md
│ │ └── xdyh.go
│ ├── xiaoji/
│ │ ├── html结构分析.md
│ │ └── xiaoji.go
│ ├── xiaozhang/
│ │ ├── html结构分析.md
│ │ └── xiaozhang.go
│ ├── xinjuc/
│ │ ├── html结构分析.md
│ │ └── xinjuc.go
│ ├── xuexizhinan/
│ │ ├── html结构分析.md
│ │ └── xuexizhinan.go
│ ├── xys/
│ │ ├── html结构分析.md
│ │ └── xys.go
│ ├── yiove/
│ │ └── yiove.go
│ ├── ypfxw/
│ │ ├── html结构分析.md
│ │ └── ypfxw.go
│ ├── yuhuage/
│ │ └── yuhuage.go
│ ├── yunsou/
│ │ ├── html结构分析.md
│ │ └── yunsou.go
│ ├── zhizhen/
│ │ ├── json结构分析.md
│ │ └── zhizhen.go
│ └── zxzj/
│ ├── html结构分析.md
│ └── zxzj.go
├── service/
│ ├── cache_integration.go
│ └── search_service.go
├── typescript/
│ ├── package.json
│ ├── src/
│ │ ├── index.ts
│ │ ├── tools/
│ │ │ ├── health.ts
│ │ │ ├── search.ts
│ │ │ └── start-backend.ts
│ │ └── utils/
│ │ ├── activity-monitor.ts
│ │ ├── backend-manager.ts
│ │ ├── config.ts
│ │ ├── http-client.ts
│ │ └── validators.ts
│ └── tsconfig.json
└── util/
├── cache/
│ ├── cache_key.go
│ ├── delayed_batch_write_manager.go
│ ├── disk_cache.go
│ ├── enhanced_two_level_cache.go
│ ├── global_buffer_manager.go
│ ├── memory_cache.go
│ ├── serializer.go
│ ├── sharded_disk_cache.go
│ ├── sharded_memory_cache.go
│ └── utils.go
├── compression.go
├── convert.go
├── http_util.go
├── json/
│ └── json.go
├── jwt.go
├── parser_util.go
├── pool/
│ ├── object_pool.go
│ └── worker_pool.go
└── regex_util.go
Showing preview only (229K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2558 symbols across 118 files)
FILE: api/auth_handler.go
type LoginRequest (line 12) | type LoginRequest struct
type LoginResponse (line 18) | type LoginResponse struct
function LoginHandler (line 25) | func LoginHandler(c *gin.Context) {
function VerifyHandler (line 72) | func VerifyHandler(c *gin.Context) {
function LogoutHandler (line 96) | func LogoutHandler(c *gin.Context) {
FILE: api/filter.go
function applyResultFilter (line 9) | func applyResultFilter(response model.SearchResponse, filter *model.Filt...
function filterMergedByType (line 51) | func filterMergedByType(mergedLinks model.MergedLinks, includeKeywords, ...
function filterResults (line 77) | func filterResults(results []model.SearchResult, includeKeywords, exclud...
function matchFilter (line 115) | func matchFilter(text string, includeKeywords, excludeKeywords []string)...
FILE: api/handler.go
function SetSearchService (line 21) | func SetSearchService(service *service.SearchService) {
function SearchHandler (line 26) | func SearchHandler(c *gin.Context) {
FILE: api/middleware.go
function CORSMiddleware (line 15) | func CORSMiddleware() gin.HandlerFunc {
function LoggerMiddleware (line 31) | func LoggerMiddleware() gin.HandlerFunc {
function AuthMiddleware (line 78) | func AuthMiddleware() gin.HandlerFunc {
FILE: api/router.go
function SetupRouter (line 12) | func SetupRouter(searchService *service.SearchService) *gin.Engine {
FILE: config/config.go
type Config (line 14) | type Config struct
function Init (line 62) | func Init() {
function getDefaultChannels (line 116) | func getDefaultChannels() []string {
function getDefaultConcurrency (line 125) | func getDefaultConcurrency() int {
function UpdateDefaultConcurrency (line 164) | func UpdateDefaultConcurrency(pluginCount int) {
function getPort (line 189) | func getPort() string {
function getProxyURL (line 197) | func getProxyURL() string {
function getHTTPProxyURL (line 201) | func getHTTPProxyURL() string {
function getHTTPSProxyURL (line 208) | func getHTTPSProxyURL() string {
function getCacheEnabled (line 216) | func getCacheEnabled() bool {
function getCachePath (line 225) | func getCachePath() string {
function getCacheMaxSize (line 239) | func getCacheMaxSize() int {
function getCacheTTL (line 252) | func getCacheTTL() int {
function getEnableCompression (line 265) | func getEnableCompression() bool {
function getMinSizeToCompress (line 274) | func getMinSizeToCompress() int {
function getGCPercent (line 287) | func getGCPercent() int {
function getOptimizeMemory (line 300) | func getOptimizeMemory() bool {
function getPluginTimeout (line 309) | func getPluginTimeout() int {
function getAsyncPluginEnabled (line 322) | func getAsyncPluginEnabled() bool {
function getEnabledPlugins (line 334) | func getEnabledPlugins() []string {
function getAsyncResponseTimeout (line 359) | func getAsyncResponseTimeout() int {
function getAsyncMaxBackgroundWorkers (line 372) | func getAsyncMaxBackgroundWorkers() int {
function getAsyncMaxBackgroundTasks (line 395) | func getAsyncMaxBackgroundTasks() int {
function getAsyncCacheTTLHours (line 417) | func getAsyncCacheTTLHours() int {
function getHTTPReadTimeout (line 430) | func getHTTPReadTimeout() time.Duration {
function getHTTPWriteTimeout (line 456) | func getHTTPWriteTimeout() time.Duration {
function getHTTPIdleTimeout (line 482) | func getHTTPIdleTimeout() time.Duration {
function getHTTPMaxConns (line 496) | func getHTTPMaxConns() int {
function getAsyncLogEnabled (line 519) | func getAsyncLogEnabled() bool {
function getAuthEnabled (line 532) | func getAuthEnabled() bool {
function getAuthUsers (line 538) | func getAuthUsers() map[string]string {
function getAuthTokenExpiry (line 560) | func getAuthTokenExpiry() time.Duration {
function getAuthJWTSecret (line 573) | func getAuthJWTSecret() string {
function applyGCSettings (line 589) | func applyGCSettings() {
FILE: main.go
function main (line 110) | func main() {
function initApp (line 119) | func initApp() {
function startServer (line 154) | func startServer() {
function printServiceInfo (line 254) | func printServiceInfo(port string, pluginManager *plugin.PluginManager) {
FILE: model/plugin_result.go
type PluginSearchResult (line 8) | type PluginSearchResult struct
method IsEmpty (line 17) | func (p *PluginSearchResult) IsEmpty() bool {
method Count (line 22) | func (p *PluginSearchResult) Count() int {
method GetResults (line 27) | func (p *PluginSearchResult) GetResults() []SearchResult {
FILE: model/request.go
type FilterConfig (line 4) | type FilterConfig struct
type SearchRequest (line 10) | type SearchRequest struct
FILE: model/response.go
type Link (line 6) | type Link struct
type SearchResult (line 15) | type SearchResult struct
type MergedLink (line 28) | type MergedLink struct
type MergedLinks (line 38) | type MergedLinks
type SearchResponse (line 41) | type SearchResponse struct
type Response (line 48) | type Response struct
function NewSuccessResponse (line 55) | func NewSuccessResponse(data interface{}) Response {
function NewErrorResponse (line 64) | func NewErrorResponse(code int, message string) Response {
FILE: plugin/ahhhhfs/ahhhhfs.go
constant pluginName (line 51) | pluginName = "ahhhhfs"
constant defaultPriority (line 54) | defaultPriority = 2
constant DefaultTimeout (line 57) | DefaultTimeout = 10 * time.Second
constant DetailTimeout (line 58) | DetailTimeout = 8 * time.Second
constant MaxConcurrency (line 61) | MaxConcurrency = 15
constant MaxIdleConns (line 64) | MaxIdleConns = 100
constant MaxIdleConnsPerHost (line 65) | MaxIdleConnsPerHost = 30
constant MaxConnsPerHost (line 66) | MaxConnsPerHost = 50
constant IdleConnTimeout (line 67) | IdleConnTimeout = 90 * time.Second
type AhhhhfsAsyncPlugin (line 79) | type AhhhhfsAsyncPlugin struct
method Search (line 129) | func (p *AhhhhfsAsyncPlugin) Search(keyword string, ext map[string]int...
method SearchWithResult (line 138) | func (p *AhhhhfsAsyncPlugin) SearchWithResult(keyword string, ext map[...
method searchImpl (line 143) | func (p *AhhhhfsAsyncPlugin) searchImpl(client *http.Client, keyword s...
method extractArticleID (line 283) | func (p *AhhhhfsAsyncPlugin) extractArticleID(detailURL string) string {
method parseDateTime (line 292) | func (p *AhhhhfsAsyncPlugin) parseDateTime(datetime string) time.Time {
method fetchDetailLinks (line 337) | func (p *AhhhhfsAsyncPlugin) fetchDetailLinks(client *http.Client, det...
method extractNetDiskLinks (line 397) | func (p *AhhhhfsAsyncPlugin) extractNetDiskLinks(doc *goquery.Document...
method determineCloudType (line 433) | func (p *AhhhhfsAsyncPlugin) determineCloudType(url string) string {
method extractPassword (line 459) | func (p *AhhhhfsAsyncPlugin) extractPassword(linkElem *goquery.Selecti...
method doRequestWithRetry (line 510) | func (p *AhhhhfsAsyncPlugin) doRequestWithRetry(req *http.Request, cli...
function init (line 85) | func init() {
function startCacheCleaner (line 93) | func startCacheCleaner() {
function createOptimizedHTTPClient (line 105) | func createOptimizedHTTPClient() *http.Client {
function NewAhhhhfsPlugin (line 121) | func NewAhhhhfsPlugin() *AhhhhfsAsyncPlugin {
FILE: plugin/aikanzy/aikanzy.go
constant pluginName (line 42) | pluginName = "aikanzy"
constant searchURLTemplate (line 45) | searchURLTemplate = "https://www.aikanzy.com/search?word=%s&molds=article"
constant defaultPriority (line 48) | defaultPriority = 3
constant defaultTimeout (line 51) | defaultTimeout = 15
constant detailTimeout (line 54) | detailTimeout = 8
constant maxRetries (line 57) | maxRetries = 3
constant detailConcurrency (line 60) | detailConcurrency = 15
constant backoffBase (line 63) | backoffBase = 200
type AikanzyAsyncPlugin (line 67) | type AikanzyAsyncPlugin struct
method Name (line 104) | func (p *AikanzyAsyncPlugin) Name() string {
method Priority (line 109) | func (p *AikanzyAsyncPlugin) Priority() int {
method Search (line 114) | func (p *AikanzyAsyncPlugin) Search(keyword string, ext map[string]int...
method SearchWithResult (line 123) | func (p *AikanzyAsyncPlugin) SearchWithResult(keyword string, ext map[...
method doSearch (line 128) | func (p *AikanzyAsyncPlugin) doSearch(client *http.Client, keyword str...
method parseArticleList (line 205) | func (p *AikanzyAsyncPlugin) parseArticleList(doc *goquery.Document) [...
method fetchDetailsWithLinks (line 264) | func (p *AikanzyAsyncPlugin) fetchDetailsWithLinks(items []ArticleItem...
method fetchDetailPageLinks (line 344) | func (p *AikanzyAsyncPlugin) fetchDetailPageLinks(detailURL string, cl...
method extractNetDiskLinks (line 386) | func (p *AikanzyAsyncPlugin) extractNetDiskLinks(doc *goquery.Document...
method determineLinkType (line 477) | func (p *AikanzyAsyncPlugin) determineLinkType(urlStr string) string {
method extractArticleID (line 495) | func (p *AikanzyAsyncPlugin) extractArticleID(urlStr string) string {
method extractViewCount (line 504) | func (p *AikanzyAsyncPlugin) extractViewCount(text string) int {
method cleanHTMLTags (line 515) | func (p *AikanzyAsyncPlugin) cleanHTMLTags(text string) string {
method parsePublishTime (line 527) | func (p *AikanzyAsyncPlugin) parsePublishTime(dateStr string) time.Time {
method extractPassword (line 557) | func (p *AikanzyAsyncPlugin) extractPassword(urlStr string) string {
method doRequestWithRetry (line 568) | func (p *AikanzyAsyncPlugin) doRequestWithRetry(req *http.Request, cli...
function init (line 76) | func init() {
function createOptimizedHTTPClient (line 81) | func createOptimizedHTTPClient() *http.Client {
function NewAikanzyAsyncPlugin (line 96) | func NewAikanzyAsyncPlugin() *AikanzyAsyncPlugin {
type ArticleItem (line 193) | type ArticleItem struct
FILE: plugin/alupan/alupan.go
type cacheEntry (line 43) | type cacheEntry struct
constant pluginName (line 49) | pluginName = "alupan"
constant defaultPriority (line 50) | defaultPriority = 2
constant searchTimeout (line 51) | searchTimeout = 12 * time.Second
constant detailTimeout (line 52) | detailTimeout = 10 * time.Second
constant maxConcurrency (line 53) | maxConcurrency = 12
constant maxIdleConns (line 54) | maxIdleConns = 64
constant maxIdlePerHost (line 55) | maxIdlePerHost = 16
constant maxConnsPerHost (line 56) | maxConnsPerHost = 32
constant idleConnLifetime (line 57) | idleConnLifetime = 90 * time.Second
constant tlsHandshakeTimeout (line 58) | tlsHandshakeTimeout = 10 * time.Second
constant expectContinueTimeout (line 59) | expectContinueTimeout = 1 * time.Second
constant searchMaxRetries (line 61) | searchMaxRetries = 3
constant detailMaxRetries (line 62) | detailMaxRetries = 2
constant retryBaseDelay (line 63) | retryBaseDelay = 200 * time.Millisecond
type AlupanPlugin (line 67) | type AlupanPlugin struct
method Search (line 86) | func (p *AlupanPlugin) Search(keyword string, ext map[string]interface...
method SearchWithResult (line 95) | func (p *AlupanPlugin) SearchWithResult(keyword string, ext map[string...
method searchImpl (line 115) | func (p *AlupanPlugin) searchImpl(client *http.Client, keyword string,...
method fetchDetailLinks (line 242) | func (p *AlupanPlugin) fetchDetailLinks(client *http.Client, detailURL...
method doRequestWithRetry (line 383) | func (p *AlupanPlugin) doRequestWithRetry(req *http.Request, client *h...
function init (line 72) | func init() {
function NewAlupanPlugin (line 78) | func NewAlupanPlugin() *AlupanPlugin {
function newHTTPClient (line 99) | func newHTTPClient() *http.Client {
function extractArticleID (line 208) | func extractArticleID(detailURL string) string {
function parsePublishTime (line 215) | func parsePublishTime(value string) time.Time {
function extractNetDiskLinks (line 286) | func extractNetDiskLinks(doc *goquery.Document) []model.Link {
function classifyLink (line 324) | func classifyLink(raw string) (string, string) {
function extractPassword (line 333) | func extractPassword(link *goquery.Selection) string {
function matchPassword (line 361) | func matchPassword(text string) string {
function setCommonHeaders (line 375) | func setCommonHeaders(req *http.Request, referer string) {
function startCacheCleaner (line 404) | func startCacheCleaner() {
FILE: plugin/ash/ash.go
type AshPlugin (line 18) | type AshPlugin struct
method Search (line 59) | func (p *AshPlugin) Search(keyword string, ext map[string]interface{})...
method SearchWithResult (line 68) | func (p *AshPlugin) SearchWithResult(keyword string, ext map[string]in...
method searchImpl (line 73) | func (p *AshPlugin) searchImpl(client *http.Client, keyword string, ex...
method extractResultsFromBytes (line 123) | func (p *AshPlugin) extractResultsFromBytes(data []byte) ([]model.Sear...
method fixPanURL (line 233) | func (p *AshPlugin) fixPanURL(url string) string {
method setRequestHeaders (line 253) | func (p *AshPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 264) | func (p *AshPlugin) doRequestWithRetry(req *http.Request, client *http...
constant wrongQuarkDomain (line 24) | wrongQuarkDomain = "pan.qualk.cn"
constant correctQuarkDomain (line 26) | correctQuarkDomain = "pan.quark.cn"
type AshResult (line 38) | type AshResult struct
function init (line 51) | func init() {
FILE: plugin/bixin/bixin.go
function init (line 20) | func init() {
constant BaseURL (line 27) | BaseURL = "https://www.bixbiy.com/api/discussions"
constant PageSize (line 30) | PageSize = 50
constant MaxRetries (line 31) | MaxRetries = 2
type BixinAsyncPlugin (line 45) | type BixinAsyncPlugin struct
method Search (line 59) | func (p *BixinAsyncPlugin) Search(keyword string, ext map[string]inter...
method SearchWithResult (line 68) | func (p *BixinAsyncPlugin) SearchWithResult(keyword string, ext map[st...
method doSearch (line 73) | func (p *BixinAsyncPlugin) doSearch(client *http.Client, keyword strin...
method fetchBatch (line 93) | func (p *BixinAsyncPlugin) fetchBatch(client *http.Client, keyword str...
method deduplicateResults (line 157) | func (p *BixinAsyncPlugin) deduplicateResults(results []model.SearchRe...
method fetchPage (line 177) | func (p *BixinAsyncPlugin) fetchPage(client *http.Client, keyword stri...
function NewBixinAsyncPlugin (line 51) | func NewBixinAsyncPlugin() *BixinAsyncPlugin {
function generateRandomIP (line 302) | func generateRandomIP() string {
function getRandomUA (line 311) | func getRandomUA() string {
function cleanHTML (line 316) | func cleanHTML(html string) string {
function extractMobileLinksFromText (line 365) | func extractMobileLinksFromText(content string) []model.Link {
function extractURLFromText (line 478) | func extractURLFromText(text string) string {
function extractPasswordFromURL (line 510) | func extractPasswordFromURL(url string) string {
function abs (line 538) | func abs(n int) int {
type BixinResponse (line 546) | type BixinResponse struct
type BixinDiscussion (line 556) | type BixinDiscussion struct
type BixinPost (line 579) | type BixinPost struct
FILE: plugin/cldi/cldi.go
type CldiPlugin (line 19) | type CldiPlugin struct
method Search (line 50) | func (p *CldiPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 59) | func (p *CldiPlugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 64) | func (p *CldiPlugin) searchImpl(client *http.Client, keyword string, e...
method searchPage (line 122) | func (p *CldiPlugin) searchPage(client *http.Client, keyword string, p...
method setRequestHeaders (line 168) | func (p *CldiPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 179) | func (p *CldiPlugin) doRequestWithRetry(req *http.Request, client *htt...
method extractSearchResults (line 208) | func (p *CldiPlugin) extractSearchResults(doc *goquery.Document) []mod...
method parseSearchResult (line 223) | func (p *CldiPlugin) parseSearchResult(s *goquery.Selection) model.Sea...
method extractMagnetInfo (line 256) | func (p *CldiPlugin) extractMagnetInfo(s *goquery.Selection, result *m...
method extractFileList (line 283) | func (p *CldiPlugin) extractFileList(s *goquery.Selection, result *mod...
method mapCategory (line 312) | func (p *CldiPlugin) mapCategory(category string) string {
method cleanTitle (line 337) | func (p *CldiPlugin) cleanTitle(title string) string {
constant MaxConcurrency (line 25) | MaxConcurrency = 10
constant MaxPages (line 28) | MaxPages = 5
function init (line 42) | func init() {
FILE: plugin/clmao/clmao.go
constant BaseURL (line 22) | BaseURL = "https://www.8800492.xyz"
constant SearchURL (line 25) | SearchURL = BaseURL + "/search-%s-0-2-%d.html"
constant MaxRetries (line 28) | MaxRetries = 3
constant TimeoutSeconds (line 29) | TimeoutSeconds = 30
constant MaxConcurrency (line 32) | MaxConcurrency = 10
constant MaxPages (line 33) | MaxPages = 5
type ClmaoPlugin (line 56) | type ClmaoPlugin struct
method Name (line 68) | func (p *ClmaoPlugin) Name() string {
method DisplayName (line 73) | func (p *ClmaoPlugin) DisplayName() string {
method Description (line 78) | func (p *ClmaoPlugin) Description() string {
method Search (line 83) | func (p *ClmaoPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 92) | func (p *ClmaoPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 97) | func (p *ClmaoPlugin) searchImpl(client *http.Client, keyword string, ...
method searchPage (line 162) | func (p *ClmaoPlugin) searchPage(client *http.Client, keyword string, ...
method extractSearchResults (line 209) | func (p *ClmaoPlugin) extractSearchResults(doc *goquery.Document) []mo...
method parseSearchResult (line 224) | func (p *ClmaoPlugin) parseSearchResult(s *goquery.Selection) model.Se...
method extractMagnetInfo (line 255) | func (p *ClmaoPlugin) extractMagnetInfo(s *goquery.Selection, result *...
method extractFileList (line 289) | func (p *ClmaoPlugin) extractFileList(s *goquery.Selection, result *mo...
method mapCategory (line 310) | func (p *ClmaoPlugin) mapCategory(category string) string {
method cleanTitle (line 332) | func (p *ClmaoPlugin) cleanTitle(title string) string {
method setRequestHeaders (line 343) | func (p *ClmaoPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 358) | func (p *ClmaoPlugin) doRequestWithRetry(req *http.Request, client *ht...
function NewClmaoPlugin (line 61) | func NewClmaoPlugin() *ClmaoPlugin {
function init (line 379) | func init() {
FILE: plugin/clxiong/clxiong.go
constant BaseURL (line 20) | BaseURL = "https://www.cilixiong.org"
constant SearchURL (line 21) | SearchURL = "https://www.cilixiong.org/e/search/index.php"
constant UserAgent (line 22) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWe...
constant MaxRetries (line 23) | MaxRetries = 3
constant RetryDelay (line 24) | RetryDelay = 2 * time.Second
constant MaxResults (line 25) | MaxResults = 30
type DetailPageInfo (line 29) | type DetailPageInfo struct
type ClxiongPlugin (line 37) | type ClxiongPlugin struct
method Search (line 51) | func (p *ClxiongPlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 60) | func (p *ClxiongPlugin) SearchWithResult(keyword string, ext map[strin...
method getSearchID (line 103) | func (p *ClxiongPlugin) getSearchID(keyword string) (string, error) {
method extractSearchIDFromLocation (line 180) | func (p *ClxiongPlugin) extractSearchIDFromLocation(location string) s...
method getSearchResults (line 191) | func (p *ClxiongPlugin) getSearchResults(searchID, keyword string) ([]...
method parseSearchResults (line 245) | func (p *ClxiongPlugin) parseSearchResults(html string) ([]model.Searc...
method extractImageFromStyle (line 334) | func (p *ClxiongPlugin) extractImageFromStyle(style string) string {
method fetchDetailLinksSync (line 345) | func (p *ClxiongPlugin) fetchDetailLinksSync(results []model.SearchRes...
method extractDetailURLFromContent (line 449) | func (p *ClxiongPlugin) extractDetailURLFromContent(content string) st...
method fetchDetailPageInfo (line 460) | func (p *ClxiongPlugin) fetchDetailPageInfo(detailURL string, movieTit...
method parseDetailPageInfo (line 507) | func (p *ClxiongPlugin) parseDetailPageInfo(html string, movieTitle st...
method parseUpdateTimeFromDetail (line 537) | func (p *ClxiongPlugin) parseUpdateTimeFromDetail(doc *goquery.Documen...
method parseMagnetLinksFromDetailDoc (line 576) | func (p *ClxiongPlugin) parseMagnetLinksFromDetailDoc(doc *goquery.Doc...
method generateUniqueID (line 628) | func (p *ClxiongPlugin) generateUniqueID(detailPath string) string {
function init (line 42) | func init() {
FILE: plugin/cyg/cyg.go
type CygPlugin (line 41) | type CygPlugin struct
method Search (line 88) | func (p *CygPlugin) Search(keyword string, ext map[string]interface{})...
method SearchWithResult (line 97) | func (p *CygPlugin) SearchWithResult(keyword string, ext map[string]in...
method searchImpl (line 102) | func (p *CygPlugin) searchImpl(client *http.Client, keyword string, ex...
method fetchSearchResults (line 130) | func (p *CygPlugin) fetchSearchResults(client *http.Client, searchURL ...
method fetchDownloadLinksAsync (line 171) | func (p *CygPlugin) fetchDownloadLinksAsync(client *http.Client, posts...
method getDownloadLinks (line 218) | func (p *CygPlugin) getDownloadLinks(client *http.Client, postID int) ...
method convertToSearchResult (line 263) | func (p *CygPlugin) convertToSearchResult(post CygPost, links []model....
method convertToLinks (line 276) | func (p *CygPlugin) convertToLinks(downloadData []CygDownload) []model...
method determineCloudTypeByURL (line 296) | func (p *CygPlugin) determineCloudTypeByURL(url string) string {
method determineCloudType (line 328) | func (p *CygPlugin) determineCloudType(name string) string {
method setRequestHeaders (line 360) | func (p *CygPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 369) | func (p *CygPlugin) doRequestWithRetry(req *http.Request, client *http...
method parseExtOptions (line 398) | func (p *CygPlugin) parseExtOptions(ext map[string]interface{}) CygSea...
method cleanHTML (line 430) | func (p *CygPlugin) cleanHTML(htmlContent string) string {
method parseDateTime (line 447) | func (p *CygPlugin) parseDateTime(dateStr string) time.Time {
type CygPost (line 46) | type CygPost struct
type CygDownload (line 63) | type CygDownload struct
type CygSearchOptions (line 72) | type CygSearchOptions struct
function init (line 80) | func init() {
FILE: plugin/daishudj/daishudj.go
type cacheEntry (line 50) | type cacheEntry struct
constant pluginName (line 56) | pluginName = "daishudj"
constant defaultPriority (line 57) | defaultPriority = 3
constant searchTimeout (line 58) | searchTimeout = 10 * time.Second
constant detailTimeout (line 59) | detailTimeout = 8 * time.Second
constant maxConcurrency (line 60) | maxConcurrency = 10
constant maxIdleConns (line 61) | maxIdleConns = 64
constant maxIdlePerHost (line 62) | maxIdlePerHost = 16
constant maxConnsPerHost (line 63) | maxConnsPerHost = 32
constant idleConnLifetime (line 64) | idleConnLifetime = 90 * time.Second
constant tlsHandshakeTimeout (line 65) | tlsHandshakeTimeout = 10 * time.Second
constant expectContinueTimeout (line 66) | expectContinueTimeout = 1 * time.Second
constant maxRetries (line 67) | maxRetries = 3
constant retryBaseDelay (line 68) | retryBaseDelay = 200 * time.Millisecond
type DaishuPlugin (line 72) | type DaishuPlugin struct
method Search (line 91) | func (p *DaishuPlugin) Search(keyword string, ext map[string]interface...
method SearchWithResult (line 100) | func (p *DaishuPlugin) SearchWithResult(keyword string, ext map[string...
method searchImpl (line 120) | func (p *DaishuPlugin) searchImpl(client *http.Client, keyword string,...
method fetchDetailLinks (line 216) | func (p *DaishuPlugin) fetchDetailLinks(client *http.Client, detailURL...
method doRequestWithRetry (line 420) | func (p *DaishuPlugin) doRequestWithRetry(req *http.Request, client *h...
function init (line 77) | func init() {
function NewDaishuPlugin (line 83) | func NewDaishuPlugin() *DaishuPlugin {
function newHTTPClient (line 104) | func newHTTPClient() *http.Client {
function extractLinks (line 268) | func extractLinks(selection *goquery.Selection) []model.Link {
function classifyLink (line 327) | func classifyLink(raw string) (string, string) {
function extractPassword (line 336) | func extractPassword(node *goquery.Selection) string {
function matchPassword (line 362) | func matchPassword(text string) string {
function substring (line 375) | func substring(text string, start, end int) string {
function extractPostID (line 385) | func extractPostID(detailURL string) string {
function parseChineseDate (line 392) | func parseChineseDate(value string) time.Time {
function setCommonHeaders (line 412) | func setCommonHeaders(req *http.Request, referer string) {
function startCacheCleaner (line 439) | func startCacheCleaner() {
FILE: plugin/ddys/ddys.go
constant PluginName (line 21) | PluginName = "ddys"
constant DisplayName (line 22) | DisplayName = "低端影视"
constant Description (line 23) | Description = "低端影视 - 影视资源网盘链接搜索"
constant BaseURL (line 24) | BaseURL = "https://ddys.pro"
constant SearchPath (line 25) | SearchPath = "/?s=%s&post_type=post"
constant UserAgent (line 26) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWe...
constant MaxResults (line 27) | MaxResults = 50
constant MaxConcurrency (line 28) | MaxConcurrency = 20
type DdysPlugin (line 32) | type DdysPlugin struct
method Name (line 58) | func (p *DdysPlugin) Name() string {
method DisplayName (line 63) | func (p *DdysPlugin) DisplayName() string {
method Description (line 68) | func (p *DdysPlugin) Description() string {
method Search (line 73) | func (p *DdysPlugin) Search(keyword string, ext map[string]interface{}...
method searchImpl (line 78) | func (p *DdysPlugin) searchImpl(client *http.Client, keyword string, e...
method executeSearch (line 111) | func (p *DdysPlugin) executeSearch(client *http.Client, keyword string...
method doRequestWithRetry (line 153) | func (p *DdysPlugin) doRequestWithRetry(req *http.Request, client *htt...
method parseSearchResults (line 182) | func (p *DdysPlugin) parseSearchResults(doc *goquery.Document) ([]mode...
method parseResultItem (line 205) | func (p *DdysPlugin) parseResultItem(s *goquery.Selection, index int) ...
method extractPostID (line 269) | func (p *DdysPlugin) extractPostID(articleClass string) string {
method extractPublishTime (line 280) | func (p *DdysPlugin) extractPublishTime(s *goquery.Selection) time.Time {
method extractCategory (line 300) | func (p *DdysPlugin) extractCategory(s *goquery.Selection) string {
method extractContent (line 309) | func (p *DdysPlugin) extractContent(s *goquery.Selection) string {
method fetchDetailLinks (line 323) | func (p *DdysPlugin) fetchDetailLinks(client *http.Client, searchResul...
method extractDetailURLFromContent (line 378) | func (p *DdysPlugin) extractDetailURLFromContent(content string) string {
method cleanContent (line 389) | func (p *DdysPlugin) cleanContent(content string) string {
method fetchDetailPageLinks (line 401) | func (p *DdysPlugin) fetchDetailPageLinks(client *http.Client, detailU...
method parseNetworkDiskLinks (line 471) | func (p *DdysPlugin) parseNetworkDiskLinks(htmlContent string) []model...
method extractPassword (line 534) | func (p *DdysPlugin) extractPassword(content string, panURL string) st...
method determineCloudType (line 573) | func (p *DdysPlugin) determineCloudType(url string) string {
function init (line 40) | func init() {
function NewDdysPlugin (line 45) | func NewDdysPlugin() *DdysPlugin {
FILE: plugin/discourse/discourse.go
constant pluginName (line 35) | pluginName = "discourse"
constant searchURLTemplate (line 37) | searchURLTemplate = "https://linux.do/search.json?q=%s%%20in%%3Atitle%%2...
constant detailURLTemplate (line 38) | detailURLTemplate = "https://linux.do/t/%d.json?track_visit=true&forceLo...
constant defaultPriority (line 39) | defaultPriority = 2
constant defaultTimeout (line 40) | defaultTimeout = 30 * time.Second
constant defaultMaxPages (line 43) | defaultMaxPages = 1
constant maxAllowedPages (line 44) | maxAllowedPages = 10
constant pageRequestDelay (line 45) | pageRequestDelay = 500 * time.Millisecond
type DiscourseAsyncPlugin (line 49) | type DiscourseAsyncPlugin struct
method Search (line 148) | func (p *DiscourseAsyncPlugin) Search(keyword string, ext map[string]i...
method SearchWithResult (line 157) | func (p *DiscourseAsyncPlugin) SearchWithResult(keyword string, ext ma...
method searchImpl (line 163) | func (p *DiscourseAsyncPlugin) searchImpl(client *http.Client, keyword...
method convertToSearchResults (line 305) | func (p *DiscourseAsyncPlugin) convertToSearchResults(resp SearchRespo...
method extractNetDiskLinksFromBlurb (line 356) | func (p *DiscourseAsyncPlugin) extractNetDiskLinksFromBlurb(blurb stri...
method cleanContent (line 441) | func (p *DiscourseAsyncPlugin) cleanContent(content string) string {
method GetTopicDetail (line 465) | func (p *DiscourseAsyncPlugin) GetTopicDetail(topicID int) ([]model.Li...
method parseNetDiskLink (line 524) | func (p *DiscourseAsyncPlugin) parseNetDiskLink(linkURL string) *model...
type SearchResponse (line 55) | type SearchResponse struct
type Post (line 62) | type Post struct
type Topic (line 73) | type Topic struct
type GroupedSearchResult (line 84) | type GroupedSearchResult struct
type DetailResponse (line 91) | type DetailResponse struct
type PostStream (line 99) | type PostStream struct
type DetailPost (line 104) | type DetailPost struct
type LinkCount (line 114) | type LinkCount struct
function init (line 125) | func init() {
function NewDiscourseAsyncPlugin (line 130) | func NewDiscourseAsyncPlugin() *DiscourseAsyncPlugin {
function max (line 297) | func max(a, b int) int {
FILE: plugin/djgou/djgou.go
constant DefaultTimeout (line 35) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 36) | DetailTimeout = 6 * time.Second
constant MaxConcurrency (line 39) | MaxConcurrency = 15
constant MaxIdleConns (line 42) | MaxIdleConns = 50
constant MaxIdleConnsPerHost (line 43) | MaxIdleConnsPerHost = 20
constant MaxConnsPerHost (line 44) | MaxConnsPerHost = 30
constant IdleConnTimeout (line 45) | IdleConnTimeout = 90 * time.Second
constant SiteURL (line 48) | SiteURL = "https://duanjugou.top"
function init (line 52) | func init() {
function startCacheCleaner (line 60) | func startCacheCleaner() {
type DjgouPlugin (line 72) | type DjgouPlugin struct
method Search (line 98) | func (p *DjgouPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 107) | func (p *DjgouPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 112) | func (p *DjgouPlugin) searchImpl(client *http.Client, keyword string, ...
method parseSearchItem (line 183) | func (p *DjgouPlugin) parseSearchItem(s *goquery.Selection, keyword st...
method parseTime (line 240) | func (p *DjgouPlugin) parseTime(timeStr string) time.Time {
method enhanceWithDetails (line 265) | func (p *DjgouPlugin) enhanceWithDetails(client *http.Client, results ...
method getDetailInfo (line 304) | func (p *DjgouPlugin) getDetailInfo(client *http.Client, detailURL str...
method fetchDetailPage (line 336) | func (p *DjgouPlugin) fetchDetailPage(client *http.Client, detailURL s...
method extractLinksFromDoc (line 386) | func (p *DjgouPlugin) extractLinksFromDoc(doc *goquery.Document) []mod...
method extractContent (line 438) | func (p *DjgouPlugin) extractContent(mainContent *goquery.Selection) s...
method doRequestWithRetry (line 453) | func (p *DjgouPlugin) doRequestWithRetry(req *http.Request, client *ht...
function createOptimizedHTTPClient (line 78) | func createOptimizedHTTPClient() *http.Client {
function NewDjgouPlugin (line 90) | func NewDjgouPlugin() *DjgouPlugin {
type DetailCacheData (line 329) | type DetailCacheData struct
FILE: plugin/duoduo/duoduo.go
constant DefaultTimeout (line 52) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 53) | DetailTimeout = 6 * time.Second
constant MaxConcurrency (line 56) | MaxConcurrency = 20
constant MaxIdleConns (line 59) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 60) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 61) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 62) | IdleConnTimeout = 90 * time.Second
function init (line 76) | func init() {
function startCacheCleaner (line 84) | func startCacheCleaner() {
type DuoduoAsyncPlugin (line 96) | type DuoduoAsyncPlugin struct
method Search (line 126) | func (p *DuoduoAsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 135) | func (p *DuoduoAsyncPlugin) SearchWithResult(keyword string, ext map[s...
method searchImpl (line 140) | func (p *DuoduoAsyncPlugin) searchImpl(client *http.Client, keyword st...
method parseSearchItem (line 211) | func (p *DuoduoAsyncPlugin) parseSearchItem(s *goquery.Selection, keyw...
method enhanceWithDetails (line 311) | func (p *DuoduoAsyncPlugin) enhanceWithDetails(client *http.Client, re...
method doRequestWithRetry (line 374) | func (p *DuoduoAsyncPlugin) doRequestWithRetry(req *http.Request, clie...
method fetchDetailLinksAndImages (line 403) | func (p *DuoduoAsyncPlugin) fetchDetailLinksAndImages(client *http.Cli...
method fetchDetailLinks (line 505) | func (p *DuoduoAsyncPlugin) fetchDetailLinks(client *http.Client, item...
method isValidNetworkDriveURL (line 511) | func (p *DuoduoAsyncPlugin) isValidNetworkDriveURL(url string) bool {
method determineLinkType (line 539) | func (p *DuoduoAsyncPlugin) determineLinkType(url string) string {
method GetPerformanceStats (line 585) | func (p *DuoduoAsyncPlugin) GetPerformanceStats() map[string]interface...
function createOptimizedHTTPClient (line 102) | func createOptimizedHTTPClient() *http.Client {
function NewDuoduoPlugin (line 118) | func NewDuoduoPlugin() *DuoduoAsyncPlugin {
function min (line 577) | func min(a, b int) int {
FILE: plugin/dyyj/dyyj.go
constant PluginName (line 22) | PluginName = "dyyj"
constant DisplayName (line 23) | DisplayName = "电影云集"
constant Description (line 24) | Description = "电影云集 - 影视资源网盘链接搜索"
constant BaseURL (line 25) | BaseURL = "https://bbs.dyyjmax.org"
constant SearchPath (line 26) | SearchPath = "/?q=%s"
constant UserAgent (line 27) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Apple...
constant MaxResults (line 28) | MaxResults = 100
constant MaxConcurrency (line 29) | MaxConcurrency = 100
constant RequestTimeout (line 30) | RequestTimeout = 30 * time.Second
constant MaxIdleConns (line 33) | MaxIdleConns = 100
constant MaxIdleConnsPerHost (line 34) | MaxIdleConnsPerHost = 100
constant MaxConnsPerHost (line 35) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 36) | IdleConnTimeout = 90 * time.Second
constant TLSHandshakeTimeout (line 37) | TLSHandshakeTimeout = 10 * time.Second
constant ExpectContinueTimeout (line 38) | ExpectContinueTimeout = 1 * time.Second
type DyyjPlugin (line 82) | type DyyjPlugin struct
method Name (line 129) | func (p *DyyjPlugin) Name() string {
method DisplayName (line 134) | func (p *DyyjPlugin) DisplayName() string {
method Description (line 139) | func (p *DyyjPlugin) Description() string {
method Search (line 144) | func (p *DyyjPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 153) | func (p *DyyjPlugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 158) | func (p *DyyjPlugin) searchImpl(client *http.Client, keyword string, e...
method executeSearch (line 202) | func (p *DyyjPlugin) executeSearch(client *http.Client, keyword string...
method doRequestWithRetry (line 354) | func (p *DyyjPlugin) doRequestWithRetry(req *http.Request, client *htt...
method parseSearchResults (line 396) | func (p *DyyjPlugin) parseSearchResults(doc *goquery.Document, htmlCon...
method parseSearchResultsWithRegex (line 487) | func (p *DyyjPlugin) parseSearchResultsWithRegex(htmlContent string) [...
method parseResultItem (line 567) | func (p *DyyjPlugin) parseResultItem(s *goquery.Selection, index int) ...
method extractPostID (line 625) | func (p *DyyjPlugin) extractPostID(url string) string {
method filterByTitleKeyword (line 635) | func (p *DyyjPlugin) filterByTitleKeyword(results []model.SearchResult...
method fetchDetailLinks (line 667) | func (p *DyyjPlugin) fetchDetailLinks(client *http.Client, searchResul...
method extractDetailURLFromContent (line 749) | func (p *DyyjPlugin) extractDetailURLFromContent(content string) string {
method cleanContent (line 760) | func (p *DyyjPlugin) cleanContent(content string) string {
method fetchDetailPageLinks (line 772) | func (p *DyyjPlugin) fetchDetailPageLinks(client *http.Client, detailU...
method extractPublishTime (line 878) | func (p *DyyjPlugin) extractPublishTime(htmlContent string) time.Time {
method parseNetworkDiskLinks (line 912) | func (p *DyyjPlugin) parseNetworkDiskLinks(htmlContent string) []model...
method parseNetworkDiskLinksWithRegex (line 1040) | func (p *DyyjPlugin) parseNetworkDiskLinksWithRegex(htmlContent string...
method isNetworkDiskName (line 1108) | func (p *DyyjPlugin) isNetworkDiskName(text string) bool {
method extractPasswordFromURL (line 1124) | func (p *DyyjPlugin) extractPasswordFromURL(linkURL string) string {
method determineCloudType (line 1144) | func (p *DyyjPlugin) determineCloudType(url string) string {
function init (line 91) | func init() {
function NewDyyjPlugin (line 96) | func NewDyyjPlugin() *DyyjPlugin {
function createOptimizedHTTPClient (line 110) | func createOptimizedHTTPClient() *http.Client {
type cacheItem (line 871) | type cacheItem struct
FILE: plugin/erxiao/erxiao.go
constant DefaultTimeout (line 21) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 22) | DetailTimeout = 6 * time.Second
constant MaxIdleConns (line 25) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 26) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 27) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 28) | IdleConnTimeout = 90 * time.Second
constant MaxConcurrency (line 31) | MaxConcurrency = 20
constant cacheTTL (line 34) | cacheTTL = 1 * time.Hour
function init (line 53) | func init() {
type ErxiaoAsyncPlugin (line 81) | type ErxiaoAsyncPlugin struct
method Search (line 110) | func (p *ErxiaoAsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 119) | func (p *ErxiaoAsyncPlugin) SearchWithResult(keyword string, ext map[s...
method searchImpl (line 124) | func (p *ErxiaoAsyncPlugin) searchImpl(client *http.Client, keyword st...
method parseSearchItem (line 193) | func (p *ErxiaoAsyncPlugin) parseSearchItem(s *goquery.Selection, keyw...
method enhanceWithDetails (line 294) | func (p *ErxiaoAsyncPlugin) enhanceWithDetails(client *http.Client, re...
method fetchDetailLinksAndImages (line 354) | func (p *ErxiaoAsyncPlugin) fetchDetailLinksAndImages(client *http.Cli...
method isValidNetworkDriveURL (line 428) | func (p *ErxiaoAsyncPlugin) isValidNetworkDriveURL(url string) bool {
method determineLinkType (line 440) | func (p *ErxiaoAsyncPlugin) determineLinkType(url string) string {
method extractPassword (line 472) | func (p *ErxiaoAsyncPlugin) extractPassword(url string) string {
method doRequestWithRetry (line 481) | func (p *ErxiaoAsyncPlugin) doRequestWithRetry(req *http.Request, clie...
method GetPerformanceStats (line 507) | func (p *ErxiaoAsyncPlugin) GetPerformanceStats() map[string]interface...
function createOptimizedHTTPClient (line 87) | func createOptimizedHTTPClient() *http.Client {
function NewErxiaoPlugin (line 102) | func NewErxiaoPlugin() *ErxiaoAsyncPlugin {
FILE: plugin/feikuai/feikuai.go
constant SearchAPIURL (line 20) | SearchAPIURL = "https://feikuai.tv/t_search/bm_search.php?kw=%s"
constant DefaultTimeout (line 23) | DefaultTimeout = 15 * time.Second
constant MaxIdleConns (line 26) | MaxIdleConns = 100
constant MaxIdleConnsPerHost (line 27) | MaxIdleConnsPerHost = 30
constant MaxConnsPerHost (line 28) | MaxConnsPerHost = 50
constant IdleConnTimeout (line 29) | IdleConnTimeout = 90 * time.Second
type FeikuaiPlugin (line 45) | type FeikuaiPlugin struct
method Search (line 79) | func (p *FeikuaiPlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 88) | func (p *FeikuaiPlugin) SearchWithResult(keyword string, ext map[strin...
method searchImpl (line 93) | func (p *FeikuaiPlugin) searchImpl(client *http.Client, keyword string...
method parseTorrent (line 197) | func (p *FeikuaiPlugin) parseTorrent(keyword string, item FeikuaiAPIIt...
method buildWorkTitle (line 236) | func (p *FeikuaiPlugin) buildWorkTitle(keyword, fileName string) string {
method cleanFileName (line 250) | func (p *FeikuaiPlugin) cleanFileName(fileName string) string {
method containsKeywords (line 266) | func (p *FeikuaiPlugin) containsKeywords(keyword, text string) bool {
method splitKeywords (line 281) | func (p *FeikuaiPlugin) splitKeywords(keyword string) []string {
method buildContent (line 314) | func (p *FeikuaiPlugin) buildContent(item FeikuaiAPIItem, torrent Feik...
method extractTags (line 336) | func (p *FeikuaiPlugin) extractTags(title, fileName string) []string {
method parsePublishedTime (line 370) | func (p *FeikuaiPlugin) parsePublishedTime(timeStr string) time.Time {
method doRequestWithRetry (line 395) | func (p *FeikuaiPlugin) doRequestWithRetry(req *http.Request, client *...
function createOptimizedHTTPClient (line 51) | func createOptimizedHTTPClient() *http.Client {
function NewFeikuaiPlugin (line 67) | func NewFeikuaiPlugin() *FeikuaiPlugin {
function init (line 74) | func init() {
type FeikuaiAPIResponse (line 164) | type FeikuaiAPIResponse struct
type FeikuaiAPIItem (line 173) | type FeikuaiAPIItem struct
type FeikuaiTorrent (line 182) | type FeikuaiTorrent struct
FILE: plugin/fox4k/fox4k.go
constant BaseURL (line 28) | BaseURL = "https://4kfox.com"
constant SearchURL (line 33) | SearchURL = BaseURL + "/search/%s-------------.html"
constant SearchPageURL (line 36) | SearchPageURL = BaseURL + "/search/%s----------%d---.html"
constant DetailURL (line 39) | DetailURL = BaseURL + "/video/%s.html"
constant DefaultTimeout (line 42) | DefaultTimeout = 15 * time.Second
constant DefaultHTTPProxy (line 45) | DefaultHTTPProxy = "http://154.219.110.34:51422"
constant DefaultSocks5Proxy (line 46) | DefaultSocks5Proxy = "socks5://154.219.110.34:51423"
constant DebugMode (line 49) | DebugMode = false
constant ProxyEnabled (line 52) | ProxyEnabled = false
constant MaxConcurrency (line 55) | MaxConcurrency = 50
constant MaxPages (line 58) | MaxPages = 10
constant MaxIdleConns (line 61) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 62) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 63) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 64) | IdleConnTimeout = 90 * time.Second
type detailPageResponse (line 121) | type detailPageResponse struct
type Fox4kPlugin (line 131) | type Fox4kPlugin struct
method Search (line 240) | func (p *Fox4kPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 249) | func (p *Fox4kPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 269) | func (p *Fox4kPlugin) searchImpl(client *http.Client, keyword string, ...
method searchPage (line 341) | func (p *Fox4kPlugin) searchPage(client *http.Client, encodedKeyword s...
method parseTotalPages (line 469) | func (p *Fox4kPlugin) parseTotalPages(doc *goquery.Document) int {
method parseSearchResultItem (line 492) | func (p *Fox4kPlugin) parseSearchResultItem(s *goquery.Selection) *mod...
method enrichWithDetailInfo (line 604) | func (p *Fox4kPlugin) enrichWithDetailInfo(results []model.SearchResul...
method getDetailInfo (line 673) | func (p *Fox4kPlugin) getDetailInfo(id string, client *http.Client) *d...
method GetPerformanceStats (line 775) | func (p *Fox4kPlugin) GetPerformanceStats() map[string]interface{} {
method extractDownloadLinks (line 802) | func (p *Fox4kPlugin) extractDownloadLinks(doc *goquery.Document, deta...
method processFoundLink (line 871) | func (p *Fox4kPlugin) processFoundLink(detail *detailPageResponse, lin...
method extractLinksFromText (line 904) | func (p *Fox4kPlugin) extractLinksFromText(detail *detailPageResponse,...
method extractPasswordFromLink (line 935) | func (p *Fox4kPlugin) extractPasswordFromLink(link string) string {
method extractPasswordFromText (line 946) | func (p *Fox4kPlugin) extractPasswordFromText(text, link string) string {
method addDownloadLink (line 963) | func (p *Fox4kPlugin) addDownloadLink(detail *detailPageResponse, link...
method doRequestWithRetry (line 991) | func (p *Fox4kPlugin) doRequestWithRetry(req *http.Request, client *ht...
function createProxyTransport (line 137) | func createProxyTransport(proxyURL string) (*http.Transport, error) {
function createOptimizedHTTPClient (line 175) | func createOptimizedHTTPClient() *http.Client {
function NewFox4kPlugin (line 205) | func NewFox4kPlugin() *Fox4kPlugin {
function debugPrintf (line 213) | func debugPrintf(format string, args ...interface{}) {
function init (line 220) | func init() {
function startCacheCleaner (line 228) | func startCacheCleaner() {
function getRandomUA (line 1052) | func getRandomUA() string {
function generateRandomIP (line 1067) | func generateRandomIP() string {
FILE: plugin/gying/gying.go
constant MaxConcurrentUsers (line 36) | MaxConcurrentUsers = 10
constant MaxConcurrentDetails (line 37) | MaxConcurrentDetails = 50
constant DebugLog (line 38) | DebugLog = false
constant HTMLTemplate (line 57) | HTMLTemplate = `<!DOCTYPE html>
type GyingPlugin (line 369) | type GyingPlugin struct
method Initialize (line 433) | func (p *GyingPlugin) Initialize() error {
method RegisterWebRoutes (line 473) | func (p *GyingPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
method Search (line 482) | func (p *GyingPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 495) | func (p *GyingPlugin) SearchWithResult(keyword string, ext map[string]...
method loadAllUsers (line 559) | func (p *GyingPlugin) loadAllUsers() {
method initDefaultAccounts (line 616) | func (p *GyingPlugin) initDefaultAccounts() {
method initOrRestoreUser (line 662) | func (p *GyingPlugin) initOrRestoreUser(username, password, source str...
method getUserByHash (line 721) | func (p *GyingPlugin) getUserByHash(hash string) (*User, bool) {
method saveUser (line 730) | func (p *GyingPlugin) saveUser(user *User) error {
method persistUser (line 736) | func (p *GyingPlugin) persistUser(user *User) error {
method deleteUser (line 746) | func (p *GyingPlugin) deleteUser(hash string) error {
method getActiveUsers (line 753) | func (p *GyingPlugin) getActiveUsers() []*User {
method handleManagePage (line 770) | func (p *GyingPlugin) handleManagePage(c *gin.Context) {
method handleManagePagePOST (line 784) | func (p *GyingPlugin) handleManagePagePOST(c *gin.Context) {
method handleGetStatus (line 814) | func (p *GyingPlugin) handleGetStatus(c *gin.Context, hash string) {
method handleLogin (line 855) | func (p *GyingPlugin) handleLogin(c *gin.Context, hash string, reqData...
method handleLogout (line 910) | func (p *GyingPlugin) handleLogout(c *gin.Context, hash string) {
method handleTestSearch (line 931) | func (p *GyingPlugin) handleTestSearch(c *gin.Context, hash string, re...
method encryptPassword (line 998) | func (p *GyingPlugin) encryptPassword(password string) (string, error) {
method decryptPassword (line 1027) | func (p *GyingPlugin) decryptPassword(encrypted string) (string, error) {
method createScraperWithCookies (line 1066) | func (p *GyingPlugin) createScraperWithCookies(cookieStr string) (*clo...
method doLogin (line 1173) | func (p *GyingPlugin) doLogin(username, password string) (*cloudscrape...
method reloginUser (line 1419) | func (p *GyingPlugin) reloginUser(user *User) error {
method executeSearchTasks (line 1467) | func (p *GyingPlugin) executeSearchTasks(users []*User, keyword string...
method searchWithScraperWithRetry (line 1534) | func (p *GyingPlugin) searchWithScraperWithRetry(keyword string, scrap...
method searchWithScraper (line 1576) | func (p *GyingPlugin) searchWithScraper(keyword string, scraper *cloud...
method fetchAllDetails (line 1720) | func (p *GyingPlugin) fetchAllDetails(searchData *SearchData, scraper ...
method fetchDetail (line 1845) | func (p *GyingPlugin) fetchDetail(resourceID, resourceType string, scr...
method buildResult (line 1924) | func (p *GyingPlugin) buildResult(detail *DetailData, searchData *Sear...
method parseUpdateTime (line 1994) | func (p *GyingPlugin) parseUpdateTime(timeStrs []string) time.Time {
method parseRelativeTime (line 2053) | func (p *GyingPlugin) parseRelativeTime(timeStr string, baseTime time....
method extractPanLinks (line 2097) | func (p *GyingPlugin) extractPanLinks(detail *DetailData) []model.Link {
method determineLinkType (line 2155) | func (p *GyingPlugin) determineLinkType(linkURL string) string {
method extractPasswordFromURL (line 2181) | func (p *GyingPlugin) extractPasswordFromURL(linkURL string) string {
method deduplicateResults (line 2202) | func (p *GyingPlugin) deduplicateResults(results []model.SearchResult)...
method generateHash (line 2219) | func (p *GyingPlugin) generateHash(username string) string {
method maskUsername (line 2230) | func (p *GyingPlugin) maskUsername(username string) string {
method isHexString (line 2241) | func (p *GyingPlugin) isHexString(s string) bool {
method startSessionKeepAlive (line 2333) | func (p *GyingPlugin) startSessionKeepAlive() {
method keepAllSessionsAlive (line 2348) | func (p *GyingPlugin) keepAllSessionsAlive() {
method startCleanupTask (line 2392) | func (p *GyingPlugin) startCleanupTask() {
method cleanupExpiredUsers (line 2404) | func (p *GyingPlugin) cleanupExpiredUsers() int {
method markInactiveUsers (line 2422) | func (p *GyingPlugin) markInactiveUsers() int {
type User (line 379) | type User struct
type SearchData (line 393) | type SearchData struct
type DetailData (line 409) | type DetailData struct
function init (line 424) | func init() {
function parseCookieString (line 1147) | func parseCookieString(cookieStr string) map[string]string {
function min (line 1409) | func min(a, b int) int {
function respondSuccess (line 2251) | func respondSuccess(c *gin.Context, message string, data interface{}) {
function respondError (line 2260) | func respondError(c *gin.Context, message string) {
function getEncryptionKey (line 2270) | func getEncryptionKey() []byte {
function encryptCookie (line 2278) | func encryptCookie(plaintext string) (string, error) {
function decryptCookie (line 2299) | func decryptCookie(encrypted string) (string, error) {
FILE: plugin/haisou/haisou.go
constant DebugLog (line 21) | DebugLog = false
constant DefaultPagesPerType (line 23) | DefaultPagesPerType = 2
constant MaxAllowedPagesPerType (line 25) | MaxAllowedPagesPerType = 3
type HaisouPlugin (line 32) | type HaisouPlugin struct
method Search (line 93) | func (p *HaisouPlugin) Search(keyword string, ext map[string]interface...
method SearchWithResult (line 102) | func (p *HaisouPlugin) SearchWithResult(keyword string, ext map[string...
method searchImpl (line 107) | func (p *HaisouPlugin) searchImpl(client *http.Client, keyword string,...
method fetchSearchPage (line 312) | func (p *HaisouPlugin) fetchSearchPage(client *http.Client, keyword st...
method fetchShareLink (line 375) | func (p *HaisouPlugin) fetchShareLink(client *http.Client, hsid string...
method doRequestWithRetry (line 449) | func (p *HaisouPlugin) doRequestWithRetry(req *http.Request, client *h...
type SearchAPIResponse (line 37) | type SearchAPIResponse struct
type ShareItem (line 51) | type ShareItem struct
type FetchAPIResponse (line 60) | type FetchAPIResponse struct
type PageResult (line 70) | type PageResult struct
type LinkResult (line 78) | type LinkResult struct
function init (line 85) | func init() {
function buildShareURL (line 478) | func buildShareURL(platform, shareCode string) string {
function mapPlatformType (line 496) | func mapPlatformType(platform string) string {
function cleanHTMLTags (line 514) | func cleanHTMLTags(text string) string {
function formatSize (line 527) | func formatSize(size int64) string {
FILE: plugin/hdmoli/hdmoli.go
constant PluginName (line 21) | PluginName = "hdmoli"
constant DisplayName (line 22) | DisplayName = "HDmoli"
constant Description (line 23) | Description = "HDmoli - 影视资源网盘下载链接搜索"
constant BaseURL (line 24) | BaseURL = "https://www.hdmoli.pro"
constant SearchPath (line 25) | SearchPath = "/search.php?searchkey=%s&submit="
constant UserAgent (line 26) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW...
constant MaxResults (line 27) | MaxResults = 50
constant MaxConcurrency (line 28) | MaxConcurrency = 20
type HdmoliPlugin (line 32) | type HdmoliPlugin struct
method Name (line 58) | func (p *HdmoliPlugin) Name() string {
method DisplayName (line 63) | func (p *HdmoliPlugin) DisplayName() string {
method Description (line 68) | func (p *HdmoliPlugin) Description() string {
method Search (line 73) | func (p *HdmoliPlugin) Search(keyword string, ext map[string]interface...
method searchImpl (line 78) | func (p *HdmoliPlugin) searchImpl(client *http.Client, keyword string,...
method executeSearch (line 111) | func (p *HdmoliPlugin) executeSearch(client *http.Client, keyword stri...
method doRequestWithRetry (line 153) | func (p *HdmoliPlugin) doRequestWithRetry(req *http.Request, client *h...
method parseSearchResults (line 182) | func (p *HdmoliPlugin) parseSearchResults(doc *goquery.Document) ([]mo...
method parseResultItem (line 205) | func (p *HdmoliPlugin) parseResultItem(s *goquery.Selection, index int...
method extractRating (line 329) | func (p *HdmoliPlugin) extractRating(s *goquery.Selection) string {
method extractUpdateStatus (line 339) | func (p *HdmoliPlugin) extractUpdateStatus(s *goquery.Selection) string {
method extractDirector (line 349) | func (p *HdmoliPlugin) extractDirector(s *goquery.Selection) string {
method extractActors (line 368) | func (p *HdmoliPlugin) extractActors(s *goquery.Selection) []string {
method extractCategoryInfo (line 386) | func (p *HdmoliPlugin) extractCategoryInfo(s *goquery.Selection) (cate...
method extractDescription (line 422) | func (p *HdmoliPlugin) extractDescription(s *goquery.Selection) string {
method fetchDetailLinks (line 446) | func (p *HdmoliPlugin) fetchDetailLinks(client *http.Client, searchRes...
method extractDetailURLFromContent (line 501) | func (p *HdmoliPlugin) extractDetailURLFromContent(content string) str...
method cleanContent (line 512) | func (p *HdmoliPlugin) cleanContent(content string) string {
method fetchDetailPageLinks (line 524) | func (p *HdmoliPlugin) fetchDetailPageLinks(client *http.Client, detai...
method parseNetworkDiskLinks (line 594) | func (p *HdmoliPlugin) parseNetworkDiskLinks(htmlContent string) []mod...
method parseNetworkDiskLinksWithRegex (line 655) | func (p *HdmoliPlugin) parseNetworkDiskLinksWithRegex(htmlContent stri...
method extractPasswordFromQuarkURL (line 691) | func (p *HdmoliPlugin) extractPasswordFromQuarkURL(panURL string) stri...
method extractPasswordFromBaiduURL (line 697) | func (p *HdmoliPlugin) extractPasswordFromBaiduURL(panURL string) stri...
function init (line 40) | func init() {
function NewHdmoliPlugin (line 45) | func NewHdmoliPlugin() *HdmoliPlugin {
FILE: plugin/hdr4k/hdr4k.go
type cachedResponse (line 47) | type cachedResponse struct
function init (line 53) | func init() {
function startCacheCleaner (line 65) | func startCacheCleaner() {
function getRandomUA (line 80) | func getRandomUA() string {
constant SearchURL (line 86) | SearchURL = "https://www.4khdr.cn/search.php?mod=forum"
constant ThreadURLPattern (line 88) | ThreadURLPattern = "https://www.4khdr.cn/thread-%s-1-1.html"
constant DefaultTimeout (line 90) | DefaultTimeout = 10 * time.Second
constant MaxRetries (line 92) | MaxRetries = 2
constant MaxConcurrency (line 94) | MaxConcurrency = 20
type Hdr4kAsyncPlugin (line 98) | type Hdr4kAsyncPlugin struct
method Search (line 110) | func (p *Hdr4kAsyncPlugin) Search(keyword string, ext map[string]inter...
method SearchWithResult (line 119) | func (p *Hdr4kAsyncPlugin) SearchWithResult(keyword string, ext map[st...
method doSearch (line 124) | func (p *Hdr4kAsyncPlugin) doSearch(client *http.Client, keyword strin...
method isEmptyRequestPost (line 321) | func (p *Hdr4kAsyncPlugin) isEmptyRequestPost(title string, links []mo...
method getLinksFromDetail (line 372) | func (p *Hdr4kAsyncPlugin) getLinksFromDetail(client *http.Client, pos...
method determineLinkType (line 490) | func (p *Hdr4kAsyncPlugin) determineLinkType(url, name string) string {
method doRequestWithRetry (line 577) | func (p *Hdr4kAsyncPlugin) doRequestWithRetry(client *http.Client, req...
method isRetriableError (line 608) | func (p *Hdr4kAsyncPlugin) isRetriableError(err error) bool {
method parseDateTime (line 626) | func (p *Hdr4kAsyncPlugin) parseDateTime(dateStr string) (time.Time, e...
method cleanHTML (line 645) | func (p *Hdr4kAsyncPlugin) cleanHTML(html string) string {
function NewHdr4kAsyncPlugin (line 103) | func NewHdr4kAsyncPlugin() *Hdr4kAsyncPlugin {
FILE: plugin/huban/huban.go
constant DefaultTimeout (line 21) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 22) | DetailTimeout = 6 * time.Second
constant MaxIdleConns (line 25) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 26) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 27) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 28) | IdleConnTimeout = 90 * time.Second
constant MaxConcurrency (line 31) | MaxConcurrency = 20
constant cacheTTL (line 34) | cacheTTL = 1 * time.Hour
constant EnableRefererCheck (line 37) | EnableRefererCheck = false
constant DebugLog (line 40) | DebugLog = false
function init (line 70) | func init() {
type HubanAsyncPlugin (line 102) | type HubanAsyncPlugin struct
method Search (line 132) | func (p *HubanAsyncPlugin) Search(keyword string, ext map[string]inter...
method SearchWithResult (line 168) | func (p *HubanAsyncPlugin) SearchWithResult(keyword string, ext map[st...
method searchImpl (line 173) | func (p *HubanAsyncPlugin) searchImpl(client *http.Client, keyword str...
method parseSearchItem (line 242) | func (p *HubanAsyncPlugin) parseSearchItem(s *goquery.Selection, keywo...
method enhanceWithDetails (line 342) | func (p *HubanAsyncPlugin) enhanceWithDetails(client *http.Client, res...
method fetchDetailLinksAndImages (line 402) | func (p *HubanAsyncPlugin) fetchDetailLinksAndImages(client *http.Clie...
method isValidNetworkDriveURL (line 478) | func (p *HubanAsyncPlugin) isValidNetworkDriveURL(url string) bool {
method determineLinkType (line 505) | func (p *HubanAsyncPlugin) determineLinkType(url string) string {
method extractPassword (line 543) | func (p *HubanAsyncPlugin) extractPassword(url string) string {
method doRequestWithRetry (line 558) | func (p *HubanAsyncPlugin) doRequestWithRetry(req *http.Request, clien...
method GetPerformanceStats (line 584) | func (p *HubanAsyncPlugin) GetPerformanceStats() map[string]interface{} {
function createOptimizedHTTPClient (line 108) | func createOptimizedHTTPClient() *http.Client {
function NewHubanPlugin (line 124) | func NewHubanPlugin() *HubanAsyncPlugin {
function AddAllowedReferer (line 615) | func AddAllowedReferer(referer string) {
function RemoveAllowedReferer (line 625) | func RemoveAllowedReferer(referer string) {
function GetAllowedReferers (line 635) | func GetAllowedReferers() []string {
function IsRefererAllowed (line 642) | func IsRefererAllowed(referer string) bool {
FILE: plugin/hunhepan/hunhepan.go
function debugLog (line 20) | func debugLog(format string, args ...interface{}) {
function init (line 27) | func init() {
constant HunhepanAPI (line 34) | HunhepanAPI = "https://hunhepan.com/open/search/disk"
constant QkpansoAPI (line 35) | QkpansoAPI = "https://qkpanso.com/v1/search/disk"
constant KuakeAPI (line 36) | KuakeAPI = "https://kuake8.com/v1/search/disk"
constant MisosoAPI (line 37) | MisosoAPI = "https://www.misoso.cc/v1/search/disk"
constant DefaultPageSize (line 40) | DefaultPageSize = 30
type HunhepanAsyncPlugin (line 44) | type HunhepanAsyncPlugin struct
method Search (line 56) | func (p *HunhepanAsyncPlugin) Search(keyword string, ext map[string]in...
method SearchWithResult (line 65) | func (p *HunhepanAsyncPlugin) SearchWithResult(keyword string, ext map...
method doSearch (line 70) | func (p *HunhepanAsyncPlugin) doSearch(client *http.Client, keyword st...
method searchAPI (line 165) | func (p *HunhepanAsyncPlugin) searchAPI(client *http.Client, apiURL, k...
method deduplicateItems (line 310) | func (p *HunhepanAsyncPlugin) deduplicateItems(items []HunhepanItem) [...
method convertResults (line 365) | func (p *HunhepanAsyncPlugin) convertResults(items []HunhepanItem) []m...
method convertDiskType (line 424) | func (p *HunhepanAsyncPlugin) convertDiskType(diskType string) string {
function NewHunhepanAsyncPlugin (line 49) | func NewHunhepanAsyncPlugin() *HunhepanAsyncPlugin {
function min (line 302) | func min(a, b int) int {
function cleanTitle (line 452) | func cleanTitle(title string) string {
type HunhepanResponse (line 475) | type HunhepanResponse struct
type HunhepanItem (line 486) | type HunhepanItem struct
FILE: plugin/javdb/javdb.go
constant PluginName (line 24) | PluginName = "javdb"
constant DisplayName (line 25) | DisplayName = "JavDB"
constant Description (line 26) | Description = "JavDB - 影片数据库,专门提供磁力链接搜索"
constant BaseURL (line 27) | BaseURL = "https://javdb.com"
constant SearchPath (line 28) | SearchPath = "/search?q=%s&f=all"
constant UserAgent (line 29) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) A...
constant MaxResults (line 30) | MaxResults = 50
constant MaxConcurrency (line 31) | MaxConcurrency = 10
constant MaxRetryOnRateLimit (line 34) | MaxRetryOnRateLimit = 0
constant MinRetryDelay (line 35) | MinRetryDelay = 4
constant MaxRetryDelay (line 36) | MaxRetryDelay = 8
type JavdbPlugin (line 40) | type JavdbPlugin struct
method Name (line 71) | func (p *JavdbPlugin) Name() string {
method DisplayName (line 76) | func (p *JavdbPlugin) DisplayName() string {
method Description (line 81) | func (p *JavdbPlugin) Description() string {
method SkipServiceFilter (line 86) | func (p *JavdbPlugin) SkipServiceFilter() bool {
method Search (line 91) | func (p *JavdbPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 100) | func (p *JavdbPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 105) | func (p *JavdbPlugin) searchImpl(client *http.Client, keyword string, ...
method executeSearchWithRateLimit (line 150) | func (p *JavdbPlugin) executeSearchWithRateLimit(client *http.Client, ...
method doRequestWithRetry (line 246) | func (p *JavdbPlugin) doRequestWithRetry(req *http.Request, client *ht...
method doRequestWithRateLimitRetry (line 275) | func (p *JavdbPlugin) doRequestWithRateLimitRetry(req *http.Request, c...
method parseSearchResults (line 332) | func (p *JavdbPlugin) parseSearchResults(doc *goquery.Document) ([]mod...
method parseResultItem (line 401) | func (p *JavdbPlugin) parseResultItem(s *goquery.Selection, index int)...
method extractVideoInfo (line 511) | func (p *JavdbPlugin) extractVideoInfo(s *goquery.Selection) (videoNum...
method extractRating (line 530) | func (p *JavdbPlugin) extractRating(s *goquery.Selection) string {
method extractReleaseDate (line 543) | func (p *JavdbPlugin) extractReleaseDate(s *goquery.Selection) string {
method extractTags (line 553) | func (p *JavdbPlugin) extractTags(s *goquery.Selection) []string {
method cleanTitle (line 565) | func (p *JavdbPlugin) cleanTitle(title string) string {
method parseTime (line 573) | func (p *JavdbPlugin) parseTime(dateStr string) time.Time {
method fetchDetailMagnetLinks (line 596) | func (p *JavdbPlugin) fetchDetailMagnetLinks(client *http.Client, sear...
method extractDetailURLFromContent (line 761) | func (p *JavdbPlugin) extractDetailURLFromContent(content string) stri...
method cleanContent (line 772) | func (p *JavdbPlugin) cleanContent(content string) string {
method fetchDetailPageMagnetLinks (line 784) | func (p *JavdbPlugin) fetchDetailPageMagnetLinks(client *http.Client, ...
method parseMagnetLinks (line 891) | func (p *JavdbPlugin) parseMagnetLinks(htmlContent string) []model.Link {
function init (line 50) | func init() {
function NewJavdbPlugin (line 55) | func NewJavdbPlugin() *JavdbPlugin {
FILE: plugin/jikepan/jikepan.go
function init (line 16) | func init() {
constant JikepanAPIURL (line 23) | JikepanAPIURL = "https://api.jikepan.xyz/search"
type JikepanAsyncV2Plugin (line 27) | type JikepanAsyncV2Plugin struct
method Search (line 39) | func (p *JikepanAsyncV2Plugin) Search(keyword string, ext map[string]i...
method SearchWithResult (line 48) | func (p *JikepanAsyncV2Plugin) SearchWithResult(keyword string, ext ma...
method doSearch (line 53) | func (p *JikepanAsyncV2Plugin) doSearch(client *http.Client, keyword s...
method convertResults (line 112) | func (p *JikepanAsyncV2Plugin) convertResults(items []JikepanItem) []m...
method convertLinkType (line 162) | func (p *JikepanAsyncV2Plugin) convertLinkType(service string) string {
function NewJikepanAsyncV2Plugin (line 32) | func NewJikepanAsyncV2Plugin() *JikepanAsyncV2Plugin {
type JikepanResponse (line 197) | type JikepanResponse struct
type JikepanItem (line 203) | type JikepanItem struct
type JikepanLink (line 209) | type JikepanLink struct
FILE: plugin/jsnoteclub/jsnoteclub.go
constant pluginName (line 21) | pluginName = "jsnoteclub"
constant defaultPriority (line 22) | defaultPriority = 2
constant postsCacheTTL (line 24) | postsCacheTTL = time.Hour
constant detailCacheTTL (line 25) | detailCacheTTL = time.Hour
constant maxMatchedPosts (line 26) | maxMatchedPosts = 30
constant maxDetailWorkers (line 27) | maxDetailWorkers = 8
constant requestTimeout (line 28) | requestTimeout = 12 * time.Second
constant detailTimeout (line 29) | detailTimeout = 10 * time.Second
constant httpMaxIdleConns (line 30) | httpMaxIdleConns = 64
constant httpMaxIdlePerHost (line 31) | httpMaxIdlePerHost = 16
constant httpMaxConnsPerHost (line 32) | httpMaxConnsPerHost = 32
constant retryBaseDelay (line 33) | retryBaseDelay = 200 * time.Millisecond
constant maxRequestRetries (line 34) | maxRequestRetries = 3
type detailCacheEntry (line 75) | type detailCacheEntry struct
type JsNoteClubPlugin (line 81) | type JsNoteClubPlugin struct
method Search (line 100) | func (p *JsNoteClubPlugin) Search(keyword string, ext map[string]inter...
method SearchWithResult (line 109) | func (p *JsNoteClubPlugin) SearchWithResult(keyword string, ext map[st...
method searchImpl (line 113) | func (p *JsNoteClubPlugin) searchImpl(client *http.Client, keyword str...
method getAllPosts (line 188) | func (p *JsNoteClubPlugin) getAllPosts(client *http.Client) ([]ghostPo...
method fetchDataKey (line 223) | func (p *JsNoteClubPlugin) fetchDataKey(client *http.Client) (string, ...
method fetchPosts (line 263) | func (p *JsNoteClubPlugin) fetchPosts(client *http.Client, dataKey str...
method fetchDetailLinks (line 299) | func (p *JsNoteClubPlugin) fetchDetailLinks(client *http.Client, detai...
method doRequestWithRetry (line 495) | func (p *JsNoteClubPlugin) doRequestWithRetry(req *http.Request, clien...
function init (line 86) | func init() {
function NewJsNoteClubPlugin (line 92) | func NewJsNoteClubPlugin() *JsNoteClubPlugin {
function extractLinksFromSelection (line 357) | func extractLinksFromSelection(sel *goquery.Selection) []model.Link {
function classifyLink (line 415) | func classifyLink(raw string) (string, string) {
function extractPassword (line 424) | func extractPassword(node *goquery.Selection) string {
function matchPassword (line 448) | func matchPassword(text string) string {
function substring (line 461) | func substring(text string, start, end int) string {
function filterPostsByKeyword (line 471) | func filterPostsByKeyword(posts []ghostPost, keyword string) []ghostPost {
function newHTTPClient (line 515) | func newHTTPClient() *http.Client {
function setHTMLHeaders (line 528) | func setHTMLHeaders(req *http.Request, referer string) {
function setAPIHeaders (line 536) | func setAPIHeaders(req *http.Request, referer string) {
function startDetailCacheCleaner (line 544) | func startDetailCacheCleaner() {
type ghostPostsResponse (line 560) | type ghostPostsResponse struct
type ghostPost (line 564) | type ghostPost struct
method updatedAtTime (line 574) | func (p ghostPost) updatedAtTime() time.Time {
FILE: plugin/jutoushe/jutoushe.go
type JutoushePlugin (line 19) | type JutoushePlugin struct
method Search (line 31) | func (p *JutoushePlugin) Search(keyword string, ext map[string]interfa...
method SearchWithResult (line 40) | func (p *JutoushePlugin) SearchWithResult(keyword string, ext map[stri...
method searchImpl (line 45) | func (p *JutoushePlugin) searchImpl(client *http.Client, keyword strin...
method doRequestWithRetry (line 132) | func (p *JutoushePlugin) doRequestWithRetry(req *http.Request, client ...
method getDetailLinks (line 161) | func (p *JutoushePlugin) getDetailLinks(client *http.Client, detailURL...
method determineCloudType (line 221) | func (p *JutoushePlugin) determineCloudType(url string) string {
method extractPassword (line 249) | func (p *JutoushePlugin) extractPassword(url string) string {
method isValidNetworkDriveURL (line 264) | func (p *JutoushePlugin) isValidNetworkDriveURL(url string) bool {
method extractIDFromURL (line 292) | func (p *JutoushePlugin) extractIDFromURL(urlPath string) string {
method extractTags (line 305) | func (p *JutoushePlugin) extractTags(title string) []string {
method parseDate (line 326) | func (p *JutoushePlugin) parseDate(dateStr string) time.Time {
function init (line 23) | func init() {
FILE: plugin/kkmao/kkmao.go
type detailCacheEntry (line 34) | type detailCacheEntry struct
constant pluginName (line 40) | pluginName = "kkmao"
constant defaultPriority (line 41) | defaultPriority = 2
constant searchTimeout (line 42) | searchTimeout = 12 * time.Second
constant detailTimeout (line 43) | detailTimeout = 10 * time.Second
constant maxConcurrency (line 44) | maxConcurrency = 8
constant maxIdleConns (line 45) | maxIdleConns = 64
constant maxIdlePerHost (line 46) | maxIdlePerHost = 8
constant maxConnsPerHost (line 47) | maxConnsPerHost = 32
constant idleConnLifetime (line 48) | idleConnLifetime = 90 * time.Second
constant tlsHandshakeTimeout (line 49) | tlsHandshakeTimeout = 10 * time.Second
constant expectContinueTimeout (line 50) | expectContinueTimeout = 1 * time.Second
constant searchMaxRetries (line 52) | searchMaxRetries = 3
constant detailMaxRetries (line 53) | detailMaxRetries = 2
constant retryBaseDelay (line 54) | retryBaseDelay = 200 * time.Millisecond
type KkMaoPlugin (line 58) | type KkMaoPlugin struct
method Search (line 77) | func (p *KkMaoPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 86) | func (p *KkMaoPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 106) | func (p *KkMaoPlugin) searchImpl(client *http.Client, keyword string, ...
method fetchDetailLinks (line 229) | func (p *KkMaoPlugin) fetchDetailLinks(client *http.Client, detailURL,...
method doRequestWithRetry (line 362) | func (p *KkMaoPlugin) doRequestWithRetry(req *http.Request, client *ht...
function init (line 63) | func init() {
function NewKkMaoPlugin (line 69) | func NewKkMaoPlugin() *KkMaoPlugin {
function newHTTPClient (line 90) | func newHTTPClient() *http.Client {
function extractArticleID (line 200) | func extractArticleID(detailURL string) string {
function parsePublishTime (line 208) | func parsePublishTime(value string) time.Time {
function extractQuarkLinks (line 273) | func extractQuarkLinks(doc *goquery.Document) []model.Link {
function extractPassword (line 308) | func extractPassword(link *goquery.Selection) string {
function matchPassword (line 339) | func matchPassword(text string) string {
function setCommonHeaders (line 354) | func setCommonHeaders(req *http.Request, referer string) {
function startDetailCacheCleaner (line 383) | func startDetailCacheCleaner() {
FILE: plugin/kkv/kkv.go
constant baseURL (line 19) | baseURL = "http://kkv.q-23.cn"
constant searchPath (line 20) | searchPath = "/"
constant maxResults (line 21) | maxResults = 10
constant maxConcurrent (line 22) | maxConcurrent = 3
function debugPrintf (line 27) | func debugPrintf(format string, args ...interface{}) {
type KKVPlugin (line 33) | type KKVPlugin struct
method Search (line 44) | func (p *KKVPlugin) Search(keyword string, ext map[string]interface{})...
method SearchWithResult (line 52) | func (p *KKVPlugin) SearchWithResult(keyword string, ext map[string]in...
method searchImpl (line 56) | func (p *KKVPlugin) searchImpl(client *http.Client, keyword string, ex...
method filterItemsByKeyword (line 99) | func (p *KKVPlugin) filterItemsByKeyword(items []searchItem, keyword s...
method fetchSearchResults (line 116) | func (p *KKVPlugin) fetchSearchResults(searchURL string, client *http....
method processDetailPages (line 180) | func (p *KKVPlugin) processDetailPages(items []searchItem, client *htt...
method processDetailPage (line 206) | func (p *KKVPlugin) processDetailPage(item searchItem, client *http.Cl...
method extractUpdateTime (line 272) | func (p *KKVPlugin) extractUpdateTime(doc *goquery.Document) time.Time {
method extractPanLinks (line 290) | func (p *KKVPlugin) extractPanLinks(doc *goquery.Document) []model.Link {
method determinePanType (line 324) | func (p *KKVPlugin) determinePanType(panURL string) string {
method extractPassword (line 355) | func (p *KKVPlugin) extractPassword(panURL, contextText string) string {
method setHeaders (line 379) | func (p *KKVPlugin) setHeaders(req *http.Request, referer string) {
method doRequestWithRetry (line 387) | func (p *KKVPlugin) doRequestWithRetry(req *http.Request, client *http...
function init (line 37) | func init() {
type searchItem (line 93) | type searchItem struct
FILE: plugin/labi/labi.go
constant DefaultTimeout (line 37) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 38) | DetailTimeout = 6 * time.Second
constant MaxConcurrency (line 40) | MaxConcurrency = 20
constant MaxIdleConns (line 42) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 43) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 44) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 45) | IdleConnTimeout = 90 * time.Second
function init (line 59) | func init() {
function startCacheCleaner (line 67) | func startCacheCleaner() {
type LabiAsyncPlugin (line 79) | type LabiAsyncPlugin struct
method Search (line 105) | func (p *LabiAsyncPlugin) Search(keyword string, ext map[string]interf...
method SearchWithResult (line 114) | func (p *LabiAsyncPlugin) SearchWithResult(keyword string, ext map[str...
method searchImpl (line 119) | func (p *LabiAsyncPlugin) searchImpl(client *http.Client, keyword stri...
method parseSearchItem (line 177) | func (p *LabiAsyncPlugin) parseSearchItem(s *goquery.Selection, keywor...
method enhanceWithDetails (line 277) | func (p *LabiAsyncPlugin) enhanceWithDetails(client *http.Client, resu...
method doRequestWithRetry (line 338) | func (p *LabiAsyncPlugin) doRequestWithRetry(req *http.Request, client...
method fetchDetailLinksAndImages (line 367) | func (p *LabiAsyncPlugin) fetchDetailLinksAndImages(client *http.Clien...
method fetchDetailLinks (line 457) | func (p *LabiAsyncPlugin) fetchDetailLinks(client *http.Client, itemID...
method isValidNetworkDriveURL (line 463) | func (p *LabiAsyncPlugin) isValidNetworkDriveURL(url string) bool {
function createOptimizedHTTPClient (line 85) | func createOptimizedHTTPClient() *http.Client {
function NewLabiPlugin (line 97) | func NewLabiPlugin() *LabiAsyncPlugin {
function min (line 477) | func min(a, b int) int {
FILE: plugin/leijing/leijing.go
constant BaseURL (line 21) | BaseURL = "https://leijing.xyz"
constant SearchPath (line 22) | SearchPath = "/search"
constant UserAgent (line 23) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW...
constant MaxConcurrency (line 24) | MaxConcurrency = 20
constant MaxPages (line 25) | MaxPages = 1
type LeijingPlugin (line 29) | type LeijingPlugin struct
method Name (line 51) | func (p *LeijingPlugin) Name() string {
method DisplayName (line 56) | func (p *LeijingPlugin) DisplayName() string {
method Description (line 61) | func (p *LeijingPlugin) Description() string {
method Search (line 66) | func (p *LeijingPlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 75) | func (p *LeijingPlugin) SearchWithResult(keyword string, ext map[strin...
method setRequestHeaders (line 80) | func (p *LeijingPlugin) setRequestHeaders(req *http.Request, referer s...
method doRequest (line 93) | func (p *LeijingPlugin) doRequest(client *http.Client, url string, ref...
method searchImpl (line 121) | func (p *LeijingPlugin) searchImpl(client *http.Client, keyword string...
method getResponseReader (line 173) | func (p *LeijingPlugin) getResponseReader(resp *http.Response) (io.Rea...
method extractSearchResults (line 195) | func (p *LeijingPlugin) extractSearchResults(doc *goquery.Document, ke...
method extractTianyiLinks (line 274) | func (p *LeijingPlugin) extractTianyiLinks(text string) []model.Link {
method enrichWithDetailLinks (line 297) | func (p *LeijingPlugin) enrichWithDetailLinks(client *http.Client, res...
method fetchDetailPageLinks (line 349) | func (p *LeijingPlugin) fetchDetailPageLinks(client *http.Client, deta...
method extractDetailPageLinks (line 410) | func (p *LeijingPlugin) extractDetailPageLinks(doc *goquery.Document) ...
method filterValidResults (line 447) | func (p *LeijingPlugin) filterValidResults(results []model.SearchResul...
function NewLeijingPlugin (line 37) | func NewLeijingPlugin() *LeijingPlugin {
function init (line 461) | func init() {
FILE: plugin/libvio/libvio.go
constant BaseURL (line 22) | BaseURL = "https://www.libvio.mov"
constant SearchPath (line 23) | SearchPath = "/search/-------------.html"
constant UserAgent (line 24) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW...
constant MaxConcurrency (line 25) | MaxConcurrency = 20
constant MaxPages (line 26) | MaxPages = 1
type LibvioPlugin (line 30) | type LibvioPlugin struct
method Name (line 53) | func (p *LibvioPlugin) Name() string {
method DisplayName (line 58) | func (p *LibvioPlugin) DisplayName() string {
method Description (line 63) | func (p *LibvioPlugin) Description() string {
method Search (line 68) | func (p *LibvioPlugin) Search(keyword string, ext map[string]interface...
method SearchWithResult (line 77) | func (p *LibvioPlugin) SearchWithResult(keyword string, ext map[string...
method setRequestHeaders (line 82) | func (p *LibvioPlugin) setRequestHeaders(req *http.Request, referer st...
method doRequest (line 95) | func (p *LibvioPlugin) doRequest(client *http.Client, url string, refe...
method searchImpl (line 123) | func (p *LibvioPlugin) searchImpl(client *http.Client, keyword string,...
method getResponseReader (line 185) | func (p *LibvioPlugin) getResponseReader(resp *http.Response) (io.Read...
method extractSearchResults (line 208) | func (p *LibvioPlugin) extractSearchResults(doc *goquery.Document, key...
method enrichWithDetailLinks (line 283) | func (p *LibvioPlugin) enrichWithDetailLinks(client *http.Client, resu...
method fetchDetailPageLinks (line 330) | func (p *LibvioPlugin) fetchDetailPageLinks(client *http.Client, detai...
method extractDownloadPlayLinks (line 428) | func (p *LibvioPlugin) extractDownloadPlayLinks(doc *goquery.Document)...
method fetchPanLink (line 494) | func (p *LibvioPlugin) fetchPanLink(client *http.Client, playURL strin...
method mapPanType (line 604) | func (p *LibvioPlugin) mapPanType(from string, url string) string {
function NewLibvioPlugin (line 39) | func NewLibvioPlugin() *LibvioPlugin {
type PlayLinkInfo (line 422) | type PlayLinkInfo struct
function init (line 647) | func init() {
FILE: plugin/lou1/lou1.go
constant pluginName (line 20) | pluginName = "lou1"
constant defaultPriority (line 21) | defaultPriority = 1
constant baseURL (line 23) | baseURL = "https://www.1lou.me"
constant searchPathFormat (line 24) | searchPathFormat = baseURL + "/search-%s.htm"
constant requestTimeout (line 25) | requestTimeout = 12 * time.Second
constant detailTimeout (line 26) | detailTimeout = 12 * time.Second
constant maxRequestRetries (line 27) | maxRequestRetries = 3
constant retryBaseDelay (line 28) | retryBaseDelay = 200 * time.Millisecond
constant searchLimit (line 29) | searchLimit = 12
constant detailWorkers (line 30) | detailWorkers = 6
constant httpMaxIdleConns (line 31) | httpMaxIdleConns = 64
constant httpMaxIdlePerHost (line 32) | httpMaxIdlePerHost = 16
constant httpMaxConnsPerHost (line 33) | httpMaxConnsPerHost = 32
type Lou1Plugin (line 64) | type Lou1Plugin struct
method Search (line 82) | func (p *Lou1Plugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 91) | func (p *Lou1Plugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 95) | func (p *Lou1Plugin) searchImpl(client *http.Client, keyword string, e...
method fetchSearchResults (line 171) | func (p *Lou1Plugin) fetchSearchResults(client *http.Client, keyword s...
method fetchDetail (line 248) | func (p *Lou1Plugin) fetchDetail(client *http.Client, detailURL string...
method doRequestWithRetry (line 550) | func (p *Lou1Plugin) doRequestWithRetry(req *http.Request, client *htt...
function init (line 69) | func init() {
function NewLou1Plugin (line 74) | func NewLou1Plugin() *Lou1Plugin {
type searchThread (line 164) | type searchThread struct
type detailResult (line 241) | type detailResult struct
function collectDetailTags (line 301) | func collectDetailTags(doc *goquery.Document) []string {
function extractPostDatetime (line 324) | func extractPostDatetime(doc *goquery.Document) time.Time {
function extractLinksFromSelection (line 339) | func extractLinksFromSelection(sel *goquery.Selection) []model.Link {
function classifyLink (line 392) | func classifyLink(raw string) (string, string) {
function extractPassword (line 405) | func extractPassword(node *goquery.Selection) string {
function matchPassword (line 427) | func matchPassword(text string) string {
function substring (line 440) | func substring(text string, start, end int) string {
function encodeKeyword (line 450) | func encodeKeyword(keyword string) string {
function toAbsoluteURL (line 463) | func toAbsoluteURL(href string) string {
function mergeTags (line 477) | func mergeTags(a []string, b []string) []string {
function buildUniqueID (line 496) | func buildUniqueID(detailURL string) string {
function truncateString (line 508) | func truncateString(text string, length int) string {
function filterQuarkLinks (line 516) | func filterQuarkLinks(links []model.Link) []model.Link {
function newHTTPClient (line 529) | func newHTTPClient() *http.Client {
function setHTMLHeaders (line 542) | func setHTMLHeaders(req *http.Request, referer string) {
FILE: plugin/meitizy/meitizy.go
constant PluginName (line 18) | PluginName = "meitizy"
constant DisplayName (line 19) | DisplayName = "美体资源"
constant Description (line 20) | Description = "美体资源 - 影视资源网盘链接搜索"
constant BaseURL (line 21) | BaseURL = "https://video.451024.xyz"
constant SearchPath (line 22) | SearchPath = "/api/search"
constant UserAgent (line 23) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Apple...
constant MaxResults (line 24) | MaxResults = 100
constant RequestTimeout (line 25) | RequestTimeout = 30 * time.Second
constant MaxPageSize (line 26) | MaxPageSize = 1000
constant MaxIdleConns (line 29) | MaxIdleConns = 100
constant MaxIdleConnsPerHost (line 30) | MaxIdleConnsPerHost = 30
constant MaxConnsPerHost (line 31) | MaxConnsPerHost = 50
constant IdleConnTimeout (line 32) | IdleConnTimeout = 90 * time.Second
constant TLSHandshakeTimeout (line 33) | TLSHandshakeTimeout = 10 * time.Second
constant ExpectContinueTimeout (line 34) | ExpectContinueTimeout = 1 * time.Second
type MeitizyPlugin (line 38) | type MeitizyPlugin struct
method Name (line 103) | func (p *MeitizyPlugin) Name() string {
method DisplayName (line 108) | func (p *MeitizyPlugin) DisplayName() string {
method Description (line 113) | func (p *MeitizyPlugin) Description() string {
method Search (line 118) | func (p *MeitizyPlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 127) | func (p *MeitizyPlugin) SearchWithResult(keyword string, ext map[strin...
method searchImpl (line 132) | func (p *MeitizyPlugin) searchImpl(client *http.Client, keyword string...
method convertToSearchResults (line 200) | func (p *MeitizyPlugin) convertToSearchResults(items []apiItem) []mode...
method mapLinkType (line 257) | func (p *MeitizyPlugin) mapLinkType(apiLinkType string) string {
method determineCloudTypeFromURL (line 286) | func (p *MeitizyPlugin) determineCloudTypeFromURL(url string) string {
method parseTime (line 320) | func (p *MeitizyPlugin) parseTime(timeStr string) time.Time {
method doRequestWithRetry (line 344) | func (p *MeitizyPlugin) doRequestWithRetry(req *http.Request, client *...
type searchRequest (line 44) | type searchRequest struct
type searchResponse (line 51) | type searchResponse struct
type apiItem (line 57) | type apiItem struct
function init (line 69) | func init() {
function NewMeitizyPlugin (line 74) | func NewMeitizyPlugin() *MeitizyPlugin {
function createOptimizedHTTPClient (line 84) | func createOptimizedHTTPClient() *http.Client {
FILE: plugin/miaoso/miaoso.go
function init (line 22) | func init() {
constant BaseURL (line 28) | BaseURL = "https://miaosou.fun/api/secendsearch"
constant MaxRetries (line 31) | MaxRetries = 3
constant TimeoutSeconds (line 32) | TimeoutSeconds = 30
constant AESKey (line 35) | AESKey = "4OToScUFOaeVTrHE"
constant AESIV (line 36) | AESIV = "9CLGao1vHKqm17Oz"
type MiaosouPlugin (line 56) | type MiaosouPlugin struct
method Search (line 68) | func (p *MiaosouPlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 77) | func (p *MiaosouPlugin) SearchWithResult(keyword string, ext map[strin...
method searchImpl (line 82) | func (p *MiaosouPlugin) searchImpl(client *http.Client, keyword string...
method setRequestHeaders (line 157) | func (p *MiaosouPlugin) setRequestHeaders(req *http.Request, keyword s...
method doRequestWithRetry (line 167) | func (p *MiaosouPlugin) doRequestWithRetry(req *http.Request, client *...
method convertToSearchResult (line 195) | func (p *MiaosouPlugin) convertToSearchResult(item MiaosouItem) (model...
method cleanHTMLTags (line 247) | func (p *MiaosouPlugin) cleanHTMLTags(text string) string {
method decryptURL (line 256) | func (p *MiaosouPlugin) decryptURL(encryptedURL string) string {
method removePKCS7Padding (line 298) | func (p *MiaosouPlugin) removePKCS7Padding(data []byte) []byte {
method determineCloudType (line 323) | func (p *MiaosouPlugin) determineCloudType(from string) string {
function NewMiaosouPlugin (line 61) | func NewMiaosouPlugin() *MiaosouPlugin {
type MiaosouResponse (line 347) | type MiaosouResponse struct
type MiaosouData (line 353) | type MiaosouData struct
type MiaosouItem (line 358) | type MiaosouItem struct
type MiaosouFileInfo (line 373) | type MiaosouFileInfo struct
FILE: plugin/mikuclub/mikuclub.go
type cacheEntry (line 52) | type cacheEntry struct
constant pluginName (line 58) | pluginName = "mikuclub"
constant defaultPriority (line 59) | defaultPriority = 2
constant searchTimeout (line 60) | searchTimeout = 12 * time.Second
constant detailTimeout (line 61) | detailTimeout = 10 * time.Second
constant maxConcurrency (line 62) | maxConcurrency = 12
constant maxIdleConns (line 63) | maxIdleConns = 64
constant maxIdlePerHost (line 64) | maxIdlePerHost = 16
constant maxConnsPerHost (line 65) | maxConnsPerHost = 32
constant idleConnLifetime (line 66) | idleConnLifetime = 90 * time.Second
constant tlsHandshakeTimeout (line 67) | tlsHandshakeTimeout = 10 * time.Second
constant expectContinueTimeout (line 68) | expectContinueTimeout = 1 * time.Second
constant searchMaxRetries (line 70) | searchMaxRetries = 3
constant detailMaxRetries (line 71) | detailMaxRetries = 2
constant retryBaseDelay (line 72) | retryBaseDelay = 200 * time.Millisecond
type MikuclubPlugin (line 76) | type MikuclubPlugin struct
method Search (line 95) | func (p *MikuclubPlugin) Search(keyword string, ext map[string]interfa...
method SearchWithResult (line 104) | func (p *MikuclubPlugin) SearchWithResult(keyword string, ext map[stri...
method searchImpl (line 125) | func (p *MikuclubPlugin) searchImpl(client *http.Client, keyword strin...
method fetchCategoryPosts (line 223) | func (p *MikuclubPlugin) fetchCategoryPosts(client *http.Client, keywo...
method fetchDetailLinks (line 264) | func (p *MikuclubPlugin) fetchDetailLinks(client *http.Client, postID ...
method doRequestWithRetry (line 444) | func (p *MikuclubPlugin) doRequestWithRetry(req *http.Request, client ...
function init (line 81) | func init() {
function NewMikuclubPlugin (line 87) | func NewMikuclubPlugin() *MikuclubPlugin {
function newHTTPClient (line 108) | func newHTTPClient() *http.Client {
function extractLinksFromSelection (line 317) | func extractLinksFromSelection(sel *goquery.Selection) []model.Link {
function classifyLink (line 376) | func classifyLink(raw string) (string, string) {
function extractPassword (line 385) | func extractPassword(node *goquery.Selection) string {
function matchPassword (line 413) | func matchPassword(text string) string {
function substring (line 426) | func substring(text string, start, end int) string {
function setCommonHeaders (line 436) | func setCommonHeaders(req *http.Request, referer string) {
function startCacheCleaner (line 465) | func startCacheCleaner() {
type postListResponse (line 481) | type postListResponse struct
type postItem (line 485) | type postItem struct
method summary (line 496) | func (p postItem) summary() string {
method tags (line 510) | func (p postItem) tags() []string {
method publishTime (line 521) | func (p postItem) publishTime() time.Time {
FILE: plugin/mizixing/mizixing.go
constant pluginName (line 21) | pluginName = "mizixing"
constant defaultPriority (line 22) | defaultPriority = 3
constant baseURL (line 24) | baseURL = "https://mizixing.com"
constant searchEndpoint (line 25) | searchEndpoint = baseURL + "/"
constant searchLimit (line 26) | searchLimit = 12
constant detailWorkers (line 27) | detailWorkers = 6
constant requestTimeout (line 28) | requestTimeout = 12 * time.Second
constant detailTimeout (line 29) | detailTimeout = 10 * time.Second
constant httpMaxIdleConns (line 30) | httpMaxIdleConns = 64
constant httpMaxIdlePerHost (line 31) | httpMaxIdlePerHost = 16
constant httpMaxConnsPerHost (line 32) | httpMaxConnsPerHost = 32
constant retryBaseDelay (line 33) | retryBaseDelay = 200 * time.Millisecond
constant maxRequestRetries (line 34) | maxRequestRetries = 3
type MizixingPlugin (line 65) | type MizixingPlugin struct
method Search (line 83) | func (p *MizixingPlugin) Search(keyword string, ext map[string]interfa...
method SearchWithResult (line 92) | func (p *MizixingPlugin) SearchWithResult(keyword string, ext map[stri...
method searchImpl (line 96) | func (p *MizixingPlugin) searchImpl(client *http.Client, keyword strin...
method fetchSearchResults (line 172) | func (p *MizixingPlugin) fetchSearchResults(client *http.Client, keywo...
method fetchDetailData (line 237) | func (p *MizixingPlugin) fetchDetailData(client *http.Client, detailUR...
method doRequestWithRetry (line 503) | func (p *MizixingPlugin) doRequestWithRetry(req *http.Request, client ...
function init (line 70) | func init() {
function NewMizixingPlugin (line 75) | func NewMizixingPlugin() *MizixingPlugin {
type searchItem (line 165) | type searchItem struct
type detailData (line 230) | type detailData struct
function collectTags (line 290) | func collectTags(doc *goquery.Document) []string {
function extractDateTime (line 308) | func extractDateTime(doc *goquery.Document) time.Time {
function extractLinksFromSelection (line 338) | func extractLinksFromSelection(sel *goquery.Selection) []model.Link {
function classifyLink (line 390) | func classifyLink(raw string) (string, string) {
function extractPassword (line 403) | func extractPassword(node *goquery.Selection) string {
function matchPassword (line 427) | func matchPassword(text string) string {
function substring (line 440) | func substring(text string, start, end int) string {
function mergeTags (line 450) | func mergeTags(primary string, extra []string) []string {
function buildUniqueID (line 470) | func buildUniqueID(detailURL string) string {
function normalizeURL (line 475) | func normalizeURL(raw string) string {
function newHTTPClient (line 482) | func newHTTPClient() *http.Client {
function setHTMLHeaders (line 495) | func setHTMLHeaders(req *http.Request, referer string) {
FILE: plugin/muou/muou.go
constant DefaultTimeout (line 47) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 48) | DetailTimeout = 6 * time.Second
constant MaxConcurrency (line 51) | MaxConcurrency = 20
constant MaxIdleConns (line 54) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 55) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 56) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 57) | IdleConnTimeout = 90 * time.Second
constant cacheTTL (line 60) | cacheTTL = 1 * time.Hour
function init (line 74) | func init() {
type MuouAsyncPlugin (line 79) | type MuouAsyncPlugin struct
method Search (line 109) | func (p *MuouAsyncPlugin) Search(keyword string, ext map[string]interf...
method SearchWithResult (line 118) | func (p *MuouAsyncPlugin) SearchWithResult(keyword string, ext map[str...
method searchImpl (line 123) | func (p *MuouAsyncPlugin) searchImpl(client *http.Client, keyword stri...
method parseSearchItem (line 194) | func (p *MuouAsyncPlugin) parseSearchItem(s *goquery.Selection, keywor...
method enhanceWithDetails (line 294) | func (p *MuouAsyncPlugin) enhanceWithDetails(client *http.Client, resu...
method doRequestWithRetry (line 357) | func (p *MuouAsyncPlugin) doRequestWithRetry(req *http.Request, client...
method fetchDetailLinksAndImages (line 386) | func (p *MuouAsyncPlugin) fetchDetailLinksAndImages(client *http.Clien...
method fetchDetailLinks (line 488) | func (p *MuouAsyncPlugin) fetchDetailLinks(client *http.Client, itemID...
method isValidNetworkDriveURL (line 494) | func (p *MuouAsyncPlugin) isValidNetworkDriveURL(url string) bool {
method determineLinkType (line 522) | func (p *MuouAsyncPlugin) determineLinkType(url string) string {
method GetPerformanceStats (line 568) | func (p *MuouAsyncPlugin) GetPerformanceStats() map[string]interface{} {
function createOptimizedHTTPClient (line 85) | func createOptimizedHTTPClient() *http.Client {
function NewMuouPlugin (line 101) | func NewMuouPlugin() *MuouAsyncPlugin {
function min (line 560) | func min(a, b int) int {
FILE: plugin/nsgame/nsgame.go
constant pluginName (line 20) | pluginName = "nsgame"
constant apiURL (line 23) | apiURL = "https://nsthwj.com/thwj/game/query"
constant defaultPriority (line 26) | defaultPriority = 2
constant defaultTimeout (line 29) | defaultTimeout = 10 * time.Second
constant pageSize (line 32) | pageSize = 1000
type NSGameAsyncPlugin (line 46) | type NSGameAsyncPlugin struct
method Search (line 85) | func (p *NSGameAsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 94) | func (p *NSGameAsyncPlugin) SearchWithResult(keyword string, ext map[s...
method searchImpl (line 99) | func (p *NSGameAsyncPlugin) searchImpl(client *http.Client, keyword st...
method parseLinks (line 187) | func (p *NSGameAsyncPlugin) parseLinks(urlText string) []model.Link {
method extractURL (line 235) | func (p *NSGameAsyncPlugin) extractURL(text string) string {
method extractBaiduLink (line 241) | func (p *NSGameAsyncPlugin) extractBaiduLink(line string) (url, passwo...
method generateUniqueID (line 265) | func (p *NSGameAsyncPlugin) generateUniqueID(gameName string) string {
method doRequestWithRetry (line 272) | func (p *NSGameAsyncPlugin) doRequestWithRetry(req *http.Request, clie...
type NSGameResponse (line 51) | type NSGameResponse struct
type NSGameItem (line 66) | type NSGameItem struct
function init (line 73) | func init() {
function NewNSGamePlugin (line 78) | func NewNSGamePlugin() *NSGameAsyncPlugin {
FILE: plugin/nyaa/nyaa.go
constant DefaultTimeout (line 29) | DefaultTimeout = 10 * time.Second
constant MaxIdleConns (line 32) | MaxIdleConns = 50
constant MaxIdleConnsPerHost (line 33) | MaxIdleConnsPerHost = 20
constant MaxConnsPerHost (line 34) | MaxConnsPerHost = 30
constant IdleConnTimeout (line 35) | IdleConnTimeout = 90 * time.Second
constant SiteURL (line 38) | SiteURL = "https://nyaa.si"
function init (line 42) | func init() {
type NyaaPlugin (line 47) | type NyaaPlugin struct
method Search (line 74) | func (p *NyaaPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 83) | func (p *NyaaPlugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 88) | func (p *NyaaPlugin) searchImpl(client *http.Client, keyword string, e...
method parseSearchRow (line 158) | func (p *NyaaPlugin) parseSearchRow(s *goquery.Selection) model.Search...
method doRequestWithRetry (line 282) | func (p *NyaaPlugin) doRequestWithRetry(req *http.Request, client *htt...
function createOptimizedHTTPClient (line 53) | func createOptimizedHTTPClient() *http.Client {
function NewNyaaPlugin (line 65) | func NewNyaaPlugin() *NyaaPlugin {
FILE: plugin/ouge/ouge.go
constant DefaultTimeout (line 21) | DefaultTimeout = 8 * time.Second
constant MaxIdleConns (line 24) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 25) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 26) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 27) | IdleConnTimeout = 90 * time.Second
function init (line 36) | func init() {
type OugeAsyncPlugin (line 61) | type OugeAsyncPlugin struct
method Search (line 91) | func (p *OugeAsyncPlugin) Search(keyword string, ext map[string]interf...
method SearchWithResult (line 100) | func (p *OugeAsyncPlugin) SearchWithResult(keyword string, ext map[str...
method searchImpl (line 105) | func (p *OugeAsyncPlugin) searchImpl(client *http.Client, keyword stri...
method parseAPIItem (line 201) | func (p *OugeAsyncPlugin) parseAPIItem(item OugeAPIItem) model.SearchR...
method parseDownloadLinks (line 261) | func (p *OugeAsyncPlugin) parseDownloadLinks(vodDownFrom, vodDownURL s...
method mapCloudType (line 305) | func (p *OugeAsyncPlugin) mapCloudType(apiType, url string) string {
method isValidNetworkDriveURL (line 335) | func (p *OugeAsyncPlugin) isValidNetworkDriveURL(url string) bool {
method determineLinkType (line 360) | func (p *OugeAsyncPlugin) determineLinkType(url string) string {
method extractPassword (line 392) | func (p *OugeAsyncPlugin) extractPassword(url string) string {
method doRequestWithRetry (line 401) | func (p *OugeAsyncPlugin) doRequestWithRetry(req *http.Request, client...
method GetPerformanceStats (line 427) | func (p *OugeAsyncPlugin) GetPerformanceStats() map[string]interface{} {
function createOptimizedHTTPClient (line 67) | func createOptimizedHTTPClient() *http.Client {
function NewOugePlugin (line 83) | func NewOugePlugin() *OugeAsyncPlugin {
type OugeAPIResponse (line 174) | type OugeAPIResponse struct
type OugeAPIItem (line 185) | type OugeAPIItem struct
FILE: plugin/pan666/pan666.go
function init (line 20) | func init() {
constant BaseURL (line 27) | BaseURL = "https://pan666.net/api/discussions"
constant PageSize (line 30) | PageSize = 50
constant MaxRetries (line 31) | MaxRetries = 2
type Pan666AsyncPlugin (line 45) | type Pan666AsyncPlugin struct
method Search (line 59) | func (p *Pan666AsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 68) | func (p *Pan666AsyncPlugin) SearchWithResult(keyword string, ext map[s...
method doSearch (line 73) | func (p *Pan666AsyncPlugin) doSearch(client *http.Client, keyword stri...
method fetchBatch (line 93) | func (p *Pan666AsyncPlugin) fetchBatch(client *http.Client, keyword st...
method deduplicateResults (line 157) | func (p *Pan666AsyncPlugin) deduplicateResults(results []model.SearchR...
method fetchPage (line 177) | func (p *Pan666AsyncPlugin) fetchPage(client *http.Client, keyword str...
function NewPan666AsyncPlugin (line 51) | func NewPan666AsyncPlugin() *Pan666AsyncPlugin {
function generateRandomIP (line 303) | func generateRandomIP() string {
function getRandomUA (line 312) | func getRandomUA() string {
function extractLinks (line 317) | func extractLinks(content string) []model.Link {
function extractLinksByPattern (line 336) | func extractLinksByPattern(content, pattern, altPattern, linkType string...
function extractLinkFromLine (line 365) | func extractLinkFromLine(line, prefix string) model.Link {
function cleanHTML (line 409) | func cleanHTML(html string) string {
function extractLinksFromText (line 458) | func extractLinksFromText(content string) []model.Link {
function extractURLFromText (line 609) | func extractURLFromText(text string) string {
function extractPasswordFromURL (line 641) | func extractPasswordFromURL(url string) string {
function abs (line 669) | func abs(n int) int {
type Pan666Response (line 677) | type Pan666Response struct
type Pan666Discussion (line 687) | type Pan666Discussion struct
type Pan666Post (line 710) | type Pan666Post struct
FILE: plugin/pansearch/pansearch.go
function init (line 35) | func init() {
function startCacheCleaner (line 44) | func startCacheCleaner() {
type cachedResponse (line 57) | type cachedResponse struct
constant WebsiteURL (line 64) | WebsiteURL = "https://www.pansearch.me/search"
constant BaseURLTemplate (line 67) | BaseURLTemplate = "https://www.pansearch.me/_next/data/%s/search.json"
constant DefaultTimeout (line 70) | DefaultTimeout = 6 * time.Second
constant PageSize (line 71) | PageSize = 10
constant MaxResults (line 72) | MaxResults = 1000
constant MaxConcurrent (line 73) | MaxConcurrent = 200
constant MaxRetries (line 74) | MaxRetries = 2
constant MaxAPIPages (line 75) | MaxAPIPages = 100
constant MaxIdleConns (line 78) | MaxIdleConns = 500
constant MaxIdleConnsPerHost (line 79) | MaxIdleConnsPerHost = 200
constant MaxConnsPerHost (line 80) | MaxConnsPerHost = 400
constant IdleConnTimeout (line 81) | IdleConnTimeout = 120 * time.Second
constant TLSHandshakeTimeout (line 82) | TLSHandshakeTimeout = 10 * time.Second
constant ExpectContinueTimeout (line 83) | ExpectContinueTimeout = 1 * time.Second
constant WriteBufferSize (line 84) | WriteBufferSize = 16 * 1024
constant ReadBufferSize (line 85) | ReadBufferSize = 16 * 1024
constant BuildIdCacheDuration (line 88) | BuildIdCacheDuration = 30
type PanSearchAsyncPlugin (line 99) | type PanSearchAsyncPlugin struct
method startBuildIdUpdater (line 262) | func (p *PanSearchAsyncPlugin) startBuildIdUpdater() {
method updateBuildId (line 273) | func (p *PanSearchAsyncPlugin) updateBuildId() {
method Name (line 358) | func (p *PanSearchAsyncPlugin) Name() string {
method Priority (line 363) | func (p *PanSearchAsyncPlugin) Priority() int {
method getBuildId (line 368) | func (p *PanSearchAsyncPlugin) getBuildId() (string, error) {
method getBaseURL (line 482) | func (p *PanSearchAsyncPlugin) getBaseURL(client *http.Client) (string...
method Search (line 492) | func (p *PanSearchAsyncPlugin) Search(keyword string, ext map[string]i...
method SearchWithResult (line 501) | func (p *PanSearchAsyncPlugin) SearchWithResult(keyword string, ext ma...
method doSearch (line 506) | func (p *PanSearchAsyncPlugin) doSearch(client *http.Client, keyword s...
method fetchFirstPage (line 792) | func (p *PanSearchAsyncPlugin) fetchFirstPage(keyword string, baseURL ...
method fetchPage (line 851) | func (p *PanSearchAsyncPlugin) fetchPage(keyword string, offset int, b...
method deduplicateItems (line 906) | func (p *PanSearchAsyncPlugin) deduplicateItems(items []PanSearchItem)...
method convertResults (line 924) | func (p *PanSearchAsyncPlugin) convertResults(items []PanSearchItem, k...
type WorkerPool (line 109) | type WorkerPool struct
method Start (line 141) | func (wp *WorkerPool) Start(ctx context.Context, handler func(ctx cont...
method Submit (line 181) | func (wp *WorkerPool) Submit(task Task) bool {
method Close (line 200) | func (wp *WorkerPool) Close() {
type Task (line 119) | type Task struct
type TaskResult (line 126) | type TaskResult struct
function NewWorkerPool (line 132) | func NewWorkerPool(size int) *WorkerPool {
function NewPanSearchPlugin (line 234) | func NewPanSearchPlugin() *PanSearchAsyncPlugin {
function extractBuildId (line 334) | func extractBuildId(body string) string {
type LinkInfo (line 979) | type LinkInfo struct
function extractLinkAndPassword (line 985) | func extractLinkAndPassword(content string) LinkInfo {
function extractTitle (line 1026) | func extractTitle(content string, keyword string) string {
function cleanHTML (line 1045) | func cleanHTML(html string) string {
function min (line 1084) | func min(a, b int) int {
type PanSearchResponse (line 1092) | type PanSearchResponse struct
type PanSearchItem (line 1106) | type PanSearchItem struct
FILE: plugin/panta/panta.go
type passwordCacheKey (line 100) | type passwordCacheKey struct
constant pluginName (line 108) | pluginName = "panta"
constant searchURLTemplate (line 111) | searchURLTemplate = "https://www.91panta.cn/search?keyword=%s"
constant threadURLTemplate (line 114) | threadURLTemplate = "https://www.91panta.cn/thread?topicId=%s"
constant defaultPriority (line 117) | defaultPriority = 1
constant defaultTimeout (line 120) | defaultTimeout = 6
constant defaultConcurrency (line 123) | defaultConcurrency = 30
constant maxRetries (line 126) | maxRetries = 2
constant minConcurrency (line 129) | minConcurrency = 5
constant maxConcurrency (line 132) | maxConcurrency = 50
constant responseTimeThreshold (line 135) | responseTimeThreshold = 500
constant concurrencyStep (line 138) | concurrencyStep = 5
constant concurrencyAdjustInterval (line 141) | concurrencyAdjustInterval = 30
constant backoffBase (line 144) | backoffBase = 100
constant maxBackoff (line 147) | maxBackoff = 5000
type PantaAsyncPlugin (line 151) | type PantaAsyncPlugin struct
method Name (line 210) | func (p *PantaAsyncPlugin) Name() string {
method Priority (line 215) | func (p *PantaAsyncPlugin) Priority() int {
method Search (line 220) | func (p *PantaAsyncPlugin) Search(keyword string, ext map[string]inter...
method SearchWithResult (line 229) | func (p *PantaAsyncPlugin) SearchWithResult(keyword string, ext map[st...
method doSearch (line 234) | func (p *PantaAsyncPlugin) doSearch(client *http.Client, keyword strin...
method parseSearchResults (line 291) | func (p *PantaAsyncPlugin) parseSearchResults(doc *goquery.Document, c...
method extractLinksFromElement (line 475) | func (p *PantaAsyncPlugin) extractLinksFromElement(s *goquery.Selectio...
method fetchThreadLinks (line 592) | func (p *PantaAsyncPlugin) fetchThreadLinks(topicID string, client *ht...
method startConcurrencyAdjuster (line 1199) | func (p *PantaAsyncPlugin) startConcurrencyAdjuster() {
method adjustConcurrency (line 1209) | func (p *PantaAsyncPlugin) adjustConcurrency() {
method recordResponseTime (line 1239) | func (p *PantaAsyncPlugin) recordResponseTime(d time.Duration) {
method doRequestWithRetry (line 1253) | func (p *PantaAsyncPlugin) doRequestWithRetry(req *http.Request, clien...
function init (line 168) | func init() {
function NewPantaAsyncPlugin (line 174) | func NewPantaAsyncPlugin() *PantaAsyncPlugin {
function startCacheCleaner (line 191) | func startCacheCleaner() {
function extractTextLinks (line 749) | func extractTextLinks(text string, yearFromTitle string) []model.Link {
function extractPassword (line 1085) | func extractPassword(content string, url string) string {
function determineLinkType (line 1138) | func determineLinkType(url string) string {
function isNetDiskLink (line 1176) | func isNetDiskLink(url string) bool {
function max (line 1296) | func max(a, b int) int {
function min (line 1304) | func min(a, b int) int {
FILE: plugin/panwiki/panwiki.go
constant PrimaryBaseURL (line 20) | PrimaryBaseURL = "https://www.panwiki.com"
constant BackupBaseURL (line 21) | BackupBaseURL = "https://pan666.net"
constant SearchPath (line 22) | SearchPath = "/search.php?mod=forum&srchtxt=%s&searchsubmit=yes&or...
constant UserAgent (line 23) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Appl...
constant MaxConcurrency (line 24) | MaxConcurrency = 40
constant MaxPages (line 25) | MaxPages = 2
type PanwikiPlugin (line 29) | type PanwikiPlugin struct
method getSearchURL (line 58) | func (p *PanwikiPlugin) getSearchURL(keyword string, page int) string {
method switchToBackupDomain (line 69) | func (p *PanwikiPlugin) switchToBackupDomain() {
method searchImpl (line 79) | func (p *PanwikiPlugin) searchImpl(client *http.Client, keyword string...
method searchPage (line 158) | func (p *PanwikiPlugin) searchPage(client *http.Client, keyword string...
method setRequestHeaders (line 259) | func (p *PanwikiPlugin) setRequestHeaders(req *http.Request) {
method extractSearchResults (line 269) | func (p *PanwikiPlugin) extractSearchResults(doc *goquery.Document) []...
method parseSearchResult (line 294) | func (p *PanwikiPlugin) parseSearchResult(s *goquery.Selection) model....
method cleanTitle (line 370) | func (p *PanwikiPlugin) cleanTitle(title string) string {
method enrichWithDetailLinks (line 389) | func (p *PanwikiPlugin) enrichWithDetailLinks(client *http.Client, res...
method fetchDetailPageLinksWithKeyword (line 452) | func (p *PanwikiPlugin) fetchDetailPageLinksWithKeyword(client *http.C...
method extractDetailPageLinksWithFilter (line 510) | func (p *PanwikiPlugin) extractDetailPageLinksWithFilter(doc *goquery....
method filterLinksByContext (line 568) | func (p *PanwikiPlugin) filterLinksByContext(links []model.Link, htmlC...
method extractWorkNameForLinkInLine (line 614) | func (p *PanwikiPlugin) extractWorkNameForLinkInLine(line, url string)...
method isSimpleCase (line 634) | func (p *PanwikiPlugin) isSimpleCase(htmlContent, keyword string) bool {
method pageContentRelevant (line 687) | func (p *PanwikiPlugin) pageContentRelevant(htmlContent, keyword strin...
method extractAllLinksDirectly (line 734) | func (p *PanwikiPlugin) extractAllLinksDirectly(contentArea *goquery.S...
method extractLinksWithGrouping (line 793) | func (p *PanwikiPlugin) extractLinksWithGrouping(htmlContent, keyword ...
method isSingleLineFormat (line 862) | func (p *PanwikiPlugin) isSingleLineFormat(lines []string, keyword str...
method extractLinksFromSingleLineFormat (line 904) | func (p *PanwikiPlugin) extractLinksFromSingleLineFormat(lines []strin...
method extractLinksFromSingleLine (line 933) | func (p *PanwikiPlugin) extractLinksFromSingleLine(line, keyword strin...
method isLineTitleRelevant (line 979) | func (p *PanwikiPlugin) isLineTitleRelevant(line, keyword string) bool {
method containsNetworkLink (line 1012) | func (p *PanwikiPlugin) containsNetworkLink(text string) bool {
method cleanHtmlText (line 1028) | func (p *PanwikiPlugin) cleanHtmlText(html string) string {
method isNewWorkTitle (line 1039) | func (p *PanwikiPlugin) isNewWorkTitle(text string) bool {
method isWorkTitleRelevant (line 1159) | func (p *PanwikiPlugin) isWorkTitleRelevant(title, keyword string) bool {
method extractLinksFromGroup (line 1209) | func (p *PanwikiPlugin) extractLinksFromGroup(group []string) []model....
method determineLinkType (line 1245) | func (p *PanwikiPlugin) determineLinkType(url string) string {
method extractLinksFromText (line 1271) | func (p *PanwikiPlugin) extractLinksFromText(text string) []model.Link {
method deduplicateLinks (line 1309) | func (p *PanwikiPlugin) deduplicateLinks(links []model.Link) []model.L...
method extractPasswordFromURL (line 1354) | func (p *PanwikiPlugin) extractPasswordFromURL(rawURL string) (normali...
method extractDetailURLFromContent (line 1396) | func (p *PanwikiPlugin) extractDetailURLFromContent(content string) st...
method Search (line 1443) | func (p *PanwikiPlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 1452) | func (p *PanwikiPlugin) SearchWithResult(keyword string, ext map[strin...
method extractPasswordFromContent (line 1457) | func (p *PanwikiPlugin) extractPasswordFromContent(content, linkURL st...
function NewPanwikiPlugin (line 38) | func NewPanwikiPlugin() *PanwikiPlugin {
type cacheItem (line 1390) | type cacheItem struct
function parseStats (line 1407) | func parseStats(statsText string, replyCount, viewCount *int) {
function parseTime (line 1421) | func parseTime(timeStr string) time.Time {
function init (line 1500) | func init() {
FILE: plugin/panyq/panyq.go
constant DefaultTimeout (line 26) | DefaultTimeout = 15 * time.Second
constant MaxConcurrency (line 28) | MaxConcurrency = 100
constant MaxRetries (line 30) | MaxRetries = 0
constant DebugLog (line 32) | DebugLog = false
constant ConfigFileName (line 34) | ConfigFileName = "panyq_config.json"
constant BaseURL (line 36) | BaseURL = "https://panyq.com"
constant EnableRefererCheck (line 38) | EnableRefererCheck = false
type Credentials (line 49) | type Credentials struct
type SearchHit (line 56) | type SearchHit struct
type SearchResponse (line 63) | type SearchResponse struct
type PanyqPlugin (line 93) | type PanyqPlugin struct
method Search (line 132) | func (p *PanyqPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 194) | func (p *PanyqPlugin) SearchWithResult(keyword string, ext map[string]...
method doSearch (line 199) | func (p *PanyqPlugin) doSearch(client *http.Client, keyword string, ex...
method getOrDiscoverActionIDs (line 375) | func (p *PanyqPlugin) getOrDiscoverActionIDs() (map[string]string, err...
method discoverActionIDs (line 398) | func (p *PanyqPlugin) discoverActionIDs() (map[string]string, error) {
method findPotentialActionIDs (line 647) | func (p *PanyqPlugin) findPotentialActionIDs(client *http.Client) ([]s...
method validateCredentialID (line 742) | func (p *PanyqPlugin) validateCredentialID(actionID string) bool {
method validateIntermediateID (line 748) | func (p *PanyqPlugin) validateIntermediateID(actionID, testHash, testS...
method validateFinalLinkID (line 754) | func (p *PanyqPlugin) validateFinalLinkID(actionID, testEID string) bo...
method doRequestWithRetry (line 780) | func (p *PanyqPlugin) doRequestWithRetry(client *http.Client, req *htt...
method getRawFinalLinkResponse (line 833) | func (p *PanyqPlugin) getRawFinalLinkResponse(actionID, eid string, cl...
method getCredentials (line 941) | func (p *PanyqPlugin) getCredentials(query, actionID string, client *h...
method getSearchResults (line 993) | func (p *PanyqPlugin) getSearchResults(sign string, pageNum int, clien...
method performIntermediateStep (line 1039) | func (p *PanyqPlugin) performIntermediateStep(actionID, hashVal, shaVa...
method getFinalLink (line 1103) | func (p *PanyqPlugin) getFinalLink(actionID, eid string, client *http....
method determineLinkType (line 1164) | func (p *PanyqPlugin) determineLinkType(url string) string {
method extractPassword (line 1204) | func (p *PanyqPlugin) extractPassword(url string, linkType string) str...
method cleanEscapedHTML (line 1231) | func (p *PanyqPlugin) cleanEscapedHTML(text string) string {
method extractTitle (line 1299) | func (p *PanyqPlugin) extractTitle(desc string) string {
method loadActionIDsFromFile (line 1365) | func (p *PanyqPlugin) loadActionIDsFromFile() (map[string]string, erro...
method saveActionIDsToFile (line 1389) | func (p *PanyqPlugin) saveActionIDsToFile(ids map[string]string) error {
function NewPanyqPlugin (line 99) | func NewPanyqPlugin() *PanyqPlugin {
function isRetriableError (line 815) | func isRetriableError(err error) bool {
function init (line 1331) | func init() {
function startCacheCleaner (line 1339) | func startCacheCleaner() {
FILE: plugin/pianku/pianku.go
function init (line 19) | func init() {
constant BaseURL (line 25) | BaseURL = "https://btnull.pro"
constant SearchPath (line 26) | SearchPath = "/search/-------------.html"
constant MaxRetries (line 29) | MaxRetries = 3
constant TimeoutSeconds (line 30) | TimeoutSeconds = 30
type PiankuPlugin (line 85) | type PiankuPlugin struct
method Search (line 97) | func (p *PiankuPlugin) Search(keyword string, ext map[string]interface...
method SearchWithResult (line 106) | func (p *PiankuPlugin) SearchWithResult(keyword string, ext map[string...
method searchImpl (line 111) | func (p *PiankuPlugin) searchImpl(client *http.Client, keyword string,...
method setRequestHeaders (line 188) | func (p *PiankuPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 197) | func (p *PiankuPlugin) doRequestWithRetry(req *http.Request, client *h...
method extractSearchResults (line 225) | func (p *PiankuPlugin) extractSearchResults(doc *goquery.Document) []m...
method extractSingleResult (line 240) | func (p *PiankuPlugin) extractSingleResult(s *goquery.Selection) model...
method extractMovieID (line 349) | func (p *PiankuPlugin) extractMovieID(url string) string {
method buildFullURL (line 368) | func (p *PiankuPlugin) buildFullURL(path string) string {
method fetchDetailPageLinks (line 376) | func (p *PiankuPlugin) fetchDetailPageLinks(client *http.Client, detai...
method extractDownloadLinks (line 413) | func (p *PiankuPlugin) extractDownloadLinks(doc *goquery.Document) []m...
method isValidLink (line 461) | func (p *PiankuPlugin) isValidLink(url string) bool {
method determineLinkType (line 484) | func (p *PiankuPlugin) determineLinkType(url string) string {
method extractPassword (line 506) | func (p *PiankuPlugin) extractPassword(url, title string) string {
function NewPiankuPlugin (line 90) | func NewPiankuPlugin() *PiankuPlugin {
function parseRegionAndTypes (line 358) | func parseRegionAndTypes(text string) (region, types string) {
FILE: plugin/plugin.go
type AsyncSearchPlugin (line 21) | type AsyncSearchPlugin interface
type PluginWithWebHandler (line 47) | type PluginWithWebHandler interface
type InitializablePlugin (line 57) | type InitializablePlugin interface
type cachedResponse (line 115) | type cachedResponse struct
function RegisterGlobalPlugin (line 128) | func RegisterGlobalPlugin(plugin AsyncSearchPlugin) {
function GetRegisteredPlugins (line 145) | func GetRegisteredPlugins() []AsyncSearchPlugin {
function GetPluginByName (line 158) | func GetPluginByName(name string) (AsyncSearchPlugin, bool) {
type PluginManager (line 167) | type PluginManager struct
method RegisterPlugin (line 179) | func (pm *PluginManager) RegisterPlugin(plugin AsyncSearchPlugin) {
method RegisterAllGlobalPlugins (line 192) | func (pm *PluginManager) RegisterAllGlobalPlugins() {
method RegisterGlobalPluginsWithFilter (line 201) | func (pm *PluginManager) RegisterGlobalPluginsWithFilter(enabledPlugin...
method GetPlugins (line 229) | func (pm *PluginManager) GetPlugins() []AsyncSearchPlugin {
function NewPluginManager (line 172) | func NewPluginManager() *PluginManager {
function FilterResultsByKeyword (line 238) | func FilterResultsByKeyword(results []model.SearchResult, keyword string...
function cleanupExpiredApiCache (line 280) | func cleanupExpiredApiCache() {
function initAsyncPlugin (line 324) | func initAsyncPlugin() {
function InitAsyncPluginSystem (line 346) | func InitAsyncPluginSystem() {
function acquireWorkerSlot (line 351) | func acquireWorkerSlot() bool {
function releaseWorkerSlot (line 374) | func releaseWorkerSlot() {
function recordCacheHit (line 380) | func recordCacheHit() {
function recordCacheMiss (line 385) | func recordCacheMiss() {
function recordAsyncCompletion (line 390) | func recordAsyncCompletion() {
function recordCacheAccess (line 395) | func recordCacheAccess(key string) {
type BaseAsyncPlugin (line 420) | type BaseAsyncPlugin struct
method SetMainCacheKey (line 507) | func (p *BaseAsyncPlugin) SetMainCacheKey(key string) {
method SetCurrentKeyword (line 512) | func (p *BaseAsyncPlugin) SetCurrentKeyword(keyword string) {
method SetMainCacheUpdater (line 517) | func (p *BaseAsyncPlugin) SetMainCacheUpdater(updater func(string, []m...
method Name (line 522) | func (p *BaseAsyncPlugin) Name() string {
method Priority (line 527) | func (p *BaseAsyncPlugin) Priority() int {
method SkipServiceFilter (line 532) | func (p *BaseAsyncPlugin) SkipServiceFilter() bool {
method GetClient (line 537) | func (p *BaseAsyncPlugin) GetClient() *http.Client {
method AsyncSearch (line 546) | func (p *BaseAsyncPlugin) AsyncSearch(
method AsyncSearchWithResult (line 803) | func (p *BaseAsyncPlugin) AsyncSearchWithResult(
method completeSearchInBackground (line 979) | func (p *BaseAsyncPlugin) completeSearchInBackground(
method refreshCacheInBackground (line 1022) | func (p *BaseAsyncPlugin) refreshCacheInBackground(
method updateMainCache (line 1094) | func (p *BaseAsyncPlugin) updateMainCache(cacheKey string, results []m...
method updateMainCacheWithFinal (line 1099) | func (p *BaseAsyncPlugin) updateMainCacheWithFinal(cacheKey string, re...
method hasUpdatedFinalCache (line 1137) | func (p *BaseAsyncPlugin) hasUpdatedFinalCache(updateKey string) bool {
method markFinalCacheUpdated (line 1144) | func (p *BaseAsyncPlugin) markFinalCacheUpdated(updateKey string) {
function NewBaseAsyncPlugin (line 435) | func NewBaseAsyncPlugin(name string, priority int) *BaseAsyncPlugin {
function NewBaseAsyncPluginWithFilter (line 469) | func NewBaseAsyncPluginWithFilter(name string, priority int, skipService...
function SetGlobalCacheSerializer (line 1155) | func SetGlobalCacheSerializer(serializer interface {
function getEnhancedCacheSerializer (line 1163) | func getEnhancedCacheSerializer() interface {
FILE: plugin/qingying/qingying.go
constant baseURL (line 20) | baseURL = "http://revohd.com"
constant searchPath (line 21) | searchPath = "/vodsearch/-------------.html"
constant maxResults (line 22) | maxResults = 10
constant maxConcurrent (line 23) | maxConcurrent = 3
function debugPrintf (line 28) | func debugPrintf(format string, args ...interface{}) {
type QingYingPlugin (line 34) | type QingYingPlugin struct
method Search (line 45) | func (p *QingYingPlugin) Search(keyword string, ext map[string]interfa...
method SearchWithResult (line 53) | func (p *QingYingPlugin) SearchWithResult(keyword string, ext map[stri...
method searchImpl (line 57) | func (p *QingYingPlugin) searchImpl(client *http.Client, keyword strin...
method filterItemsByKeyword (line 100) | func (p *QingYingPlugin) filterItemsByKeyword(items []searchItem, keyw...
method fetchSearchResults (line 117) | func (p *QingYingPlugin) fetchSearchResults(searchURL string, client *...
method processDetailPages (line 186) | func (p *QingYingPlugin) processDetailPages(items []searchItem, client...
method processDetailPage (line 212) | func (p *QingYingPlugin) processDetailPage(item searchItem, client *ht...
method extract123PanLink (line 314) | func (p *QingYingPlugin) extract123PanLink(doc *goquery.Document) *mod...
method parseUpdateTimeFromHTML (line 366) | func (p *QingYingPlugin) parseUpdateTimeFromHTML(timeText string) time...
method extractPassword (line 386) | func (p *QingYingPlugin) extractPassword(panURL string) string {
method buildAbsURL (line 405) | func (p *QingYingPlugin) buildAbsURL(path string) string {
method setHeaders (line 418) | func (p *QingYingPlugin) setHeaders(req *http.Request, referer string) {
method doRequestWithRetry (line 426) | func (p *QingYingPlugin) doRequestWithRetry(req *http.Request, client ...
function init (line 38) | func init() {
type searchItem (line 94) | type searchItem struct
function min (line 307) | func min(a, b int) int {
FILE: plugin/qqpd/qqpd.go
constant MaxConcurrentUsers (line 33) | MaxConcurrentUsers = 10
constant MaxConcurrentChannels (line 34) | MaxConcurrentChannels = 50
constant DebugLog (line 35) | DebugLog = false
constant HTMLTemplate (line 44) | HTMLTemplate = `<!DOCTYPE html>
type QQPDPlugin (line 477) | type QQPDPlugin struct
method Initialize (line 520) | func (p *QQPDPlugin) Initialize() error {
method RegisterWebRoutes (line 556) | func (p *QQPDPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
method Search (line 565) | func (p *QQPDPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 574) | func (p *QQPDPlugin) SearchWithResult(keyword string, ext map[string]i...
method loadAllUsers (line 636) | func (p *QQPDPlugin) loadAllUsers() {
method getUserByHash (line 668) | func (p *QQPDPlugin) getUserByHash(hash string) (*User, bool) {
method saveUser (line 677) | func (p *QQPDPlugin) saveUser(user *User) error {
method persistUser (line 686) | func (p *QQPDPlugin) persistUser(user *User) error {
method deleteUser (line 698) | func (p *QQPDPlugin) deleteUser(hash string) error {
method getActiveUsers (line 708) | func (p *QQPDPlugin) getActiveUsers() []*User {
method handleManagePage (line 773) | func (p *QQPDPlugin) handleManagePage(c *gin.Context) {
method handleManagePagePOST (line 789) | func (p *QQPDPlugin) handleManagePagePOST(c *gin.Context) {
method handleGetStatus (line 832) | func (p *QQPDPlugin) handleGetStatus(c *gin.Context, hash string) {
method handleRefreshQRCode (line 923) | func (p *QQPDPlugin) handleRefreshQRCode(c *gin.Context, hash string) {
method handleLogout (line 950) | func (p *QQPDPlugin) handleLogout(c *gin.Context, hash string) {
method handleCheckLogin (line 977) | func (p *QQPDPlugin) handleCheckLogin(c *gin.Context, hash string) {
method handleManualLogin (line 1038) | func (p *QQPDPlugin) handleManualLogin(c *gin.Context, hash string, re...
method handleSetChannelsWithData (line 1092) | func (p *QQPDPlugin) handleSetChannelsWithData(c *gin.Context, hash st...
method handleTestSearchWithData (line 1224) | func (p *QQPDPlugin) handleTestSearchWithData(c *gin.Context, hash str...
method buildChannelTasks (line 1312) | func (p *QQPDPlugin) buildChannelTasks(users []*User) []ChannelTask {
method executeTasks (line 1373) | func (p *QQPDPlugin) executeTasks(tasks []ChannelTask, keyword string)...
method extractGuildIDFromChannelNumber (line 1405) | func (p *QQPDPlugin) extractGuildIDFromChannelNumber(channelNumber str...
method searchSingleChannel (line 1458) | func (p *QQPDPlugin) searchSingleChannel(keyword, cookieStr, channelID...
method extractResultInfo (line 1638) | func (p *QQPDPlugin) extractResultInfo(item map[string]interface{}, ch...
method extractLinksFromContent (line 1687) | func (p *QQPDPlugin) extractLinksFromContent(content string) []model.L...
method checkQRLoginStatus (line 1757) | func (p *QQPDPlugin) checkQRLoginStatus(qrsig string) (*LoginResult, e...
method extractLoginInfo (line 1836) | func (p *QQPDPlugin) extractLoginInfo(responseText string) (string, st...
method fetchFullCookie (line 1873) | func (p *QQPDPlugin) fetchFullCookie(uin, ptsigx, setCookieHeader stri...
method parseSetCookieHeader (line 1935) | func (p *QQPDPlugin) parseSetCookieHeader(setCookie string) (string, s...
method refreshCookie (line 1965) | func (p *QQPDPlugin) refreshCookie(cookieStr string) string {
method maskQQ (line 2057) | func (p *QQPDPlugin) maskQQ(uin string) string {
method generateQRCodeWithSig (line 2069) | func (p *QQPDPlugin) generateQRCodeWithSig() ([]byte, string, error) {
method testCookieValid (line 2137) | func (p *QQPDPlugin) testCookieValid(cookieStr string) bool {
method generateHash (line 2241) | func (p *QQPDPlugin) generateHash(qq string) string {
method normalizeChannel (line 2252) | func (p *QQPDPlugin) normalizeChannel(input string) string {
method isHexString (line 2268) | func (p *QQPDPlugin) isHexString(s string) bool {
method startCleanupTask (line 2365) | func (p *QQPDPlugin) startCleanupTask() {
method cleanupExpiredUsers (line 2378) | func (p *QQPDPlugin) cleanupExpiredUsers() int {
method markInactiveUsers (line 2400) | func (p *QQPDPlugin) markInactiveUsers() int {
type User (line 485) | type User struct
type ChannelTask (line 504) | type ChannelTask struct
function init (line 511) | func init() {
type LoginResult (line 1750) | type LoginResult struct
function extractQrsig (line 2106) | func extractQrsig(setCookie string) string {
function getptqrtoken (line 2118) | func getptqrtoken(qrsig string) string {
function bkn (line 2127) | func bkn(skey string) int64 {
function parseCookieString (line 2208) | func parseCookieString(cookieStr string) map[string]string {
function respondSuccess (line 2278) | func respondSuccess(c *gin.Context, message string, data interface{}) {
function respondError (line 2287) | func respondError(c *gin.Context, message string) {
function getEncryptionKey (line 2298) | func getEncryptionKey() []byte {
function encryptCookie (line 2307) | func encryptCookie(plaintext string) (string, error) {
function decryptCookie (line 2330) | func decryptCookie(encrypted string) (string, error) {
FILE: plugin/quark4k/quark4k.go
function init (line 20) | func init() {
constant BaseURL (line 27) | BaseURL = "https://quark4k.com/api/discussions"
constant PageSize (line 30) | PageSize = 50
constant MaxRetries (line 31) | MaxRetries = 2
type Quark4KAsyncPlugin (line 45) | type Quark4KAsyncPlugin struct
method Search (line 59) | func (p *Quark4KAsyncPlugin) Search(keyword string, ext map[string]int...
method SearchWithResult (line 68) | func (p *Quark4KAsyncPlugin) SearchWithResult(keyword string, ext map[...
method doSearch (line 73) | func (p *Quark4KAsyncPlugin) doSearch(client *http.Client, keyword str...
method fetchBatch (line 93) | func (p *Quark4KAsyncPlugin) fetchBatch(client *http.Client, keyword s...
method deduplicateResults (line 157) | func (p *Quark4KAsyncPlugin) deduplicateResults(results []model.Search...
method fetchPage (line 177) | func (p *Quark4KAsyncPlugin) fetchPage(client *http.Client, keyword st...
function NewQuark4KAsyncPlugin (line 51) | func NewQuark4KAsyncPlugin() *Quark4KAsyncPlugin {
function generateRandomIP (line 330) | func generateRandomIP() string {
function getRandomUA (line 339) | func getRandomUA() string {
function cleanHTML (line 344) | func cleanHTML(html string) string {
function extractQuarkLinksFromText (line 393) | func extractQuarkLinksFromText(content string) []model.Link {
function extractURLFromText (line 506) | func extractURLFromText(text string) string {
function extractPasswordFromURL (line 538) | func extractPasswordFromURL(url string) string {
function abs (line 566) | func abs(n int) int {
type Quark4KResponse (line 574) | type Quark4KResponse struct
type Quark4KLinks (line 581) | type Quark4KLinks struct
type Quark4KDiscussion (line 587) | type Quark4KDiscussion struct
type Quark4KDiscussionAttributes (line 595) | type Quark4KDiscussionAttributes struct
type Quark4KRelationships (line 608) | type Quark4KRelationships struct
type Quark4KPostRef (line 613) | type Quark4KPostRef struct
type Quark4KPostData (line 618) | type Quark4KPostData struct
type Quark4KIncludedItem (line 624) | type Quark4KIncludedItem struct
type Quark4KPost (line 631) | type Quark4KPost struct
type Quark4KPostAttributes (line 638) | type Quark4KPostAttributes struct
FILE: plugin/quarksoo/quarksoo.go
function init (line 20) | func init() {
constant BaseURL (line 27) | BaseURL = "https://quarksoo.cc/search.php"
constant MaxRetries (line 30) | MaxRetries = 2
type QuarksooAsyncPlugin (line 44) | type QuarksooAsyncPlugin struct
method Search (line 58) | func (p *QuarksooAsyncPlugin) Search(keyword string, ext map[string]in...
method SearchWithResult (line 67) | func (p *QuarksooAsyncPlugin) SearchWithResult(keyword string, ext map...
method doSearch (line 72) | func (p *QuarksooAsyncPlugin) doSearch(client *http.Client, keyword st...
method parseSearchResults (line 146) | func (p *QuarksooAsyncPlugin) parseSearchResults(htmlContent string, k...
method deduplicateResults (line 220) | func (p *QuarksooAsyncPlugin) deduplicateResults(results []model.Searc...
function NewQuarksooAsyncPlugin (line 50) | func NewQuarksooAsyncPlugin() *QuarksooAsyncPlugin {
function generateRandomIP (line 241) | func generateRandomIP() string {
function getRandomUA (line 250) | func getRandomUA() string {
FILE: plugin/qupanshe/qupanshe.go
constant BaseURL (line 22) | BaseURL = "https://www.qupanshe.com"
constant UserAgent (line 23) | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537....
constant MaxRetries (line 24) | MaxRetries = 3
type QupanshePlugin (line 32) | type QupanshePlugin struct
method Search (line 44) | func (p *QupanshePlugin) Search(keyword string, ext map[string]interfa...
method SearchWithResult (line 53) | func (p *QupanshePlugin) SearchWithResult(keyword string, ext map[stri...
method searchImpl (line 58) | func (p *QupanshePlugin) searchImpl(client *http.Client, keyword strin...
method createSessionClient (line 119) | func (p *QupanshePlugin) createSessionClient(baseClient *http.Client) ...
method getFormhash (line 141) | func (p *QupanshePlugin) getFormhash(client *http.Client) (string, err...
method postSearchRequest (line 218) | func (p *QupanshePlugin) postSearchRequest(client *http.Client, keywor...
method getSearchResults (line 312) | func (p *QupanshePlugin) getSearchResults(client *http.Client, searchU...
method extractSearchResults (line 364) | func (p *QupanshePlugin) extractSearchResults(doc *goquery.Document) [...
method parseSearchResult (line 394) | func (p *QupanshePlugin) parseSearchResult(s *goquery.Selection) model...
method cleanTitle (line 509) | func (p *QupanshePlugin) cleanTitle(titleHTML string) string {
method determineLinkType (line 525) | func (p *QupanshePlugin) determineLinkType(urlStr string) string {
method extractLinksFromText (line 554) | func (p *QupanshePlugin) extractLinksFromText(text string) []model.Link {
method extractPasswordFromContent (line 597) | func (p *QupanshePlugin) extractPasswordFromContent(content, linkURL s...
method deduplicateLinks (line 643) | func (p *QupanshePlugin) deduplicateLinks(links []model.Link) []model....
method extractPasswordFromURL (line 683) | func (p *QupanshePlugin) extractPasswordFromURL(rawURL string) (normal...
method parseStats (line 719) | func (p *QupanshePlugin) parseStats(statsText string, replyCount, view...
method parseTime (line 734) | func (p *QupanshePlugin) parseTime(timeStr string) time.Time {
method doRequestWithRetry (line 756) | func (p *QupanshePlugin) doRequestWithRetry(req *http.Request, client ...
method setRequestHeaders (line 793) | func (p *QupanshePlugin) setRequestHeaders(req *http.Request) {
function NewQupanshePlugin (line 37) | func NewQupanshePlugin() *QupanshePlugin {
function init (line 804) | func init() {
FILE: plugin/qupansou/qupansou.go
function init (line 30) | func init() {
function startCacheCleaner (line 39) | func startCacheCleaner() {
constant ApiURL (line 53) | ApiURL = "https://v.funletu.com/search"
constant DefaultTimeout (line 56) | DefaultTimeout = 6 * time.Second
constant DefaultPageSize (line 59) | DefaultPageSize = 1000
type QuPanSouAsyncPlugin (line 63) | type QuPanSouAsyncPlugin struct
method Name (line 82) | func (p *QuPanSouAsyncPlugin) Name() string {
method Priority (line 87) | func (p *QuPanSouAsyncPlugin) Priority() int {
method Search (line 92) | func (p *QuPanSouAsyncPlugin) Search(keyword string, ext map[string]in...
method SearchWithResult (line 101) | func (p *QuPanSouAsyncPlugin) SearchWithResult(keyword string, ext map...
method doSearch (line 106) | func (p *QuPanSouAsyncPlugin) doSearch(client *http.Client, keyword st...
method searchAPI (line 126) | func (p *QuPanSouAsyncPlugin) searchAPI(keyword string, client *http.C...
method convertResults (line 195) | func (p *QuPanSouAsyncPlugin) convertResults(items []QuPanSouItem) []m...
method determineLinkType (line 248) | func (p *QuPanSouAsyncPlugin) determineLinkType(url string) string {
function NewQuPanSouPlugin (line 69) | func NewQuPanSouPlugin() *QuPanSouAsyncPlugin {
type cachedResponse (line 120) | type cachedResponse struct
function cleanHTML (line 279) | func cleanHTML(html string) string {
type QuPanSouResponse (line 302) | type QuPanSouResponse struct
type QuPanSouItem (line 311) | type QuPanSouItem struct
FILE: plugin/sdso/sdso.go
constant DebugLog (line 23) | DebugLog = false
constant DefaultPagesPerType (line 25) | DefaultPagesPerType = 2
constant MaxAllowedPagesPerType (line 27) | MaxAllowedPagesPerType = 5
constant AESKey (line 29) | AESKey = "4OToScUFOaeVTrHE"
constant AESIV (line 30) | AESIV = "9CLGao1vHKqm17Oz"
type SDSOPlugin (line 37) | type SDSOPlugin struct
method Search (line 92) | func (p *SDSOPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 101) | func (p *SDSOPlugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 106) | func (p *SDSOPlugin) searchImpl(client *http.Client, keyword string, e...
method fetchSinglePageWithType (line 223) | func (p *SDSOPlugin) fetchSinglePageWithType(client *http.Client, keyw...
method doRequestWithRetry (line 347) | func (p *SDSOPlugin) doRequestWithRetry(req *http.Request, client *htt...
type APIResponse (line 42) | type APIResponse struct
type DataItem (line 52) | type DataItem struct
type FileInfo (line 68) | type FileInfo struct
type PageResult (line 77) | type PageResult struct
function init (line 84) | func init() {
function DecryptURL (line 378) | func DecryptURL(encryptedURL string) (string, error) {
function removePKCS7Padding (line 427) | func removePKCS7Padding(data []byte) ([]byte, error) {
function cleanHTMLTags (line 452) | func cleanHTMLTags(text string) string {
function mapPanType (line 465) | func mapPanType(from string) string {
function isValidPanURL (line 481) | func isValidPanURL(url string) bool {
FILE: plugin/shandian/shandian.go
constant DefaultTimeout (line 37) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 38) | DetailTimeout = 6 * time.Second
constant MaxConcurrency (line 40) | MaxConcurrency = 20
constant MaxIdleConns (line 42) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 43) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 44) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 45) | IdleConnTimeout = 90 * time.Second
function init (line 59) | func init() {
function startCacheCleaner (line 67) | func startCacheCleaner() {
type ShandianAsyncPlugin (line 79) | type ShandianAsyncPlugin struct
method Search (line 105) | func (p *ShandianAsyncPlugin) Search(keyword string, ext map[string]in...
method SearchWithResult (line 114) | func (p *ShandianAsyncPlugin) SearchWithResult(keyword string, ext map...
method searchImpl (line 119) | func (p *ShandianAsyncPlugin) searchImpl(client *http.Client, keyword ...
method parseSearchItem (line 177) | func (p *ShandianAsyncPlugin) parseSearchItem(s *goquery.Selection, ke...
method enhanceWithDetails (line 270) | func (p *ShandianAsyncPlugin) enhanceWithDetails(client *http.Client, ...
method doRequestWithRetry (line 326) | func (p *ShandianAsyncPlugin) doRequestWithRetry(req *http.Request, cl...
method fetchDetailLinks (line 355) | func (p *ShandianAsyncPlugin) fetchDetailLinks(client *http.Client, it...
method isValidNetworkDriveURL (line 439) | func (p *ShandianAsyncPlugin) isValidNetworkDriveURL(url string) bool {
function createOptimizedHTTPClient (line 85) | func createOptimizedHTTPClient() *http.Client {
function NewShandianPlugin (line 97) | func NewShandianPlugin() *ShandianAsyncPlugin {
function min (line 453) | func min(a, b int) int {
FILE: plugin/sousou/sousou.go
function debugLog (line 20) | func debugLog(format string, args ...interface{}) {
function init (line 27) | func init() {
constant SousouAPI (line 33) | SousouAPI = "https://sousou.pro/api.php"
constant DefaultPerSize (line 36) | DefaultPerSize = 30
constant DefaultMaxPages (line 37) | DefaultMaxPages = 3
type SousouAsyncPlugin (line 51) | type SousouAsyncPlugin struct
method Search (line 63) | func (p *SousouAsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 72) | func (p *SousouAsyncPlugin) SearchWithResult(keyword string, ext map[s...
method doSearch (line 77) | func (p *SousouAsyncPlugin) doSearch(client *http.Client, keyword stri...
method searchByType (line 151) | func (p *SousouAsyncPlugin) searchByType(client *http.Client, keyword ...
method deduplicateItems (line 281) | func (p *SousouAsyncPlugin) deduplicateItems(items []SousouItem) []Sou...
method convertResults (line 336) | func (p *SousouAsyncPlugin) convertResults(items []SousouItem) []model...
method convertDiskType (line 399) | func (p *SousouAsyncPlugin) convertDiskType(diskType string) string {
method processTags (line 427) | func (p *SousouAsyncPlugin) processTags(tags interface{}) []string {
function NewSousouAsyncPlugin (line 56) | func NewSousouAsyncPlugin() *SousouAsyncPlugin {
function min (line 273) | func min(a, b int) int {
type SousouResponse (line 449) | type SousouResponse struct
type SousouItem (line 461) | type SousouItem struct
FILE: plugin/susu/susu.go
function init (line 50) | func init() {
function startCacheCleaner (line 62) | func startCacheCleaner() {
function getRandomUA (line 77) | func getRandomUA() string {
constant SearchURL (line 83) | SearchURL = "https://susuifa.com/?type=post&s=%s"
constant ButtonListURL (line 85) | ButtonListURL = "https://susuifa.com/wp-json/b2/v1/getDownloadData?post_...
constant ButtonDetailURL (line 87) | ButtonDetailURL = "https://susuifa.com/wp-json/b2/v1/getDownloadPageData...
constant MaxRetries (line 89) | MaxRetries = 0
constant MaxConcurrency (line 91) | MaxConcurrency = 100
type SusuAsyncPlugin (line 95) | type SusuAsyncPlugin struct
method Search (line 107) | func (p *SusuAsyncPlugin) Search(keyword string, ext map[string]interf...
method SearchWithResult (line 116) | func (p *SusuAsyncPlugin) SearchWithResult(keyword string, ext map[str...
method doSearch (line 121) | func (p *SusuAsyncPlugin) doSearch(client *http.Client, keyword string...
method extractPostID (line 277) | func (p *SusuAsyncPlugin) extractPostID(s *goquery.Selection) string {
method getLinks (line 311) | func (p *SusuAsyncPlugin) getLinks(client *http.Client, postID string)...
method getButtonDetail (line 361) | func (p *SusuAsyncPlugin) getButtonDetail(client *http.Client, postID ...
method decodeJWTURL (line 432) | func (p *SusuAsyncPlugin) decodeJWTURL(jwtToken string) (string, error) {
method determineLinkType (line 467) | func (p *SusuAsyncPlugin) determineLinkType(url, name string) string {
method doRequestWithRetry (line 538) | func (p *SusuAsyncPlugin) doRequestWithRetry(client *http.Client, req ...
function NewSusuAsyncPlugin (line 100) | func NewSusuAsyncPlugin() *SusuAsyncPlugin {
function isRetriableError (line 569) | func isRetriableError(err error) bool {
function md5sum (line 587) | func md5sum(s string) uint32 {
FILE: plugin/thepiratebay/thepiratebay.go
constant SearchURL (line 22) | SearchURL = "https://thpibay.xyz/search/%s/1/99/0"
constant SearchPageURL (line 25) | SearchPageURL = "https://thpibay.xyz/search/%s/%d/99/0"
constant DefaultTimeout (line 28) | DefaultTimeout = 10 * time.Second
constant MaxConcurrency (line 31) | MaxConcurrency = 200
constant MaxPages (line 34) | MaxPages = 30
constant MaxIdleConns (line 37) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 38) | MaxIdleConnsPerHost = 80
constant MaxConnsPerHost (line 39) | MaxConnsPerHost = 150
constant IdleConnTimeout (line 40) | IdleConnTimeout = 90 * time.Second
type pageResponse (line 72) | type pageResponse struct
type ThePirateBayPlugin (line 79) | type ThePirateBayPlugin struct
method Search (line 132) | func (p *ThePirateBayPlugin) Search(keyword string, ext map[string]int...
method SearchWithResult (line 141) | func (p *ThePirateBayPlugin) SearchWithResult(keyword string, ext map[...
method searchImpl (line 146) | func (p *ThePirateBayPlugin) searchImpl(client *http.Client, keyword s...
method searchPage (line 225) | func (p *ThePirateBayPlugin) searchPage(client *http.Client, encodedKe...
method parseTotalPages (line 313) | func (p *ThePirateBayPlugin) parseTotalPages(doc *goquery.Document) int {
method parseSearchResultItem (line 352) | func (p *ThePirateBayPlugin) parseSearchResultItem(s *goquery.Selectio...
method parseUploadTime (line 450) | func (p *ThePirateBayPlugin) parseUploadTime(timeStr string) time.Time {
method doRequestWithRetry (line 476) | func (p *ThePirateBayPlugin) doRequestWithRetry(req *http.Request, cli...
function createOptimizedHTTPClient (line 85) | func createOptimizedHTTPClient() *http.Client {
function NewThePirateBayPlugin (line 104) | func NewThePirateBayPlugin() *ThePirateBayPlugin {
function init (line 112) | func init() {
function startCacheCleaner (line 120) | func startCacheCleaner() {
FILE: plugin/u3c3/u3c3.go
constant BaseURL (line 19) | BaseURL = "https://u3c3u3c3.u3c3u3c3u3c3.com"
constant UserAgent (line 20) | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537....
constant MaxRetries (line 21) | MaxRetries = 3
constant RetryDelay (line 22) | RetryDelay = 2 * time.Second
type U3c3Plugin (line 26) | type U3c3Plugin struct
method Search (line 42) | func (p *U3c3Plugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 51) | func (p *U3c3Plugin) SearchWithResult(keyword string, ext map[string]i...
method getSearch2Parameter (line 91) | func (p *U3c3Plugin) getSearch2Parameter() (string, error) {
method extractSearch2FromHTML (line 163) | func (p *U3c3Plugin) extractSearch2FromHTML(html string) string {
method doSearch (line 210) | func (p *U3c3Plugin) doSearch(keyword, search2 string) ([]model.Search...
method parseSearchResults (line 267) | func (p *U3c3Plugin) parseSearchResults(html string) ([]model.SearchRe...
method cleanTitle (line 363) | func (p *U3c3Plugin) cleanTitle(title string) string {
method parseDateTime (line 374) | func (p *U3c3Plugin) parseDateTime(dateStr string) time.Time {
method generateUniqueID (line 397) | func (p *U3c3Plugin) generateUniqueID(title, size string) string {
function init (line 33) | func init() {
FILE: plugin/wanou/wanou.go
constant DefaultTimeout (line 21) | DefaultTimeout = 8 * time.Second
constant MaxIdleConns (line 24) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 25) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 26) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 27) | IdleConnTimeout = 90 * time.Second
function init (line 36) | func init() {
type WanouAsyncPlugin (line 61) | type WanouAsyncPlugin struct
method Search (line 91) | func (p *WanouAsyncPlugin) Search(keyword string, ext map[string]inter...
method SearchWithResult (line 100) | func (p *WanouAsyncPlugin) SearchWithResult(keyword string, ext map[st...
method searchImpl (line 105) | func (p *WanouAsyncPlugin) searchImpl(client *http.Client, keyword str...
method parseAPIItem (line 201) | func (p *WanouAsyncPlugin) parseAPIItem(item WanouAPIItem) model.Searc...
method parseDownloadLinks (line 261) | func (p *WanouAsyncPlugin) parseDownloadLinks(vodDownFrom, vodDownURL ...
method determineLinkTypeOptimized (line 309) | func (p *WanouAsyncPlugin) determineLinkTypeOptimized(apiType, url str...
method determineLinkType (line 394) | func (p *WanouAsyncPlugin) determineLinkType(url string) string {
method extractPassword (line 426) | func (p *WanouAsyncPlugin) extractPassword(url string) string {
method doRequestWithRetry (line 435) | func (p *WanouAsyncPlugin) doRequestWithRetry(req *http.Request, clien...
method GetPerformanceStats (line 461) | func (p *WanouAsyncPlugin) GetPerformanceStats() map[string]interface{} {
function createOptimizedHTTPClient (line 67) | func createOptimizedHTTPClient() *http.Client {
function NewWanouPlugin (line 83) | func NewWanouPlugin() *WanouAsyncPlugin {
type WanouAPIResponse (line 174) | type WanouAPIResponse struct
type WanouAPIItem (line 185) | type WanouAPIItem struct
FILE: plugin/weibo/weibo.go
constant MaxConcurrentUsers (line 29) | MaxConcurrentUsers = 10
constant MaxConcurrentWeibo (line 30) | MaxConcurrentWeibo = 30
constant MaxComments (line 31) | MaxComments = 1
constant DebugLog (line 32) | DebugLog = false
constant HTMLTemplate (line 38) | HTMLTemplate = `<!DOCTYPE html>
type WeiboPlugin (line 409) | type WeiboPlugin struct
method Initialize (line 446) | func (p *WeiboPlugin) Initialize() error {
method RegisterWebRoutes (line 469) | func (p *WeiboPlugin) RegisterWebRoutes(router *gin.RouterGroup) {
method SkipServiceFilter (line 477) | func (p *WeiboPlugin) SkipServiceFilter() bool {
method Search (line 482) | func (p *WeiboPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 490) | func (p *WeiboPlugin) SearchWithResult(keyword string, ext map[string]...
method loadAllUsers (line 528) | func (p *WeiboPlugin) loadAllUsers() {
method getUserByHash (line 558) | func (p *WeiboPlugin) getUserByHash(hash string) (*User, bool) {
method saveUser (line 566) | func (p *WeiboPlugin) saveUser(user *User) error {
method persistUser (line 571) | func (p *WeiboPlugin) persistUser(user *User) error {
method deleteUser (line 580) | func (p *WeiboPlugin) deleteUser(hash string) error {
method getActiveUsers (line 586) | func (p *WeiboPlugin) getActiveUsers() []*User {
method handleManagePage (line 614) | func (p *WeiboPlugin) handleManagePage(c *gin.Context) {
method handleManagePagePOST (line 626) | func (p *WeiboPlugin) handleManagePagePOST(c *gin.Context) {
method handleGetStatus (line 659) | func (p *WeiboPlugin) handleGetStatus(c *gin.Context, hash string) {
method handleRefreshQRCode (line 724) | func (p *WeiboPlugin) handleRefreshQRCode(c *gin.Context, hash string) {
method handleLogout (line 749) | func (p *WeiboPlugin) handleLogout(c *gin.Context, hash string) {
method handleCheckLogin (line 769) | func (p *WeiboPlugin) handleCheckLogin(c *gin.Context, hash string) {
method handleSetUserIDs (line 830) | func (p *WeiboPlugin) handleSetUserIDs(c *gin.Context, hash string, re...
method handleTestSearch (line 880) | func (p *WeiboPlugin) handleTestSearch(c *gin.Context, hash string, re...
method buildUserTasks (line 938) | func (p *WeiboPlugin) buildUserTasks(users []*User) []UserTask {
method refreshCookie (line 990) | func (p *WeiboPlugin) refreshCookie(cookieStr string) string {
method executeTasks (line 1059) | func (p *WeiboPlugin) executeTasks(tasks []UserTask, keyword string) [...
method searchUserWeibo (line 1086) | func (p *WeiboPlugin) searchUserWeibo(uid, cookie, keyword string) []m...
method getComments (line 1285) | func (p *WeiboPlugin) getComments(weiboID, cookie string, maxComments ...
method parseWeibo (line 1464) | func (p *WeiboPlugin) parseWeibo(weibo map[string]interface{}, uid str...
method checkQRLoginStatus (line 1663) | func (p *WeiboPlugin) checkQRLoginStatus(qrsig string) (*LoginResult, ...
method generateQRCodeWithSig (line 1777) | func (p *WeiboPlugin) generateQRCodeWithSig() ([]byte, string, error) {
method initCookieFromAlt (line 1849) | func (p *WeiboPlugin) initCookieFromAlt(alt string) (string, error) {
method generateHash (line 1979) | func (p *WeiboPlugin) generateHash(input string) string {
method isHexString (line 1989) | func (p *WeiboPlugin) isHexString(s string) bool {
method startCleanupTask (line 2014) | func (p *WeiboPlugin) startCleanupTask() {
method cleanupExpiredUsers (line 2026) | func (p *WeiboPlugin) cleanupExpiredUsers() int {
method markInactiveUsers (line 2044) | func (p *WeiboPlugin) markInactiveUsers() int {
type User (line 416) | type User struct
type UserTask (line 432) | type UserTask struct
function init (line 437) | func init() {
function extractURLsFromComment (line 1404) | func extractURLsFromComment(htmlText string) []string {
function fetchPageAndExtractLinks (line 1426) | func fetchPageAndExtractLinks(pageURL string, datetime time.Time) []mode...
type Comment (line 1459) | type Comment struct
function extractNetworkDriveLinks (line 1577) | func extractNetworkDriveLinks(text string, datetime time.Time) []model.L...
function min (line 1642) | func min(a, b int) int {
function cleanHTML (line 1649) | func cleanHTML(html string) string {
type LoginResult (line 1657) | type LoginResult struct
function respondSuccess (line 1998) | func respondSuccess(c *gin.Context, message string, data interface{}) {
function respondError (line 2006) | func respondError(c *gin.Context, message string) {
FILE: plugin/wuji/wuji.go
constant BaseURL (line 22) | BaseURL = "https://xcili.net"
constant SearchURL (line 25) | SearchURL = BaseURL + "/search?q=%s&page=%d"
constant MaxRetries (line 28) | MaxRetries = 3
constant TimeoutSeconds (line 29) | TimeoutSeconds = 30
constant MaxConcurrency (line 32) | MaxConcurrency = 10
constant MaxPages (line 33) | MaxPages = 5
type magnetCacheEntry (line 47) | type magnetCacheEntry struct
type WujiPlugin (line 60) | type WujiPlugin struct
method Name (line 72) | func (p *WujiPlugin) Name() string {
method DisplayName (line 77) | func (p *WujiPlugin) DisplayName() string {
method Description (line 82) | func (p *WujiPlugin) Description() string {
method Search (line 87) | func (p *WujiPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 96) | func (p *WujiPlugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 101) | func (p *WujiPlugin) searchImpl(client *http.Client, keyword string, e...
method searchPage (line 170) | func (p *WujiPlugin) searchPage(client *http.Client, keyword string, p...
method extractSearchResults (line 217) | func (p *WujiPlugin) extractSearchResults(doc *goquery.Document) []mod...
method parseSearchResult (line 232) | func (p *WujiPlugin) parseSearchResult(s *goquery.Selection) model.Sea...
method fetchMagnetLink (line 289) | func (p *WujiPlugin) fetchMagnetLink(client *http.Client, detailURL st...
method cleanTitle (line 359) | func (p *WujiPlugin) cleanTitle(title string) string {
method setRequestHeaders (line 372) | func (p *WujiPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 385) | func (p *WujiPlugin) doRequestWithRetry(req *http.Request, client *htt...
method enrichWithMagnetLinks (line 404) | func (p *WujiPlugin) enrichWithMagnetLinks(results []model.SearchResul...
function NewWujiPlugin (line 65) | func NewWujiPlugin() *WujiPlugin {
function init (line 466) | func init() {
FILE: plugin/xb6v/xb6v.go
constant BaseURL (line 21) | BaseURL = "https://www.66ss.org"
constant BackupURL (line 22) | BackupURL = "https://www.xb6v.com"
constant SearchPath (line 23) | SearchPath = "/e/search/1index.php"
constant UserAgent (line 24) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW...
constant MaxConcurrency (line 25) | MaxConcurrency = 50
constant MaxResults (line 26) | MaxResults = 50
type Xb6vPlugin (line 30) | type Xb6vPlugin struct
method Name (line 64) | func (p *Xb6vPlugin) Name() string {
method DisplayName (line 69) | func (p *Xb6vPlugin) DisplayName() string {
method Description (line 74) | func (p *Xb6vPlugin) Description() string {
method Search (line 79) | func (p *Xb6vPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 88) | func (p *Xb6vPlugin) SearchWithResult(keyword string, ext map[string]i...
method setRequestHeaders (line 93) | func (p *Xb6vPlugin) setRequestHeaders(req *http.Request, referer stri...
method doRequest (line 106) | func (p *Xb6vPlugin) doRequest(client *http.Client, method, url, postD...
method searchImpl (line 145) | func (p *Xb6vPlugin) searchImpl(client *http.Client, keyword string, e...
method getResponseReader (line 343) | func (p *Xb6vPlugin) getResponseReader(resp *http.Response) (io.Reader...
method extractDetailURLs (line 365) | func (p *Xb6vPlugin) extractDetailURLs(doc *goquery.Document) []Detail...
method isInSidebar (line 447) | func (p *Xb6vPlugin) isInSidebar(s *goquery.Selection) bool {
method isValidContentURL (line 465) | func (p *Xb6vPlugin) isValidContentURL(href string) bool {
method cleanTitle (line 491) | func (p *Xb6vPlugin) cleanTitle(title string) string {
method fetchMagnetLinksFromDetails (line 543) | func (p *Xb6vPlugin) fetchMagnetLinksFromDetails(client *http.Client, ...
method fetchDetailPageMagnetLinks (line 580) | func (p *Xb6vPlugin) fetchDetailPageMagnetLinks(client *http.Client, d...
method extractMagnetLinks (line 690) | func (p *Xb6vPlugin) extractMagnetLinks(doc *goquery.Document, mainTit...
method extractResourceID (line 774) | func (p *Xb6vPlugin) extractResourceID(detailURL string) string {
method filterValidResults (line 787) | func (p *Xb6vPlugin) filterValidResults(results []model.SearchResult) ...
type DetailPageInfo (line 39) | type DetailPageInfo struct
function NewXb6vPlugin (line 45) | func NewXb6vPlugin() *Xb6vPlugin {
type MagnetLinkInfo (line 684) | type MagnetLinkInfo struct
function init (line 801) | func init() {
FILE: plugin/xdpan/xdpan.go
constant BaseURL (line 19) | BaseURL = "https://xiongdipan.com"
constant UserAgent (line 20) | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/...
constant MaxConcurrency (line 21) | MaxConcurrency = 10
constant MaxRetries (line 22) | MaxRetries = 3
type XdpanPlugin (line 30) | type XdpanPlugin struct
method Search (line 45) | func (p *XdpanPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 54) | func (p *XdpanPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 59) | func (p *XdpanPlugin) searchImpl(client *http.Client, keyword string, ...
method fetchSearchResults (line 89) | func (p *XdpanPlugin) fetchSearchResults(client *http.Client, keyword ...
method extractSearchResults (line 127) | func (p *XdpanPlugin) extractSearchResults(doc *goquery.Document) []mo...
method parseSearchResult (line 155) | func (p *XdpanPlugin) parseSearchResult(s *goquery.Selection) model.Se...
method enrichWithDetailInfo (line 232) | func (p *XdpanPlugin) enrichWithDetailInfo(client *http.Client, result...
method fetchDetailPageLinks (line 268) | func (p *XdpanPlugin) fetchDetailPageLinks(client *http.Client, detail...
method extractDetailPageLinks (line 319) | func (p *XdpanPlugin) extractDetailPageLinks(doc *goquery.Document) []...
method extractDetailURLFromContent (line 367) | func (p *XdpanPlugin) extractDetailURLFromContent(content string) stri...
method parseTime (line 378) | func (p *XdpanPlugin) parseTime(timeStr string) time.Time {
method doRequestWithRetry (line 404) | func (p *XdpanPlugin) doRequestWithRetry(req *http.Request, client *ht...
method setRequestHeaders (line 435) | func (p *XdpanPlugin) setRequestHeaders(req *http.Request) {
function NewXdpanPlugin (line 37) | func NewXdpanPlugin() *XdpanPlugin {
type cacheItem (line 445) | type cacheItem struct
function init (line 450) | func init() {
FILE: plugin/xdyh/xdyh.go
constant pluginName (line 19) | pluginName = "xdyh"
constant apiURL (line 20) | apiURL = "https://ys.66ds.de/search"
constant refererURL (line 21) | refererURL = "https://ys.66ds.de/"
constant DefaultTimeout (line 24) | DefaultTimeout = 15 * time.Second
constant MaxConcurrency (line 27) | MaxConcurrency = 10
constant MaxIdleConns (line 30) | MaxIdleConns = 100
constant MaxIdleConnsPerHost (line 31) | MaxIdleConnsPerHost = 30
constant MaxConnsPerHost (line 32) | MaxConnsPerHost = 50
constant IdleConnTimeout (line 33) | IdleConnTimeout = 90 * time.Second
function init (line 44) | func init() {
function startCacheCleaner (line 52) | func startCacheCleaner() {
type XdyhAsyncPlugin (line 64) | type XdyhAsyncPlugin struct
method Search (line 91) | func (p *XdyhAsyncPlugin) Search(keyword string, ext map[string]interf...
method SearchWithResult (line 100) | func (p *XdyhAsyncPlugin) SearchWithResult(keyword string, ext map[str...
method searchImpl (line 105) | func (p *XdyhAsyncPlugin) searchImpl(client *http.Client, keyword stri...
method setRequestHeaders (line 184) | func (p *XdyhAsyncPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 196) | func (p *XdyhAsyncPlugin) doRequestWithRetry(req *http.Request, client...
method convertToSearchResults (line 225) | func (p *XdyhAsyncPlugin) convertToSearchResults(apiResp APIResponse, ...
method convertDriveLinks (line 270) | func (p *XdyhAsyncPlugin) convertDriveLinks(item SearchResultItem) []m...
method parseDateTime (line 300) | func (p *XdyhAsyncPlugin) parseDateTime(dateStr string) time.Time {
method buildContentDescription (line 320) | func (p *XdyhAsyncPlugin) buildContentDescription(item SearchResultIte...
method extractTags (line 352) | func (p *XdyhAsyncPlugin) extractTags(title, sourceSite string) []stri...
method isValidURL (line 386) | func (p *XdyhAsyncPlugin) isValidURL(urlStr string) bool {
method determineCloudType (line 405) | func (p *XdyhAsyncPlugin) determineCloudType(url string) string {
function createOptimizedHTTPClient (line 70) | func createOptimizedHTTPClient() *http.Client {
function NewXdyhPlugin (line 83) | func NewXdyhPlugin() *XdyhAsyncPlugin {
type SearchRequest (line 433) | type SearchRequest struct
type APIResponse (line 442) | type APIResponse struct
type Summary (line 453) | type Summary struct
type SearchResultItem (line 463) | type SearchResultItem struct
type Performance (line 476) | type Performance struct
FILE: plugin/xiaoji/xiaoji.go
constant pluginName (line 38) | pluginName = "xiaoji"
constant baseURL (line 39) | baseURL = "https://www.xiaojitv.com"
constant DefaultTimeout (line 42) | DefaultTimeout = 10 * time.Second
constant DetailTimeout (line 43) | DetailTimeout = 8 * time.Second
constant MaxConcurrency (line 46) | MaxConcurrency = 15
constant MaxIdleConns (line 49) | MaxIdleConns = 100
constant MaxIdleConnsPerHost (line 50) | MaxIdleConnsPerHost = 30
constant MaxConnsPerHost (line 51) | MaxConnsPerHost = 50
constant IdleConnTimeout (line 52) | IdleConnTimeout = 90 * time.Second
function init (line 56) | func init() {
function startCacheCleaner (line 64) | func startCacheCleaner() {
type XiaojiAsyncPlugin (line 76) | type XiaojiAsyncPlugin struct
method Search (line 103) | func (p *XiaojiAsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 112) | func (p *XiaojiAsyncPlugin) SearchWithResult(keyword string, ext map[s...
method searchImpl (line 117) | func (p *XiaojiAsyncPlugin) searchImpl(client *http.Client, keyword st...
method setRequestHeaders (line 161) | func (p *XiaojiAsyncPlugin) setRequestHeaders(req *http.Request) {
method doRequestWithRetry (line 172) | func (p *XiaojiAsyncPlugin) doRequestWithRetry(req *http.Request, clie...
method parseSearchResults (line 201) | func (p *XiaojiAsyncPlugin) parseSearchResults(doc *goquery.Document, ...
method parseSearchResultItem (line 216) | func (p *XiaojiAsyncPlugin) parseSearchResultItem(s *goquery.Selection...
method fetchDetailPageLinks (line 291) | func (p *XiaojiAsyncPlugin) fetchDetailPageLinks(detailURL string) []m...
method parseDetailPageLinks (line 336) | func (p *XiaojiAsyncPlugin) parseDetailPageLinks(doc *goquery.Document...
method decodeGoLink (line 379) | func (p *XiaojiAsyncPlugin) decodeGoLink(goLink string) string {
method isValidURL (line 423) | func (p *XiaojiAsyncPlugin) isValidURL(urlStr string) bool {
method determineCloudType (line 452) | func (p *XiaojiAsyncPlugin) determineCloudType(url string) string {
function createOptimizedHTTPClient (line 82) | func createOptimizedHTTPClient() *http.Client {
function NewXiaojiPlugin (line 95) | func NewXiaojiPlugin() *XiaojiAsyncPlugin {
FILE: plugin/xiaozhang/xiaozhang.go
constant BaseURL (line 21) | BaseURL = "https://xzys.fun"
constant SearchPath (line 22) | SearchPath = "/search.html"
constant UserAgent (line 23) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW...
constant MaxConcurrency (line 24) | MaxConcurrency = 20
constant MaxPages (line 25) | MaxPages = 1
type XiaozhangPlugin (line 29) | type XiaozhangPlugin struct
method Name (line 51) | func (p *XiaozhangPlugin) Name() string {
method DisplayName (line 56) | func (p *XiaozhangPlugin) DisplayName() string {
method Description (line 61) | func (p *XiaozhangPlugin) Description() string {
method Search (line 66) | func (p *XiaozhangPlugin) Search(keyword string, ext map[string]interf...
method SearchWithResult (line 75) | func (p *XiaozhangPlugin) SearchWithResult(keyword string, ext map[str...
method setRequestHeaders (line 80) | func (p *XiaozhangPlugin) setRequestHeaders(req *http.Request, referer...
method doRequest (line 93) | func (p *XiaozhangPlugin) doRequest(client *http.Client, url string, r...
method searchImpl (line 135) | func (p *XiaozhangPlugin) searchImpl(client *http.Client, keyword stri...
method extractSearchResults (line 201) | func (p *XiaozhangPlugin) extractSearchResults(doc *goquery.Document, ...
method enrichWithDetailLinks (line 304) | func (p *XiaozhangPlugin) enrichWithDetailLinks(client *http.Client, r...
method fetchDetailPageLinks (line 351) | func (p *XiaozhangPlugin) fetchDetailPageLinks(client *http.Client, de...
method extractDetailPageLinks (line 423) | func (p *XiaozhangPlugin) extractDetailPageLinks(resp *http.Response, ...
function NewXiaozhangPlugin (line 37) | func NewXiaozhangPlugin() *XiaozhangPlugin {
function isValidPanLink (line 512) | func isValidPanLink(url string) bool {
function determineLinkType (line 537) | func determineLinkType(url string) string {
function init (line 561) | func init() {
FILE: plugin/xinjuc/xinjuc.go
constant DefaultTimeout (line 41) | DefaultTimeout = 10 * time.Second
constant DetailTimeout (line 42) | DetailTimeout = 8 * time.Second
constant MaxConcurrency (line 45) | MaxConcurrency = 15
constant MaxIdleConns (line 48) | MaxIdleConns = 50
constant MaxIdleConnsPerHost (line 49) | MaxIdleConnsPerHost = 20
constant MaxConnsPerHost (line 50) | MaxConnsPerHost = 30
constant IdleConnTimeout (line 51) | IdleConnTimeout = 90 * time.Second
constant SiteURL (line 54) | SiteURL = "https://www.xinjuc.com"
function init (line 58) | func init() {
function startCacheCleaner (line 66) | func startCacheCleaner() {
type XinjucPlugin (line 78) | type XinjucPlugin struct
method Search (line 104) | func (p *XinjucPlugin) Search(keyword string, ext map[string]interface...
method SearchWithResult (line 113) | func (p *XinjucPlugin) SearchWithResult(keyword string, ext map[string...
method searchImpl (line 118) | func (p *XinjucPlugin) searchImpl(client *http.Client, keyword string,...
method parseSearchItem (line 181) | func (p *XinjucPlugin) parseSearchItem(s *goquery.Selection, keyword s...
method parseTime (line 245) | func (p *XinjucPlugin) parseTime(timeStr string) time.Time {
method enhanceWithDetails (line 273) | func (p *XinjucPlugin) enhanceWithDetails(client *http.Client, results...
method getDetailInfo (line 312) | func (p *XinjucPlugin) getDetailInfo(client *http.Client, detailURL st...
method fetchDetailPage (line 344) | func (p *XinjucPlugin) fetchDetailPage(client *http.Client, detailURL ...
method extractLinksFromDoc (line 394) | func (p *XinjucPlugin) extractLinksFromDoc(doc *goquery.Document) []mo...
method isValidBaiduLink (line 480) | func (p *XinjucPlugin) isValidBaiduLink(link string) bool {
method extractContent (line 500) | func (p *XinjucPlugin) extractContent(articleContent *goquery.Selectio...
method doRequestWithRetry (line 522) | func (p *XinjucPlugin) doRequestWithRetry(req *http.Request, client *h...
function createOptimizedHTTPClient (line 84) | func createOptimizedHTTPClient() *http.Client {
function NewXinjucPlugin (line 96) | func NewXinjucPlugin() *XinjucPlugin {
type DetailCacheData (line 337) | type DetailCacheData struct
FILE: plugin/xuexizhinan/xuexizhinan.go
constant SearchURL (line 21) | SearchURL = "https://xuexizhinan.com/?post_type=book&s=%s"
constant DetailURLPattern (line 24) | DetailURLPattern = `https://xuexizhinan.com/book/(\d+)\.html`
constant DefaultTimeout (line 27) | DefaultTimeout = 10 * time.Second
constant MaxConcurrency (line 30) | MaxConcurrency = 8
type detailPageResponse (line 53) | type detailPageResponse struct
type XuexizhinanPlugin (line 64) | type XuexizhinanPlugin struct
method Search (line 96) | func (p *XuexizhinanPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 105) | func (p *XuexizhinanPlugin) SearchWithResult(keyword string, ext map[s...
method doSearch (line 110) | func (p *XuexizhinanPlugin) doSearch(client *http.Client, keyword stri...
method processDetailPage (line 256) | func (p *XuexizhinanPlugin) processDetailPage(client *http.Client, det...
method detailResponseToResult (line 370) | func (p *XuexizhinanPlugin) detailResponseToResult(detailURL string, r...
function NewXuexizhinanPlugin (line 69) | func NewXuexizhinanPlugin() *XuexizhinanPlugin {
function init (line 76) | func init() {
function startCacheCleaner (line 84) | func startCacheCleaner() {
FILE: plugin/xys/xys.go
constant PluginName (line 23) | PluginName = "xys"
constant DisplayName (line 24) | DisplayName = "小云搜索"
constant Description (line 25) | Description = "小云搜索 - 阿里云盘、夸克网盘、百度网盘等多网盘搜索引擎"
constant BaseURL (line 26) | BaseURL = "https://www.yunso.net"
constant TokenPath (line 27) | TokenPath = "/index/user/s"
constant SearchPath (line 28) | SearchPath = "/api/validate/searchX2"
constant UserAgent (line 29) | UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWe...
constant MaxResults (line 30) | MaxResults = 50
type XysPlugin (line 34) | type XysPlugin struct
method Name (line 75) | func (p *XysPlugin) Name() string {
method DisplayName (line 80) | func (p *XysPlugin) DisplayName() string {
method Description (line 85) | func (p *XysPlugin) Description() string {
method Search (line 90) | func (p *XysPlugin) Search(keyword string, ext map[string]interface{})...
method searchImpl (line 95) | func (p *XysPlugin) searchImpl(client *http.Client, keyword string, ex...
method getToken (line 124) | func (p *XysPlugin) getToken(client *http.Client, keyword string) (str...
method doRequestWithRetry (line 208) | func (p *XysPlugin) doRequestWithRetry(req *http.Request, client *http...
method executeSearch (line 237) | func (p *XysPlugin) executeSearch(client *http.Client, token, keyword ...
method parseSearchResults (line 296) | func (p *XysPlugin) parseSearchResults(htmlData, keyword string) ([]mo...
method parseResultItem (line 331) | func (p *XysPlugin) parseResultItem(s *goquery.Selection, index int) *...
method cleanTitle (line 406) | func (p *XysPlugin) cleanTitle(title string) string {
method parseTime (line 427) | func (p *XysPlugin) parseTime(timeStr string) time.Time {
method extractPlatform (line 446) | func (p *XysPlugin) extractPlatform(s *goquery.Selection, href string)...
type TokenCache (line 42) | type TokenCache struct
type SearchResponse (line 48) | type SearchResponse struct
function init (line 56) | func init() {
function NewXysPlugin (line 61) | func NewXysPlugin() *XysPlugin {
function determineCloudType (line 451) | func determineCloudType(url string) string {
FILE: plugin/yiove/yiove.go
constant pluginName (line 20) | pluginName = "yiove"
constant defaultPriority (line 21) | defaultPriority = 3
constant baseURL (line 23) | baseURL = "https://bbs.yiove.com"
constant searchPathFormat (line 24) | searchPathFormat = baseURL + "/search-%s-1.htm"
constant requestTimeout (line 25) | requestTimeout = 12 * time.Second
constant detailTimeout (line 26) | detailTimeout = 12 * time.Second
constant retryBaseDelay (line 27) | retryBaseDelay = 200 * time.Millisecond
constant maxRequestRetries (line 28) | maxRequestRetries = 3
constant searchResultLimit (line 29) | searchResultLimit = 12
constant detailLinkLimit (line 30) | detailLinkLimit = 6
constant detailWorkerCount (line 31) | detailWorkerCount = 6
constant httpMaxIdleConns (line 32) | httpMaxIdleConns = 64
constant httpMaxIdlePerHost (line 33) | httpMaxIdlePerHost = 16
constant httpMaxConnsPerHost (line 34) | httpMaxConnsPerHost = 32
type YiovePlugin (line 67) | type YiovePlugin struct
method Search (line 85) | func (p *YiovePlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 94) | func (p *YiovePlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 98) | func (p *YiovePlugin) searchImpl(client *http.Client, keyword string, ...
method fetchSearchResults (line 214) | func (p *YiovePlugin) fetchSearchResults(client *http.Client, keyword ...
method fetchDetail (line 295) | func (p *YiovePlugin) fetchDetail(client *http.Client, detailURL strin...
method doRequestWithRetry (line 621) | func (p *YiovePlugin) doRequestWithRetry(req *http.Request, client *ht...
function init (line 72) | func init() {
function NewYiovePlugin (line 77) | func NewYiovePlugin() *YiovePlugin {
type searchThread (line 208) | type searchThread struct
type detailPayload (line 288) | type detailPayload struct
function logDebug (line 349) | func logDebug(enabled bool, format string, args ...interface{}) {
function collectTags (line 356) | func collectTags(doc *goquery.Document) []string {
function extractDatetime (line 381) | func extractDatetime(doc *goquery.Document) time.Time {
function extractLinks (line 404) | func extractLinks(selection *goquery.Selection) []model.Link {
function classifyLink (line 457) | func classifyLink(raw string) (string, string) {
function extractPassword (line 470) | func extractPassword(node *goquery.Selection) string {
function matchPassword (line 496) | func matchPassword(text string) string {
function limitLinks (line 509) | func limitLinks(links []model.Link, limit int) []model.Link {
function applyWorkTitle (line 516) | func applyWorkTitle(links []model.Link, title string) []model.Link {
function mergeTags (line 526) | func mergeTags(a, b []string) []string {
function encodeKeyword (line 546) | func encodeKeyword(keyword string) string {
function toAbsoluteURL (line 559) | func toAbsoluteURL(href string) string {
function truncateText (line 573) | func truncateText(text string, limit int) string {
function substring (line 582) | func substring(text string, start, end int) string {
function buildUniqueID (line 592) | func buildUniqueID(detailURL string) string {
function newHTTPClient (line 600) | func newHTTPClient() *http.Client {
function setHTMLHeaders (line 613) | func setHTMLHeaders(req *http.Request, referer string) {
FILE: plugin/ypfxw/ypfxw.go
type cacheEntry (line 48) | type cacheEntry struct
constant pluginName (line 54) | pluginName = "ypfxw"
constant defaultPriority (line 55) | defaultPriority = 2
constant searchTimeout (line 56) | searchTimeout = 12 * time.Second
constant detailTimeout (line 57) | detailTimeout = 10 * time.Second
constant maxConcurrency (line 58) | maxConcurrency = 12
constant maxIdleConns (line 59) | maxIdleConns = 64
constant maxIdlePerHost (line 60) | maxIdlePerHost = 16
constant maxConnsPerHost (line 61) | maxConnsPerHost = 32
constant idleConnLifetime (line 62) | idleConnLifetime = 90 * time.Second
constant tlsHandshakeTimeout (line 63) | tlsHandshakeTimeout = 10 * time.Second
constant expectContinueTimeout (line 64) | expectContinueTimeout = 1 * time.Second
constant searchMaxRetries (line 66) | searchMaxRetries = 3
constant detailMaxRetries (line 67) | detailMaxRetries = 2
constant retryBaseDelay (line 68) | retryBaseDelay = 200 * time.Millisecond
type YpfxwPlugin (line 72) | type YpfxwPlugin struct
method Search (line 91) | func (p *YpfxwPlugin) Search(keyword string, ext map[string]interface{...
method SearchWithResult (line 100) | func (p *YpfxwPlugin) SearchWithResult(keyword string, ext map[string]...
method searchImpl (line 120) | func (p *YpfxwPlugin) searchImpl(client *http.Client, keyword string, ...
method fetchDetailLinks (line 251) | func (p *YpfxwPlugin) fetchDetailLinks(client *http.Client, detailURL,...
method doRequestWithRetry (line 432) | func (p *YpfxwPlugin) doRequestWithRetry(req *http.Request, client *ht...
function init (line 77) | func init() {
function NewYpfxwPlugin (line 83) | func NewYpfxwPlugin() *YpfxwPlugin {
function newHTTPClient (line 104) | func newHTTPClient() *http.Client {
function extractArticleID (line 223) | func extractArticleID(detailURL string) string {
function parsePublishTime (line 230) | func parsePublishTime(value string) time.Time {
function extractNetDiskLinks (line 295) | func extractNetDiskLinks(doc *goquery.Document) []model.Link {
function extractPlainTextLinks (line 340) | func extractPlainTextLinks(text string, seen map[string]struct{}) []mode...
function substringWithBounds (line 366) | func substringWithBounds(text string, start, end int) string {
function classifyLink (line 376) | func classifyLink(raw string) (string, string) {
function extractPassword (line 385) | func extractPassword(link *goquery.Selection) string {
function matchPassword (line 411) | func matchPassword(text string) string {
function setCommonHeaders (line 424) | func setCommonHeaders(req *http.Request, referer string) {
function startCacheCleaner (line 453) | func startCacheCleaner() {
FILE: plugin/yuhuage/yuhuage.go
constant BaseURL (line 22) | BaseURL = "https://www.iyuhuage.fun"
constant SearchPath (line 23) | SearchPath = "/search/"
constant UserAgent (line 24) | UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebK...
constant MaxConcurrency (line 25) | MaxConcurrency = 5
constant MaxRetryCount (line 26) | MaxRetryCount = 2
type YuhuagePlugin (line 30) | type YuhuagePlugin struct
method Search (line 48) | func (p *YuhuagePlugin) Search(keyword string, ext map[string]interfac...
method SearchWithResult (line 57) | func (p *YuhuagePlugin) SearchWithResult(keyword string, ext map[strin...
method searchImpl (line 62) | func (p *YuhuagePlugin) searchImpl(client *http.Client, keyword string...
method parseSearchResults (line 137) | func (p *YuhuagePlugin) parseSearchResults(html string) ([]model.Searc...
method fetchDetailsSync (line 195) | func (p *YuhuagePlugin) fetchDetailsSync(detailURLs []string, results ...
method fetchDetailLinks (line 233) | func (p *YuhuagePlugin) fetchDetailLinks(detailURL string) []model.Link {
method parseDetailLinks (line 300) | func (p *YuhuagePlugin) parseDetailLinks(html string) []model.Link {
method extractHashFromURL (line 344) | func (p *YuhuagePlugin) extractHashFromURL(detailURL string) string {
method cleanTitle (line 354) | func (p *YuhuagePlugin) cleanTitle(title string) string {
method parseDateTime (line 366) | func (p *YuhuagePlugin) parseDateTime(timeStr string) time.Time {
method doRequestWithRetry (line 389) | func (p *YuhuagePlugin) doRequestWithRetry(req *http.Request, client *...
function init (line 38) | func init() {
FILE: plugin/yunsou/yunsou.go
constant pluginName (line 32) | pluginName = "yunsou"
constant searchURLTemplate (line 35) | searchURLTemplate = "https://yunsou.xyz/s/%s.html"
constant defaultPriority (line 38) | defaultPriority = 2
constant defaultTimeout (line 41) | defaultTimeout = 30 * time.Second
constant maxRetries (line 44) | maxRetries = 3
constant timeLayout (line 47) | timeLayout = "2006-01-02"
type YunsouAsyncPlugin (line 51) | type YunsouAsyncPlugin struct
method Search (line 108) | func (p *YunsouAsyncPlugin) Search(keyword string, ext map[string]inte...
method SearchWithResult (line 117) | func (p *YunsouAsyncPlugin) SearchWithResult(keyword string, ext map[s...
method searchImpl (line 122) | func (p *YunsouAsyncPlugin) searchImpl(client *http.Client, keyword st...
method extractJSONData (line 190) | func (p *YunsouAsyncPlugin) extractJSONData(htmlContent string) (strin...
method convertToSearchResult (line 209) | func (p *YunsouAsyncPlugin) convertToSearchResult(item YunsouItem) mod...
method convertNetDiskType (line 260) | func (p *YunsouAsyncPlugin) convertNetDiskType(isType int) string {
method extractPwdFromURL (line 278) | func (p *YunsouAsyncPlugin) extractPwdFromURL(urlStr string) string {
method doRequestWithRetry (line 287) | func (p *YunsouAsyncPlugin) doRequestWithRetry(req *http.Request, clie...
type YunsouCategory (line 57) | type YunsouCategory struct
type YunsouItem (line 63) | type YunsouItem struct
function init (line 80) | func init() {
function createOptimizedHTTPClient (line 85) | func createOptimizedHTTPClient() *http.Client {
function NewYunsouAsyncPlugin (line 100) | func NewYunsouAsyncPlugin() *YunsouAsyncPlugin {
FILE: plugin/zhizhen/zhizhen.go
constant DefaultTimeout (line 21) | DefaultTimeout = 8 * time.Second
constant DetailTimeout (line 22) | DetailTimeout = 6 * time.Second
constant MaxConcurrency (line 25) | MaxConcurrency = 20
constant MaxIdleConns (line 28) | MaxIdleConns = 200
constant MaxIdleConnsPerHost (line 29) | MaxIdleConnsPerHost = 50
constant MaxConnsPerHost (line 30) | MaxConnsPerHost = 100
constant IdleConnTimeout (line 31) | IdleConnTimeout = 90 * time.Second
constant cacheTTL (line 34) | cacheTTL = 1 * time.Hour
function init (line 47) | func init() {
type ZhizhenAsyncPlugin (line 81) | type ZhizhenAsyncPlugin struct
method Search (line 111) | func (p *ZhizhenAsyncPlugin) Search(keyword string, ext map[string]int...
method SearchWithResult (line 120) | func (p *ZhizhenAsyncPlugin) SearchWithResult(keyword string, ext map[...
method searchImpl (line 125) | func (p *ZhizhenAsyncPlugin) searchImpl(client *http.Client, keyword s...
method parseSearchItem (line 196) | func (p *ZhizhenAsyncPlugin) parseSearchItem(s *goquery.Selection, key...
method isValidNetworkDriveURL (line 296) | func (p *ZhizhenAsyncPlugin) isValidNetworkDriveURL(url string) bool {
method determineLinkType (line 324) | func (p *ZhizhenAsyncPlugin) determineLinkType(url string) string {
method extractPassword (line 362) | func (p *ZhizhenAsyncPlugin) extractPassword(url string) string {
method enhanceWithDetails (line 371) | func (p *ZhizhenAsyncPlugin) enhanceWithDetails(client *http.Client, r...
method doRequestWithRetry (line 434) | func (p *ZhizhenAsyncPlugin) doRequestWithRetry(req *http.Request, cli...
method fetchDetailLinksAndImages (line 463) | func (p *ZhizhenAsyncPlugin) fetchDetailLinksAndImages(client *http.Cl...
method fetchDetailLinks (line 565) | func (p *ZhizhenAsyncPlugin) fetchDetailLinks(client *http.Client, ite...
method GetPerformanceStats (line 579) | func (p *ZhizhenAsyncPlugin) GetPerformanceStats() map[string]interfac...
function createOptimizedHTTPClient (line 87) | func createOptimizedHTTPClient() *http.Client {
function NewZhizhenPlugin (line 103) | func NewZhizhenPlugin() *ZhizhenAsyncPlugin {
function min (line 571) | func min(a, b int) int {
FILE: plugin/zxzj/zxzj.go
constant baseURL (line 21) | baseURL = "https://www.zxzjhd.com"
constant searchPath (line 22) | searchPath = "/vodsearch/-------------.html"
constant maxResults (line 23) | maxResults = 10
constant maxConcurrent (line 24) | maxConcurrent = 5
type ZXZJPlugin (line 27) | type ZXZJPlugin struct
method Search (line 47) | func (p *ZXZJPlugin) Search(keyword string, ext map[string]interface{}...
method SearchWithResult (line 55) | func (p *ZXZJPlugin) SearchWithResult(keyword string, ext map[string]i...
method searchImpl (line 59) | func (p *ZXZJPlugin) searchImpl(client *http.Client, keyword string, e...
method fetchSearchResults (line 86) | func (p *ZXZJPlugin) fetchSearchResults(searchURL string) ([]searchIte...
method processDetailPages (line 144) | func (p *ZXZJPlugin) processDetailPages(items []searchItem) []model.Se...
method processDetailPage (line 170) | func (p *ZXZJPlugin) processDetailPage(item searchItem) *model.SearchR...
method extractPlayLinks (line 247) | func (p *ZXZJPlugin) extractPlayLinks(doc *goquery.Document) []playLink {
method detectPanType (line 288) | func (p *ZXZJPlugin) detectPanType(title string) string {
method fetchPanLinks (line 304) | func (p *ZXZJPlugin) fetchPanLinks(playLinks []playLink) []model.Link {
method fetchSinglePanLink (line 330) | func (p *ZXZJPlugin) fetchSinglePanLink(pl playLink) *model.Link {
method parsePlayerData (line 378) | func (p *ZXZJPlugin) parsePlayerData(body []byte) (string, string) {
method extractPassword (line 402) | func (p *ZXZJPlugin) extractPassword(panURL string) string {
method determinePanType (line 431) | func (p *ZXZJPlugin) determinePanType(panURL, lineType string) string {
method buildAbsURL (line 454) | func (p *ZXZJPlugin) buildAbsURL(path string) string {
method setHeaders (line 467) | func (p *ZXZJPlugin) setHeaders(req *http.Request, referer string) {
method parseUpdateTime (line 475) | func (p *ZXZJPlugin) parseUpdateTime(text string) time.Time {
method doRequestWithRetry (line 498) | func (p *ZXZJPlugin) doRequestWithRetry(req *http.Request, client *htt...
function init (line 32) | func init() {
type searchItem (line 80) | type searchItem struct
type playLink (line 241) | type playLink struct
type playerData (line 373) | type playerData struct
FILE: service/cache_integration.go
type CacheWriteIntegration (line 13) | type CacheWriteIntegration struct
method createMainCacheUpdater (line 48) | func (c *CacheWriteIntegration) createMainCacheUpdater() func(string, ...
method HandleCacheWrite (line 56) | func (c *CacheWriteIntegration) HandleCacheWrite(key string, results [...
method getPluginPriority (line 85) | func (c *CacheWriteIntegration) getPluginPriority(pluginName string) i...
method estimateDataSize (line 96) | func (c *CacheWriteIntegration) estimateDataSize(results []model.Searc...
method Shutdown (line 102) | func (c *CacheWriteIntegration) Shutdown(timeout time.Duration) error {
method GetStats (line 111) | func (c *CacheWriteIntegration) GetStats() interface{} {
method SetStrategy (line 120) | func (c *CacheWriteIntegration) SetStrategy(strategy cache.CacheWriteS...
method GetStrategy (line 125) | func (c *CacheWriteIntegration) GetStrategy() cache.CacheWriteStrategy {
function NewCacheWriteIntegration (line 21) | func NewCacheWriteIntegration(mainCache *cache.EnhancedTwoLevelCache) (*...
FILE: service/search_service.go
function normalizeUrl (line 24) | func normalizeUrl(rawUrl string) string {
function SetGlobalCacheWriteManager (line 38) | func SetGlobalCacheWriteManager(manager *cache.DelayedBatchWriteManager) {
function GetGlobalCacheWriteManager (line 43) | func GetGlobalCacheWriteManager() *cache.DelayedBatchWriteManager {
function GetEnhancedTwoLevelCache (line 48) | func GetEnhancedTwoLevelCache() *cache.EnhancedTwoLevelCache {
function extractKeywordFromCacheKey (line 56) | func extractKeywordFromCacheKey(cacheKey string) string {
function logAsyncCacheWithKeyword (line 63) | func logAsyncCacheWithKeyword(keyword, cacheKey string, format string, a...
function init (line 93) | func init() {
function mergeSearchResults (line 105) | func mergeSearchResults(existing []model.SearchResult, newResults []mode...
function generateResultKey (line 142) | func generateResultKey(result model.SearchResult) string {
function selectBetterResult (line 154) | func selectBetterResult(existing, new model.SearchResult) model.SearchRe...
function calculateCompletenessScore (line 166) | func calculateCompletenessScore(result model.SearchResult) int {
type SearchService (line 201) | type SearchService struct
method Search (line 351) | func (s *SearchService) Search(keyword string, channels []string, conc...
method searchChannel (line 591) | func (s *SearchService) searchChannel(keyword string, channel string) ...
method searchTG (line 1211) | func (s *SearchService) searchTG(keyword string, channels []string, fo...
method searchPlugins (line 1283) | func (s *SearchService) searchPlugins(keyword string, plugins []string...
method GetPluginManager (line 1440) | func (s *SearchService) GetPluginManager() *plugin.PluginManager {
function NewSearchService (line 206) | func NewSearchService(pluginManager *plugin.PluginManager) *SearchService {
function injectMainCacheToAsyncPlugins (line 233) | func injectMainCacheToAsyncPlugins(pluginManager *plugin.PluginManager, ...
function filterResponseByType (line 513) | func filterResponseByType(response model.SearchResponse, resultType stri...
function sortResultsByTimeAndKeywords (line 542) | func sortResultsByTimeAndKeywords(results []model.SearchResult) {
function getKeywordPriority (line 579) | func getKeywordPriority(title string) int {
function extractLinkTitlePairs (line 631) | func extractLinkTitlePairs(content string) map[string]string {
function extractLinkTitlePairsWithNewlines (line 642) | func extractLinkTitlePairsWithNewlines(content string) map[string]string {
function extractLinkTitlePairsWithoutNewlines (line 751) | func extractLinkTitlePairsWithoutNewlines(content string) map[string]str...
function extractTitleBeforeLink (line 860) | func extractTitleBeforeLink(text string) string {
function isLinkLine (line 880) | func isLinkLine(line string) bool {
function extractTitleFromLinkLine (line 891) | func extractTitleFromLinkLine(line string) string {
function isLinkPrefix (line 910) | func isLinkPrefix(text string) bool {
function cleanTitle (line 965) | func cleanTitle(title string) string {
function isEmpty (line 983) | func isEmpty(line string) bool {
function mergeResultsByType (line 988) | func mergeResultsByType(results []model.SearchResult, keyword string, cl...
type ResultScore (line 1449) | type ResultScore struct
function getResultSource (line 1463) | func getResultSource(result model.SearchResult) string {
function getPluginLevelBySource (line 1478) | func getPluginLevelBySource(source string) int {
function getPluginPriorityByName (line 1506) | func getPluginPriorityByName(pluginName string) int {
function getPluginLevelScore (line 1515) | func getPluginLevelScore(source string) int {
function calculateTimeScore (line 1533) | func calculateTimeScore(datetime time.Time) float64 {
FILE: typescript/src/index.ts
class PanSouMCPServer (line 24) | class PanSouMCPServer {
method constructor (line 30) | constructor() {
method setupHandlers (line 56) | private setupHandlers(): void {
method getPluginsResource (line 190) | private async getPluginsResource() {
method getChannelsResource (line 220) | private async getChannelsResource() {
method getCloudTypesResource (line 249) | private async getCloudTypesResource() {
method setupProcessHandlers (line 297) | private setupProcessHandlers(): void {
method start (line 325) | public async start(): Promise<void> {
function main (line 364) | async function main(): Promise<void> {
FILE: typescript/src/tools/health.ts
function executeHealthTool (line 20) | async function executeHealthTool(args: unknown, httpClient: HttpClient):...
function formatHealthResult (line 40) | function formatHealthResult(healthData: any, serverUrl: string): string {
function formatErrorResult (line 98) | function formatErrorResult(errorMessage: string, serverUrl: string): str...
FILE: typescript/src/tools/search.ts
type SearchToolArgs (line 21) | type SearchToolArgs =
Condensed preview — 198 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,251K chars).
[
{
"path": ".dockerignore",
"chars": 253,
"preview": "# Git相关\n.git\n.gitignore\n.github\n\n# 文档和其他非必要文件\nREADME.md\ndocs/\n*.md\nLICENSE\n\n# 开发和测试相关\n*_test.go\n*.test\n*.out\n*.prof\n\n# 构"
},
{
"path": ".github/workflows/docker_ci.yml",
"chars": 1853,
"preview": "name: 构建并发布Docker镜像\n\non:\n push:\n branches:\n - \"main\"\n paths-ignore:\n - \"README.md\"\n - \"docs/**\"\n "
},
{
"path": ".gitignore",
"chars": 10,
"preview": ".DS_Store\n"
},
{
"path": "Dockerfile",
"chars": 3451,
"preview": "# 构建阶段\n# 使用 --platform=$BUILDPLATFORM 确保构建器始终在运行 Actions 的机器的原生架构上运行 (通常是 linux/amd64)\n# $BUILDPLATFORM 是 buildx 自动提供的变量"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2025 fish2018\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "README.md",
"chars": 15194,
"preview": "# PanSou 网盘搜索API\n\nPanSou是一个高性能的网盘资源搜索API服务,支持TG搜索和自定义插件搜索。系统设计以性能和可扩展性为核心,支持并发搜索、结果智能排序和网盘类型分类。\n\n[//]: # (MCP服务文档: [MCP-"
},
{
"path": "api/auth_handler.go",
"chars": 2009,
"preview": "package api\n\nimport (\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"pansou/config\"\n\t\"pansou/util\"\n)\n\n// LoginRequest 登录请求结构\ntyp"
},
{
"path": "api/filter.go",
"chars": 3418,
"preview": "package api\n\nimport (\n\t\"pansou/model\"\n\t\"strings\"\n)\n\n// applyResultFilter 应用过滤器到搜索响应\nfunc applyResultFilter(response mode"
},
{
"path": "api/handler.go",
"chars": 5753,
"preview": "package api\n\nimport (\n\t// \"fmt\"\n\t\"net/http\"\n\t// \"os\"\n\t\n\t\"github.com/gin-gonic/gin\"\n\t\"pansou/config\"\n\t\"pansou/model\"\n\t\"pa"
},
{
"path": "api/middleware.go",
"chars": 2935,
"preview": "package api\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"pansou/config\"\n\t\"pansou/util\"\n"
},
{
"path": "api/router.go",
"chars": 2203,
"preview": "package api\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"pansou/config\"\n\t\"pansou/plugin\"\n\t\"pansou/service\"\n\t\"pansou/util\"\n)\n\n"
},
{
"path": "config/config.go",
"chars": 13429,
"preview": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Config 应"
},
{
"path": "docker-compose.yml",
"chars": 2876,
"preview": "version: '3.8'\n\nservices:\n pansou:\n image: ghcr.io/fish2018/pansou:latest\n container_name: pansou\n restart: un"
},
{
"path": "docs/MCP-SERVICE.md",
"chars": 8937,
"preview": "# PanSou MCP 服务文档\n\n## 功能介绍\n\nPanSou MCP 服务是一个基于 [Model Context Protocol (MCP)](https://modelcontextprotocol.io) 的工具服务,它将 "
},
{
"path": "docs/插件开发指南.md",
"chars": 30426,
"preview": "# PanSou 插件开发指南\n\n## 概述\n\nPanSou 采用异步插件架构,支持通过插件扩展搜索来源。插件系统基于 Go 接口设计,提供高性能的并发搜索能力和智能缓存机制。\n\n## 系统架构\n\n### 核心组件\n\n- **插件管理器 ("
},
{
"path": "docs/系统开发设计文档.md",
"chars": 28028,
"preview": "# PanSou 网盘搜索系统开发设计文档\n\n## 📋 文档目录\n\n- [1. 项目概述](#1-项目概述)\n- [2. 系统架构设计](#2-系统架构设计)\n- [3. 异步插件系统](#3-异步插件系统)\n- [4. 二级缓存系统](#"
},
{
"path": "go.mod",
"chars": 1659,
"preview": "module pansou\n\ngo 1.24.1\n\ntoolchain go1.24.9\n\nrequire (\n\tgithub.com/Advik-B/cloudscraper v0.0.0-20250623142001-d5e0e4355"
},
{
"path": "go.sum",
"chars": 12343,
"preview": "github.com/Advik-B/cloudscraper v0.0.0-20250623142001-d5e0e43555db h1:r1hesdkYWgm4Bf7abv6UsIUlrCdFxRdKy+DuVypOpw4=\ngithu"
},
{
"path": "main.go",
"chars": 10049,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"sy"
},
{
"path": "mcp-config.json",
"chars": 1991,
"preview": "{\n \"mcpServers\": {\n \"pansou\": {\n \"command\": \"node\",\n \"args\": [\n \"C:\\\\full\\\\path\\\\to\\\\your\\\\project\\"
},
{
"path": "model/plugin_result.go",
"chars": 721,
"preview": "package model\n\nimport (\n\t\"time\"\n)\n\n// PluginSearchResult 插件搜索结果\ntype PluginSearchResult struct {\n\tResults []SearchResu"
},
{
"path": "model/request.go",
"chars": 1232,
"preview": "package model\n\n// FilterConfig 过滤配置\ntype FilterConfig struct {\n\tInclude []string `json:\"include,omitempty\"` // 包含关键词列表(O"
},
{
"path": "model/response.go",
"chars": 2330,
"preview": "package model\n\nimport \"time\"\n\n// Link 网盘链接\ntype Link struct {\n\tType string `json:\"type\" sonic:\"type\"`\n\tURL "
},
{
"path": "package.json",
"chars": 71,
"preview": "{\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.17.4\"\n }\n}\n"
},
{
"path": "plugin/ahhhhfs/ahhhhfs.go",
"chars": 13365,
"preview": "package ahhhhfs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\""
},
{
"path": "plugin/ahhhhfs/html结构分析.md",
"chars": 4322,
"preview": "# ahhhhfs (A姐分享) HTML结构分析\n\n## 网站信息\n- **网站名称**: ahhhhfs (A姐分享)\n- **域名**: www.ahhhhfs.com\n- **类型**: 资源分享网站(WordPress 站点)\n-"
},
{
"path": "plugin/aikanzy/aikanzy.go",
"chars": 14159,
"preview": "package aikanzy\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\""
},
{
"path": "plugin/alupan/alupan.go",
"chars": 10108,
"preview": "package alupan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Pue"
},
{
"path": "plugin/alupan/html结构分析.md",
"chars": 4052,
"preview": "# alupan (阿里U盘) HTML结构分析\n\n## 网站信息\n- **站点名称**: 阿里U盘\n- **域名**: `www.aliupan.com`\n- **类型**: 影视/图书等资源聚合站(WordPress D8 主题)\n- "
},
{
"path": "plugin/ash/ash.go",
"chars": 7307,
"preview": "package ash\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pan"
},
{
"path": "plugin/ash/html结构分析.md",
"chars": 3365,
"preview": "# ASH搜剧助手 HTML结构分析\n\n## 网站信息\n- **网站名称**: ASH搜剧助手\n- **域名**: so.allsharehub.com\n- **类型**: 影视资源搜索引擎\n- **特点**: 专门搜索影视剧资源,主要提供"
},
{
"path": "plugin/bixin/bixin.go",
"chars": 13453,
"preview": "package bixin\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/mo"
},
{
"path": "plugin/bixin/json结构分析.md",
"chars": 16659,
"preview": "# Bixin API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API \n- **API URL格式**: `https://www.bixbiy.com/api/discussions?filter[q]={"
},
{
"path": "plugin/cldi/cldi.go",
"chars": 8282,
"preview": "package cldi\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com"
},
{
"path": "plugin/cldi/html结构分析.md",
"chars": 2815,
"preview": "# CLDI (磁力帝) HTML结构分析\n\n## 网站信息\n- **网站名称**: 磁力帝\n- **域名**: cldcld.cc (通过动态域名访问)\n- **类型**: 磁力搜索引擎\n- **特点**: 专门搜索BT种子和磁力链接\n\n"
},
{
"path": "plugin/clmao/clmao.go",
"chars": 9111,
"preview": "package clmao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.co"
},
{
"path": "plugin/clmao/html结构分析.md",
"chars": 3027,
"preview": "# Clmao (磁力猫) HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 磁力猫 - 磁力搜索引擎\n- **基础URL**: https://www.8800492.xyz/\n- **功能**: BT种子磁力链接搜索\n- *"
},
{
"path": "plugin/clxiong/clxiong.go",
"chars": 15697,
"preview": "package clxiong\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/"
},
{
"path": "plugin/clxiong/html结构分析.md",
"chars": 3911,
"preview": "# 磁力熊(CiLiXiong) HTML结构分析文档\n\n## 网站信息\n- **域名**: `www.cilixiong.org` \n- **名称**: 磁力熊\n- **类型**: 影视磁力链接搜索网站\n- **特点**: 两步式搜索流"
},
{
"path": "plugin/cyg/cyg.go",
"chars": 11867,
"preview": "package cyg\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"html\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pan"
},
{
"path": "plugin/daishudj/daishudj.go",
"chars": 11462,
"preview": "package daishudj\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/P"
},
{
"path": "plugin/daishudj/html结构分析.md",
"chars": 2463,
"preview": "# daishudj (袋鼠短剧网) HTML结构分析\n\n## 搜索页面\n\n- **URL**: `https://www.daishuduanju.com/?s={关键词}`\n- **页面结构**:\n - 列表容器:`div.item-"
},
{
"path": "plugin/ddys/ddys.go",
"chars": 14219,
"preview": "package ddys\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"git"
},
{
"path": "plugin/ddys/html结构分析.md",
"chars": 5493,
"preview": "# DDYS(低端影视)插件HTML结构分析\n\n## 网站概述\n- **网站名称**: 低端影视\n- **域名**: https://ddys.pro/\n- **类型**: 影视资源网站,提供在线播放和网盘下载链接\n\n## API流程概述\n"
},
{
"path": "plugin/discourse/discourse.go",
"chars": 14452,
"preview": "package discourse\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"pansou/util/json\"\n\t\"r"
},
{
"path": "plugin/discourse/json结构分析.md",
"chars": 11290,
"preview": "# Linux.do 搜索API JSON结构分析\n\n## 接口信息\n\n- **接口名称**: Linux.do 论坛搜索API (Discourse)\n- **接口地址**: `https://linux.do/search.json`\n"
},
{
"path": "plugin/djgou/djgou.go",
"chars": 11148,
"preview": "package djgou\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\"\n\t"
},
{
"path": "plugin/duoduo/duoduo.go",
"chars": 17430,
"preview": "package duoduo\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\"\n"
},
{
"path": "plugin/duoduo/html结构分析.md",
"chars": 2721,
"preview": "# Duoduo网站 (多多) 搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 多多\n- **域名**: `tv.yydsys.top`\n- **搜索URL格式**: `https://tv.yydsys.top/in"
},
{
"path": "plugin/dyyj/dyyj.go",
"chars": 30896,
"preview": "package dyyj\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n"
},
{
"path": "plugin/dyyj/html结构分析.md",
"chars": 3574,
"preview": "# DYYJ(电影云集)插件HTML结构分析\n\n## 网站概述\n- **网站名称**: 电影云集\n- **域名**: https://bbs.dyyjmax.org\n- **类型**: 影视资源论坛,提供网盘下载链接\n- **技术栈**: "
},
{
"path": "plugin/erxiao/erxiao.go",
"chars": 14639,
"preview": "package erxiao\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t"
},
{
"path": "plugin/erxiao/html结构分析.md",
"chars": 3971,
"preview": "# Erxiao HTML 数据结构分析\n\n## 基本信息\n- **数据源类型**: HTML 网页\n- **搜索URL格式**: `https://erxiaofn.click/index.php/vod/search/wd/{关键词}."
},
{
"path": "plugin/feikuai/feikuai.go",
"chars": 10633,
"preview": "package feikuai\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t"
},
{
"path": "plugin/feikuai/html结构分析.md",
"chars": 14449,
"preview": "# Feikuai网站 (飞快TV) HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 飞快TV\n- **域名**: `feikuai.tv`\n- **搜索URL格式**: `https://feikuai.tv/vodsear"
},
{
"path": "plugin/feikuai/json结构分析.md",
"chars": 8679,
"preview": "# Feikuai API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API \n- **网站名称**: 飞快TV (feikuai.tv)\n- **API URL格式**: `https://feikuai.tv"
},
{
"path": "plugin/fox4k/fox4k.go",
"chars": 28500,
"preview": "package fox4k\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"s"
},
{
"path": "plugin/fox4k/html结构分析.md",
"chars": 9334,
"preview": "# 极狐4K (4kfox.com) 网站搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 极狐4K\n- **网站域名**: www.4kfox.com\n- **搜索URL格式**: `https://www.4kfox"
},
{
"path": "plugin/gying/README.md",
"chars": 4722,
"preview": "# Gying 搜索插件\n\n## 📖 简介\n\nGying是PanSou的搜索插件,用于从 www.gying.net 网站搜索影视资源。支持多用户登录并配置账户,在搜索时自动聚合所有用户的搜索结果。\n\n## ✨ 核心特性\n\n- ✅ **多用"
},
{
"path": "plugin/gying/gying.go",
"chars": 63943,
"preview": "package gying\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex"
},
{
"path": "plugin/gying/html结构分析.md",
"chars": 10679,
"preview": "# Gying 网站结构分析\n\n## 基本信息\n- **网站URL**: https://www.gying.net\n- **数据源类型**: 混合型(HTML + JSON API)\n- **特殊架构**: 需要登录 + 搜索结果在HTM"
},
{
"path": "plugin/haisou/haisou.go",
"chars": 14107,
"preview": "package haisou\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/m"
},
{
"path": "plugin/haisou/json结构分析.md",
"chars": 6917,
"preview": "# Haisou 搜索API JSON结构分析\n\n## 接口信息\n\n- **接口名称**: 海搜网盘资源搜索API\n- **接口地址**: `https://haisou.cc/api/pan/share/search` (搜索API)\n-"
},
{
"path": "plugin/hdmoli/hdmoli.go",
"chars": 17676,
"preview": "package hdmoli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"g"
},
{
"path": "plugin/hdmoli/html结构分析.md",
"chars": 4653,
"preview": "# HDMOLI(HDmoli)插件HTML结构分析\n\n## 网站概述\n- **网站名称**: HDmoli\n- **域名**: https://www.hdmoli.pro/\n- **类型**: 影视资源网站,主要提供网盘下载链接(夸克网"
},
{
"path": "plugin/hdr4k/hdr4k.go",
"chars": 16118,
"preview": "package hdr4k\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github"
},
{
"path": "plugin/hdr4k/html结构分析.md",
"chars": 4727,
"preview": "# 4KHDR网站搜索结果HTML结构分析\n\n## 搜索接口\n\n- **搜索URL**: `https://www.4khdr.cn/search.php?mod=forum`\n- **请求方法**: POST\n- **请求参数**: \n "
},
{
"path": "plugin/hdr4k/设计文档.md",
"chars": 18115,
"preview": "# 4KHDR插件开发设计文档\n\n## 📋 目录\n\n1. [项目概述](#项目概述)\n2. [架构设计](#架构设计)\n3. [技术实现](#技术实现)\n4. [核心优化](#核心优化)\n5. [API设计](#api设计)\n6. [性能优"
},
{
"path": "plugin/huban/html结构分析.md",
"chars": 3869,
"preview": "# Huban HTML 数据结构分析\n\n## 基本信息\n- **数据源类型**: HTML 网页\n- **搜索URL格式**: `http://xsayang.fun:12512/index.php/vod/search/wd/{关键词}"
},
{
"path": "plugin/huban/huban.go",
"chars": 17800,
"preview": "package huban\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\""
},
{
"path": "plugin/huban/json结构分析.md",
"chars": 9599,
"preview": "# Huban API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API \n- **特殊架构**: **双域名支持** - 需要处理两个不同的API端点\n- **API URL格式1**: `http://103"
},
{
"path": "plugin/hunhepan/hunhepan.go",
"chars": 11762,
"preview": "package hunhepan\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pansou"
},
{
"path": "plugin/javdb/javdb.go",
"chars": 24792,
"preview": "package javdb\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"str"
},
{
"path": "plugin/jikepan/jikepan.go",
"chars": 4778,
"preview": "package jikepan\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"pansou"
},
{
"path": "plugin/jsnoteclub/jsnoteclub.go",
"chars": 14902,
"preview": "package jsnoteclub\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"ti"
},
{
"path": "plugin/jutoushe/html结构分析.md",
"chars": 4060,
"preview": "# JUTOUSHE(剧透社)HTML结构分析\n\n## 网站信息\n- **网站名称**: 剧透社\n- **域名**: https://1.star2.cn/\n- **类型**: 网盘资源分享站,主要提供电视剧、电影、短剧、综艺等影视资源\n-"
},
{
"path": "plugin/jutoushe/jutoushe.go",
"chars": 8656,
"preview": "package jutoushe\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.co"
},
{
"path": "plugin/kkmao/html结构分析.md",
"chars": 3546,
"preview": "# kkmao (夸克猫) HTML结构分析\n\n## 网站信息\n- **网站名称**: 夸克猫资源\n- **域名**: `www.kuakemao.com`\n- **类型**: 夸克网盘影视资源分享站(WordPress 主题站)\n- **"
},
{
"path": "plugin/kkmao/kkmao.go",
"chars": 9519,
"preview": "package kkmao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Puer"
},
{
"path": "plugin/kkv/html结构分析.md",
"chars": 3929,
"preview": "# KKV (小悠家) HTML结构分析\n\n## 网站信息\n- 网站名称: 小悠家\n- 域名: kkv.q-23.cn\n- 类型: 影视资源搜索(支持多种网盘)\n\n## 1. 搜索页面\n\n### URL格式\n```\nhttp://kkv.q"
},
{
"path": "plugin/kkv/kkv.go",
"chars": 10341,
"preview": "package kkv\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Puerki"
},
{
"path": "plugin/labi/html结构分析.md",
"chars": 8100,
"preview": "# Labi网站 (xiaocge.fun/duopan.fun) 搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 免费的云盘分享平台\n- **搜索URL格式**: `http://xiaocge.fun/index."
},
{
"path": "plugin/labi/labi.go",
"chars": 12411,
"preview": "package labi\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\"\n\t\""
},
{
"path": "plugin/leijing/html结构分析.md",
"chars": 3998,
"preview": "# leijing插件HTML结构分析\n\n## 网站信息\n- 网站名称:雷鲸小站-天翼云盘交流站\n- 主域名:https://leijing.xyz\n- 网站类型:天翼云盘资源分享论坛\n- 特点:\n - **专注天翼云盘**(只有天翼云盘"
},
{
"path": "plugin/leijing/leijing.go",
"chars": 10493,
"preview": "package leijing\n\nimport (\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"tim"
},
{
"path": "plugin/libvio/html结构分析.md",
"chars": 3861,
"preview": "# libvio插件HTML结构分析\n\n## 网站信息\n- 网站名称:LIBVIO\n- 主域名:https://www.libvio.mov\n- 网站类型:影视资源在线播放/下载网站\n- 特点:提供网盘下载链接(夸克、UC等)\n\n## 访问"
},
{
"path": "plugin/libvio/libvio.go",
"chars": 14895,
"preview": "package libvio\n\nimport (\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time"
},
{
"path": "plugin/lou1/lou1.go",
"chars": 14050,
"preview": "package lou1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Pu"
},
{
"path": "plugin/meitizy/json结构分析.md",
"chars": 3008,
"preview": "# Meitizy(美体资源)插件JSON API结构分析\n\n## 网站概述\n- **网站名称**: 美体资源\n- **API域名**: https://video.451024.xyz\n- **类型**: JSON API接口,提供影视资"
},
{
"path": "plugin/meitizy/meitizy.go",
"chars": 9317,
"preview": "package meitizy\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pansou/plug"
},
{
"path": "plugin/miaoso/json结构分析.md",
"chars": 4567,
"preview": "# Miaoso API JSON 结构分析\n\n## 概述\n\nMiaoso 是一个网盘搜索平台,提供 RESTful API 接口进行内容搜索。本文档基于对 `1.txt` 文件中 API 响应数据的分析,详细说明 Miaoso API 的"
},
{
"path": "plugin/miaoso/miaoso.go",
"chars": 8743,
"preview": "package miaoso\n\nimport (\n\t\"context\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url"
},
{
"path": "plugin/mikuclub/html结构分析.md",
"chars": 1532,
"preview": "# mikuclub (初音社) HTML结构分析\n\n## 页面概览\n- 详情页 URL: `https://www.mikuclub.uk/{post_id}`\n- 主体容器:`div.article_content`\n- 常见内容顺序:"
},
{
"path": "plugin/mikuclub/json结构分析.md",
"chars": 2420,
"preview": "# mikuclub JSON结构分析\n\n## 搜索接口\n- **URL**\n```\nhttps://www.mikuclub.uk/wp-json/utils/v2/post_list?search={kw}&s={kw}&page_ty"
},
{
"path": "plugin/mikuclub/mikuclub.go",
"chars": 12934,
"preview": "package mikuclub\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time"
},
{
"path": "plugin/mizixing/mizixing.go",
"chars": 13105,
"preview": "package mizixing\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n"
},
{
"path": "plugin/muou/html结构分析.md",
"chars": 4236,
"preview": "# 木偶(muou)网站HTML结构分析\n\n## 基本信息\n- **网站名称**: 中华人民共和国万岁(木偶网站)\n- **域名**: `123.666291.xyz`\n- **搜索URL格式**: `http://123.666291.x"
},
{
"path": "plugin/muou/muou.go",
"chars": 17023,
"preview": "package muou\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\"\n\t\""
},
{
"path": "plugin/nsgame/json结构分析.md",
"chars": 7106,
"preview": "# NSGame API JSON 结构分析\n\n## 概述\n\nNSGame (NS游戏网) 是一个专门提供 Nintendo Switch 游戏资源的搜索平台,提供 RESTful API 接口进行游戏资源搜索。本文档详细说明 NSGame"
},
{
"path": "plugin/nsgame/nsgame.go",
"chars": 7241,
"preview": "package nsgame\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t"
},
{
"path": "plugin/nyaa/html结构分析.md",
"chars": 6434,
"preview": "# Nyaa.si BT种子搜索站结构分析\n\n## 网站信息\n\n- **网站名称**: Nyaa.si\n- **网站URL**: https://nyaa.si\n- **网站类型**: 动漫BT种子搜索引擎\n- **数据源**: HTML页"
},
{
"path": "plugin/nyaa/nyaa.go",
"chars": 7595,
"preview": "package nyaa\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strconv\"\n\t\""
},
{
"path": "plugin/ouge/json结构分析.md",
"chars": 4277,
"preview": "# Ouge API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API \n- **API URL格式**: `https://woog.nxog.eu.org/api.php/provide/vod?ac=det"
},
{
"path": "plugin/ouge/ouge.go",
"chars": 11726,
"preview": "package ouge\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\t\"context\"\n\t\"sync/atomic\"\n\n\t\"pan"
},
{
"path": "plugin/pan666/pan666.go",
"chars": 16576,
"preview": "package pan666\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/m"
},
{
"path": "plugin/pansearch/pansearch.go",
"chars": 25549,
"preview": "package pansearch\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"panso"
},
{
"path": "plugin/panta/panta.go",
"chars": 29991,
"preview": "package panta\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\"\n\t"
},
{
"path": "plugin/panta/panta插件设计文档.md",
"chars": 9563,
"preview": "# PanTa 搜索插件设计文档\n\n## 1. 概述\n\nPanTa搜索插件是一个用于从91panta.cn网站搜索并提取网盘链接的Go语言插件。该插件能够智能识别多种网盘链接类型,并自动关联提取码,是一个高性能、高可靠性的网络爬虫实现。\n\n"
},
{
"path": "plugin/panwiki/panwiki.go",
"chars": 37777,
"preview": "package panwiki\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github"
},
{
"path": "plugin/panyq/panyq.go",
"chars": 34333,
"preview": "package panyq\n\nimport (\n\t\"crypto/tls\"\n\t\"pansou/util/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepa"
},
{
"path": "plugin/pianku/html结构分析.md",
"chars": 12342,
"preview": "# 片库网 (btnull.pro) 网站搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 片库网 BTNULL\n- **网站域名**: btnull.pro \n- **搜索URL格式**: `https://btnu"
},
{
"path": "plugin/pianku/pianku.go",
"chars": 13137,
"preview": "package pianku\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pansou"
},
{
"path": "plugin/plugin.go",
"chars": 28810,
"preview": "package plugin\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"pan"
},
{
"path": "plugin/qingying/html结构分析.md",
"chars": 2932,
"preview": "# 清影 (revohd.com) HTML结构分析\n\n## 网站信息\n- 网站名称: 清影\n- 域名: www.revohd.com\n- 类型: 影视资源搜索(仅123网盘)\n\n## 1. 搜索页面\n\n### URL格式\n```\nhttp"
},
{
"path": "plugin/qingying/qingying.go",
"chars": 11336,
"preview": "package qingying\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github"
},
{
"path": "plugin/qqpd/README.md",
"chars": 8952,
"preview": "# QQ频道搜索插件 (QQPD)\n\n## 📖 简介\n\nQQPD是PanSou的QQ频道搜索插件,支持多用户登录QQ频道并配置频道列表,在搜索时自动聚合所有用户的频道资源。\n\n## ✨ 核心特性\n\n- ✅ **多用户支持** - 每个用户独"
},
{
"path": "plugin/qqpd/qqpd.go",
"chars": 63775,
"preview": "package qqpd\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t"
},
{
"path": "plugin/quark4k/json结构分析.md",
"chars": 17727,
"preview": "# Quark4K API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API \n- **API URL格式**: `https://quark4k.com/api/discussions?include=user"
},
{
"path": "plugin/quark4k/quark4k.go",
"chars": 15196,
"preview": "package quark4k\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/"
},
{
"path": "plugin/quarksoo/html结构分析.md",
"chars": 6986,
"preview": "# Quarksoo HTML 结构分析\n\n## 基本信息\n- **数据源类型**: HTML 页面 \n- **API URL格式**: `https://quarksoo.cc/search.php?q={关键词}`\n- **请求方法*"
},
{
"path": "plugin/quarksoo/quarksoo.go",
"chars": 6243,
"preview": "package quarksoo\n\nimport (\n\t\"crypto/md5\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t"
},
{
"path": "plugin/qupanshe/qupanshe.go",
"chars": 20509,
"preview": "package qupanshe\n\nimport (\n\t\"compress/gzip\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"regex"
},
{
"path": "plugin/qupansou/qupansou.go",
"chars": 8118,
"preview": "package qupansou\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pansou/plugin"
},
{
"path": "plugin/sdso/sdso.go",
"chars": 12482,
"preview": "package sdso\n\nimport (\n\t\"context\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n"
},
{
"path": "plugin/shandian/html结构分析.md",
"chars": 7791,
"preview": "# Shandian网站 (闪电优汐) 搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 闪电优汐\n- **主域名**: `1.95.79.193`\n- **备用域名**: `feimaouc.cloud:666`\n- "
},
{
"path": "plugin/shandian/shandian.go",
"chars": 11688,
"preview": "package shandian\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings"
},
{
"path": "plugin/sousou/json结构分析.md",
"chars": 8684,
"preview": "# Sousou API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API (GET 请求)\n- **API URL格式**: `https://sousou.pro/api.php?action=search&q"
},
{
"path": "plugin/sousou/sousou.go",
"chars": 11347,
"preview": "package sousou\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pansou"
},
{
"path": "plugin/susu/html结构分析.md",
"chars": 1605,
"preview": "# SuSu网站搜索结果HTML结构分析\n\n## 页面整体结构\n\n搜索结果页面的主要内容位于`.post-1.post-list.post-item-1`元素内,每个搜索结果项包含在`.post-list-item.item-post-st"
},
{
"path": "plugin/susu/susu.go",
"chars": 13646,
"preview": "package susu\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t"
},
{
"path": "plugin/susu/susu插件设计文档.md",
"chars": 28462,
"preview": "# SuSu 搜索插件设计文档\n\n## 目录\n\n1. [概述](#概述)\n2. [设计背景](#设计背景)\n3. [总体架构](#总体架构)\n4. [核心组件](#核心组件)\n5. [关键算法](#关键算法)\n6. [性能优化](#性能优化"
},
{
"path": "plugin/thepiratebay/html结构分析.md",
"chars": 5612,
"preview": "# ThePirateBay 网站搜索结果HTML结构分析\n\n## 网站概述\n\nThePirateBay (tpirbay.xyz) 是一个专门提供BitTorrent种子资源的搜索网站,**只提供磁力链接**,不提供网盘下载链接。搜索结果"
},
{
"path": "plugin/thepiratebay/thepiratebay.go",
"chars": 12307,
"preview": "package thepiratebay\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n"
},
{
"path": "plugin/u3c3/u3c3.go",
"chars": 8925,
"preview": "package u3c3\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/PuerkitoBio"
},
{
"path": "plugin/wanou/json结构分析.md",
"chars": 4537,
"preview": "# Wanou API 数据结构分析\n\n## 基本信息\n- **数据源类型**: JSON API\n- **API URL格式**: `https://woog.nxog.eu.org/api.php/provide/vod?ac=deta"
},
{
"path": "plugin/wanou/wanou.go",
"chars": 12332,
"preview": "package wanou\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\t\"context\"\n\t\"sync/atomic\"\n\n\t\"pa"
},
{
"path": "plugin/weibo/README.md",
"chars": 8778,
"preview": "# 微博搜索插件 (Weibo)\n\n## 📖 简介\n\nWeibo是PanSou的微博搜索插件,支持多用户登录微博并配置要搜索的微博用户,在搜索时自动聚合所有配置的微博用户发布的资源链接(从微博正文和评论中提取)。\n\n## ✨ 核心特性\n\n-"
},
{
"path": "plugin/weibo/weibo.go",
"chars": 54303,
"preview": "package weibo\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/"
},
{
"path": "plugin/weibo/微博用户搜索API文档.md",
"chars": 10422,
"preview": "# 微博用户搜索 API 文档\n\n---\n\n## 一、Cookie认证\n\n### 1.1 Cookie字段分类\n\n微博Cookie包含多种类型的字段,有效期各不相同:\n\n| Cookie字段 | 有效期 | 作用 |\n|----------"
},
{
"path": "plugin/wuji/html结构分析.md",
"chars": 3832,
"preview": "# Wuji (无极磁链) HTML结构分析\n\n## 网站信息\n\n- **网站名称**: ØMagnet 无极磁链\n- **基础URL**: https://xcili.net/\n- **功能**: 磁力链接搜索引擎\n- **搜索URL格式"
},
{
"path": "plugin/wuji/wuji.go",
"chars": 11180,
"preview": "package wuji\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com"
},
{
"path": "plugin/xb6v/xb6v.go",
"chars": 19035,
"preview": "package xb6v\n\nimport (\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n"
},
{
"path": "plugin/xdpan/html结构分析.md",
"chars": 4546,
"preview": "# 兄弟盘 (xiongdipan.com) HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 兄弟盘\n- **域名**: xiongdipan.com\n- **类型**: 百度网盘资源搜索引擎\n- **特点**: 专门搜索百度"
},
{
"path": "plugin/xdpan/xdpan.go",
"chars": 10964,
"preview": "package xdpan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Puer"
},
{
"path": "plugin/xdyh/json结构分析.md",
"chars": 6573,
"preview": "# XDYH 搜索API JSON结构分析\n\n## 接口信息\n\n- **接口名称**: XDYH 聚合搜索API\n- **接口地址**: `https://ys.66ds.de/search`\n- **请求方法**: `POST`\n- **"
},
{
"path": "plugin/xdyh/xdyh.go",
"chars": 12041,
"preview": "package xdyh\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"pansou/util/json\""
},
{
"path": "plugin/xiaoji/html结构分析.md",
"chars": 6662,
"preview": "# 小鸡影视 (xiaojitv.com) 搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 小鸡影视\n- **域名**: `www.xiaojitv.com`\n- **搜索URL格式**: `https://www.x"
},
{
"path": "plugin/xiaoji/xiaoji.go",
"chars": 11415,
"preview": "package xiaoji\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\""
},
{
"path": "plugin/xiaozhang/html结构分析.md",
"chars": 3173,
"preview": "# xiaozhang插件HTML结构分析\n\n## 网站信息\n- 网站名称:校长影视\n- 主域名:https://xzys.fun\n- 网站类型:影视资源搜索网站\n\n## 1. 搜索页面结构\n\n### 搜索URL格式\n```\nhttps:/"
},
{
"path": "plugin/xiaozhang/xiaozhang.go",
"chars": 13249,
"preview": "package xiaozhang\n\nimport (\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"t"
},
{
"path": "plugin/xinjuc/html结构分析.md",
"chars": 7209,
"preview": "# 新剧坊 (xinjuc.com) 网站结构分析\n\n## 网站信息\n\n- **网站名称**: 新剧坊 - 一个网盘资源分享小站\n- **网站URL**: https://www.xinjuc.com\n- **网站类型**: 影视资源网盘分"
},
{
"path": "plugin/xinjuc/xinjuc.go",
"chars": 12775,
"preview": "package xinjuc\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\"\n"
},
{
"path": "plugin/xuexizhinan/html结构分析.md",
"chars": 5058,
"preview": "# 4K指南 (xuexizhinan) 网站搜索结果HTML结构分析\n\n## 搜索结果页面结构\n\n搜索结果页面的主要内容位于`.content-wrap .content-layout .row`元素内,每个搜索结果项包含在`.url-c"
},
{
"path": "plugin/xuexizhinan/xuexizhinan.go",
"chars": 9675,
"preview": "package xuexizhinan\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.co"
},
{
"path": "plugin/xys/html结构分析.md",
"chars": 2466,
"preview": "# XYS(小云搜索)插件HTML结构分析\n\n## API流程概述\n\n### 第一步:获取Token\n- **请求URL**: `https://www.yunso.net/index/user/s?wd={keyword}&mode=un"
},
{
"path": "plugin/xys/xys.go",
"chars": 11360,
"preview": "package xys\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sy"
},
{
"path": "plugin/yiove/yiove.go",
"chars": 16425,
"preview": "package yiove\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/P"
},
{
"path": "plugin/ypfxw/html结构分析.md",
"chars": 2806,
"preview": "# ypfxw (网盘资源分享网) HTML结构分析\n\n## 网站信息\n- **站点名称**: 网盘资源分享网\n- **域名**: `ypfxw.com`\n- **系统**: Z-Blog (主题 `qk_teat`)\n- **特点**: "
},
{
"path": "plugin/ypfxw/ypfxw.go",
"chars": 11528,
"preview": "package ypfxw\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Puer"
},
{
"path": "plugin/yuhuage/yuhuage.go",
"chars": 9791,
"preview": "package yuhuage\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atom"
},
{
"path": "plugin/yunsou/html结构分析.md",
"chars": 6691,
"preview": "# 云搜影视 (yunsou) 网站搜索结果HTML结构分析\n\n## 网站信息\n\n- **网站名称**: 云搜影视\n- **域名**: `yunsou.xyz`\n- **搜索URL格式**: `https://yunsou.xyz/s/{关"
},
{
"path": "plugin/yunsou/yunsou.go",
"chars": 7838,
"preview": "package yunsou\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"pansou/util/j"
},
{
"path": "plugin/zhizhen/json结构分析.md",
"chars": 5565,
"preview": "# Zhizhen HTML 数据结构分析\n\n## 基本信息\n- **数据源类型**: HTML 网页\n- **搜索URL格式**: `https://xiaomi666.fun/index.php/vod/search/wd/{关键词}."
},
{
"path": "plugin/zhizhen/zhizhen.go",
"chars": 17285,
"preview": "package zhizhen\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"regexp\"\n\t\"strings\""
},
{
"path": "plugin/zxzj/html结构分析.md",
"chars": 3726,
"preview": "# 在线之家(zxzjhd.com)HTML结构分析\n\n## 网站信息\n\n- **站点名称**:在线之家\n- **主域名**:`https://www.zxzjhd.com`\n- **搜索入口**:`https://www.zxzjhd.c"
},
{
"path": "plugin/zxzj/zxzj.go",
"chars": 11438,
"preview": "package zxzj\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com"
},
{
"path": "service/cache_integration.go",
"chars": 3018,
"preview": "package service\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"pansou/model\"\n\t\"pansou/plugin\"\n\t\"pansou/util/cache\"\n)\n\n// CacheWriteIntegrat"
},
{
"path": "service/search_service.go",
"chars": 37426,
"preview": "package service\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"t"
},
{
"path": "typescript/package.json",
"chars": 1104,
"preview": "{\n \"name\": \"@pansou/mcp-server\",\n \"version\": \"1.0.0\",\n \"description\": \"MCP server for PanSou netdisk search service\","
},
{
"path": "typescript/src/index.ts",
"chars": 9665,
"preview": "#!/usr/bin/env node\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport }"
},
{
"path": "typescript/src/tools/health.ts",
"chars": 2996,
"preview": "import { Tool } from '@modelcontextprotocol/sdk/types.js';\nimport { HttpClient } from '../utils/http-client.js';\n\n/**\n *"
},
{
"path": "typescript/src/tools/search.ts",
"chars": 7806,
"preview": "import { Tool } from '@modelcontextprotocol/sdk/types.js';\nimport { z } from 'zod';\nimport { HttpClient, SearchRequest }"
},
{
"path": "typescript/src/tools/start-backend.ts",
"chars": 3205,
"preview": "import { Tool } from '@modelcontextprotocol/sdk/types.js';\nimport { BackendManager } from '../utils/backend-manager.js';"
},
{
"path": "typescript/src/utils/activity-monitor.ts",
"chars": 3118,
"preview": "/**\n * 活动监控器 - 跟踪MCP工具调用活动\n */\nexport class ActivityMonitor {\n private lastActivityTime: number;\n private idleTimeout:"
},
{
"path": "typescript/src/utils/backend-manager.ts",
"chars": 12211,
"preview": "import { spawn, ChildProcess } from 'child_process';\nimport { promises as fs } from 'fs';\nimport path from 'path';\nimpor"
},
{
"path": "typescript/src/utils/config.ts",
"chars": 2553,
"preview": "import { z } from 'zod';\nimport { ConfigSchema } from './validators.js';\n\nexport type Config = z.infer<typeof ConfigSche"
},
{
"path": "typescript/src/utils/http-client.ts",
"chars": 6934,
"preview": "import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';\nimport { Config, validateCloudTypes } f"
},
{
"path": "typescript/src/utils/validators.ts",
"chars": 2919,
"preview": "import { z } from 'zod';\n\n/**\n * 支持的网盘类型列表\n */\nexport const SUPPORTED_CLOUD_TYPES = [\n 'baidu', // 百度网盘\n 'aliyun', "
},
{
"path": "typescript/tsconfig.json",
"chars": 930,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2020\",\n \"module\": \"ES2020\",\n \"lib\": [\"ES2020\"],\n \"outDir\": \"./dist\",\n"
},
{
"path": "util/cache/cache_key.go",
"chars": 6354,
"preview": "package cache\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\n\t\"pansou/plugin\"\n)\n\n// 预计算的哈希值"
},
{
"path": "util/cache/delayed_batch_write_manager.go",
"chars": 23806,
"preview": "package cache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n"
},
{
"path": "util/cache/disk_cache.go",
"chars": 6751,
"preview": "package cache\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\t\n\t\"pan"
},
{
"path": "util/cache/enhanced_two_level_cache.go",
"chars": 3649,
"preview": "package cache\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"pansou/config\"\n)\n\n// EnhancedTwoLevelCache 改进的两级缓存\ntype EnhancedTwoLev"
},
{
"path": "util/cache/global_buffer_manager.go",
"chars": 12355,
"preview": "package cache\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// GlobalBufferStrategy 全局缓冲策略\ntype GlobalBufferStrateg"
},
{
"path": "util/cache/memory_cache.go",
"chars": 3449,
"preview": "package cache\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\n// 简单的内存缓存项\ntype memoryCacheItem struct {\n\tdata []byte\n\texpiry "
},
{
"path": "util/cache/serializer.go",
"chars": 2039,
"preview": "package cache\n\nimport (\n\t\"bytes\"\n\t\"encoding/gob\"\n\t\"sync\"\n\t\"time\"\n\t\n\t\"pansou/model\"\n)\n\n// 初始化函数,注册model包中的类型到gob\nfunc ini"
},
{
"path": "util/cache/sharded_disk_cache.go",
"chars": 3794,
"preview": "package cache\n\nimport (\n\t\"fmt\"\n\t\"hash/fnv\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n)\n\n// ShardedDiskCache 分片磁盘缓存\ntyp"
},
{
"path": "util/cache/sharded_memory_cache.go",
"chars": 8453,
"preview": "package cache\n\nimport (\n\t\"hash/fnv\"\n\t\"runtime\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// 全局清理任务相关变量(单例模式)\nvar (\n\tglobalCleanu"
},
{
"path": "util/cache/utils.go",
"chars": 857,
"preview": "package cache\n\nimport (\n\t\"bytes\"\n\t\"sync\"\n\t\n\t\"pansou/util/json\"\n)\n\n// 缓冲区对象池\nvar bufferPool = sync.Pool{\n\tNew: func() int"
},
{
"path": "util/compression.go",
"chars": 2504,
"preview": "package util\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"io/ioutil\"\n\t\"strings\"\n\t\n\t\"github.com/gin-gonic/gin\"\n\t\"pansou/config\"\n"
},
{
"path": "util/convert.go",
"chars": 206,
"preview": "package util\n\nimport (\n\t\"strconv\"\n)\n\n// StringToInt 将字符串转换为整数,如果转换失败则返回0\nfunc StringToInt(s string) int {\n\tif s == \"\" {\n"
},
{
"path": "util/http_util.go",
"chars": 2710,
"preview": "package util\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"golang.org/x/net/proxy\"\n\t"
},
{
"path": "util/json/json.go",
"chars": 972,
"preview": "package json\n\nimport (\n\t\"github.com/bytedance/sonic\"\n)\n\n// API是sonic的全局配置实例\nvar API = sonic.ConfigDefault\n\n// 初始化sonic配置"
},
{
"path": "util/jwt.go",
"chars": 1473,
"preview": "package util\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\n// Claims JWT载荷结构\ntype Claims struct {\n\tUse"
},
{
"path": "util/parser_util.go",
"chars": 20771,
"preview": "package util\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"pansou/model\"\n)\n\n// normalizeU"
},
{
"path": "util/pool/object_pool.go",
"chars": 1396,
"preview": "package pool\n\nimport (\n\t\"sync\"\n\n\t\"pansou/model\"\n)\n\n// LinkPool 网盘链接对象池\nvar LinkPool = sync.Pool{\n\tNew: func() interface{"
},
{
"path": "util/pool/worker_pool.go",
"chars": 3266,
"preview": "package pool\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Task 表示一个工作任务\ntype Task func() interface{}\n\n// WorkerPool 工作池结构体"
},
{
"path": "util/regex_util.go",
"chars": 22575,
"preview": "package util\n\nimport (\n\tnetUrl \"net/url\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// 通用网盘链接匹配正则表达式 - 修改为更精确的匹配模式\nvar AllPanLinksPattern ="
}
]
About this extraction
This page contains the full source code of the fish2018/pansou GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 198 files (1.9 MB), approximately 704.4k tokens, and a symbol index with 2558 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.