Update the algorithm for 2^51 ranges AV/BV conversion.

This commit is contained in:
A.C.Sukazyo Eyre 2024-02-14 20:06:32 +08:00
parent 9814b3ccab
commit 35c9eeb9a4
Signed by: Eyre_S
GPG Key ID: C17CE40291207874
7 changed files with 117 additions and 58 deletions

2
.gitignore vendored
View File

@ -9,7 +9,9 @@
/build/ /build/
/bin/ /bin/
/out/ /out/
target/
.metals/ .metals/
.bsp/
.bloop/ .bloop/
.project .project
lcoal.properties lcoal.properties

View File

@ -5,7 +5,7 @@ MORNY_ARCHIVE_NAME = morny-coeur
MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono
MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
VERSION = 1.3.1 VERSION = 1.3.2
USE_DELTA = false USE_DELTA = false
VERSION_DELTA = VERSION_DELTA =

View File

@ -7,7 +7,6 @@ import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode}
import scala.language.postfixOps import scala.language.postfixOps
import scala.util.matching.Regex
class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { 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_AV = "[morny/share/bili/av]"
private val ID_PREFIX_BILI_BV = "[morny/share/bili/bv]" private val ID_PREFIX_BILI_BV = "[morny/share/bili/bv]"
private val LINK_PREFIX = "https://bilibili.com/video/" 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 = "<a href='%s'>%s</a>" private val SHARE_FORMAT_HTML = "<a href='%s'>%s</a>"
override def query (event: Update): List[InlineQueryUnit[_]] | Null = { override def query (event: Update): List[InlineQueryUnit[_]] | Null = {

View File

@ -13,9 +13,9 @@ object BilibiliForms {
case class BiliVideoId (av: Long, bv: String, part: Int|Null = null) 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_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. /** 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) => 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 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 val part_part = if (_url_param == null) null else
REGEX_BILI_V_PART_IN_URL_PARAM.findFirstMatchIn(_url_param) match REGEX_BILI_V_PART_IN_URL_PARAM.findFirstMatchIn(_url_param) match

View File

@ -2,8 +2,6 @@ package cc.sukazyo.cono.morny.util
import cc.sukazyo.cono.morny.util.UseMath.** import cc.sukazyo.cono.morny.util.UseMath.**
import scala.collection.mutable
/** Utils about $Bilibili /** Utils about $Bilibili
* *
* contains utils: * contains utils:
@ -15,29 +13,35 @@ import scala.collection.mutable
* *
* @define AvBvFormat * @define AvBvFormat
* === About AV/BV id format === * === 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 * the AV id is a int64 number, the max value is 2<sup>51</sup>, and should not be smaller that `1`; the BV
* in programming. * 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 * e.g. while the link ''`https://www.bilibili.com/video/BV17x411w7KC/`'' shows
* the same with ''`https://www.bilibili.com/video/av170001/`'', the AV id * the same with ''`https://www.bilibili.com/video/av170001/`'', the AV id
* is __`170001`__, the BV id is __`BV17x411w7KC`__. * is __`170001`__, the BV id is __`BV17x411w7KC`__.
* *
* @define AvBvSeeAlso [[https://www.zhihu.com/question/381784377/answer/1099438784 mcfx的回复: 如何看待 2020 3 23 日哔哩哔哩将稿件的av 变更为BV ]] * These algorithms accept and return a AV id as a [[Long]] number, and BV id as a 10 digits base58 [[String]]
* @todo Maybe make a class `AV`/`BV` and implement the parse in the class * 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 { object BiliTool {
private val V_CONV_XOR = 177451812L private val V_CONV_XOR: Long = 23442827791579L
private val V_CONV_ADD = 8728348608L
private val X_AV_MAX = Math.pow(2, 30).toLong private val X_AV_MAX = 1L << 51
private val X_AV_ALT = Int.MaxValue.toLong + 1 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: Map[Int, Char] = BB58_TABLE_REV.map((k,v) => (v, k))
private val BB58_TABLE_SIZE: Long = BB58_TABLE.size private val BB58_BASE: Long = BB58_TABLE.size
private val BV_TEMPLATE = "1 4 1 7 " private val BV_TEMPLATE: String = "1---------"
private val BV_TEMPLATE_FILTER = Array(9, 8, 1, 6, 2, 4) private val BV_TEMPLATE_FILTER: Array[Int] = Array(9, 8, 1, 6, 2, 4, 3, 5, 7)
/** Error of illegal BV id. /** Error of illegal BV id.
* *
@ -45,7 +49,7 @@ object BiliTool {
* @param bv the source illegal BV id. * @param bv the source illegal BV id.
* @param reason why it is illegal. * @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`") { 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. /** 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) = def this (bv: String, c: Char, location: Int) =
this(bv, s"char `$c` is not in base58 char table (in position $location)") 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 /** Convert an AV video id format to BV video id format for $Bilibili
* *
* $AvBvFormat * $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 * @see $AvBvSeeAlso
* *
* @param bv a BV id, which should be exactly 10 digits and all chars should be * @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]]). * a legal base58 char (which means can be found in [[BB58_TABLE]]) and
* otherwise, an [[IllegalFormatException]] will be thrown. * 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 * @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 * @throws IllegalBVFormatException when the input __bv__ is not a legal 10 digits base58
* formatted BV id. * formatted BV id.
*/ */
@throws[IllegalFormatException] @throws[IllegalBVFormatException]
def toAv (bv: String): Long = { def toAv (bv: String): Long = {
var av = 0L if (bv.length != 10) throw IllegalBVFormatException(bv, bv.length)
if (bv.length != 10) throw IllegalFormatException(bv, bv.length) if (bv(0) != '1') throw IllegalBVFormatException(bv)
for (i <- BV_TEMPLATE_FILTER.indices) { val _bv = bv.toCharArray
val _get = BV_TEMPLATE_FILTER(i) val av =
val tableToken = BB58_TABLE_REV get bv(_get) ( for (i <- BV_TEMPLATE_FILTER.indices) yield {
if tableToken isEmpty then throw IllegalFormatException(bv, bv(_get), _get) val _get = BV_TEMPLATE_FILTER(i)
av = av + (tableToken.get.toLong * (BB58_TABLE_SIZE**i).toLong) val tableToken = BB58_TABLE_REV get _bv(_get)
} if tableToken isEmpty then throw IllegalBVFormatException(bv, _bv(_get), _get)
av = (av - V_CONV_ADD) ^ V_CONV_XOR tableToken.get * (BB58_BASE**i)
if (av < 0) } ).sum
av+ X_AV_ALT (av & X_AV_MASK) ^ V_CONV_XOR
else av
} }
/** Convert an AV video format to a BV video format for $Bilibili. /** Convert an AV video format to a BV video format for $Bilibili.
* *
* this method '''available while the __av-id < 2^27^__''', while it theoretically * $AvBvFormat
* available when the av-id < 2^30^. *
* @see $AvBvSeeAlso
* *
* @param av an AV id. * @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 = { def toBv (av: Long): String = {
val __av =if (av > X_AV_MAX) av - X_AV_ALT else av if (av > X_AV_MAX) throw IllegalAVFormatException.thusTooLarge(av)
val _av = (__av^V_CONV_XOR)+V_CONV_ADD if (av < 1) throw IllegalAVFormatException.thusTooSmall(av)
var _av = (X_AV_MAX | av) ^ V_CONV_XOR
val bv = Array(BV_TEMPLATE:_*) val bv = Array(BV_TEMPLATE:_*)
for (i <- BV_TEMPLATE_FILTER.indices) { 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 String copyValueOf bv
} }

View File

@ -15,11 +15,12 @@ object UseMath {
@targetName("pow") @targetName("pow")
def ** (other: Int): Double = Math.pow(self, other) def ** (other: Int): Double = Math.pow(self, other)
} }
extension (self: Long) { extension (self: Long) {
@targetName("pow") @targetName("pow")
def ** (other: Long): Long = def ** (other: Long): Long =
Math.pow(self, other).toLong var x = 1L
for (_ <- 0L until other) x *= self
x
} }
extension (base: Int) { extension (base: Int) {

View File

@ -17,6 +17,7 @@ class BiliToolTest extends MornyTests with TableDrivenPropertyChecks {
("1DB421k7zX", 1350018000L), ("1DB421k7zX", 1350018000L),
("19m411D7wx", 1900737470L), ("19m411D7wx", 1900737470L),
("1LQ4y1A7im", 709042411L), ("1LQ4y1A7im", 709042411L),
("1L9Uoa9EUx", 111298867365120L),
) )
forAll (examples) { (bv, av) => s"while using av$av/BV$bv :" - { 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 :" - { "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( val examples = Table(
"bv", "bv",
"12345", "12345",
@ -39,7 +40,7 @@ class BiliToolTest extends MornyTests with TableDrivenPropertyChecks {
) )
forAll(examples) { bv => forAll(examples) { bv =>
s"length ${bv.length} should throws IllegalFormatException" in: 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"), ("1mK4O1C7Bl", "l"),
("1--4O1C7Bl", "[symbols]") ("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) => forAll(examples) { (bv, with_sp) =>
s"'$with_sp' should throws IllegalFormatException" in: s"BV id with '$with_sp' should throws IllegalBVFormatException" in:
an [IllegalFormatException] should be thrownBy toAv(bv) 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 { "av/bv converting should be reversible" in {
for (_ <- 1 to 20) { 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} import cc.sukazyo.cono.morny.util.BiliTool.{toAv, toBv}
val my_bv = toBv(rand_av) val my_bv = toBv(rand_av)
toAv(my_bv) shouldEqual rand_av toAv(my_bv) shouldEqual rand_av