ResourceVersion.vb 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. ''' <summary>
  2. ''' 社区资源的单个可下载版本。
  3. ''' </summary>
  4. Public Class ResourceVersion
  5. '基础字段
  6. ''' <summary>
  7. ''' 用于唯一性鉴别该版本的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
  8. ''' </summary>
  9. Public Id As String
  10. ''' <summary>
  11. ''' 该版本所对应的单一资源类别。
  12. ''' </summary>
  13. Public ResourceType As ResourceTypes
  14. '描述性字段
  15. ''' <summary>
  16. ''' 版本描述名。
  17. ''' 并非文件名或版本号,是由上传者完全自定义的字段。
  18. ''' </summary>
  19. Public Display As String
  20. ''' <summary>
  21. ''' 发布时间。
  22. ''' </summary>
  23. Public ReleaseDate As Date
  24. ''' <summary>
  25. ''' 下载量计数。
  26. ''' 注意,仅为一个平台的计数,无法反映多平台加起来的下载量,且 CurseForge 可能错误地返回 0。
  27. ''' </summary>
  28. Public DownloadCount As Integer
  29. ''' <summary>
  30. ''' 版本号。
  31. ''' 不一定是标准格式。CurseForge 默认为 Nothing。
  32. ''' </summary>
  33. Public Version As String
  34. ''' <summary>
  35. ''' 所有支持的 Mod 加载器列表。可能为 <see cref="ModLoaders.None"/>。
  36. ''' </summary>
  37. Public ModLoaders As ModLoaders = ModLoaders.None
  38. ''' <summary>
  39. ''' 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。
  40. ''' </summary>
  41. Public GameVersions As List(Of String)
  42. ''' <summary>
  43. ''' 发布状态。
  44. ''' </summary>
  45. Public ReleaseType As ReleaseTypes
  46. ''' <summary>
  47. ''' 发布状态的友好描述。
  48. ''' 例如:"正式版","Beta 版"。
  49. ''' </summary>
  50. Public ReadOnly Property ReleaseTypeDisplay As String
  51. Get
  52. Select Case ReleaseType
  53. Case ReleaseTypes.Release
  54. Return "正式版"
  55. Case ReleaseTypes.Beta
  56. Return If(ModeDebug, "Beta 版", "测试版")
  57. Case Else
  58. Return If(ModeDebug, "Alpha 版", "早期测试版")
  59. End Select
  60. End Get
  61. End Property
  62. Public Enum ReleaseTypes
  63. 'https://docs.curseforge.com/rest-api/#tocS_FileReleaseType
  64. Release = 1
  65. Beta = 2
  66. Alpha = 3
  67. End Enum
  68. '下载信息
  69. ''' <summary>
  70. ''' 下载信息是否可用。
  71. ''' </summary>
  72. Public ReadOnly Property DownloadAvailable As Boolean
  73. Get
  74. Return FileName IsNot Nothing AndAlso DownloadUrls IsNot Nothing
  75. End Get
  76. End Property
  77. ''' <summary>
  78. ''' 文件名。
  79. ''' </summary>
  80. Public FileName As String = Nothing
  81. ''' <summary>
  82. ''' 对应文件的所有可能的下载 URL。
  83. ''' </summary>
  84. Public DownloadUrls As List(Of String)
  85. ''' <summary>
  86. ''' 对应文件的 SHA1 或 MD5。固定为小写。
  87. ''' </summary>
  88. Public Hash As String = Nothing
  89. ''' <summary>
  90. ''' 文件大小。不可用时为 -1。
  91. ''' </summary>
  92. Public Size As Integer = -1
  93. ''' <summary>
  94. ''' 作为该版本依赖项的项目 ID。
  95. ''' 它们可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
  96. ''' </summary>
  97. Private RawDependencies As New List(Of String)
  98. ''' <summary>
  99. ''' 作为该版本依赖项的项目 ID。
  100. ''' </summary>
  101. Public Dependencies As New List(Of String)
  102. '初始化
  103. ''' <summary>
  104. ''' 从平台 API 返回的 JSON 中初始化实例。若出错会抛出异常。
  105. ''' </summary>
  106. Public Shared Function FromPlatformJson(Data As JObject, ExpectedResourceType As ResourceTypes) As ResourceVersion
  107. Dim Result As New ResourceVersion
  108. With Result
  109. .ResourceType = ExpectedResourceType
  110. If Data.ContainsKey("gameId") Then
  111. #Region "CurseForge"
  112. '简单信息
  113. .Id = Data("id")
  114. .Display = Data("displayName").ToString.Replace(" ", "").Trim(" ")
  115. .Version = Nothing
  116. .ReleaseDate = Data("fileDate")
  117. .ReleaseType = CType(Data("releaseType").ToObject(Of Integer), ReleaseTypes)
  118. .DownloadCount = Data("downloadCount")
  119. .FileName = Data("fileName")
  120. .Size = Data("fileLength")
  121. If .Size = 0 Then .Size = -1 'CurseForge 可能错误地返回 0 尺寸(#9104)
  122. .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")?.ToString().Lower()
  123. If .Hash Is Nothing Then .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")?.ToString().Lower()
  124. 'DownloadAddress
  125. Dim Url = Data("downloadUrl").ToString
  126. If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(.Id.ToString.Substring(0, 4))}/{CInt(.Id.ToString.Substring(4))}/{ .FileName}"
  127. .DownloadUrls = ParseCurseForgeDownloadUrls(Url.Replace(.FileName, StringUtils.UrlEscape(.FileName))) '对脑残 CurseForge 的下载地址进行多种修正
  128. .DownloadUrls.Add(DlSourceModGet(Url)) '添加镜像源;注意 MCIM 源不支持 URL 编码后的文件名,必须传入 URL 编码前的文件名
  129. .DownloadUrls = .DownloadUrls.Distinct.ToList '最终去重
  130. 'Dependencies
  131. If Data.ContainsKey("dependencies") Then
  132. .RawDependencies = Data("dependencies").
  133. Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
  134. d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
  135. Select(Function(d) d("modId").ToString).ToList
  136. End If
  137. 'GameVersions
  138. Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
  139. .GameVersions = RawVersions.
  140. Where(Function(v) McVersion.IsStandardFormat(v)).
  141. Select(Function(v) v.Replace("-snapshot", " 预览版")).
  142. Distinct.ToList
  143. If .GameVersions.IsSingle Then
  144. .GameVersions = .GameVersions.ToList
  145. ElseIf .GameVersions.Count > 1 Then
  146. .GameVersions = .GameVersions.SortByComparison(AddressOf McUtils.CompareVersionGE).ToList
  147. If .ResourceType = ResourceTypes.ModPack Then .GameVersions = { .GameVersions(0)}.ToList '整合包理应只 “支持” 一个版本
  148. Else
  149. .GameVersions = New List(Of String) From {"未知版本"}
  150. End If
  151. 'ModLoaders
  152. .ModLoaders = ModLoaders.None
  153. For Each Loader In EnumUtils.GetAllFlags(Of ModLoaders)()
  154. If RawVersions.Contains(Loader.ToString.Lower) Then .ModLoaders = .ModLoaders Or Loader
  155. Next
  156. #End Region
  157. Else
  158. #Region "Modrinth"
  159. '简单信息
  160. .Id = Data("id")
  161. .Display = Data("name").ToString.Replace(" ", "").Trim(" ")
  162. .Version = Data("version_number")
  163. .ReleaseDate = Data("date_published")
  164. .ReleaseType = If(Data("version_type").ToString = "release", ReleaseTypes.Release, If(Data("version_type").ToString = "beta", ReleaseTypes.Beta, ReleaseTypes.Alpha))
  165. .DownloadCount = Data("downloads")
  166. If CType(Data("files"), JArray).Any() Then '可能为空
  167. Dim File As JToken = Data("files")(0)
  168. .FileName = File("filename")
  169. .DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
  170. .Size = File("size")
  171. .Hash = File("hashes")("sha1")?.ToString.Lower()
  172. End If
  173. '类别与 Loaders
  174. '结果可能混杂着 Mod、数据包和服务端插件
  175. Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
  176. .ModLoaders = ModLoaders.None
  177. For Each Loader As ModLoaders In EnumUtils.GetAllFlags(Of ModLoaders)()
  178. If RawLoaders.Contains(Loader.ToString.Lower) Then .ModLoaders = .ModLoaders Or Loader
  179. Next
  180. If .ResourceType.HasFlagF(ResourceTypes.Mod) OrElse .ResourceType.HasFlagF(ResourceTypes.DataPack) Then
  181. If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then .ResourceType = ResourceTypes.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric
  182. If RawLoaders.Contains("datapack") Then .ResourceType = ResourceTypes.DataPack
  183. If .ModLoaders.Flags().Any() Then .ResourceType = ResourceTypes.Mod
  184. Else
  185. '使用传入的类别,不作修改(#8377)
  186. End If
  187. 'Dependencies
  188. If Data.ContainsKey("dependencies") Then
  189. .RawDependencies = Data("dependencies").
  190. Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
  191. d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
  192. d("project_id").ToString.Length > 0). '有时候真的会空……
  193. Select(Function(d) d("project_id").ToString).ToList
  194. End If
  195. 'GameVersions
  196. Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.Lower).ToList
  197. .GameVersions = RawVersions.Where(Function(v) v.Contains(".")).
  198. Select(Function(v) If(v.Contains("-"), v.BeforeFirst("-") & " 预览版", If(v.StartsWithF("b1."), "远古版本", v))).
  199. Distinct.ToList
  200. If .GameVersions.IsSingle Then
  201. '无需处理
  202. ElseIf .GameVersions.Count > 1 Then
  203. .GameVersions = .GameVersions.SortByComparison(AddressOf McUtils.CompareVersionGE).ToList
  204. If .ResourceType = ResourceTypes.ModPack Then .GameVersions = New List(Of String) From { .GameVersions(0)} '整合包理应只 “支持” 一个版本
  205. ElseIf RawVersions.Any(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")) Then
  206. .GameVersions = RawVersions.Where(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")).ToList
  207. Else
  208. .GameVersions = New List(Of String) From {"未知版本"}
  209. End If
  210. #End Region
  211. End If
  212. End With
  213. Return Result
  214. End Function
  215. ''' <summary>
  216. ''' 从项目 ID 获取其下的全部版本列表,并获取其前置项目信息。
  217. ''' </summary>
  218. Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms, Optional LoadDependencies As Boolean = True) As List(Of ResourceVersion)
  219. If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
  220. Dim TargetProject = ResourceProject.FromProjectId(ProjectId, Platform)
  221. '获取项目对象的文件列表
  222. If Not ProjectFilesCache.ContainsKey(ProjectId) Then
  223. Logger.Info($"开始获取项目版本列表:{ProjectId}")
  224. Dim ResultJsonArray As JArray = Nothing
  225. If Platform = ResourcePlatforms.CurseForge Then
  226. For RetryCount As Integer = 0 To 2
  227. Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" &
  228. (10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存
  229. 'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522)
  230. '之前只请求一部分文件的方法备份如下:
  231. 'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
  232. ' ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data")
  233. 'Else '否则使用全部文件
  234. ' ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data")
  235. 'End If
  236. If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then
  237. ResultJsonArray = ResultJson("data")
  238. Exit For
  239. ElseIf RetryCount < 2 Then
  240. Logger.Warn($"CurseForge 返回的资源版本列表存在缺失,即将进行第 {RetryCount + 1} 次重试") '#6224
  241. Logger.Info($"返回的原始内容如下:{vbCrLf}{ResultJson}")
  242. Else
  243. Logger.Info($"CurseForge 返回的资源版本列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}")
  244. Throw New Exception("CurseForge 返回的资源版本列表存在缺失")
  245. End If
  246. Next
  247. Else 'Modrinth
  248. ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version?include_changelog=false")
  249. End If
  250. ProjectFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) FromPlatformJson(a, TargetProject.Types)).
  251. Where(Function(a) a.DownloadAvailable).
  252. DistinctBy(Function(a) a.Id).ToList 'CurseForge 可能会重复返回相同项(#1330)
  253. End If
  254. Dim Result As List(Of ResourceVersion) = ProjectFilesCache(ProjectId)
  255. If Not LoadDependencies Then Return Result
  256. '加载前置信息
  257. Dim Dependencies As List(Of String) = Result.SelectMany(Function(f) f.RawDependencies).Distinct().ToList
  258. If Dependencies.Any Then
  259. Logger.Info($"项目 {ProjectId} 中需要加载的前置:{Dependencies.Join(",")}")
  260. For Each DependencyProject In ResourceProject.FromProjectIds(Dependencies, TargetProject.Platform)
  261. For Each File In Result
  262. If File.RawDependencies.Contains(DependencyProject.Id) AndAlso DependencyProject.Id <> ProjectId AndAlso Not File.Dependencies.Contains(DependencyProject.Id) Then
  263. File.Dependencies.Add(DependencyProject.Id)
  264. End If
  265. Next
  266. Next
  267. End If
  268. Return Result
  269. End Function
  270. ''' <summary>
  271. ''' 项目 ID 对应的版本信息的缓存。不会保存到文件。
  272. ''' </summary>
  273. Protected Friend Shared ProjectFilesCache As New Dictionary(Of String, List(Of ResourceVersion))
  274. '序列化
  275. ''' <summary>
  276. ''' 从缓存 JSON 读取字段数据。
  277. ''' </summary>
  278. Public Shared Function FromCacheJson(Data As JObject) As ResourceVersion
  279. Dim Result As New ResourceVersion
  280. With Result
  281. .ResourceType = Data("ResourceType").ToObject(Of ResourceTypes)
  282. .Id = Data("Id").ToString
  283. .Display = Data("DisplayName").ToString
  284. If Data.ContainsKey("Version") Then .Version = Data("Version").ToString
  285. .ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
  286. .DownloadCount = Data("DownloadCount").ToObject(Of Integer)
  287. .ReleaseType = Data("ReleaseType").ToObject(Of ReleaseTypes)
  288. If Data.ContainsKey("FileName") Then .FileName = Data("FileName").ToString
  289. If Data.ContainsKey("DownloadUrls") Then .DownloadUrls = Data("DownloadUrls").ToObject(Of List(Of String))
  290. If Data.ContainsKey("ModLoaders") Then .ModLoaders = Data("ModLoaders").ToObject(Of ModLoaders)
  291. If Data.ContainsKey("Size") Then .Size = Data("Size").ToObject(Of Integer)
  292. If Data.ContainsKey("Hash") Then .Hash = Data("Hash").ToString
  293. If Data.ContainsKey("GameVersions") Then .GameVersions = Data("GameVersions").ToObject(Of List(Of String))
  294. If Data.ContainsKey("RawDependencies") Then .RawDependencies = Data("RawDependencies").ToObject(Of List(Of String))
  295. If Data.ContainsKey("Dependencies") Then .Dependencies = Data("Dependencies").ToObject(Of List(Of String))
  296. End With
  297. Return Result
  298. End Function
  299. ''' <summary>
  300. ''' 转为缓存 JSON。
  301. ''' </summary>
  302. Public Function ToCacheJson() As JObject
  303. Dim Result As New JObject
  304. With Result
  305. .Add("ResourceType", CInt(ResourceType))
  306. .Add("Id", Id)
  307. If Version IsNot Nothing Then .Add("Version", Version)
  308. .Add("DisplayName", Display)
  309. .Add("ReleaseDate", ReleaseDate)
  310. .Add("DownloadCount", DownloadCount)
  311. .Add("ModLoaders", CInt(ModLoaders))
  312. .Add("GameVersions", New JArray(GameVersions))
  313. .Add("ReleaseType", CInt(ReleaseType))
  314. If FileName IsNot Nothing Then .Add("FileName", FileName)
  315. If DownloadUrls IsNot Nothing Then .Add("DownloadUrls", New JArray(DownloadUrls))
  316. If Hash IsNot Nothing Then .Add("Hash", Hash)
  317. If Size >= 0 Then .Add("Size", Size)
  318. .Add("RawDependencies", New JArray(RawDependencies))
  319. .Add("Dependencies", New JArray(Dependencies))
  320. End With
  321. Return Result
  322. End Function
  323. '杂项
  324. ''' <summary>
  325. ''' 转为列表项。
  326. ''' </summary>
  327. Public Function ToListItem(ClickHandler As MyListItem.ClickEventHandler, Optional SaveAsButtonHandler As MyIconButton.ClickEventHandler = Nothing,
  328. Optional IsBadDisplay As Boolean = False) As MyVirtualizingElement(Of MyListItem)
  329. Return New MyVirtualizingElement(Of MyListItem)(
  330. Function()
  331. Dim Result As New MyListItem
  332. With Result
  333. '基础属性
  334. .SnapsToDevicePixels = True
  335. .Height = 42
  336. .Type = MyListItem.CheckType.Clickable
  337. .Tag = Me
  338. .Title = If(IsBadDisplay, FileName, Display).BeforeLastOfAny({".zip", ".jar", ".mrpack", ".litemod"}, True)
  339. .Logo = $"{PathImage}ReleaseTypes/{ReleaseType}.png"
  340. AddHandler .Click, ClickHandler
  341. '描述
  342. .Info =
  343. Iterator Function()
  344. If ModLoaders <> ModLoaders.None Then Yield ModLoaders.Flags.Join("、"c)
  345. If .Title <> PathUtils.GetFileNameWithoutExtension(FileName) Then Yield PathUtils.GetFileNameWithoutExtension(FileName)
  346. If Dependencies.Any Then Yield $"{Dependencies.Count} 项前置"
  347. If GameVersions.All(Function(v) Not v.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) v.ContainsIgnoreCase(s))) Then Yield $"游戏版本 {GameVersions.Join("、"c)}"
  348. If DownloadCount > 0 Then Yield $"下载 {If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万", DownloadCount.ToString() + " ")}次" 'CurseForge 的下载次数经常错误地返回 0
  349. Yield $"更新于 {StringUtils.FormatTimeSpan(ReleaseDate - Date.Now, False)}"
  350. If ReleaseType <> ReleaseTypes.Release Then Yield ReleaseTypeDisplay
  351. End Function().Join(","c)
  352. '另存为按钮
  353. If SaveAsButtonHandler IsNot Nothing Then
  354. Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
  355. AddHandler BtnSave.Click, SaveAsButtonHandler
  356. .Buttons = {BtnSave}
  357. End If
  358. End With
  359. Return Result
  360. End Function) With {.Height = 42}
  361. End Function
  362. ''' <summary>
  363. ''' 获取下载信息。
  364. ''' </summary>
  365. ''' <param name="LocalFileOrFolder">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
  366. Public Function ToNetFile(LocalFileOrFolder As String,
  367. Optional TrackDownloadReason As DownloadReason? = Nothing, Optional TrackGameVersion As String = Nothing, Optional TrackLoader As ModLoaders = ModLoaders.None) As NetFile
  368. Return New NetFile(
  369. ParseModrinthTrackArguments(DownloadUrls, TrackDownloadReason, TrackGameVersion, TrackLoader),
  370. LocalFileOrFolder & If(LocalFileOrFolder.EndsWithF("\"), FileName, ""),
  371. New FileChecker With {.Hash = Hash, .ActualSize = Size},
  372. SimulateBrowserHeaders:=True)
  373. End Function
  374. ''' <summary>
  375. ''' 添加 Modrinth 追踪参数。
  376. ''' </summary>
  377. Public Shared Iterator Function ParseModrinthTrackArguments(Urls As List(Of String),
  378. Optional TrackDownloadReason As DownloadReason? = Nothing, Optional TrackGameVersion As String = Nothing, Optional TrackLoader As ModLoaders = ModLoaders.None) As IEnumerable(Of String)
  379. Dim Arguments As New Dictionary(Of String, String)
  380. If TrackDownloadReason IsNot Nothing Then Arguments.Add("mr_download_reason", TrackDownloadReason.ToString.Lower)
  381. If TrackGameVersion IsNot Nothing Then Arguments.Add("mr_game_version", TrackGameVersion)
  382. If TrackLoader.Flags.Any Then Arguments.Add("mr_loader", TrackLoader.Flags.First.ToString.Lower)
  383. Dim Argument = Arguments.Select(Function(a) $"{a.Key}={a.Value}").Join("&")
  384. For Each Url In Urls
  385. If Not Url.ContainsIgnoreCase("cdn.modrinth.com/") Then Yield Url
  386. Yield $"{Url}{If(Url.Contains("?"), "&", "?")}{Argument}"
  387. Next
  388. End Function
  389. Public Enum DownloadReason
  390. Standalone
  391. Dependency
  392. ModPack
  393. Update
  394. End Enum
  395. ''' <summary>
  396. ''' 重新整理 CurseForge 的下载地址。
  397. ''' </summary>
  398. Public Shared Function ParseCurseForgeDownloadUrls(Url As String) As List(Of String)
  399. Return {
  400. Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
  401. Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
  402. Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
  403. Url.Replace("://media.", "://edge."),
  404. Url
  405. }.Distinct.ToList
  406. End Function
  407. Public Overrides Function ToString() As String
  408. Return $"{Id}: {FileName}"
  409. End Function
  410. End Class