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