Skip to content

Commit

Permalink
Merge pull request #440 from EstrellaXD/fix/bangumi-search-api
Browse files Browse the repository at this point in the history
fix: 修复 bangumi search API 中的流式接口,webui 对应接口定义方法改造
  • Loading branch information
EstrellaXD authored Sep 1, 2023
2 parents 16bbf6f + 7d50e36 commit f8fdd16
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 23 deletions.
18 changes: 18 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"recommendations": [
// https://marketplace.visualstudio.com/items?itemName=antfu.unocss
"antfu.unocss",
// https://marketplace.visualstudio.com/items?itemName=formulahendry.auto-rename-tag
"formulahendry.auto-rename-tag",
// https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker
"streetsidesoftware.code-spell-checker",
// https://marketplace.visualstudio.com/items?itemName=naumovs.color-highlight
"naumovs.color-highlight",
// https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance
"ms-python.vscode-pylance",
// https://marketplace.visualstudio.com/items?itemName=ms-python.python
"ms-python.python",
// https://marketplace.visualstudio.com/items?itemName=vue.volar
"vue.volar"
]
}
17 changes: 17 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Dev Backend",
"type": "python",
"request": "launch",
"cwd": "${workspaceFolder}/backend/src",
"program": "main.py",
"console": "integratedTerminal",
"justMyCode": true
}
]
}
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@
"editor.wordWrap": "off",
},
"python.venvPath": "./backend/venv",
"cSpell.words": [
"Bangumi",
"fastapi",
"mikan",
"starlette"
],
}
3 changes: 2 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ python-jose==3.3.0
passlib==1.7.4
bcrypt==4.0.1
python-multipart==0.0.6
sqlmodel
sqlmodel==0.0.8
sse-starlette==1.6.5
12 changes: 7 additions & 5 deletions backend/src/module/api/search.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
from fastapi import APIRouter, Query, Depends
from fastapi.responses import StreamingResponse
from sse_starlette.sse import EventSourceResponse

from module.searcher import SearchTorrent, SEARCH_CONFIG
from module.security.api import get_current_user, UNAUTHORIZED
from module.models import Torrent
from module.models import Bangumi


router = APIRouter(prefix="/search", tags=["search"])


@router.get("/", response_model=list[Torrent])
@router.get("/", response_model=EventSourceResponse[Bangumi])
async def search_torrents(
site: str = "mikan",
keywords: str = Query(None),
current_user=Depends(get_current_user),
):
"""
Server Send Event for per Bangumi item
"""
if not current_user:
raise UNAUTHORIZED
if not keywords:
return []
keywords = keywords.split(" ")
with SearchTorrent() as st:
return StreamingResponse(
return EventSourceResponse(
content=st.analyse_keyword(keywords=keywords, site=site),
media_type="application/json",
)


Expand Down
6 changes: 5 additions & 1 deletion backend/src/module/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ class Program(BaseModel):

class Downloader(BaseModel):
type: str = Field("qbittorrent", description="Downloader type")
host: str = Field("172.17.0.1:8080", description="Downloader host")
host_: str = Field("172.17.0.1:8080", alias="host", description="Downloader host")
username_: str = Field("admin", alias="username", description="Downloader username")
password_: str = Field(
"adminadmin", alias="password", description="Downloader password"
)
path: str = Field("/downloads/Bangumi", description="Downloader path")
ssl: bool = Field(False, description="Downloader ssl")

@property
def host(self):
return expandvars(self.host_)

@property
def username(self):
return expandvars(self.username_)
Expand Down
14 changes: 6 additions & 8 deletions backend/src/module/searcher/searcher.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from typing import TypeAlias

from module.models import Bangumi, Torrent, RSSItem
from module.network import RequestContent
Expand All @@ -15,6 +16,7 @@
"dpi",
]

BangumiJSON: TypeAlias = str

