mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-30 03:36:37 +08:00
fix: improve cloud mount scanning feedback
This commit is contained in:
@@ -99,9 +99,9 @@ export const cloudAPI = {
|
||||
.post(`/admin/cloud/${type}/import`, { ref, name, size })
|
||||
.then((r) => r.data),
|
||||
|
||||
mount: (type: StorageType, dir = '', name = '', media_type = 'movie') =>
|
||||
mount: (type: StorageType, dir = '', name = '', media_type = 'movie', dir_path = '') =>
|
||||
api
|
||||
.post(`/admin/cloud/${type}/mount`, { dir, name, media_type })
|
||||
.post(`/admin/cloud/${type}/mount`, { dir, dir_path, name, media_type })
|
||||
.then((r) => r.data),
|
||||
|
||||
qrStart: (type: StorageType) =>
|
||||
|
||||
@@ -10,8 +10,28 @@ export function GlobalEvents() {
|
||||
const onEvent = useCallback((topic: string, payload: unknown) => {
|
||||
if (!payload || typeof payload !== 'object') return
|
||||
const p = payload as Record<string, unknown>
|
||||
if (topic === 'scan' && p.finished) {
|
||||
toast.success(`扫描完成:已添加 ${p.added ?? 0} 项,已探测 ${p.probed ?? 0} 项`)
|
||||
if (topic === 'scan') {
|
||||
const id = `scan-${String(p.library_id ?? 'global')}`
|
||||
if (p.error) {
|
||||
toast.error(`扫描失败:${String(p.error)}`, { id })
|
||||
return
|
||||
}
|
||||
if (p.finished) {
|
||||
const elapsed = Number(p.elapsed_seconds ?? p.elapsed ?? 0)
|
||||
const elapsedText = elapsed > 0 ? ` · 耗时 ${formatDuration(elapsed)}` : ''
|
||||
toast.success(`扫描完成:发现 ${p.discovered ?? p.visited ?? 0} · 新增 ${p.added ?? 0} · 更新 ${p.updated ?? 0} · 跳过 ${p.skipped ?? 0}${elapsedText}`, { id })
|
||||
return
|
||||
}
|
||||
if (p.queued) {
|
||||
toast.loading(String(p.message ?? '云盘扫描已加入后台队列,会自动入库'), { id })
|
||||
return
|
||||
}
|
||||
if (p.cloud && p.stage) {
|
||||
const stage = p.stage === 'importing' ? '正在入库' : '正在遍历目录'
|
||||
const speed = Number(p.files_per_second ?? 0)
|
||||
const speedText = speed > 0 ? ` · ${speed.toFixed(speed >= 10 ? 0 : 1)} 个/秒` : ''
|
||||
toast.loading(`${stage}:目录 ${p.dirs ?? 0} · 已发现 ${p.discovered ?? 0} · 已入库 ${p.visited ?? 0}${speedText}`, { id })
|
||||
}
|
||||
}
|
||||
if (topic === 'scrape' && p.finished) {
|
||||
toast.success(`刮削完成:成功匹配 ${p.matched ?? 0} 项`)
|
||||
@@ -25,3 +45,13 @@ export function GlobalEvents() {
|
||||
useWebSocket(onEvent)
|
||||
return null
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return ''
|
||||
if (seconds < 60) return `${Math.round(seconds)}秒`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const rest = Math.round(seconds % 60)
|
||||
if (minutes < 60) return `${minutes}分${rest}秒`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
return `${hours}小时${minutes % 60}分`
|
||||
}
|
||||
|
||||
@@ -155,7 +155,8 @@ function LibraryPanel() {
|
||||
className="rounded-lg border border-primary-400/40 px-2 py-1 text-xs text-brand-500 hover:bg-primary-400/10"
|
||||
onClick={async () => {
|
||||
const r = await libraryAPI.scan(l.id)
|
||||
toast.success(`扫描完成,新增 ${r.added},更新 ${r.updated ?? 0}`)
|
||||
if (r.queued) toast.success('云盘扫描已加入后台队列,会自动入库')
|
||||
else toast.success(`扫描完成,新增 ${r.added},更新 ${r.updated ?? 0}`)
|
||||
}}
|
||||
>
|
||||
扫描
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import toast from 'react-hot-toast'
|
||||
@@ -11,6 +11,7 @@ import { ExternalPlayerButton } from '../components/ExternalPlayerButton'
|
||||
import { imageURL } from '../api/client'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { getSeriesKey, groupSeries, isEpisodeLike, seriesTitle, type SeriesCard } from '../utils/groupSeries'
|
||||
import { useWebSocket } from '../hooks/useWebSocket'
|
||||
|
||||
export function LibraryPage() {
|
||||
const { id = '' } = useParams()
|
||||
@@ -22,6 +23,7 @@ export function LibraryPage() {
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [scanning, setScanning] = useState(false)
|
||||
const [scanProgress, setScanProgress] = useState('')
|
||||
const [scraping, setScraping] = useState(false)
|
||||
|
||||
// 剧集模式:选中某个剧集后展开详情
|
||||
@@ -71,18 +73,73 @@ export function LibraryPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !library) return
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
// 所有库都可能包含季集型内容(综艺/纪录片/课程等),统一拉取较大页数用于前端分组。
|
||||
const limit = 2000
|
||||
libraryAPI
|
||||
.listMedia(id, 1, limit)
|
||||
.then((d) => {
|
||||
setItems(d.items)
|
||||
setTotal(d.total)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
setItems([])
|
||||
const loadAll = async () => {
|
||||
const pageSize = 2000
|
||||
let page = 1
|
||||
let collected: Media[] = []
|
||||
try {
|
||||
for (;;) {
|
||||
const d = await libraryAPI.listMedia(id, page, pageSize)
|
||||
if (cancelled) return
|
||||
collected = collected.concat(d.items)
|
||||
setItems(collected)
|
||||
setTotal(d.total)
|
||||
if (collected.length >= d.total || d.items.length < pageSize) break
|
||||
page += 1
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
loadAll().catch(() => {
|
||||
if (!cancelled) {
|
||||
toast.error('媒体库加载失败')
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
return () => { cancelled = true }
|
||||
}, [id, library])
|
||||
|
||||
const reloadCurrentLibrary = useCallback(() => {
|
||||
setLibrary((l) => (l ? { ...l } : l))
|
||||
}, [])
|
||||
|
||||
const onRealtimeEvent = useCallback((topic: string, payload: unknown) => {
|
||||
if (topic !== 'scan' || !payload || typeof payload !== 'object') return
|
||||
const p = payload as Record<string, unknown>
|
||||
if (p.library_id !== id) return
|
||||
if (p.error) {
|
||||
setScanning(false)
|
||||
setScanProgress(`扫描失败:${String(p.error)}`)
|
||||
return
|
||||
}
|
||||
if (p.finished) {
|
||||
setScanning(false)
|
||||
const elapsed = Number(p.elapsed_seconds ?? p.elapsed ?? 0)
|
||||
const elapsedText = elapsed > 0 ? ` · 耗时 ${formatDuration(elapsed)}` : ''
|
||||
setScanProgress(`扫描完成:发现 ${p.discovered ?? p.visited ?? 0} · 新增 ${p.added ?? 0} · 更新 ${p.updated ?? 0} · 跳过 ${p.skipped ?? 0}${elapsedText}`)
|
||||
reloadCurrentLibrary()
|
||||
return
|
||||
}
|
||||
if (p.queued) {
|
||||
setScanning(true)
|
||||
setScanProgress(String(p.message ?? '扫描已排队,后台会自动入库'))
|
||||
return
|
||||
}
|
||||
if (p.cloud && p.stage) {
|
||||
const stage = p.stage === 'importing' ? '正在入库' : '正在遍历目录'
|
||||
const speed = Number(p.files_per_second ?? 0)
|
||||
const speedText = speed > 0 ? ` · ${speed.toFixed(speed >= 10 ? 0 : 1)} 个/秒` : ''
|
||||
setScanning(true)
|
||||
setScanProgress(`${stage}:目录 ${p.dirs ?? 0} · 已发现 ${p.discovered ?? 0} · 已入库 ${p.visited ?? 0}${speedText}`)
|
||||
}
|
||||
}, [id, reloadCurrentLibrary])
|
||||
|
||||
useWebSocket(onRealtimeEvent)
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return
|
||||
if (!isSeries) {
|
||||
@@ -113,13 +170,27 @@ export function LibraryPage() {
|
||||
|
||||
const handleScan = async () => {
|
||||
setScanning(true)
|
||||
setScanProgress('正在提交扫描任务…')
|
||||
let keepScanning = false
|
||||
try {
|
||||
const r = await libraryAPI.scan(id)
|
||||
toast.success(`扫描完成:新增 ${r.added} 项,更新 ${r.updated ?? 0} 项`)
|
||||
setLibrary((l) => (l ? { ...l } : l))
|
||||
if (r.queued) {
|
||||
keepScanning = true
|
||||
setScanProgress(`${r.message ?? '云盘扫描已在后台运行,发现的媒体会自动加入当前媒体库'};${r.estimate_message ?? '大目录耗时取决于网盘接口速度'}`)
|
||||
toast.success('云盘扫描已加入后台队列')
|
||||
} else {
|
||||
toast.success(`扫描完成:新增 ${r.added} 项,更新 ${r.updated ?? 0} 项`)
|
||||
setScanProgress(`扫描完成:新增 ${r.added} · 更新 ${r.updated ?? 0}`)
|
||||
reloadCurrentLibrary()
|
||||
setScanning(false)
|
||||
}
|
||||
} catch {
|
||||
toast.error('扫描失败')
|
||||
} finally { setScanning(false) }
|
||||
setScanProgress('扫描失败,请查看日志或稍后重试')
|
||||
setScanning(false)
|
||||
} finally {
|
||||
if (!keepScanning) setScanning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleScrape = async () => {
|
||||
@@ -168,6 +239,7 @@ export function LibraryPage() {
|
||||
<span className="text-sand-500"> ({isSeries ? seriesCards.length : total})</span>
|
||||
</h1>
|
||||
{library && <p className="text-sm text-ink-50">{library.type} · {library.path}</p>}
|
||||
{scanProgress && <p className="mt-1 text-xs text-brand-500">{scanProgress}</p>}
|
||||
</div>
|
||||
{role === 'admin' && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -327,6 +399,16 @@ export function LibraryPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return ''
|
||||
if (seconds < 60) return `${Math.round(seconds)}秒`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const rest = Math.round(seconds % 60)
|
||||
if (minutes < 60) return `${minutes}分${rest}秒`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
return `${hours}小时${minutes % 60}分`
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (!bytes || bytes <= 0) return '—'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react'
|
||||
import { Cloud, FileVideo, Folder, Loader2, QrCode, Save, Send, Upload } from 'lucide-react'
|
||||
import { Cloud, FileVideo, Folder, Loader2, QrCode, Save, Send, Trash2, Upload } from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { libraryAPI } from '../api/library'
|
||||
import {
|
||||
cloudAPI,
|
||||
storageAPI,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
type QRSession,
|
||||
type StorageType,
|
||||
} from '../api/storage_config'
|
||||
import { confirmAction } from '../components/ConfirmDialog'
|
||||
import type { Library } from '../types'
|
||||
|
||||
const CLOUD_TYPES: StorageType[] = ['cloud115', 'quark', 'clouddrive2', 'openlist']
|
||||
const isCloud = (t: StorageType) => CLOUD_TYPES.includes(t)
|
||||
@@ -21,6 +24,46 @@ const TYPE_LABEL: Record<string, string> = {
|
||||
quark: '夸克网盘',
|
||||
clouddrive2: 'CloudDrive2',
|
||||
}
|
||||
const PATH_BASED_CLOUD = new Set<StorageType>(['openlist', 'clouddrive2'])
|
||||
|
||||
function normalizeCloudDisplayPath(value: string) {
|
||||
let text = value.trim()
|
||||
try {
|
||||
text = decodeURIComponent(text)
|
||||
} catch {
|
||||
// Keep the original value if it is not URI-encoded.
|
||||
}
|
||||
return text.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')
|
||||
}
|
||||
|
||||
function cloudLibraryProvider(path: string) {
|
||||
if (!path.toLowerCase().startsWith('cloud://')) return ''
|
||||
try {
|
||||
return new URL(path).host
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function cloudLibraryLabel(path: string) {
|
||||
try {
|
||||
const parsed = new URL(path)
|
||||
const display = normalizeCloudDisplayPath(parsed.pathname)
|
||||
const scanDir = normalizeCloudDisplayPath(parsed.searchParams.get('dir') ?? '')
|
||||
return display || scanDir || '根目录'
|
||||
} catch {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
function cloudMountDisplayPath(type: StorageType, stack: { id: string; name: string }[], child?: CloudEntry) {
|
||||
if (PATH_BASED_CLOUD.has(type)) {
|
||||
return normalizeCloudDisplayPath(child?.id ?? stack[stack.length - 1]?.id ?? '')
|
||||
}
|
||||
const parts = stack.slice(1).map((item) => item.name).filter(Boolean)
|
||||
if (child?.name) parts.push(child.name)
|
||||
return parts.map(normalizeCloudDisplayPath).filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
// StorageConfigPage manages the Alist / S3 / WebDAV adapters used by
|
||||
// the import / playback / STRM subsystems. Mirrors the Vue UI's
|
||||
@@ -433,6 +476,7 @@ function QRLoginPanel({ type, onCookie }: { type: StorageType; onCookie: (c: str
|
||||
function CloudBrowser({ type }: { type: StorageType }) {
|
||||
const [stack, setStack] = useState<{ id: string; name: string }[]>([{ id: '', name: '根目录' }])
|
||||
const [items, setItems] = useState<CloudEntry[]>([])
|
||||
const [mounts, setMounts] = useState<Library[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [mounting, setMounting] = useState(false)
|
||||
const [batchMounting, setBatchMounting] = useState(false)
|
||||
@@ -455,13 +499,39 @@ function CloudBrowser({ type }: { type: StorageType }) {
|
||||
}
|
||||
}
|
||||
|
||||
const loadMounts = async () => {
|
||||
const libs = await libraryAPI.list({ includeHidden: true })
|
||||
setMounts(libs.filter((lib) => cloudLibraryProvider(lib.path) === type))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load(cur.id).catch(() => undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stack.length, type])
|
||||
|
||||
useEffect(() => {
|
||||
loadMounts().catch(() => undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [type])
|
||||
|
||||
const enter = (e: CloudEntry) => setStack((s) => [...s, { id: e.id, name: e.name }])
|
||||
const goTo = (i: number) => setStack((s) => s.slice(0, i + 1))
|
||||
const currentMountPath = () => cloudMountDisplayPath(type, stack)
|
||||
const childMountPath = (child: CloudEntry) => cloudMountDisplayPath(type, stack, child)
|
||||
|
||||
const handleMountResult = (res: unknown, label: string) => {
|
||||
const out = res as { already_mounted?: boolean; skipped?: boolean; reason?: string; library?: Library; message?: string; estimate_message?: string }
|
||||
if (out.skipped) {
|
||||
toast(`已跳过「${label}」:和已挂载目录重叠`)
|
||||
return 'skipped'
|
||||
}
|
||||
if (out.already_mounted) {
|
||||
toast(`「${label}」已经挂载,后台会刷新扫描并自动入库`)
|
||||
return 'mounted'
|
||||
}
|
||||
toast.success(`已挂载「${label}」,${out.message ?? '后台会递归扫描并自动加入媒体库'}。${out.estimate_message ?? ''}`)
|
||||
return 'mounted'
|
||||
}
|
||||
|
||||
const doImport = async (e: CloudEntry) => {
|
||||
const ref = type === 'cloud115' ? e.pick_code || e.id : e.id
|
||||
@@ -478,9 +548,9 @@ function CloudBrowser({ type }: { type: StorageType }) {
|
||||
try {
|
||||
const label = TYPE_LABEL[type] ?? type
|
||||
const name = cur.id ? `${label} · ${cur.name}` : label
|
||||
const res = await cloudAPI.mount(type, cur.id, name, mountMediaType)
|
||||
const scan = (res as { scan?: { added?: number; updated?: number; removed?: number; visited?: number } }).scan
|
||||
toast.success(`已挂载为媒体库并扫描:新增 ${scan?.added ?? 0} · 更新 ${scan?.updated ?? 0} · 访问 ${scan?.visited ?? 0}`)
|
||||
const res = await cloudAPI.mount(type, cur.id, name, mountMediaType, currentMountPath())
|
||||
handleMountResult(res, cur.name)
|
||||
await loadMounts()
|
||||
} catch (err: unknown) {
|
||||
toast.error((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '挂载失败')
|
||||
} finally {
|
||||
@@ -496,26 +566,64 @@ function CloudBrowser({ type }: { type: StorageType }) {
|
||||
}
|
||||
setBatchMounting(true)
|
||||
let ok = 0
|
||||
let skipped = 0
|
||||
let failed = 0
|
||||
const label = TYPE_LABEL[type] ?? type
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
await cloudAPI.mount(type, dir.id, `${label} · ${dir.name}`, 'auto')
|
||||
ok += 1
|
||||
const result = await cloudAPI.mount(type, dir.id, `${label} · ${dir.name}`, 'auto', childMountPath(dir))
|
||||
const state = handleMountResult(result, dir.name)
|
||||
if (state === 'skipped') skipped += 1
|
||||
else ok += 1
|
||||
} catch {
|
||||
failed += 1
|
||||
}
|
||||
}
|
||||
if (failed > 0) {
|
||||
toast.error(`已挂载 ${ok} 个目录,失败 ${failed} 个;请查看日志或逐个挂载定位`)
|
||||
toast.error(`已挂载 ${ok} 个目录,跳过 ${skipped} 个重叠目录,失败 ${failed} 个`)
|
||||
} else {
|
||||
toast.success(`已挂载 ${ok} 个目录为媒体库,后台会自动生成 302/STRM 播放入口`)
|
||||
toast.success(`已挂载 ${ok} 个目录,跳过 ${skipped} 个重叠目录,后台会自动生成 302/STRM 播放入口`)
|
||||
}
|
||||
await loadMounts()
|
||||
setBatchMounting(false)
|
||||
}
|
||||
|
||||
const removeMount = async (lib: Library) => {
|
||||
const ok = await confirmAction({
|
||||
title: '移除网盘挂载',
|
||||
message: `仅移除「${lib.name}」在本项目中的媒体库和媒体记录,不会删除网盘文件。`,
|
||||
confirmText: '移除',
|
||||
})
|
||||
if (!ok) return
|
||||
await libraryAPI.remove(lib.id)
|
||||
toast.success('已移除挂载')
|
||||
await loadMounts()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2 rounded-lg border border-gray-200 p-3" onClick={(e) => e.preventDefault()}>
|
||||
{mounts.length > 0 && (
|
||||
<div className="mb-3 rounded border border-blue-100 bg-blue-50/60 p-2">
|
||||
<div className="mb-1 text-xs font-semibold text-ink-100">已挂载目录</div>
|
||||
<div className="space-y-1">
|
||||
{mounts.map((lib) => (
|
||||
<div key={lib.id} className="flex items-center gap-2 rounded bg-white/80 px-2 py-1 text-xs">
|
||||
<span className="min-w-0 flex-1 truncate text-ink-100">
|
||||
{lib.name} · {cloudLibraryLabel(lib.path)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-red-200 px-1.5 py-0.5 text-red-500 hover:bg-red-50"
|
||||
onClick={() => removeMount(lib)}
|
||||
title="移除挂载"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-1 text-xs text-ink-50">
|
||||
<span className="text-ink-100">网盘资源:</span>
|
||||
@@ -528,6 +636,9 @@ function CloudBrowser({ type }: { type: StorageType }) {
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="basis-full text-xs text-ink-50">
|
||||
挂载后不会复制网盘文件;后台会递归读取该目录里的子文件夹和媒体文件,扫描到的影片会自动加入对应媒体库。小目录通常几十秒,大目录取决于网盘接口速度。
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
className="rounded border border-gray-200 bg-white px-2 py-0.5 text-xs text-ink-100"
|
||||
@@ -547,7 +658,7 @@ function CloudBrowser({ type }: { type: StorageType }) {
|
||||
disabled={mounting || batchMounting || loading}
|
||||
onClick={mountCurrent}
|
||||
>
|
||||
{mounting ? '挂载扫描中…' : '挂载当前目录为媒体库'}
|
||||
{mounting ? '挂载中…' : '挂载当前目录为媒体库并递归扫描'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -555,7 +666,7 @@ function CloudBrowser({ type }: { type: StorageType }) {
|
||||
disabled={mounting || batchMounting || loading || items.every((item) => !item.is_dir)}
|
||||
onClick={mountVisibleDirectories}
|
||||
>
|
||||
{batchMounting ? '批量挂载中…' : '一键挂载当前目录下文件夹'}
|
||||
{batchMounting ? '批量挂载中…' : '一键把当前目录下所有文件夹挂载为媒体库'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,6 +152,12 @@ export interface ScanResult {
|
||||
probed: number
|
||||
local_metadata?: number
|
||||
removed?: number
|
||||
skipped?: number
|
||||
discovered?: number
|
||||
queued?: boolean
|
||||
cloud?: boolean
|
||||
message?: string
|
||||
estimate_message?: string
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
|
||||
Reference in New Issue
Block a user