ResourceVersion.vb 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")
  122. If .Hash Is Nothing Then .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")
  123. 'DownloadAddress
  124. Dim Url = Data("downloadUrl").ToString
  125. If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(.Id.ToString.Substring(0, 4))}/{CInt(.Id.ToString.Substring(4))}/{ .FileName}"
  126. .DownloadUrls = ParseCurseForgeDownloadUrls(Url.Replace(.FileName, StringUtils.UrlEscape(.FileName))) '对脑残 CurseForge 的下载地址进行多种修正
  127. .DownloadUrls.Add(DlSourceModGet(Url)) '添加镜像源;注意 MCIM 源不支持 URL 编码后的文件名,必须传入 URL 编码前的文件名
  128. .DownloadUrls = .DownloadUrls.Distinct.ToList '最终去重
  129. 'Dependencies
  130. If Data.ContainsKey("dependencies") Then
  131. .RawDependencies = Data("dependencies").
  132. Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
  133. d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
  134. Select(Function(d) d("modId").ToString).ToList
  135. End If
  136. 'GameVersions
  137. Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
  138. .GameVersions = RawVersions.
  139. Where(Function(v) McVersion.IsFormatFit(v)).
  140. Select(Function(v) v.Replace("-snapshot", " 预览版")).
  141. Distinct.ToList
  142. If .GameVersions.IsSingle Then
  143. .GameVersions = .GameVersions.ToList
  144. ElseIf .GameVersions.Count > 1 Then
  145. .GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
  146. If .ResourceType = ResourceTypes.ModPack Then .GameVersions = { .GameVersions(0)}.ToList '整合包理应只 “支持” 一个版本
  147. Else
  148. .GameVersions = New List(Of String) From {"未知版本"}
  149. End If
  150. 'ModLoaders
  151. .ModLoaders = ModLoaders.None
  152. For Each Loader In EnumUtils.GetAllFlags(Of ModLoaders)()
  153. If RawVersions.Contains(Loader.ToString.Lower) Then .ModLoaders = .ModLoaders Or Loader
  154. Next
  155. #End Region
  156. Else
  157. #Region "Modrinth"
  158. '简单信息
  159. .Id = Data("id")
  160. .Display = Data("name").ToString.Replace(" ", "").Trim(" ")
  161. .Version = Data("version_number")
  162. .ReleaseDate = Data("date_published")
  163. .ReleaseType = If(Data("version_type").ToString = "release", ReleaseTypes.Release, If(Data("version_type").ToString = "beta", ReleaseTypes.Beta, ReleaseTypes.Alpha))
  164. .DownloadCount = Data("downloads")
  165. If CType(Data("files"), JArray).Any() Then '可能为空
  166. Dim File As JToken = Data("files")(0)
  167. .FileName = File("filename")
  168. .DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
  169. .Size = File("size")
  170. .Hash = File("hashes")("sha1")
  171. End If
  172. '类别与 Loaders
  173. '结果可能混杂着 Mod、数据包和服务端插件
  174. Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
  175. .ModLoaders = ModLoaders.None
  176. For Each Loader As ModLoaders In EnumUtils.GetAllFlags(Of ModLoaders)()
  177. If RawLoaders.Contains(Loader.ToString.Lower) Then .ModLoaders = .ModLoaders Or Loader
  178. Next
  179. If .ResourceType.HasFlag(ResourceTypes.Mod) OrElse .ResourceType.HasFlag(ResourceTypes.DataPack) Then
  180. If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then .ResourceType = ResourceTypes.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric
  181. If RawLoaders.Contains("datapack") Then .ResourceType = ResourceTypes.DataPack
  182. If .ModLoaders.Flags().Any() Then .ResourceType = ResourceTypes.Mod
  183. Else
  184. '使用传入的类别,不作修改(#8377)
  185. End If
  186. 'Dependencies
  187. If Data.ContainsKey("dependencies") Then
  188. .RawDependencies = Data("dependencies").
  189. Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
  190. d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
  191. d("project_id").ToString.Length > 0). '有时候真的会空……
  192. Select(Function(d) d("project_id").ToString).ToList
  193. End If
  194. 'GameVersions
  195. Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.Lower).ToList
  196. .GameVersions = RawVersions.Where(Function(v) v.Contains(".")).
  197. Select(Function(v) If(v.Contains("-"), v.BeforeFirst("-") & " 预览版", If(v.StartsWithF("b1."), "远古版本", v))).
  198. Distinct.ToList
  199. If .GameVersions.IsSingle Then
  200. '无需处理
  201. ElseIf .GameVersions.Count > 1 Then
  202. .GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
  203. If .ResourceType = ResourceTypes.ModPack Then .GameVersions = New List(Of String) From { .GameVersions(0)} '整合包理应只 “支持” 一个版本
  204. ElseIf RawVersions.Any(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")) Then
  205. .GameVersions = RawVersions.Where(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")).ToList
  206. Else
  207. .GameVersions = New List(Of String) From {"未知版本"}
  208. End If
  209. #End Region
  210. End If
  211. End With
  212. Return Result
  213. End Function
  214. ''' <summary>
  215. ''' 从项目 ID 获取其下的全部版本列表,并获取其前置项目信息。
  216. ''' </summary>
  217. Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms, Optional LoadDependencies As Boolean = True) As List(Of ResourceVersion)
  218. If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
  219. Dim TargetProject = ResourceProject.FromProjectId(ProjectId, Platform)
  220. '获取项目对象的文件列表
  221. If Not ProjectFilesCache.ContainsKey(ProjectId) Then
  222. Logger.Info($"开始获取项目版本列表:{ProjectId}")
  223. Dim ResultJsonArray As JArray = Nothing
  224. If Platform = ResourcePlatforms.CurseForge Then
  225. For RetryCount As Integer = 0 To 2
  226. Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" &
  227. (10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存
  228. 'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522)
  229. '之前只请求一部分文件的方法备份如下:
  230. 'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
  231. ' ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data")
  232. 'Else '否则使用全部文件
  233. ' ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data")
  234. 'End If
  235. If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then
  236. ResultJsonArray = ResultJson("data")
  237. Exit For
  238. ElseIf RetryCount < 2 Then
  239. Logger.Warn($"CurseForge 返回的资源版本列表存在缺失,即将进行第 {RetryCount + 1} 次重试") '#6224
  240. Logger.Info($"返回的原始内容如下:{vbCrLf}{ResultJson}")
  241. Else
  242. Logger.Info($"CurseForge 返回的资源版本列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}")
  243. Throw New Exception("CurseForge 返回的资源版本列表存在缺失")
  244. End If
  245. Next
  246. Else 'Modrinth
  247. ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version?include_changelog=false")
  248. End If
  249. ProjectFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) FromPlatformJson(a, TargetProject.Types)).
  250. Where(Function(a) a.DownloadAvailable).
  251. DistinctBy(Function(a) a.Id).ToList 'CurseForge 可能会重复返回相同项(#1330)
  252. End If
  253. Dim Result As List(Of ResourceVersion) = ProjectFilesCache(ProjectId)
  254. If Not LoadDependencies Then Return Result
  255. '加载前置信息
  256. Dim Dependencies As List(Of String) = Result.SelectMany(Function(f) f.RawDependencies).Distinct().ToList
  257. If Dependencies.Any Then
  258. Logger.Info($"项目 {ProjectId} 中需要加载的前置:{Dependencies.Join(",")}")
  259. For Each DependencyProject In ResourceProject.FromProjectIds(Dependencies, TargetProject.Platform)
  260. For Each File In Result
  261. If File.RawDependencies.Contains(DependencyProject.Id) AndAlso DependencyProject.Id <> ProjectId AndAlso Not File.Dependencies.Contains(DependencyProject.Id) Then
  262. File.Dependencies.Add(DependencyProject.Id)
  263. End If
  264. Next
  265. Next
  266. End If
  267. Return Result
  268. End Function
  269. ''' <summary>
  270. ''' 项目 ID 对应的版本信息的缓存。不会保存到文件。
  271. ''' </summary>
  272. Protected Friend Shared ProjectFilesCache As New Dictionary(Of String, List(Of ResourceVersion))
  273. '序列化
  274. ''' <summary>
  275. ''' 从缓存 JSON 读取字段数据。
  276. ''' </summary>
  277. Public Shared Function FromCacheJson(Data As JObject) As ResourceVersion
  278. Dim Result As New ResourceVersion
  279. With Result
  280. .ResourceType = Data("ResourceType").ToObject(Of ResourceTypes)
  281. .Id = Data("Id").ToString
  282. .Display = Data("DisplayName").ToString
  283. If Data.ContainsKey("Version") Then .Version = Data("Version").ToString
  284. .ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
  285. .DownloadCount = Data("DownloadCount").ToObject(Of Integer)
  286. .ReleaseType = Data("ReleaseType").ToObject(Of ReleaseTypes)
  287. If Data.ContainsKey("FileName") Then .FileName = Data("FileName").ToString
  288. If Data.ContainsKey("DownloadUrls") Then .DownloadUrls = Data("DownloadUrls").ToObject(Of List(Of String))
  289. If Data.ContainsKey("ModLoaders") Then .ModLoaders = Data("ModLoaders").ToObject(Of ModLoaders)
  290. If Data.ContainsKey("Size") Then .Size = Data("Size").ToObject(Of Integer)
  291. If Data.ContainsKey("Hash") Then .Hash = Data("Hash").ToString
  292. If Data.ContainsKey("GameVersions") Then .GameVersions = Data("GameVersions").ToObject(Of List(Of String))
  293. If Data.ContainsKey("RawDependencies") Then .RawDependencies = Data("RawDependencies").ToObject(Of List(Of String))
  294. If Data.ContainsKey("Dependencies") Then .Dependencies = Data("Dependencies").ToObject(Of List(Of String))
  295. End With
  296. Return Result
  297. End Function
  298. ''' <summary>
  299. ''' 转为缓存 JSON。
  300. ''' </summary>
  301. Public Function ToCacheJson() As JObject
  302. Dim Result As New JObject
  303. With Result
  304. .Add("ResourceType", CInt(ResourceType))
  305. .Add("Id", Id)
  306. If Version IsNot Nothing Then .Add("Version", Version)
  307. .Add("DisplayName", Display)
  308. .Add("ReleaseDate", ReleaseDate)
  309. .Add("DownloadCount", DownloadCount)
  310. .Add("ModLoaders", CInt(ModLoaders))
  311. .Add("GameVersions", New JArray(GameVersions))
  312. .Add("ReleaseType", CInt(ReleaseType))
  313. If FileName IsNot Nothing Then .Add("FileName", FileName)
  314. If DownloadUrls IsNot Nothing Then .Add("DownloadUrls", New JArray(DownloadUrls))
  315. If Hash IsNot Nothing Then .Add("Hash", Hash)
  316. If Size >= 0 Then .Add("Size", Size)
  317. .Add("RawDependencies", New JArray(RawDependencies))
  318. .Add("Dependencies", New JArray(Dependencies))
  319. End With
  320. Return Result
  321. End Function
  322. '杂项
  323. ''' <summary>
  324. ''' 转为列表项。
  325. ''' </summary>
  326. Public Function ToListItem(ClickHandler As MyListItem.ClickEventHandler, Optional SaveAsButtonHandler As MyIconButton.ClickEventHandler = Nothing,
  327. Optional IsBadDisplay As Boolean = False) As MyVirtualizingElement(Of MyListItem)
  328. Return New MyVirtualizingElement(Of MyListItem)(
  329. Function()
  330. Dim Result As New MyListItem
  331. With Result
  332. '基础属性
  333. .SnapsToDevicePixels = True
  334. .Height = 42
  335. .Type = MyListItem.CheckType.Clickable
  336. .Tag = Me
  337. .Title = If(IsBadDisplay, FileName, Display)
  338. .Logo = $"{PathImage}ReleaseTypes/{ReleaseType}.png"
  339. AddHandler .Click, ClickHandler
  340. '描述
  341. .Info =
  342. Iterator Function()
  343. If ModLoaders <> ModLoaders.None Then Yield ModLoaders.Flags.Join("、"c)
  344. If .Title <> FileName.BeforeLast(".") Then Yield FileName.BeforeLast(".")
  345. If Dependencies.Any Then Yield $"{Dependencies.Count} 项前置"
  346. 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)}"
  347. If DownloadCount > 0 Then Yield $"下载 {If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万", DownloadCount.ToString() + " ")}次" 'CurseForge 的下载次数经常错误地返回 0
  348. Yield $"更新于 {StringUtils.FormatTimeSpan(ReleaseDate - Date.Now, False)}"
  349. If ReleaseType <> ReleaseTypes.Release Then Yield ReleaseTypeDisplay
  350. End Function().Join(","c)
  351. '另存为按钮
  352. If SaveAsButtonHandler IsNot Nothing Then
  353. Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
  354. AddHandler BtnSave.Click, SaveAsButtonHandler
  355. .Buttons = {BtnSave}
  356. End If
  357. End With
  358. Return Result
  359. End Function) With {.Height = 42}
  360. End Function
  361. ''' <summary>
  362. ''' 获取下载信息。
  363. ''' </summary>
  364. ''' <param name="LocalFileOrFolder">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
  365. Public Function ToNetFile(LocalFileOrFolder As String,
  366. Optional TrackDownloadReason As DownloadReason? = Nothing, Optional TrackGameVersion As String = Nothing, Optional TrackLoader As ModLoaders = ModLoaders.None) As NetFile
  367. Return New NetFile(
  368. ParseModrinthTrackArguments(DownloadUrls, TrackDownloadReason, TrackGameVersion, TrackLoader),
  369. LocalFileOrFolder & If(LocalFileOrFolder.EndsWithF("\"), FileName, ""),
  370. New FileChecker With {.Hash = Hash, .ActualSize = Size},
  371. SimulateBrowserHeaders:=True)
  372. End Function
  373. ''' <summary>
  374. ''' 添加 Modrinth 追踪参数。
  375. ''' </summary>
  376. Public Shared Iterator Function ParseModrinthTrackArguments(Urls As List(Of String),
  377. Optional TrackDownloadReason As DownloadReason? = Nothing, Optional TrackGameVersion As String = Nothing, Optional TrackLoader As ModLoaders = ModLoaders.None) As IEnumerable(Of String)
  378. Dim Arguments As New Dictionary(Of String, String)
  379. If TrackDownloadReason IsNot Nothing Then Arguments.Add("mr_download_reason", TrackDownloadReason.ToString.Lower)
  380. If TrackGameVersion IsNot Nothing Then Arguments.Add("mr_game_version", TrackGameVersion)
  381. If TrackLoader.Flags.Any Then Arguments.Add("mr_loader", TrackLoader.Flags.First.ToString.Lower)
  382. Dim Argument = Arguments.Select(Function(a) $"{a.Key}={a.Value}").Join("&")
  383. For Each Url In Urls
  384. If Not Url.ContainsIgnoreCase("cdn.modrinth.com/") Then Yield Url
  385. Yield $"{Url}{If(Url.Contains("?"), "&", "?")}{Argument}"
  386. Next
  387. End Function
  388. Public Enum DownloadReason
  389. Standalone
  390. Dependency
  391. ModPack
  392. Update
  393. End Enum
  394. ''' <summary>
  395. ''' 重新整理 CurseForge 的下载地址。
  396. ''' </summary>
  397. Public Shared Function ParseCurseForgeDownloadUrls(Url As String) As List(Of String)
  398. Return {
  399. Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
  400. Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
  401. Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
  402. Url.Replace("://media.", "://edge."),
  403. Url
  404. }.Distinct.ToList
  405. End Function
  406. Public Overrides Function ToString() As String
  407. Return $"{Id}: {FileName}"
  408. End Function
  409. End Class