class SearchTorrent(RequestContent, RSSAnalyser):
def search_torrents(
Expand All @@ -23,18 +25,14 @@ def search_torrents(
torrents = self.get_torrents(rss_item.url, limit=limit)
return torrents

def analyse_keyword(self, keywords: list[str], site: str = "mikan"):
def analyse_keyword(self, keywords: list[str], site: str = "mikan") -> BangumiJSON:
rss_item = search_url(site, keywords)
torrents = self.search_torrents(rss_item)
# Generate a list of json
yield "["
for idx, torrent in enumerate(torrents):
# yield for EventSourceResponse (Server Send)
for torrent in torrents:
bangumi = self.torrent_to_data(torrent=torrent, rss=rss_item)
if bangumi:
yield json.dumps(bangumi.dict())
if idx != len(torrents) - 1:
yield ","
yield "]"
yield json.dumps(bangumi.dict(), separators=(',', ':'))

def search_season(self, data: Bangumi):
keywords = [getattr(data, key) for key in SEARCH_KEY if getattr(data, key)]
Expand Down
3 changes: 3 additions & 0 deletions webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"lodash": "^4.17.21",
"naive-ui": "^2.34.4",
"pinia": "^2.1.3",
"rxjs": "^7.8.1",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-inline-svg": "^3.1.2",
Expand All @@ -42,9 +43,11 @@
"@storybook/vue3-vite": "^7.0.12",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.14",
"@unocss/preset-attributify": "^0.55.3",
"@unocss/preset-rem-to-px": "^0.51.13",
"@unocss/reset": "^0.51.13",
"@vitejs/plugin-vue": "^4.2.0",
"@vue/runtime-dom": "^3.3.4",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-storybook": "^0.6.12",
Expand Down
26 changes: 25 additions & 1 deletion webui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 76 additions & 7 deletions webui/src/api/search.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,80 @@
import {
Observable,
} from 'rxjs';

import type { BangumiRule } from '#/bangumi';

export const apiSearch = {
async get(keyword: string, site = 'mikan') {
const { data } = await axios.get('api/v1/search', {
params: {
site,
keyword,
},
/**
* 番剧搜索接口是 Server Send 流式数据,每条是一个 Bangumi JSON 字符串,
* 使用接口方式需要订阅使用
*
* Usage Example:
*
* ```ts
* import {
* Subject,
* tap,
* map,
* switchMap,
* debounceTime,
* } from 'rxjs';
*
*
* const input$ = new Subject<string>();
* const onInput = (e: Event) => input$.next(e.target);
*
* // vue: <input @input="onInput">
*
* const bangumiInfo$ = apiSearch.get('魔女之旅');
*
* // vue: start loading animation
*
* input$.pipe(
* debounceTime(1000),
* tap((input: string) => {
* console.log('input', input)
* // clear Search Result List
* }),
*
* // switchMap 把输入 keyword 查询为 bangumiInfo$ 流,多次输入停用前一次查询
* switchMap((input: string) => apiSearch(input, site)),
*
* tap((bangumi: BangumiRule) => console.log(bangumi)),
* tap((bangumi: BangumiRule) => {
* console.log('bangumi', bangumi)
* // set bangumi info to Search Result List
* }),
* ).subscribe({
* complete() {
* // end of stream, stop loading animation
* },
* })
* ```
*/
get(keyword: string, site = 'mikan'): Observable<BangumiRule> {
const bangumiInfo$ = new Observable<BangumiRule>(observer => {
const eventSource = new EventSource(
`api/v1/search?site=${site}&keyword=${encodeURIComponent(keyword)}`,
{ withCredentials: true },
);

eventSource.onmessage = ev => {
try {
const data: BangumiRule = JSON.parse(ev.data);
observer.next(data);
} catch (error) {
observer.error(error);
}
};

eventSource.onerror = ev => observer.error(ev);

return () => {
eventSource.close();
};
});
return data!;

return bangumiInfo$;
},
};
3 changes: 3 additions & 0 deletions webui/types/bangumi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @type `Bangumi` in backend/src/module/models/bangumi.py
*/
export interface BangumiRule {
added: boolean;
deleted: boolean;
Expand Down
9 changes: 9 additions & 0 deletions webui/types/dts/html.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* https://unocss.dev/presets/attributify#vue-3
*/

import type { AttributifyAttributes } from '@unocss/preset-attributify'

declare module '@vue/runtime-dom' {
interface HTMLAttributes extends AttributifyAttributes {}
}

0 comments on commit f8fdd16

Please sign in to comment.