ResourceSearcher.vb 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. Public Class ResourceSearcher
  2. Private Const RESULT_PAGE_SIZE = 40
  3. Private Const MSG_NO_CHINESE_RESULT As String = "无搜索结果,请尝试搜索其英文名称"
  4. '输入
  5. Public Class SearchRequest
  6. '结果要求
  7. ''' <summary>
  8. ''' 结果存储器,会从中获取需加载的页面等。
  9. ''' 加载后也应输出到此处。
  10. ''' </summary>
  11. Public Storage As SearchResult
  12. ''' <summary>
  13. ''' 应当尽量达成的结果数量。
  14. ''' </summary>
  15. Public TargetResultCount As Integer
  16. ''' <summary>
  17. ''' 根据加载位置记录,是否还可以继续获取内容。
  18. ''' </summary>
  19. Public ReadOnly Property CanContinue As Boolean
  20. Get
  21. If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.CurseForge) Then Storage.CurseForgeTotal = 0
  22. If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.Modrinth) Then Storage.ModrinthTotal = 0
  23. If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
  24. Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
  25. End Get
  26. End Property
  27. '输入内容
  28. ''' <summary>
  29. ''' 筛选资源种类。
  30. ''' </summary>
  31. Public Type As ResourceTypes
  32. ''' <summary>
  33. ''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
  34. ''' </summary>
  35. Public Tag As String = ""
  36. ''' <summary>
  37. ''' 筛选 Mod 加载器类别。
  38. ''' <see cref="ModLoaders.None"/> 代表不筛选。
  39. ''' 对于 Modrinth 请求,支持多个枚举值。
  40. ''' </summary>
  41. Public ModLoaders As ModLoaders = ModLoaders.None
  42. ''' <summary>
  43. ''' 筛选 MC 版本。
  44. ''' </summary>
  45. Public GameVersion As String = Nothing
  46. ''' <summary>
  47. ''' 搜索的初始内容。
  48. ''' </summary>
  49. Public SearchText As String = Nothing
  50. ''' <summary>
  51. ''' 允许的来源。
  52. ''' </summary>
  53. Public Sources As ResourcePlatforms = ResourcePlatforms.Any
  54. Public Sub New(Type As ResourceTypes, Storage As SearchResult, TargetResultCount As Integer)
  55. Me.Type = Type
  56. Me.Storage = Storage
  57. Me.TargetResultCount = TargetResultCount
  58. End Sub
  59. '构造请求
  60. ''' <summary>
  61. ''' 获取对应的 CurseForge API 请求链接。
  62. ''' </summary>
  63. Public Function GetCurseForgeAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
  64. If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
  65. '应用筛选参数
  66. Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={RESULT_PAGE_SIZE}"
  67. Select Case Type
  68. Case ResourceTypes.Mod
  69. Address += "&classId=6"
  70. Case ResourceTypes.ModPack
  71. Address += "&classId=4471"
  72. Case ResourceTypes.DataPack
  73. Address += "&classId=6945"
  74. Case ResourceTypes.Shader
  75. Address += "&classId=6552"
  76. Case ResourceTypes.ResourcePack
  77. Address += "&classId=12"
  78. End Select
  79. If Tag <> "" Then Address += "&categoryId=" & Tag.BeforeFirst("/")
  80. If ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter Then Address += "&modLoaderType=" & Resource.ToCurseForgeModLoaderType(ModLoaders.Flags.First)
  81. If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
  82. If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & EscapeUtils.UrlEscape(SearchText)
  83. If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
  84. Return Address
  85. End Function
  86. ''' <summary>
  87. ''' 获取对应的 Modrinth API 请求链接。
  88. ''' </summary>
  89. Public Function GetModrinthAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
  90. If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
  91. '应用筛选参数
  92. Dim Address As String = $"https://api.modrinth.com/v2/search?limit={RESULT_PAGE_SIZE}&index=relevance"
  93. If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & EscapeUtils.UrlEscape(SearchText)
  94. If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
  95. 'facets=[["categories:'game-mechanics'"],["categories:'forge',categories:'fabric'"],["versions:1.19.3"],["project_type:mod"]]
  96. Dim Facets As New List(Of String)
  97. Facets.Add($"[""project_type:{Type.ToString.Lower}""]")
  98. If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
  99. If ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter Then
  100. Facets.Add($"[""categories:'{ModLoaders.Flags.Select(Function(f) f.ToString.Lower).Join("',categories:'")}'""]")
  101. End If
  102. If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
  103. Address += "&facets=[" & String.Join(",", Facets) & "]"
  104. Return Address
  105. End Function
  106. '相同判断
  107. Public Overrides Function Equals(obj As Object) As Boolean
  108. Dim request = TryCast(obj, SearchRequest)
  109. Return request IsNot Nothing AndAlso
  110. Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
  111. Tag = request.Tag AndAlso ModLoaders = request.ModLoaders AndAlso Sources = request.Sources AndAlso
  112. GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
  113. End Function
  114. Public Shared Operator =(left As SearchRequest, right As SearchRequest) As Boolean
  115. Return EqualityComparer(Of SearchRequest).Default.Equals(left, right)
  116. End Operator
  117. Public Shared Operator <>(left As SearchRequest, right As SearchRequest) As Boolean
  118. Return Not left = right
  119. End Operator
  120. Public Overrides Function GetHashCode() As Integer
  121. Return (Type, Tag, ModLoaders, GameVersion, SearchText, Sources).GetHashCode()
  122. End Function
  123. End Class
  124. '输出
  125. Public Class SearchResult
  126. '加载位置记录
  127. Public CurseForgeOffset As Integer = 0
  128. Public CurseForgeTotal As Integer = -1
  129. Public ModrinthOffset As Integer = 0
  130. Public ModrinthTotal As Integer = -1
  131. '结果列表
  132. ''' <summary>
  133. ''' 可供展示的所有工程的列表。
  134. ''' </summary>
  135. Public Results As New List(Of ResourceProject)
  136. ''' <summary>
  137. ''' 当前的错误信息。如果没有则为 Nothing。
  138. ''' </summary>
  139. Public ErrorMessage As String = Nothing
  140. End Class
  141. ''' <summary>
  142. ''' 根据搜索请求获取一系列的工程列表。
  143. ''' </summary>
  144. Public Shared Function GetLoader(InputDelegate As Func(Of SearchRequest)) As LoaderTask(Of SearchRequest, Integer)
  145. Return New LoaderTask(Of SearchRequest, Integer)("社区资源获取", AddressOf Run, InputDelegate) With {.ReloadTimeout = 60 * 1000}
  146. End Function
  147. Private Shared Sub Run(Task As LoaderTask(Of SearchRequest, Integer))
  148. Dim Request As SearchRequest = Task.Input
  149. Dim Storage = Request.Storage '避免其他线程对 Request.Storage 重新进行了赋值
  150. #Region "前置检查"
  151. If Storage.Results.Count >= Request.TargetResultCount Then
  152. Logger.Info($"已有 {Storage.Results.Count} 个结果,多于所需的 {Request.TargetResultCount} 个结果,结束处理")
  153. Return
  154. ElseIf Not Request.CanContinue Then
  155. If Not Storage.Results.Any() Then
  156. Throw New Exception("没有符合条件的结果")
  157. Else
  158. Logger.Info($"已有 {Storage.Results.Count} 个结果,少于所需的 {Request.TargetResultCount} 个结果,但无法继续获取,结束处理")
  159. Return
  160. End If
  161. End If
  162. '拒绝 1.13- Quilt(这个版本根本没有 Quilt)
  163. If Request.ModLoaders = ModLoaders.Quilt AndAlso CompareVersion(If(Request.GameVersion, "1.15"), "1.14") = -1 Then
  164. Throw New Exception("Quilt 不支持 Minecraft " & Request.GameVersion)
  165. End If
  166. #End Region
  167. #Region "中文搜索"
  168. Dim RawSearchText As String = If(Request.SearchText, "").Trim
  169. RawSearchText = RawSearchText.Lower
  170. Logger.Info($"工程列表搜索原始文本:{RawSearchText}")
  171. Dim IsChineseSearch As Boolean =
  172. RawSearchText.RegexCheck("[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawSearchText) AndAlso
  173. (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) '目前仅对 Mod 和数据包进行中文搜索,注意整合包的名称可能已经有中文了
  174. Dim CurseForgeAltSearchText As String = Nothing, ModrinthAltSearchText As String = Nothing, ModrinthSlugs As New List(Of String) '从中文转为英文的替代搜索内容
  175. If IsChineseSearch Then
  176. '帮助方法:从搜索项提取可能的英文单词
  177. Dim ExtractWords =
  178. Function(Result As SearchEntry(Of WikiEntry), Source As ResourcePlatforms) As IEnumerable(Of String)
  179. '从各个可能的来源提取候选
  180. Dim Candidates As New List(Of String)
  181. If Result.Item.CurseForgeSlug IsNot Nothing AndAlso Source = ResourcePlatforms.CurseForge Then
  182. Candidates.Add(Result.Item.CurseForgeSlug.Replace("-", " ").Replace("/", " "))
  183. End If
  184. If Result.Item.ModrinthSlug IsNot Nothing AndAlso Source = ResourcePlatforms.Modrinth Then
  185. Candidates.Add(Result.Item.ModrinthSlug.Replace("-", " ").Replace("/", " "))
  186. End If
  187. Candidates.Add(Result.Item.ChineseName.AfterLast(" (").TrimEnd(") ").BeforeFirst(" - ").
  188. Replace("-", " ").Replace("/", " ").Replace(":", " ").Replace("(", " ").Replace(")", ""))
  189. '分词、清洗、去重
  190. Candidates = Candidates.
  191. SelectMany(Function(c) c.Split(" ")).
  192. Select(Function(w) w.TrimStart("{[(").TrimEnd("}])").Lower).
  193. Where(
  194. Function(w)
  195. If w.Length <= 1 Then Return False '单字或空白
  196. If {"the", "of", "mod", "and", "forge", "fabric", "for", "quilt", "neoforge"}.Contains(w) Then Return False '常见词
  197. If Val(w) > 0 Then Return False '数字
  198. Return True
  199. End Function).Distinct.ToList
  200. '如果一个词可以由其他词拼成,则去掉这个词(例如将 ender io enderio 的 enderio 剔除,只保留 ender io)
  201. Dim CanForm As Func(Of String, Boolean) = Nothing
  202. CanForm = Function(s) Candidates.Contains(s) OrElse Candidates.Any(Function(c) s.StartsWith(c) AndAlso CanForm(s.Substring(c.Length)))
  203. Candidates = Candidates.Where(Function(w) Not Candidates.Any(Function(c) c.Length < w.Length AndAlso w.StartsWith(c) AndAlso CanForm(w.Substring(c.Length)))).ToList()
  204. Return Candidates
  205. End Function
  206. 'CurseForge
  207. If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) Then
  208. '数据库搜索
  209. Static CurseForgeSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
  210. WikiEntry.All.Where(Function(Entry) Entry.CurseForgeSlug IsNot Nothing).Select(
  211. Function(Entry) New SearchEntry(Of WikiEntry) With {
  212. .Item = Entry,
  213. .SearchSource = New List(Of SearchSource) From {
  214. New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split("/"c, True), 1), '部分 Mod 有别名
  215. New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.CurseForgeSlug, 0.5)}
  216. }).ToList
  217. Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25)
  218. If CurseForgeSearchResults.Any Then
  219. '选取目标(CurseForge 要求每个词都必须匹配上,所以只能选择一个 Mod 进行搜索)
  220. Dim CurseForgeTarget =
  221. If(CurseForgeSearchResults.First.AbsoluteRight,
  222. CurseForgeSearchResults.Where(Function(s) s.AbsoluteRight).ToList, '优先使用所有完全匹配的
  223. CurseForgeSearchResults.MaxByAll(Function(s) s.Similarity)). '其次使用所有相似度最高的
  224. MaxBy(Function(s) s.Item.Popularity) '然后从中选择最受欢迎的那一个
  225. '后处理
  226. CurseForgeAltSearchText = ExtractWords(CurseForgeTarget, ResourcePlatforms.CurseForge).Join(" ")
  227. Logger.Warn($"中文搜索关键词(CurseForge):{CurseForgeAltSearchText}")
  228. End If
  229. End If
  230. 'Modrinth
  231. If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) Then
  232. '数据库搜索
  233. Static ModrinthSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
  234. WikiEntry.All.Where(Function(Entry) Entry.ModrinthSlug IsNot Nothing).Select(
  235. Function(Entry) New SearchEntry(Of WikiEntry) With {
  236. .Item = Entry,
  237. .SearchSource = New List(Of SearchSource) From {
  238. New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split("/"c, True), 1), '部分 Mod 有别名
  239. New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.ModrinthSlug, 0.5)}
  240. }).ToList
  241. Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25)
  242. If ModrinthSearchResults.Any Then
  243. '分词
  244. Dim WordWeights As New Dictionary(Of String, Double) '各个单词及其出现的权重
  245. For Each Result In ModrinthSearchResults
  246. For Each Word In ExtractWords(Result, ResourcePlatforms.Modrinth)
  247. If Not WordWeights.ContainsKey(Word) Then WordWeights.Add(Word, 0)
  248. Dim Similarity = If(Result.SearchSource.Any(Function(s) s.Aliases.Contains(RawSearchText)), 1000, Result.Similarity) '完全匹配为 1000
  249. WordWeights(Word) += Similarity * Result.Item.Popularity '权重 += 相似度 * 受欢迎程度
  250. Next
  251. Next
  252. ModrinthAltSearchText = WordWeights.MaxBy(Function(w) w.Value).Key
  253. Logger.Warn($"中文搜索关键词(Modrinth):{ModrinthAltSearchText}")
  254. '直接请求工程
  255. ModrinthSlugs = ModrinthSearchResults.Take(100).Select(Function(r) r.Item.ModrinthSlug).ToList
  256. End If
  257. End If
  258. '结束
  259. If String.IsNullOrEmpty(CurseForgeAltSearchText) AndAlso String.IsNullOrEmpty(ModrinthAltSearchText) AndAlso Not ModrinthSlugs.Any Then
  260. Throw New Exception(MSG_NO_CHINESE_RESULT)
  261. End If
  262. End If
  263. Task.Progress = 0.05
  264. #End Region
  265. Dim RealResults As New List(Of ResourceProject)
  266. NextPage:
  267. Dim RawResults As New ConcurrentList(Of ResourceProject)
  268. #Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
  269. Dim WorkThreads As New List(Of Thread)
  270. Dim Errors As New ConcurrentList(Of (Ex As Exception, Source As ResourcePlatforms))
  271. '在 1.14-,部分老 Mod 没有设置支持的加载器,因此添加 Forge 筛选就会出现遗漏
  272. '所以,在发起请求时不筛选加载器,然后在返回的结果中自行筛除不是 Forge 的 Mod
  273. Dim IgnoreModLoaderFilter = Request.ModLoaders = ModLoaders.Forge AndAlso McVersion.VersionToDrop(Request.GameVersion) < 140
  274. Try
  275. 'CurseForge 搜索
  276. If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) AndAlso
  277. Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足
  278. (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
  279. WorkThreads.Add(RunInNewThread(
  280. Sub()
  281. Try
  282. '获取工程列表
  283. Dim CurseForgeUrl As String = Request.GetCurseForgeAddress(If(CurseForgeAltSearchText, RawSearchText), IgnoreModLoaderFilter)
  284. Logger.Info($"开始 CurseForge 搜索:{CurseForgeUrl}")
  285. Dim RequestResult As JObject = DlModRequest(CurseForgeUrl)
  286. Dim ProjectList As New List(Of ResourceProject)
  287. For Each JsonEntry As JObject In RequestResult("data")
  288. Dim Project As New ResourceProject(JsonEntry)
  289. If Request.Type = ResourceTypes.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包
  290. If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For '过滤分区不匹配的搜索结果(#8265)
  291. ProjectList.Add(Project)
  292. Next
  293. '更新结果
  294. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  295. Storage.CurseForgeOffset += RequestResult("data").Count
  296. Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
  297. Logger.Info($"从 CurseForge 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
  298. Catch ex As Exception
  299. Logger.Warn(ex, "CurseForge 搜索失败")
  300. Storage.CurseForgeTotal = -1
  301. Errors.Add((ex, ResourcePlatforms.CurseForge))
  302. End Try
  303. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  304. End Sub, "CurseForge Search"))
  305. End If
  306. 'Modrinth 搜索
  307. If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
  308. Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
  309. (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
  310. WorkThreads.Add(RunInNewThread(
  311. Sub()
  312. Try
  313. Dim ModrinthUrl As String = Request.GetModrinthAddress(If(ModrinthAltSearchText, RawSearchText), IgnoreModLoaderFilter)
  314. Logger.Info($"开始 Modrinth 搜索:{ModrinthUrl}")
  315. Dim RequestResult As JObject = DlModRequest(ModrinthUrl)
  316. Dim ProjectList As New List(Of ResourceProject)
  317. For Each JsonEntry As JObject In RequestResult("hits")
  318. ProjectList.Add(New ResourceProject(JsonEntry))
  319. Next
  320. '更新结果
  321. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  322. Storage.ModrinthOffset += RequestResult("hits").Count
  323. Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
  324. Logger.Info($"从 Modrinth 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
  325. Catch ex As Exception
  326. Logger.Warn(ex, "Modrinth 搜索失败")
  327. Storage.ModrinthTotal = -1
  328. Errors.Add((ex, ResourcePlatforms.Modrinth))
  329. End Try
  330. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  331. End Sub, "Modrinth Search"))
  332. End If
  333. 'Modrinth 直接获取工程
  334. If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
  335. Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
  336. ModrinthSlugs.Any Then '有直接获取的 Slug
  337. WorkThreads.Add(RunInNewThread(
  338. Sub()
  339. Try
  340. Dim ModrinthUrl As String = $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthSlugs.Join(""",""")}""]"
  341. Logger.Info($"开始 Modrinth 直接获取:{ModrinthUrl}")
  342. Dim ProjectList As New List(Of ResourceProject)
  343. For Each JsonEntry As JObject In DlModRequest(ModrinthUrl)
  344. Dim Project As New ResourceProject(JsonEntry)
  345. '应用筛选
  346. If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For
  347. If Not String.IsNullOrEmpty(Request.Tag) AndAlso
  348. Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
  349. If Request.ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter AndAlso
  350. Not Project.ModLoaders.Flags.Intersect(Request.ModLoaders.Flags).Any() Then Continue For
  351. If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
  352. Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
  353. ProjectList.Add(Project)
  354. Next
  355. '更新结果
  356. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  357. Logger.Info($"从 Modrinth 直接获取到了 {ProjectList.Count} 个工程")
  358. ModrinthSlugs.Clear() '防止重试/加载下一页时重复获取
  359. Catch ex As Exception
  360. Logger.Warn(ex, "Modrinth 直接获取失败")
  361. Errors.Add((ex, ResourcePlatforms.Modrinth))
  362. End Try
  363. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  364. End Sub, "Modrinth Get"))
  365. End If
  366. '等待线程结束
  367. For Each Thread In WorkThreads
  368. Thread.Join()
  369. If Task.IsInterrupted Then Return '会自动触发 Finally 以清理线程
  370. Next
  371. '仅保留兼容 Forge 的 Mod,或老版本中没有标注任何 Mod Loader 的 Mod
  372. If IgnoreModLoaderFilter Then
  373. RawResults.KeepIf(Function(p) p.ModLoaders = ModLoaders.None OrElse p.ModLoaders.HasFlag(ModLoaders.Forge))
  374. End If
  375. '确保存在结果
  376. Storage.ErrorMessage = Nothing
  377. If Not RawResults.Any() Then
  378. If Errors.Any() Then
  379. Throw Errors.First.Ex
  380. Else
  381. If IsChineseSearch AndAlso Not (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) Then
  382. Throw New Exception(MSG_NO_CHINESE_RESULT)
  383. ElseIf Request.Sources = ResourcePlatforms.CurseForge AndAlso Request.Tag.StartsWithF("/") Then
  384. Throw New Exception("CurseForge 不兼容所选的类型")
  385. ElseIf Request.Sources = ResourcePlatforms.Modrinth AndAlso Request.Tag.EndsWithF("/") Then
  386. Throw New Exception("Modrinth 不兼容所选的类型")
  387. Else
  388. Throw New Exception("没有搜索结果")
  389. End If
  390. End If
  391. ElseIf Errors.Any() Then
  392. '有结果但是有错误
  393. If Errors.Any(Function(e) e.Source = ResourcePlatforms.CurseForge) Then
  394. Storage.ErrorMessage = $"无法连接到 CurseForge,所以目前仅显示了来自 Modrinth 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
  395. Else
  396. Storage.ErrorMessage = $"无法连接到 Modrinth,所以目前仅显示了来自 CurseForge 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
  397. End If
  398. End If
  399. Finally
  400. For Each Thread In WorkThreads
  401. If Thread.IsAlive Then Thread.Interrupt()
  402. Next
  403. End Try
  404. #End Region
  405. #Region "提取非重复项,存储于 RealResults"
  406. '将 CurseForge 排在 Modrinth 的前面,避免加载结束顺序不同导致排名不同
  407. RawResults = RawResults.Where(Function(x) x.Platform = ResourcePlatforms.CurseForge).Concat(RawResults.Where(Function(x) x.Platform <> ResourcePlatforms.CurseForge)).ToList
  408. '去重
  409. RawResults = RawResults.Distinct(Function(a, b) a.IsLike(b)).ToList
  410. '已有内容去重
  411. RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
  412. Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
  413. '加入列表
  414. RealResults.AddRange(RawResults)
  415. Logger.Info($"去重、筛选后累计新增结果 {RealResults.Count} 个(目前已有结果 {Storage.Results.Count} 个)")
  416. #End Region
  417. #Region "检查结果数量,如果不足且可继续,会继续加载下一页"
  418. If RealResults.Count + Storage.Results.Count < Request.TargetResultCount Then
  419. Logger.Info($"总结果数需求最少 {Request.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
  420. If Request.CanContinue AndAlso Not Errors.Any Then '如果有某个源失败则不再重试,这时候重试可能导致无限循环
  421. Logger.Info("将继续尝试加载下一页")
  422. GoTo NextPage
  423. Else
  424. Logger.Info("无法继续加载,将强制结束")
  425. End If
  426. End If
  427. #End Region
  428. #Region "将结果排序并添加"
  429. Dim Scores As New Dictionary(Of ResourceProject, Double) '排序分
  430. Dim GetDownloadCountMult =
  431. Function(Project As ResourceProject) As Double
  432. Select Case Request.Type
  433. Case ResourceTypes.Mod, ResourceTypes.ModPack
  434. Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 5)
  435. Case ResourceTypes.DataPack
  436. Return If(Project.Platform = ResourcePlatforms.CurseForge, 10, 1)
  437. Case ResourceTypes.ResourcePack, ResourceTypes.Shader
  438. Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 4)
  439. Case Else
  440. Return 1
  441. End Select
  442. End Function
  443. If String.IsNullOrEmpty(RawSearchText) Then
  444. '如果没有搜索文本,按下载量将结果排序
  445. For Each Result As ResourceProject In RealResults
  446. Scores(Result) = Result.DownloadCount * GetDownloadCountMult(Result)
  447. Next
  448. Else
  449. '如果有搜索文本,按关联度将结果排序
  450. '排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
  451. Dim SearchEntries As New List(Of SearchEntry(Of ResourceProject))
  452. For Each Result As ResourceProject In RealResults
  453. Scores(Result) = If(Result.WikiId > 0, 0.2, 0) +
  454. Math.Log10(Math.Max(Result.DownloadCount, 1) * GetDownloadCountMult(Result)) / 9
  455. SearchEntries.Add(New SearchEntry(Of ResourceProject) With {.Item = Result, .SearchSource = New List(Of SearchSource) From {
  456. New SearchSource(If(IsChineseSearch, Result.TranslatedName, Result.RawName).Split("/"c, True), 1),
  457. New SearchSource(Result.Description, 0.05)}})
  458. Next
  459. Dim SearchResult = Search(SearchEntries, RawSearchText, 10000, -1)
  460. For Each OneResult In SearchResult
  461. Scores(OneResult.Item) +=
  462. If(OneResult.AbsoluteRight, 10, OneResult.Similarity) /
  463. If(SearchResult.First.AbsoluteRight, 10, SearchResult.First.Similarity) '最高 1 分的相似度分
  464. Next
  465. End If
  466. '根据排序分得出结果并添加
  467. If Task.IsInterrupted Then Throw New ThreadInterruptedException '#8246
  468. Storage.Results.AddRange(
  469. Scores.OrderByDescending(Function(s) s.Value).Select(Function(r) r.Key))
  470. #End Region
  471. End Sub
  472. End Class