# ebook-skill :: so-novel command line # # novel search [-Limit N] [-Source ID] [-Timeout SEC] [-Log] [-Json] # novel download [-Ext txt|epub|html|pdf] [-Out DIR] [-Log] [-Json] # novel toc [-Json] # novel sources [-Json] # $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot '..\tools\lib.ps1') $paths = Get-ToolPaths function Show-Help { @" ebook-skill / so-novel novel search <关键词> [-Limit N] [-Source ID] [-Timeout SEC] [-Log] [-Json] novel download <详情页链接> [-Ext txt|epub|html|pdf] [-Out 目录] [-Log] [-Json] novel toc <详情页链接> [-Json] 只看目录,不下载 novel sources [-Json] 列出内置书源 说明 -Json 输出原始 JSON(默认输出人读表格) -Log 附带 so-novel 运行日志尾部,便于排查源站问题 -Limit 每个书源最多取多少条搜索结果(默认 30) 下载落盘默认在 <仓库>\books,文件名形如 书名(作者).txt "@ | Write-Host } function Ensure-Built { if ((Test-Path $paths.AppJar) -and (Test-Path $paths.Classes)) { return } Write-Host '[setup] first run: building so-novel engine + wrapper ...' -ForegroundColor Yellow & (Join-Path $paths.Tools 'build.ps1') if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" } } function Invoke-Tool { param([string[]]$JavaArgs) Ensure-Built $java = Get-JavaCommand $cp = $paths.AppJar + ';' + $paths.Classes Push-Location $paths.Runtime try { $raw = (& $java '-Dfile.encoding=UTF-8' "-Dconfig.file=$($paths.Config)" '-cp' $cp 'skills.novel.ToolMain' @JavaArgs 2>&1 | Out-String) $code = $LASTEXITCODE } finally { Pop-Location } return [pscustomobject]@{ Raw = $raw.Trim(); Code = $code } } function Get-Option { param([string[]]$Tokens, [string]$Name, $Default = $null) for ($i = 0; $i -lt $Tokens.Count; $i++) { $t = $Tokens[$i] if ($t -ieq $Name -and ($i + 1) -lt $Tokens.Count) { return $Tokens[$i + 1] } if ($t -like "$Name=*") { return $t.Substring($Name.Length + 1) } } return $Default } function Test-Flag { param([string[]]$Tokens, [string]$Name) foreach ($t in $Tokens) { if ($t -ieq $Name) { return $true } } return $false } function Get-Positionals { param([string[]]$Tokens, [string[]]$ValueOptions) $values = @() for ($i = 0; $i -lt $Tokens.Count; $i++) { $t = $Tokens[$i] if ($t.StartsWith('-')) { if ($ValueOptions -contains $t -and ($i + 1) -lt $Tokens.Count) { $i++ } continue } $values += $t } return $values } function Show-SearchResult { param($Result) if (-not $Result.ok) { Write-Host ("搜索失败: " + $Result.error) -ForegroundColor Red return } $failedCount = 0 if ($Result.sources.failed) { $failedCount = @($Result.sources.failed).Count } Write-Host ("关键词「{0}」:命中 {1} 条,书源 {2} 个(空 {3},异常 {4}),耗时 {5} ms" -f ` $Result.keyword, $Result.count, $Result.sources.total, $Result.sources.empty, $failedCount, $Result.tookMs) -ForegroundColor Cyan if ($Result.count -eq 0) { Write-Host '没有命中。试试更完整的书名/作者,或 -Limit 调大。' -ForegroundColor Yellow return } $rows = @() foreach ($r in $Result.results) { $latest = [string]$r.latestChapter if ($latest.Length -gt 24) { $latest = $latest.Substring(0, 24) + '...' } $author = [string]$r.author if (-not $author) { $author = '?' } Write-Host ("[{0}] {1} —— {2}" -f $r.index, $r.bookName, $author) -ForegroundColor White Write-Host (" 最新章节: {0} 更新: {1} 书源: {2}" -f $latest, $r.lastUpdateTime, $r.sourceName) -ForegroundColor DarkGray Write-Host (" {0}" -f $r.url) -ForegroundColor DarkCyan } Write-Host '下载示例: novel download <链接> -Ext txt' -ForegroundColor DarkGray } function Show-DownloadResult { param($Result) if (-not $Result.ok) { Write-Host ("下载失败: " + $Result.error) -ForegroundColor Red if ($Result.bookName) { Write-Host (" 书籍: {0} / {1}" -f $Result.bookName, $Result.author) } return } Write-Host ("完成《{0}》({1}) {2} 章 {3} s" -f $Result.bookName, $Result.author, $Result.chapters, $Result.seconds) -ForegroundColor Green Write-Host (" 文件: {0} ({1:N0} KB)" -f $Result.file, ($Result.bytes / 1KB)) Write-Host (" 书源: #{0} {1}" -f $Result.sourceId, $Result.sourceName) -ForegroundColor DarkGray } function Show-TocResult { param($Result) if (-not $Result.ok) { Write-Host ("目录解析失败: " + $Result.error) -ForegroundColor Red return } Write-Host ("《{0}》 书源 #{1} {2}" -f $Result.url, $Result.sourceId, $Result.sourceName) -ForegroundColor Cyan Write-Host (" 章节数: {0}" -f $Result.chapters) Write-Host (" 开头: " + (@($Result.firstChapters) -join ' / ')) Write-Host (" 结尾: " + (@($Result.lastChapters) -join ' / ')) } function Show-Sources { param($Result) Write-Host ("激活规则: {0} 共 {1} 个书源" -f $Result.activeRules, $Result.count) -ForegroundColor Cyan foreach ($s in $Result.sources) { $searchable = '-' if ($s.searchable) { $searchable = '支持搜索' } Write-Host ("{0,2}. {1,-14} {2,-8} {3}" -f $s.id, $s.name, $searchable, $s.url) } } # ---------------------------------------------------------------- dispatch function Invoke-SearchCommand { param([string[]]$Tokens) $valueOptions = @('-Limit', '-Source', '-Timeout') $keyword = (@(Get-Positionals -Tokens $Tokens -ValueOptions $valueOptions) -join ' ').Trim() if (-not $keyword) { Write-Host '用法: novel search <关键词>' -ForegroundColor Yellow; exit 2 } $javaArgs = @('search', $keyword, '--limit', [string](Get-Option $Tokens '-Limit' 30), '--source', [string](Get-Option $Tokens '-Source' '0'), '--timeout', [string](Get-Option $Tokens '-Timeout' '180')) if (Test-Flag $Tokens '-Log') { $javaArgs += '--log' } $result = Invoke-Tool -JavaArgs $javaArgs if (Test-Flag $Tokens '-Json') { $result.Raw | Write-Output } else { Show-SearchResult ($result.Raw | ConvertFrom-Json) } exit $result.Code } function Invoke-DownloadCommand { param([string[]]$Tokens) $valueOptions = @('-Ext', '-Out', '-Url') $url = Get-Option $Tokens '-Url' $null if (-not $url) { $positionals = @(Get-Positionals -Tokens $Tokens -ValueOptions $valueOptions) if ($positionals.Count -gt 0) { $url = $positionals[0] } } if (-not $url) { Write-Host '用法: novel download <详情页链接> [-Ext txt] [-Out 目录]' -ForegroundColor Yellow; exit 2 } $out = Get-Option $Tokens '-Out' $null if ($out) { $out = (New-Item -ItemType Directory -Force -Path $out).FullName } $javaArgs = @('download', $url, '--ext', [string](Get-Option $Tokens '-Ext' 'txt')) if ($out) { $javaArgs += @('--out', $out) } if (Test-Flag $Tokens '-Log') { $javaArgs += '--log' } $result = Invoke-Tool -JavaArgs $javaArgs if (Test-Flag $Tokens '-Json') { $result.Raw | Write-Output } else { Show-DownloadResult ($result.Raw | ConvertFrom-Json) } exit $result.Code } function Invoke-TocCommand { param([string[]]$Tokens) $url = Get-Option $Tokens '-Url' $null if (-not $url) { $positionals = @(Get-Positionals -Tokens $Tokens -ValueOptions @('-Url')) if ($positionals.Count -gt 0) { $url = $positionals[0] } } if (-not $url) { Write-Host '用法: novel toc <详情页链接>' -ForegroundColor Yellow; exit 2 } $javaArgs = @('toc', $url) if (Test-Flag $Tokens '-Log') { $javaArgs += '--log' } $result = Invoke-Tool -JavaArgs $javaArgs if (Test-Flag $Tokens '-Json') { $result.Raw | Write-Output } else { Show-TocResult ($result.Raw | ConvertFrom-Json) } exit $result.Code } $argv = @($args) if ($argv.Count -eq 0) { Show-Help; exit 0 } $command = ([string]$argv[0]).ToLowerInvariant() $rest = @() if ($argv.Count -gt 1) { $rest = $argv[1..($argv.Count - 1)] } switch ($command) { 'search' { Invoke-SearchCommand -Tokens $rest } 'download' { Invoke-DownloadCommand -Tokens $rest } 'dl' { Invoke-DownloadCommand -Tokens $rest } 'toc' { Invoke-TocCommand -Tokens $rest } 'chapters' { Invoke-TocCommand -Tokens $rest } 'sources' { $result = Invoke-Tool -JavaArgs @('sources') if (Test-Flag $rest '-Json') { $result.Raw | Write-Output } else { Show-Sources ($result.Raw | ConvertFrom-Json) } exit $result.Code } 'build' { & (Join-Path $paths.Tools 'build.ps1') @rest exit $LASTEXITCODE } default { if ($command -in @('-h', '--help', 'help')) { Show-Help; exit 0 } Write-Host "未知命令: $command" -ForegroundColor Red Show-Help exit 2 } }