mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-02-03 02:04:06 +08:00
fix: 修复 bangumi search API 中的流式接口,webui 对应接口定义方法改造
- StreamingResponse 换成 EventSourceResponse,即 Server Send Event Source 方式发送数据 - webui 中 search 接口改成 rxjs Observable 方式, 这次来不及改到 vue 里,但是写了接口 usage example; - 顺手补了一些 vscode 通用的开发配置, 补了之前配置文件解析漏了一个可用环境变量的 host 字段
This commit is contained in:
18
.vscode/extensions.json
vendored
Normal file
18
.vscode/extensions.json
vendored
Normal file
@@ -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
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@@ -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
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -10,4 +10,10 @@
|
||||
"editor.wordWrap": "off",
|
||||
},
|
||||
"python.venvPath": "./backend/venv",
|
||||
"cSpell.words": [
|
||||
"Bangumi",
|
||||
"fastapi",
|
||||
"mikan",
|
||||
"starlette"
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ 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"
|
||||
@@ -18,6 +18,10 @@ class Downloader(BaseModel):
|
||||
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_)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import TypeAlias
|
||||
|
||||
from module.models import Bangumi, Torrent, RSSItem
|
||||
from module.network import RequestContent
|
||||
@@ -15,6 +16,7 @@ SEARCH_KEY = [
|
||||
"dpi",
|
||||
]
|
||||
|
||||
BangumiJSON: TypeAlias = str
|
||||
|
||||
class SearchTorrent(RequestContent, RSSAnalyser):
|
||||
def search_torrents(
|
||||
@@ -23,18 +25,14 @@ class SearchTorrent(RequestContent, RSSAnalyser):
|
||||
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)]
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
26
webui/pnpm-lock.yaml
generated
26
webui/pnpm-lock.yaml
generated
@@ -23,6 +23,9 @@ dependencies:
|
||||
pinia:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3(typescript@4.9.5)(vue@3.3.4)
|
||||
rxjs:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.1
|
||||
vue:
|
||||
specifier: ^3.3.4
|
||||
version: 3.3.4
|
||||
@@ -73,6 +76,9 @@ devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^18.16.14
|
||||
version: 18.16.14
|
||||
'@unocss/preset-attributify':
|
||||
specifier: ^0.55.3
|
||||
version: 0.55.3
|
||||
'@unocss/preset-rem-to-px':
|
||||
specifier: ^0.51.13
|
||||
version: 0.51.13
|
||||
@@ -82,6 +88,9 @@ devDependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.3(vite@4.3.5)(vue@3.3.4)
|
||||
'@vue/runtime-dom':
|
||||
specifier: ^3.3.4
|
||||
version: 3.3.4
|
||||
eslint:
|
||||
specifier: ^8.41.0
|
||||
version: 8.41.0
|
||||
@@ -3549,6 +3558,10 @@ packages:
|
||||
resolution: {integrity: sha512-SclWkqY2c+p5+PiqrbQkhJNEExPdeo71/aGFye10tpBkgPJWd5xC7dhg5F8M4VPNBtuNCrvBWyqNnunMyuz/WQ==}
|
||||
dev: true
|
||||
|
||||
/@unocss/core@0.55.3:
|
||||
resolution: {integrity: sha512-2hV9QlE/iOM4DHQ7i6L8sMC1t5/OVAz6AfGHjetTXcgbNfDCsHWqE8jhLZ1y2DeUvKwJvj2A09sYbYQ8E27+Gg==}
|
||||
dev: true
|
||||
|
||||
/@unocss/extractor-arbitrary-variants@0.51.13:
|
||||
resolution: {integrity: sha512-lF7p0ea/MeNf4IsjzNhRNYP8u+f1h5JjhTzcvFpQo/vpBvuM5ZCyqp4mkXxYnLNLFfTLsc+MxXaU34IXxpw1QA==}
|
||||
dependencies:
|
||||
@@ -3582,6 +3595,12 @@ packages:
|
||||
'@unocss/core': 0.51.13
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-attributify@0.55.3:
|
||||
resolution: {integrity: sha512-h3t6hPIk8pll3LubIIIsgRigvJivK3PX308Pi9Q0IUdw0vFq4S80iLQ1N0kRchQtgOaAIGffo9ux+TCbyunP3A==}
|
||||
dependencies:
|
||||
'@unocss/core': 0.55.3
|
||||
dev: true
|
||||
|
||||
/@unocss/preset-icons@0.51.13:
|
||||
resolution: {integrity: sha512-iL9s1NUVeWe3WSh5LHn7vy+veCAag9AFA50IfNlHuAARhuI8JtrMQA8dOXrWrzM0zWBMB+BVIkVaMVrF257n+Q==}
|
||||
dependencies:
|
||||
@@ -8466,6 +8485,12 @@ packages:
|
||||
queue-microtask: 1.2.3
|
||||
dev: true
|
||||
|
||||
/rxjs@7.8.1:
|
||||
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
|
||||
dependencies:
|
||||
tslib: 2.5.2
|
||||
dev: false
|
||||
|
||||
/safe-buffer@5.1.1:
|
||||
resolution: {integrity: sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==}
|
||||
dev: true
|
||||
@@ -9104,7 +9129,6 @@ packages:
|
||||
|
||||
/tslib@2.5.2:
|
||||
resolution: {integrity: sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==}
|
||||
dev: true
|
||||
|
||||
/tsutils@3.21.0(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
|
||||
@@ -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$;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @type `Bangumi` in backend/src/module/models/bangumi.py
|
||||
*/
|
||||
export interface BangumiRule {
|
||||
added: boolean;
|
||||
deleted: boolean;
|
||||
|
||||
9
webui/types/dts/html.d.ts
vendored
Normal file
9
webui/types/dts/html.d.ts
vendored
Normal file
@@ -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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user