From 35c9eeb9a40640e5f2b90f7696574fe35b06e606 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Wed, 14 Feb 2024 20:06:32 +0800 Subject: [PATCH] Update the algorithm for 2^51 ranges AV/BV conversion. --- .gitignore | 2 + gradle.properties | 2 +- .../morny/bot/query/ShareToolBilibili.scala | 2 - .../cono/morny/extra/BilibiliForms.scala | 6 +- .../cc/sukazyo/cono/morny/util/BiliTool.scala | 116 +++++++++++------- .../cc/sukazyo/cono/morny/util/UseMath.scala | 5 +- .../cono/morny/test/utils/BiliToolTest.scala | 42 ++++++- 7 files changed, 117 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index f02e4e8..350ff6f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,9 @@ /build/ /bin/ /out/ +target/ .metals/ +.bsp/ .bloop/ .project lcoal.properties diff --git a/gradle.properties b/gradle.properties index 6c986a3..863c986 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s -VERSION = 1.3.1 +VERSION = 1.3.2 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index f7b6872..e2a6bed 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -7,7 +7,6 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} import scala.language.postfixOps -import scala.util.matching.Regex class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { @@ -16,7 +15,6 @@ class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { private val ID_PREFIX_BILI_AV = "[morny/share/bili/av]" private val ID_PREFIX_BILI_BV = "[morny/share/bili/bv]" private val LINK_PREFIX = "https://bilibili.com/video/" - private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(\\?(?:p=(\\d+))?.*)?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"r private val SHARE_FORMAT_HTML = "%s" override def query (event: Update): List[InlineQueryUnit[_]] | Null = { diff --git a/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala b/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala index 3dc55f3..a44246a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala @@ -13,9 +13,9 @@ object BilibiliForms { case class BiliVideoId (av: Long, bv: String, part: Int|Null = null) - private val REGEX_BILI_ID = "^((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"r + private val REGEX_BILI_ID = "^((?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))$"r private val REGEX_BILI_V_PART_IN_URL_PARAM = "(?:&|^)p=(\\d+)"r - private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:(?:www\\.)?bilibili\\.com(?:/s)?/video/|b23\\.tv/)((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(?:\\?((?:p=(\\d+))?.*))?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$" r + private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:(?:www\\.)?bilibili\\.com(?:/s)?/video/|b23\\.tv/)((?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))/?(?:\\?((?:p=(\\d+))?.*))?|(?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))$" r /** parse a Bilibili video link to a [[BiliVideoId]] format Bilibili Video Id. * @@ -34,7 +34,7 @@ object BilibiliForms { case REGEX_BILI_VIDEO(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) => val av = select(_url_av, _raw_av) - val bv = select(_url_bv, _raw_bv) + val bv = "1" + select(_url_bv, _raw_bv) val part_part = if (_url_param == null) null else REGEX_BILI_V_PART_IN_URL_PARAM.findFirstMatchIn(_url_param) match diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala b/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala index 5097d16..d0d05c4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/BiliTool.scala @@ -2,8 +2,6 @@ package cc.sukazyo.cono.morny.util import cc.sukazyo.cono.morny.util.UseMath.** -import scala.collection.mutable - /** Utils about $Bilibili * * contains utils: @@ -15,29 +13,35 @@ import scala.collection.mutable * * @define AvBvFormat * === About AV/BV id format === - * the AV id is a number; the BV id is a special 10 digits base58 number, it shows as String - * in programming. + * the AV id is a int64 number, the max value is 251, and should not be smaller that `1`; the BV + * id is a special 10 digits base58 number with the first character is constant `1`, it shows as String in + * programming. * * e.g. while the link ''`https://www.bilibili.com/video/BV17x411w7KC/`'' shows * the same with ''`https://www.bilibili.com/video/av170001/`'', the AV id * is __`170001`__, the BV id is __`BV17x411w7KC`__. * - * @define AvBvSeeAlso [[https://www.zhihu.com/question/381784377/answer/1099438784 mcfx的回复: 如何看待 2020 年 3 月 23 日哔哩哔哩将稿件的「av 号」变更为「BV 号」?]] - * @todo Maybe make a class `AV`/`BV` and implement the parse in the class + * These algorithms accept and return a AV id as a [[Long]] number, and BV id as a 10 digits base58 [[String]] + * without the `BV` prefix. For example, the `BV17x411w7KC` will be `"17x411w7KC"` [[String]] in this format, and + * the `av170001` should be `170001L` [[Long]] val. + * + * @define AvBvSeeAlso + * [bvid说明 - 哔哩哔哩-API收集整理](https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/bvid_desc.html) + * [旧版本:mcfx 的回答...](https://www.zhihu.com/question/381784377/answer/1099438784) + * */ object BiliTool { - private val V_CONV_XOR = 177451812L - private val V_CONV_ADD = 8728348608L + private val V_CONV_XOR: Long = 23442827791579L - private val X_AV_MAX = Math.pow(2, 30).toLong - private val X_AV_ALT = Int.MaxValue.toLong + 1 + private val X_AV_MAX = 1L << 51 + private val X_AV_MASK: Long = X_AV_MAX - 1 - private val BB58_TABLE_REV: Map[Char, Int] = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF".toCharArray.zipWithIndex.toMap + private val BB58_TABLE_REV: Map[Char, Int] = "FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf".toCharArray.zipWithIndex.toMap private val BB58_TABLE: Map[Int, Char] = BB58_TABLE_REV.map((k,v) => (v, k)) - private val BB58_TABLE_SIZE: Long = BB58_TABLE.size - private val BV_TEMPLATE = "1 4 1 7 " - private val BV_TEMPLATE_FILTER = Array(9, 8, 1, 6, 2, 4) + private val BB58_BASE: Long = BB58_TABLE.size + private val BV_TEMPLATE: String = "1---------" + private val BV_TEMPLATE_FILTER: Array[Int] = Array(9, 8, 1, 6, 2, 4, 3, 5, 7) /** Error of illegal BV id. * @@ -45,7 +49,7 @@ object BiliTool { * @param bv the source illegal BV id. * @param reason why it is illegal. */ - class IllegalFormatException private (bv: String, reason: String) + class IllegalBVFormatException private (bv: String, reason: String) extends RuntimeException (s"`$bv is not a valid 10 digits base58 BV id: $reason`") { /** Error of illegal BV id, where the reason is the BV id is not 10 digits. @@ -64,57 +68,81 @@ object BiliTool { */ def this (bv: String, c: Char, location: Int) = this(bv, s"char `$c` is not in base58 char table (in position $location)") + + /** Error of illegal BV id, where the reason is the BV id is not started with `1`. + * + * @param bv the source of illegal BV id. + */ + def this (bv: String) = + this(bv, s"given BV id $bv is not started with 1 which is required in current version.") + } + + /** Error of illegal AV id. + * + * @constructor Build a error with illegal AV details. + * @param av the source illegal AV id. + * @param reason why it is illegal. + */ + class IllegalAVFormatException private (av: Long, reason: String) + extends RuntimeException(s"`$av is not a valid AV id: $reason`") + object IllegalAVFormatException: + /** Error of illegal AV id, where the reason is the AV id is too large. */ + def thusTooLarge (av: Long) = + new IllegalAVFormatException(av, s"Given AV id $av is too large, should not be larger than 2^51($X_AV_MAX)") + /** Error of illegal AV id, where the reason is the AV id is too small. */ + def thusTooSmall (av: Long) = + new IllegalAVFormatException(av, s"Given AV id $av is too small, should not be smaller than 1") + /** Convert an AV video id format to BV video id format for $Bilibili * * $AvBvFormat * - * this method '''available while the __av-id < 2^27^__''', while it theoretically - * available when the av-id < 2^30^. Meanwhile some digits of the BV id is a fixed - * value (like the [[BV_TEMPLATE]] shows) -- input __bv__ can do not follow the format, - * but it will almost certainly gives a wrong AV id (because the fixed number is not - * processed at all!) - * * @see $AvBvSeeAlso * * @param bv a BV id, which should be exactly 10 digits and all chars should be - * a legal base58 char (which means can be found in [[BB58_TABLE]]). - * otherwise, an [[IllegalFormatException]] will be thrown. + * a legal base58 char (which means can be found in [[BB58_TABLE]]) and + * the first character should must be 1. BV id in this format does NOT + * contains `BV` prefix. Otherwise, an [[IllegalBVFormatException]] + * will be thrown. * @return an AV id which will shows the save video of input __bv__ in $Bilibili - * @throws IllegalFormatException when the input __bv__ is not a legal 10 digits base58 - * formatted BV id. + * @throws IllegalBVFormatException when the input __bv__ is not a legal 10 digits base58 + * formatted BV id. */ - @throws[IllegalFormatException] + @throws[IllegalBVFormatException] def toAv (bv: String): Long = { - var av = 0L - if (bv.length != 10) throw IllegalFormatException(bv, bv.length) - for (i <- BV_TEMPLATE_FILTER.indices) { - val _get = BV_TEMPLATE_FILTER(i) - val tableToken = BB58_TABLE_REV get bv(_get) - if tableToken isEmpty then throw IllegalFormatException(bv, bv(_get), _get) - av = av + (tableToken.get.toLong * (BB58_TABLE_SIZE**i).toLong) - } - av = (av - V_CONV_ADD) ^ V_CONV_XOR - if (av < 0) - av+ X_AV_ALT - else av + if (bv.length != 10) throw IllegalBVFormatException(bv, bv.length) + if (bv(0) != '1') throw IllegalBVFormatException(bv) + val _bv = bv.toCharArray + val av = + ( for (i <- BV_TEMPLATE_FILTER.indices) yield { + val _get = BV_TEMPLATE_FILTER(i) + val tableToken = BB58_TABLE_REV get _bv(_get) + if tableToken isEmpty then throw IllegalBVFormatException(bv, _bv(_get), _get) + tableToken.get * (BB58_BASE**i) + } ).sum + (av & X_AV_MASK) ^ V_CONV_XOR } /** Convert an AV video format to a BV video format for $Bilibili. * - * this method '''available while the __av-id < 2^27^__''', while it theoretically - * available when the av-id < 2^30^. + * $AvBvFormat + * + * @see $AvBvSeeAlso * * @param av an AV id. - * @return a BV id which will shows the save video of input __av__ in $Bilibili + * @return a BV id which will shows the save video of input __av__ in $Bilibili. A 10 digits + * base58 formatted BV id, does NOT contains `BV` prefix. */ def toBv (av: Long): String = { - val __av =if (av > X_AV_MAX) av - X_AV_ALT else av - val _av = (__av^V_CONV_XOR)+V_CONV_ADD + if (av > X_AV_MAX) throw IllegalAVFormatException.thusTooLarge(av) + if (av < 1) throw IllegalAVFormatException.thusTooSmall(av) + var _av = (X_AV_MAX | av) ^ V_CONV_XOR val bv = Array(BV_TEMPLATE:_*) for (i <- BV_TEMPLATE_FILTER.indices) { - bv(BV_TEMPLATE_FILTER(i)) = BB58_TABLE( (_av/(BB58_TABLE_SIZE**i) % BB58_TABLE_SIZE) toInt ) + bv(BV_TEMPLATE_FILTER(i)) = BB58_TABLE((_av % BB58_BASE).toInt) + _av /= BB58_BASE } String copyValueOf bv } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala index 30c7879..b5d15f4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseMath.scala @@ -15,11 +15,12 @@ object UseMath { @targetName("pow") def ** (other: Int): Double = Math.pow(self, other) } - extension (self: Long) { @targetName("pow") def ** (other: Long): Long = - Math.pow(self, other).toLong + var x = 1L + for (_ <- 0L until other) x *= self + x } extension (base: Int) { diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala index c8e2de2..db8f4df 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/utils/BiliToolTest.scala @@ -17,6 +17,7 @@ class BiliToolTest extends MornyTests with TableDrivenPropertyChecks { ("1DB421k7zX", 1350018000L), ("19m411D7wx", 1900737470L), ("1LQ4y1A7im", 709042411L), + ("1L9Uoa9EUx", 111298867365120L), ) forAll (examples) { (bv, av) => s"while using av$av/BV$bv :" - { @@ -26,7 +27,7 @@ class BiliToolTest extends MornyTests with TableDrivenPropertyChecks { }} "BV with unsupported length :" - { - import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException} + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalBVFormatException} val examples = Table( "bv", "12345", @@ -39,7 +40,7 @@ class BiliToolTest extends MornyTests with TableDrivenPropertyChecks { ) forAll(examples) { bv => s"length ${bv.length} should throws IllegalFormatException" in: - an [IllegalFormatException] should be thrownBy toAv(bv) + an [IllegalBVFormatException] should be thrownBy toAv(bv) } } @@ -52,16 +53,45 @@ class BiliToolTest extends MornyTests with TableDrivenPropertyChecks { ("1mK4O1C7Bl", "l"), ("1--4O1C7Bl", "[symbols]") ) - import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalFormatException} + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalBVFormatException} forAll(examples) { (bv, with_sp) => - s"'$with_sp' should throws IllegalFormatException" in: - an [IllegalFormatException] should be thrownBy toAv(bv) + s"BV id with '$with_sp' should throws IllegalBVFormatException" in: + an [IllegalBVFormatException] should be thrownBy toAv(bv) } } + "BV id must started with `1` in current version" in { + import cc.sukazyo.cono.morny.util.BiliTool.{toAv, IllegalBVFormatException} + an [IllegalBVFormatException] should be thrownBy toAv("2mK4y1C7Bz") + } + + "AV id must not smaller that `1`" in { + import cc.sukazyo.cono.morny.util.BiliTool.{toBv, IllegalAVFormatException} + an [IllegalAVFormatException] should be thrownBy toBv(0) + an [IllegalAVFormatException] should be thrownBy toBv(-826624291) + an [IllegalAVFormatException] should be thrownBy toBv(-296798903L) + } + + s"AV id must not bigger that 2^51 (or ${1L << 51})" in { + import cc.sukazyo.cono.morny.util.BiliTool.{toBv, IllegalAVFormatException} + an [IllegalAVFormatException] should (not be thrownBy( toBv(1L << 51) )) + an [IllegalAVFormatException] should be thrownBy toBv((1L << 51) + 1) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 52) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 53) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 54) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 55) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 56) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 57) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 58) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 59) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 60) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 61) + an [IllegalAVFormatException] should be thrownBy toBv(1L << 62) + } + "av/bv converting should be reversible" in { for (_ <- 1 to 20) { - val rand_av = Random.between(0, 999999999L) + val rand_av = Random.between(1, (1L<<51)+1) import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv} val my_bv = toBv(rand_av) toAv(my_bv) shouldEqual rand_av