# qn_read_app
**Repository Path**: qbrid/qn_read_app
## Basic Information
- **Project Name**: qn_read_app
- **Description**: 青鸟站点开发套件
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-07-06
- **Last Updated**: 2023-09-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 青鸟站点规则
>以下为青鸟站点的解析规则
>方便大家自行添加自己想适配的站点
>本手册分为 [前言]、[环境准备]、[站点规则]、[使用案例] 四个章节
## 前言
### 1、兼容阅读源了吗?
答案是并没有。
主要是下面几个原因
+ 阅读的源规则其实比较混乱,略显复杂。
+ 青鸟是致力于全平台(iOS、Android、MacOS、Windows、Linux、Web)的阅读工具。
阅读中部分高级源使用了Java平台本身的API,在其它平台无法兼容。
+ jsoup的语法是真的看不出逻辑...
### 2、会一直维护吗?
会一直维护,类似站点规则、站点编辑APP、站点源汇总,目前都已开源。
## 环境准备
推荐使用 Flutter(3.X)版本 + VSCode编辑器 + 青鸟站点编辑工具(发布必备)。
其中 Flutter(3.X) 及 VSCode编辑器 非必须。如使用VSCode,VSCode需安装Flutter插件。
Flutter下载地址:https://flutter.cn/docs/get-started/install
VSCode下载地址:https://code.visualstudio.com/Download
青鸟站点编辑工具下载地址:MacOS端(http://img.novel.onedayapp.cn/packages/qn_source_app.app.zip) Windows端(http://img.novel.onedayapp.cn/packages/qn_source_app.windows.zip)
### 站点规则
______
青鸟站点规则分为两类
+ 文本站点规则
使用类似阅读的源规则进行声明的JSON文本规则,适用于简单站点
+ 动态Dart站点规则
通过实现规则阅读接口,使用Dart语言进行编程的高级动态规则
## 文本站点规则
文本站点规则,格式为Json,范例如下:
```json
{
"siteId": "站点ID,建议使用http地址",
"info": {
"type": "站点类型: book 书籍; listenBook 听书; comic 漫画",
"showName": "站点显示的名称",
"group": "站点分组",
"desc": "站点简介",
"comicShowType": "漫画目录显示类型,仅站点类型为漫画生效,只为 small 小; middle 中等; big 大。不传值默认为middle",
"updateTime": "更新时间戳,",
"versionNumb": 0,
"supportSearch": true,
"supportExplore": true,
"vpnWebsite": "声明此站点需要VPN。如不需要,置空,如需要,填入需VPN转接的网址"
},
"exploreUrl": "发现页URL,格式为 title::url\ntitle::url,多个Url以\n分割",
"searchUrl": "搜索页URL,书名搜素"
"ruleExplore": {
"bookList": "获取发现列表",
"bookName": "获取 书名",
"author": "获取 作者名称",
"cover": "获取 封面图",
"desc": "获取 简介",
"tags": "获取标签,格式为 List,多个标签使用\n分割",
"newestChapter": "获取 最新章节",
"wordCount": "获取字数,格式为 数字",
"star": "获取评分,格式为 double,大小位于 0.0 ~10.0,默认为8.5",
"detailUrl": "获取 详情页URL",
"tocUrl": "获取 目录页URL"
},
"ruleSearch": {
"bookList": "获取搜索列表",
"bookName": "获取 书名",
"author": "获取 作者名称",
"cover": "获取 封面图",
"desc": "获取 简介",
"tags": "获取标签,格式为 List,多个标签使用\n分割",
"newestChapter": "获取 最新章节",
"wordCount": "获取字数,格式为 数字",
"star": "获取评分,格式为 double,大小位于 0.0 ~10.0,默认为8.5",
"detailUrl": "获取 详情页URL",
"tocUrl": "获取 目录页URL"
},
"ruleDetail": {
"bookName": "获取 书名",
"author": "获取 作者名称",
"cover": "获取 封面图",
"desc": "获取 简介",
"tags": "获取标签,格式为 List,多个标签使用\n分割",
"newestChapter": "获取 最新章节",
"wordCount": "获取字数,格式为 数字",
"star": "获取评分,格式为 double,大小位于 0.0 ~10.0,默认为8.5",
"detailUrl": "获取 详情页URL",
"tocUrl": "获取 目录页URL"
},
"ruleToc": {
"tocList": "获取 目录列表",
"nextTocUrl": "获取 下一页目录,如存在多个值,使用\n分割",
"title": "获取 章节标题",
"url": "获取 章节URL",
"needVip": "获取 是否需要VIP",
"wordCount": "获取 章节字数,格式为数字,默认为0"
},
"ruleContent": {
"content": "获取章节内容,如获取的是漫画,多个URL之间使用\n分割",
"nextUrls": "获取 下一页内容,如存在多个值,使用\n分割",
"nextJoin": "仅 获取书籍时且存在下一页内容时生效"
},
"verify": {
"searchKey": "验证搜索的书名,确保 搜索时搜到的第一个item的书名与searchKey相同,否则验证失败",
"detailPrefix": "发现页的第一个Item的detailUrl 及 搜索结果的第一个item的detailUrl 必须以 detailPrefix为前缀,否则验证失败"
}
}
```
_____
`siteId`值为站点ID,一般为站点的网址URL
_____
`info`内包含站点规则的基本信息,格式如下
```json
{
"type": "站点类型: book 书籍; listenBook 听书; comic 漫画",
"showName": "站点显示的名称",
"group": "站点分组",
"desc": "站点简介",
"comicShowType": "漫画目录显示类型,仅站点类型为漫画生效,只为 small 小; middle 中等; big 大。不传值默认为middle",
"updateTime": "更新时间戳,",
"versionNumb": 0,
"supportSearch": true,
"supportExplore": true,
"vpnWebsite": "声明此站点需要VPN。如不需要,置空,如需要,填入需VPN转接的网址"
}
```
_____
`exploreUrl`值为发现页URL
`searchUrl`值为搜索页URL,此为书名搜索
_____
`ruleExplore`描述了发现页列表的取值规则,格式如下:
```json
{
"bookList": "获取发现列表",
"bookName": "获取 书名",
"author": "获取 作者名称",
"cover": "获取 封面图",
"desc": "获取 简介",
"tags": "获取标签,格式为 List,多个标签使用\n分割",
"newestChapter": "获取 最新章节",
"wordCount": "获取字数,格式为 数字",
"star": "获取评分,格式为 double,大小位于 0.0 ~10.0,默认为8.5",
"detailUrl": "获取 详情页URL",
"tocUrl": "获取 目录页URL"
}
```
______
`ruleSearch`描述了搜索页列表的取值规则,格式如下:
```json
{
"bookList": "获取发现列表",
"bookName": "获取 书名",
"author": "获取 作者名称",
"cover": "获取 封面图",
"desc": "获取 简介",
"tags": "获取标签,格式为 List,多个标签使用\n分割",
"newestChapter": "获取 最新章节",
"wordCount": "获取字数,格式为 数字",
"star": "获取评分,格式为 double,大小位于 0.0 ~10.0,默认为8.5",
"detailUrl": "获取 详情页URL",
"tocUrl": "获取 目录页URL"
}
```
_____
`ruleDetail`描述了详情页的取值规则,格式如下:
```json
{
"bookName": "获取 书名",
"author": "获取 作者名称",
"cover": "获取 封面图",
"desc": "获取 简介",
"tags": "获取标签,格式为 List,多个标签使用\n分割",
"newestChapter": "获取 最新章节",
"wordCount": "获取字数,格式为 数字",
"star": "获取评分,格式为 double,大小位于 0.0 ~10.0,默认为8.5",
"detailUrl": "获取 详情页URL",
"tocUrl": "获取 目录页URL"
}
```
_______
### 注意,`ruleExplore`与`ruleSearch`的取值字段完全一样,`ruleDetail`相较于前者,少了一个`bookList`字段。
______
`ruleToc`描述了目录列表的取值规则,格式如下:
```json
{
"tocList": "获取 目录列表",
"nextTocUrl": "获取 下一页目录,如存在多个值,使用\n分割",
"title": "获取 章节标题",
"url": "获取 章节URL",
"needVip": "获取 是否需要VIP",
"wordCount": "获取 章节字数,格式为数字,默认为0"
}
```
_______
`ruleContent`描述了内容页的取值规则,格式如下:
```json
{
"content": "获取章节内容,如获取的是漫画,多个URL之间使用\n分割",
"nextUrls": "获取 下一页内容,如存在多个值,使用\n分割",
"nextJoin": "仅 获取书籍时且存在下一页内容时生效"
}
```
________
`verify`描述了整体站点的验证规则,格式如下:
```json
{
"searchKey": "验证搜索的书名,确保 搜索时搜到的第一个item的书名与searchKey相同,否则验证失败",
"detailPrefix": "发现页的第一个Item的detailUrl 及 搜索结果的第一个item的detailUrl 必须以 detailPrefix为前缀,否则验证失败"
}
```
________
### 字段规则
文本规则中可划分为 4 类
+ 网络调用
+ 字段取值
#### **网络调用**
整体规则中, `exploreUrl`、`searchUrl`、`detailUrl`、`tocUrl`以及目录章节中的`ruleToc->url`会在特定时机进行网络调用。
网络调用的格式为:
```
http://www.baidu.com,{"method":"get","requestData":"xx=1&&yy=2" "headers":{"header1":"headerValue1","header2":"headerValue2"},"webview":"0","responseCharset":"gbk"}
```
其中,`http://www.baidu.com`为url地址,`,`为分割符,后面接入的参数为Json字符串,描述了 url的调用规则。
参数详解:
* `method`描述了http调用方法,可取值为 `get`、`post`、`put`、`delete`、`options`,默认为`get`,为默认值时,可省略。
* `requestData`描述了http请求体,只在调用方法为 `post`、`delete`、`put`时生效,默认为空,可省略。
* `headers`描述了http请求头,默认为空,可省略。
* `webview`描述了是否启用web容器进行网络请求,可取值为 `1`或者`0`,默认为`0`,可省略。
一般无需设置,当调用的网页需要让网页自身调用自身的JS逻辑时,可设置为`1`
* `responseCharset`描述了请求体的解码规则,可选值为`utf8`或`gbk`,默认为`utf8`,可省略。
注意,`gbk`兼容`gb2312`,当文本编码格式为`gb2312`时,`responseCharset`设置为`gbk`即可解析。
因此 `https://www.baidu.com?xx=1`等价于`https://www.baidu.com?xx=1,{"method":"get","requestData":"" "headers":{},"webview":"0","responseCharset":"utf8"}`
#### **字段取值**
`{{xx}}`规则中使用`{{}}`进行取值操作。
取值操作支持 `XPath`、`JsonPath`、`App方法调用`、`书籍数据`、`章节数据`、`过程记录数据` 、`字符串取值`
+ `XPath`取值以`/`开头,如`{{/div[@class="test"]/p/text()}}`
+ `JsonPath`取值以`$`开头,如`$.id`
+ `App方法调用`取值以`QB.方法(参数1、参数2、参数3)`开头。
如 `{{QB.gbk($.key)}}`计算为 取得参数`key`,并对`key`进行`gbk`编码
+ `书籍数据`取值以`book.`开头,如`{{book.bookId}}`取值为`书籍`的`bookId`,`book`对象拥有如下属性
> + bookId 书籍ID
> + siteId 站点ID
> + siteVersion 站点版本
> + bookName 书名
> + detailUrl 详情页URL
> + tags 标签,数组,对个标签以`\n`分割
> + star 评分
> + wordCount 字数
> + newestChapter 最新章节
> + tocUrl 目录页URL
> + author 作者
> + cover 封面图
> + desc 简介
+ `章节数据`取值以`chapter.`开头,如`{{chapter.title}}`取值为`章节`的`title`,`chapter`对象拥有如下属性
> + title 标题
> + url 章节URL(也是内容页的URL)
> + needVip 是否需要VIP
> + wordCount 章节字数
**注意 `chapter`取值只在详情页可用。**
+ `过程记录数据`指取得计算过程中的记录值,关键字为 `preV`,例如 http://baidu.com,,,{{preV}} 计算值为 http://baidu.com 。
关于`,,,`的用法后续介绍。
________
+ `字符串取值`取值指,当取值中的字符,无法匹配上述规则时,则会被当成字符串处理。
如 `{{this is a data}}`计算值为`this is a data`。
> 为便于大家理解,以下为相关范例。
> 假定现在返回的字符串为 `{"id":"10086","pId":"10324","bookName":"圣墟","tag":"玄幻","tag1":"仙侠"}`
> + 如需取值 书名,可写为 `"{{$.id}}"`,可简写为 `"$.id"` ,计算值为 `"10086"`
> + 如需取标签,可写为 `"{{$.tag}},{{$.tag1}}"`,计算值为 `"玄幻,仙侠"`
**注意,可以对对个取值进行拼接**
> + 如需取详情页地址,可写为`"http://test.com/{{$.id}}/{{$.pId}}.html"`,计算值为`"http://test.com/10086/10324.html"`
#### 赋值及取值操作
赋值操作以`@put(key,value)`格式执行。如进行赋值后,后续可通过`{{G_key}}`进行取值操作。
以下为范例
> 假定现在返回的字符串为 `{"id":"10086","pId":"10324","bookName":"圣墟","tag":"玄幻","tag1":"仙侠"}`
> `@put(bookId,$.id),,,http://test.com/{{G_bookId}}`计算值为 http://test.com/10086 ,关于`,,,`分割后后续介绍
#### 正则替换操作
正则操作格式分为两种。
+ #正则表达式#替换的值# ---单次匹配
+ #正则表达式#替换的值## ---全部匹配
正则匹配的输入值为 `上一操作的过程值`,即`{{preV}}`
为便于大家理解,范例如下:
(单次匹配)表达式`"http://test.com/10086/10086.html,,,#10086#-#"`取值为 http://test.com/-/10086.html
(全部匹配)表达式`"http://test.com/10086/10086.html,,,#10086#-##"`取值为 http://test.com/-/-.html
以上述单次匹配表达式进行执行拆解。
`"http://test.com/10086/10086.html,,,#10086#-#"` 执行时被分拆为 `http://test.com/10086/10086.html` ,`,,,`,`#10086#-#`,其中 `http://test.com/10086/10086.html`的执行结果为 匹配为字符串http://test.com/10086/10086 , `,,,`为表达式分隔符,会将前一步骤的计算结果保存到`preV`变量中,然后执行第三部分正则替换`#10086#-#`
**正则表达式取值关键词**
`$0`只在正则表达式中生效。代表获取正则匹配的值。
为便于大家理解,以下为相关范例:
+ 范例一:返回值为`"这个是反馈的数据"`,表达式为`#.*#{{$0}}#`,取值为`这个是反馈的数据`
+ 范例二:返回值为`"这个是反馈的数据"`,表达式为`#这个是(.*)#{{$0}}`,取值为`这个是反馈的数据`
`$(index)`只在正则表达式中生效,类似`$1`、`$2`、`$3` ... ,代表取`第一个`、`第二个`、`第三个分组`、... 的数据。
以下为相关范例:
+ 范例一: 返回值为`"这个是反馈的数据"`,表达式为`#这个是反馈的(.*)#{{$1}}#`,取值为`数据`
+ 范例二: 返回值为`"这个是反馈的数据"`,表达式为`#这个是(.*)的(.*)#{{$0}}--{{$1}}--{{$2}}`,取值为`这个是反馈的数据--反馈--数据`。
_______________
#### 操作符详解
**列表取值**
列表取值不支持`{{}}`取值操作。
`ruleExplore -> bookList`、`ruleSearch -> bookList`及`ruleToc -> tocList`这三个字段为列表取值操作。目前只支持 `&&` 、`||`、`%%`操作。
+ `&&` 为取合集,格式为 `A&&B&&C`
+ `||` 为取或集合,格式为 `A||B`,计算时,当`A`存在值,且不为空时,直接返回结果,不进行`B`的计算,否则返回`B`的结果
+ `%%` 为轮询插入,格式为 `A%%B%%C`,计算是,组装列表,会先从 `A`取第一个元素,再取`B`的第一个元素,再取`C`的第一个元素。
为便于大家理解,范例如下:
假定当前返回值为
```json
{
"dataList_1":["AAA","BBB","CCC"],
"dataList_2":["111","222","333"],
"dataList_3":["aaa","bbb","ccc"],
}
```
基于上述反馈值,计算结果如下:
* `$.dataList_1&&$.dataList_2&&$.dataList_3`计算值为 `["AAA","BBB","CCC","111","222","333","aaa","bbb","ccc"]`
* `$.dataList_1||$.dataList_2`计算值为 `["AAA","BBB","CCC"]`
* `$.dataList_1%%$.dataList_2`计算值为 `["AAA","111","BBB","222","CCC","333"]`
**字段取值**
字段取值支持 `{{}}`、`&&`、`||`、`,,,`、`#正则#替换#`、`@put(key,value)`、`字符拼接`等操作规范。
`{{}}`取值在上述章节已有描述,在此不进行重复描述。
`字符拼接` 格式为 `"字符串{{key1}}字符串{{key2}}字符串"`,相关范例如下:`http://test.com/{{$.id}}.html`,在计算过程中,会计算为 `http://test.com/` + `{{$.id}}` + `.html`
`,,,` 为多计算分隔符,顶级运算符,最高优线级运算符。并声明`preV`为前次计算的结果。
**为便于大家理解,以下为相关范例。**
+ 范例一:`http://test.com,,,{{preV}}`,取值结果为 `http://test.com`,这里注意,如想取值`上一次操作的计算结果`,使用`{{preV}}`,不能使用`preV`,单纯的`preV`会被当做字符串解析。
以下为解析过程:`http://test.com,,,{{preV}}`被`,,,`分割为`http://test.com`与`{{preV}}`两部分,从左到右执行,`http://test.com`的计算结果为 `http://test.com`,因被`,,,`分割,将`http://test.com`的结果赋值到`preV`局域计算变量中,后续的`{{preV}}`则执行`preV变量`的取值操作。
+ 范例二:`http://test.com,,,abc`,取值结果为`abc`。
+ 范例三: `http://test.com,,,@put(url,preV)`,取值结果为 `http://test.com`,并且插入自定义字段`url`,值为`http://test.com`
+ 范例四: `http://test.com,,,@put(url,abc)`,取值结果为 `http://test.com`,并且插入自定义字段`url`,值为`abc`
+ **`@put`** 操作不会对计算结果产生影响,只做值传递。并且`@put(key,value)`中的value取值不需要带`{{}}`。
+ 范例五:`http://test.com/{{$.id}},,,@put(bId,$.id),,,#{{preV}}}#replace#` 取值为 `replace`,并且插入值`bId`为`$.id`
+ 范例六: 输入值为`{"id":"10086","pId":"1"}`,计算表达式为`http://test.com/{{$.id}}-new/{{$.pId}}-new.html,,,@put(bId,$.id),,,@put(pId,$.pId),,,#http://test.com/(.*)/(.*)\.html#{{$1}}&{{$2}}#`,计算值为 `10086-new&1-new`
`&&`为字段组合运算符,类比`+`,以下为相关实例:
* 范例一:返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id&&$.name`取值为`1元尊`,也可类比为 `{{$.id}}{{$.name}}`
* 范例二:返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id&&---&&$.name`取值为`1元尊`,也可类比为 `{{$.id}}---{{$.name}}`,以及`{{$.id}}{{---}}{{$.name}}`
* 范例三: 返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id&&$.name,,,{{preV}}`取值为`1元尊`。
**`,,,`** 的优先级高于 **`&&`**
`||`为字段组合运算符,类比`或运算`,以下为相关实例:
* 范例一:返回结果为`{"id":1,"name":"元尊"}`,表达式`$.id||$.name`取值为`1`
* 范例二:返回结果为`{"id":1,"name":"元尊"}`,表达式`http://test/{{$.id||$.name}}`此表达式无法计算,`{{}}`为取值操作,不支持`||`运算,正确写法应为`$.id||$.name,,,http://test/{{preV}}`或者`$.id||$.name,,,#.*#http://test/{{$0}}`
_____________
**特殊字段处理**
`发现页Url`-`exploreUrl`特殊规则
发现页在App操作时,会触发翻页,因此在取值时会注入一个Json字符串`{"page":"1"}`,因此`exploreUrl`需获取页码时,应写成 `http://explore.test/{{$.page}}`,页码默认为1。
`exploreUrl`支持App内置方法。
+ 比如,站点的页码`从0开始`,那么可使用表达式`http://explore.test/{{QB.plus(page,-1)}}`,`QB.plus(page,-1)`会执行 `page` `+` `(-1)`
+ 比如,站点的间隔为`页码的10倍`,那么可使用表达式`http://explore.test/{{QB.muti(page,10)}}`,`QB.muti(page,10)`会执行`page` `*` `10`
+ 比如,站点的间隔为`页码的10倍+12的偏移量`,那么可使用表达式`http://explore/test/{{QB.plus(QB.muti(page,10),12)}}`,`QB.plus(QB.muti(page,10),12)`会执行`(page * 10) + 12`
+ 比如,站点的第一页为`http://explore/test/`,第二页为`http://explore/test/2`,那么可使用表达式`http://explore/test/{{QB.emptyOr(page,1)}}`
`QB.emptyOr(A,B)`表达式的计算逻辑为:当`A == B`时,输出`空字符串`,否则输出`A`,等价于三目运算符 ` A == B ? "" : A`
`搜索页`-`searchUrl`特殊规则
App在进行搜索时,会将用户搜索的书名以Json形式`{"key":"元尊"}`的形式注入。因此`searchUrl`在获取书名进行搜索时,应写成:
+ 范例一: `http://example/search?bookName={{$.key}}`
+ 范例二: `http://example/search,{"method=post","requestData":"type=bookName&&searchKey={{$.key}}"}`
`内容页`-`content`特殊规则
因书源支持 `书籍(book)`、`听书(listenBook)`、`漫画(comic)`等多种类型。
+ 其中 如果`info -> type`取值为`书籍(book)`时,`content`返回`内容文本`即可。
+ 其中 如果`info -> type`取值为`听书(listenBook)`时,`content`返回`音频http地址`即可,格式如下:
> 1. http://music.test/test.mp3
> 2. http://music.test/test.mp3,{"headers":{"refer":"http://music.test"},"method":"get"}
+ 其中 如果`info -> type`取值为`漫画(comic)`时,`content`返回`漫画http地址数组`即可,数组以`\n`分割,格式如下:
> 1. `"http://comic.test/1.jpg\nhttp://comic.test/2.jpg\nhttp://comic.test/3.jpg"`
> 2. `http://comic.test/1.jpg,{"headers":{"refer":"http://comic.test"}}\nhttp://comic.test/2.jpg,{"headers":{"refer":"http://comic.test"}}\nhttp://comic.test/3.jpg,{"headers":{"refer":"http://comic.test"}}`
### 书源范例
+ 范例一:
```json
{
"siteId": "http://www.biqu5200.net",
"info": {
"type": "book",
"showName": "B5200",
"group": "青鸟",
"desc": "笔趣阁@5200",
"updateTime": "1234",
"versionNumb": 100,
"supportSearch": true,
"supportExplore": true
},
"exploreUrl": "玄幻小说::http://www.biqu5200.net/xuanhuanxiaoshuo/,{\"responseCharset\":\"gbk\"}\n修真小说::http://www.biqu5200.net/xiuzhenxiaoshuo/,{\"responseCharset\":\"gbk\"}\n都市小说::http://www.biqu5200.net/dushixiaoshuo/,{\"responseCharset\":\"gbk\"}\n穿越小说::http://www.biqu5200.net/chuanyuexiaoshuo/,{\"responseCharset\":\"gbk\"}",
"searchUrl":"http://www.biqu5200.net/modules/article/search.php?searchkey={{$.key}},{\"responseCharset\":\"gbk\"}",
"ruleExplore": {
"bookList": "//div[@class=\"l\"]/ul/li",
"bookName": "//span[@class=\"s2\"]/a/text()",
"author": "//span[@class=\"s5\"]/text()",
"newestChapter": "//span[@class=\"s3\"]/a/text()",
"detailUrl": "http://www.biqu5200.net{{//span[@class=\"s2\"]/a/@href}},{\"responseCharset\":\"gbk\"}",
"tocUrl": "http://www.biqu5200.net{{//span[@class=\"s2\"]/a/@href}},{\"responseCharset\":\"gbk\"}"
},
"ruleSearch": {
"bookList": "//table[@class=\"grid\"]/tbody/tr[position() > 1]",
"bookName": "/td[1]/a/text()",
"author": "/td[3]/text(),,,#作 者:##",
"newestChapter": "/td[2]/text()",
"wordCount": "/td[4]/text()",
"detailUrl": "http://www.biqu5200.net{{/td[1]/a/@href}},{\"responseCharset\":\"gbk\"}",
"tocUrl": "http://www.biqu5200.net{{/td[1]/a/@href}},{\"responseCharset\":\"gbk\"}"
},
"ruleDetail": {
"cover": "//*[@id=\"fmimg\"]/img/@src",
"desc": "//*[@id=\"intro\"]/p/text()",
"tags": "//*[@id=\"wrapper\"]/div[5]/div[1]/a[2]/text()"
},
"ruleToc": {
"tocList": "//*[@id=\"list\"]/dl/dd[position() > 9]",
"title": "/a/text()",
"url": "http://www.biqu5200.net{{/a/@href}},{\"responseCharset\":\"gbk\"}",
"needVip": "0"
},
"ruleContent": {
"content": "//*[@id=\"content\"]/p/text()"
},
"verify": {
"searchKey": "圣墟",
"detailPrefix": "http://www.biqu5200.net"
}
}
```
## 动态Dart站点规则
无法使用文本规则适配的站点可以通过动态Dart站点规则进行适配。
动态Dart站点规则,格式为Dart,需实现`BookSourceProvide`接口
`BookSourceProvide`设计如下:
```dart
abstract class BookSourceProvide {
BookSourceProvide();
/// 站点ID,必须,唯一
String siteId();
/// 当前站点版本
int currentSiteVersion();
/// 站点更新信息。
/// 格式为 版本号:更新信息
Map siteUpdateInfo();
/// 站点ID,必须,基础信息
QNBaseModel info();
/// 探索模型
/// 建议区分 男频 和 女频
QNExploreModel exploreModel();
/// 返回值为 0:不需要进行数据迁移,适用于 逻辑优化、去广告等
/// 返回值为 1:代表 书籍详情页地址发生变更,当用户 在书架打开书籍时,会 将旧书籍删除,使用新的规则 进行 搜索 -> 获取详情 -> 获取目录 -> 获取章节列表,重新生成完整的书籍数据。
/// 返回值为 2:代表 书籍的目录集合页地址发生变更,当用户 在书架打开书籍时,会通过 新的规则 ->获取目录 -> 获取章节列表,重新生成书籍数据。
/// 返回值为 3:代表 书籍的目录章节地址(正文地址)发生变更,当用户打开书籍时,会通过 新的规则 -> 获取章节列表,重新生成书籍数据。
int migrate(int oldVersion);
/// 根据书名搜索 书籍
/// 入参为 key
Future> queryExplore(String exploreUrl);
/// 根据书名搜索 书籍
/// 入参为 key
Future> searchBookName(String key);
/// 获取书籍详情
/// 入参为 bookId 及 detailUrl
Future queryBookDetail(QNBookModel model);
/// 获取书籍目录
/// 入参为 bookId 及 tocUrl
Future queryBookToc(QNBookModel model);
/// 获取书籍内容
/// /// 入参为 bookId 及 contentKey
Future queryBookContent(QNBookModel model, String contentKey);
/// 包含两个字段 searchKey 及 bookDetailPrefix
Map verifyBookConfig();
}
```
具体实现及调用逻辑可看源码
**范例如下**
```dart
import 'dart:async';
import 'dart:convert';
import 'package:qn_read_rule/book_provide/base.dart';
import 'package:qn_read_rule/book_provide/base_model.dart';
/// 书源 白鹤
class BookSource_Baihe extends BookSourceProvide {
/// 站点信息
@override
String siteId() => "https://apk-lb-play.fodexin.com";
/// 当前书源版本
@override
int currentSiteVersion() => 120;
@override
Map siteUpdateInfo() => {
80: '初始化版本',
100: '修改正文正则',
110: '网站变更,旧数据无法兼容,需进行迁移',
currentSiteVersion(): '站点信息无变更,去除正文内容的广告'
};
/// 配置信息
@override
QNBaseModel info() => QNBaseModel.init(
/// 站点ID ,一般为站点网站
siteId: siteId(),
/// 类型: book 书籍 listenBook听书 comic漫画
type: 'listenBook',
/// 站点显示名称
showName: '🍉白鹤故事(推荐)',
/// 站点分类
group: '搜狗',
/// 站点描述
desc: '🍉白鹤故事',
/// 漫画详情章节展示,仅漫画有效,分为 small 小,middle 中等 big大
comicShowType: '',
/// 站点作者
maker: 'laobai',
/// 站点更新时间戳
updateTime: DateTime.now().millisecondsSinceEpoch,
/// 站点版本
versionNumb: currentSiteVersion(),
vpnWebsite: '',
/// 是否支持 探索
supportExplore: true,
/// 是否支持 书名搜索
supportSearchBookName: true);
/// 数据迁移模式
@override
int migrate(int oldVersion) {
/// 从 110版本,进行了网站地址变更,低于此版本的书籍数据均不可用,需要进行数据迁移
///
/// 数据迁移由 App进行处理,站点规则 需确认是否进行旧数据处理及迁移。
///
///
/// 返回值为 0:不需要进行数据迁移,适用于 逻辑优化、去广告等
/// 返回值为 1:代表 书籍详情页地址发生变更,当用户 在书架打开书籍时,会 将旧书籍删除,使用新的规则 进行 搜索 -> 获取详情 -> 获取目录 -> 获取章节列表,重新生成完整的书籍数据。
/// 返回值为 2:代表 书籍的目录集合页地址发生变更,当用户 在书架打开书籍时,会通过 新的规则 ->获取目录 -> 获取章节列表,重新生成书籍数据。
/// 返回值为 3:代表 书籍的目录章节地址(正文地址)发生变更,当用户打开书籍时,会通过 新的规则 -> 获取章节列表,重新生成书籍数据。
return 0;
}
/// 探索 模型 ,区分 男女生
@override
QNExploreModel exploreModel() {
/// 使用文本模型中{{$.page}}的方式进行页码注入。
const tag =
"玄幻奇幻::https://apk-lb-json.fodexin.com/json/v1/cat_list/46/index/{{\$.page}}.json\n武侠小说::https://apk-lb-json.fodexin.com/json/v1/cat_list/11/index/{{\$.page}}.json\n言情通俗::https://apk-lb-json.fodexin.com/json/v1/cat_list/19/index/{{\$.page}}.json\n恐怖惊悚::https://apk-lb-json.fodexin.com/json/v1/cat_list/14/index/{{\$.page}}.json\n历史军事::https://apk-lb-json.fodexin.com/json/v1/cat_list/15/index/{{\$.page}}.json\n官场商战::https://apk-lb-json.fodexin.com/json/v1/cat_list/17/index/{{\$.page}}.json\n有声文学::https://apk-lb-json.fodexin.com/json/v1/cat_list/10/index/{{\$.page}}.json\n人物纪实::https://apk-lb-json.fodexin.com/json/v1/cat_list/18/index/{{\$.page}}.json\n刑侦反腐::https://apk-lb-json.fodexin.com/json/v1/cat_list/16/index/{{\$.page}}.json\n百家讲坛::https://apk-lb-json.fodexin.com/json/v1/cat_list/9/index/{{\$.page}}.json\n单田芳::https://apk-lb-json.fodexin.com/json/v1/cat_list/1/index/{{\$.page}}.json\n刘兰芳::https://apk-lb-json.fodexin.com/json/v1/cat_list/2/index/{{\$.page}}.json\n田连元::https://apk-lb-json.fodexin.com/json/v1/cat_list/3/index/{{\$.page}}.json\n袁阔成::https://apk-lb-json.fodexin.com/json/v1/cat_list/4/index/{{\$.page}}.json\n连丽如::https://apk-lb-json.fodexin.com/json/v1/cat_list/5/index/{{\$.page}}.json\n孙一::https://apk-lb-json.fodexin.com/json/v1/cat_list/8/index/{{\$.page}}.json\n王子封臣::https://apk-lb-json.fodexin.com/json/v1/cat_list/30/index/{{\$.page}}.json\n马长辉::https://apk-lb-json.fodexin.com/json/v1/cat_list/25/index/{{\$.page}}.json\n昊儒书场::https://apk-lb-json.fodexin.com/json/v1/cat_list/26/index/{{\$.page}}.json\n王军::https://apk-lb-json.fodexin.com/json/v1/cat_list/27/index/{{\$.page}}.json\n王玥波::https://apk-lb-json.fodexin.com/json/v1/cat_list/28/index/{{\$.page}}.json\n石连军::https://apk-lb-json.fodexin.com/json/v1/cat_list/29/index/{{\$.page}}.json\n粤语评书::https://apk-lb-json.fodexin.com/json/v1/cat_list/12/index/{{\$.page}}.json\n关永超::https://apk-lb-json.fodexin.com/json/v1/cat_list/35/index/{{\$.page}}.json\n张少佐::https://apk-lb-json.fodexin.com/json/v1/cat_list/6/index/{{\$.page}}.json\n田战义::https://apk-lb-json.fodexin.com/json/v1/cat_list/7/index/{{\$.page}}.json\n其他评书::https://apk-lb-json.fodexin.com/json/v1/cat_list/13/index/{{\$.page}}.json\n童话寓言::https://apk-lb-json.fodexin.com/json/v1/cat_list/20/index/{{\$.page}}.json\n教育培训::https://apk-lb-json.fodexin.com/json/v1/cat_list/44/index/{{\$.page}}.json\n亲子教育::https://apk-lb-json.fodexin.com/json/v1/cat_list/43/index/{{\$.page}}.json\n商业财经::https://apk-lb-json.fodexin.com/json/v1/cat_list/42/index/{{\$.page}}.json\n脱口秀::https://apk-lb-json.fodexin.com/json/v1/cat_list/41/index/{{\$.page}}.json\n戏曲::https://apk-lb-json.fodexin.com/json/v1/cat_list/38/index/{{\$.page}}.json\n头条::https://apk-lb-json.fodexin.com/json/v1/cat_list/40/index/{{\$.page}}.json\n综艺娱乐::https://apk-lb-json.fodexin.com/json/v1/cat_list/34/index/{{\$.page}}.json\n健康养生::https://apk-lb-json.fodexin.com/json/v1/cat_list/33/index/{{\$.page}}.json\n二人转::https://apk-lb-json.fodexin.com/json/v1/cat_list/31/index/{{\$.page}}.json\n广播剧::https://apk-lb-json.fodexin.com/json/v1/cat_list/36/index/{{\$.page}}.json\n轻音清心::https://apk-lb-json.fodexin.com/json/v1/cat_list/23/index/{{\$.page}}.json\n英文读物::https://apk-lb-json.fodexin.com/json/v1/cat_list/22/index/{{\$.page}}.json\n相声小品::https://apk-lb-json.fodexin.com/json/v1/cat_list/21/index/{{\$.page}}.json\n时尚生活::https://apk-lb-json.fodexin.com/json/v1/cat_list/45/index/{{\$.page}}.json";
final List boyExploreList = [];
final List girlExploreList = [];
var split = tag.split('\n');
int length = split.length;
for (var i = 0; i < length; i++) {
String s = split[i];
var _array = s.split("::");
final String tag = _array[0];
final String url = _array[1];
if (s.isNotEmpty) {
if (tag.startsWith('女')) {
girlExploreList.add(QNExploreItem(tag: tag, url: url));
} else {
boyExploreList.add(QNExploreItem(tag: tag, url: url));
}
}
}
return QNExploreModel.init(
boyExploreList: boyExploreList, girlExploreList: girlExploreList);
}
/// 探索 获取详情
@override
Future> queryExplore(String exploreUrl) async {
var networkData = await BUtils.qn_Network(exploreUrl, headers: {
'User-Agent':
'laobai_tv/1.1.3(Mozilla/5.0 (Linux; Android 10; Redmi Note 8 Build/PKQ1.190616.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36)',
'Referer': "https://apk.jqkan.com/",
});
var dataList =
BUtils.qn_queryList(withRule: r'$.data.books[*]', node: networkData);
List bookList = [];
for (var s in dataList) {
final data = json.decode(s);
final bId = "${data['book_id']}";
final detailUrl =
"https://apk-lb-json.fodexin.com/json/v1/cont/$bId.json";
var bookModel = QNBookModel.init(
siteId: siteId(),
bookName: data['name'],
siteVersion: currentSiteVersion(),
encode: '',
author: data['teller'],
tags: ["${data['type']}"],
desc: data['synopsis'],
cover: 'https://pic.iiszg.com/${data['pic']}',
detailUrl: detailUrl,
tocUrl: detailUrl);
BUtils.qn_Put(bookModel.bookId, 'bId', bId);
bookList.add(bookModel);
}
return bookList;
}
/// 通过书名搜索书籍
@override
Future> searchBookName(String key) async {
var requestPath =
'https://apk-lb-play.fodexin.com/api2/web/index.php/?r=api';
// 获取网络数据
var encodeSearchKey = '';
{
var ti = DateTime.now().millisecondsSinceEpoch * 1.0 / 1000;
var me = ti % 60;
final _5 = BUtils.roundToInt(ti - me);
var md_5 = '7d0526fa291e8baa4173afcf8e08acea1449682949$_5';
final t = BUtils.qn_UtilsSync('md5', [md_5]);
final str = '{"m":"search","t":"$t","aid":"0","pid":"0","key":"$key"}';
List encryptData =
BUtils.qn_UtilsSync('rsaEncrypt', [str, jmkey, 'PKCS1']);
final String encodeStr = laobaiEncode(encryptData);
encodeSearchKey = BUtils.qn_UtilsSync('urlEncode', [encodeStr]);
}
var formRequestData = 'params=$encodeSearchKey&version=1.1.3';
var result = await BUtils.qn_Network(
requestPath,
method: 'post',
requestData: formRequestData,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent':
'laobai_tv/1.1.3(Mozilla/5.0 (Linux; Android 10; Redmi Note 8 Build/PKQ1.190616.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36)',
'Referer': "https://apk.jqkan.com/",
},
);
// xPath解析
final list = BUtils.qn_queryList(withRule: r"$..books[*]", node: result);
final List bookList = [];
for (var s in list) {
final data = json.decode(s);
final bId = "${data['book_id']}";
final detailUrl =
"https://apk-lb-json.fodexin.com/json/v1/cont/$bId.json";
var bookModel = QNBookModel.init(
siteId: siteId(),
bookName: data['name'],
siteVersion: currentSiteVersion(),
encode: '',
author: data['teller'],
tags: ["${data['type']}"],
desc: data['synopsis'],
cover: 'https://pic.iiszg.com/${data['pic']}',
detailUrl: detailUrl,
tocUrl: detailUrl);
BUtils.qn_Put(bookModel.bookId, 'bId', bId);
bookList.add(bookModel);
}
return bookList;
}
/// 获取详情页
@override
Future queryBookDetail(QNBookModel model) async {
return model;
}
/// 获取章节目录
@override
Future queryBookToc(QNBookModel model) async {
final tocUrl = model.tocUrl;
if (tocUrl.trimLeft().trimRight().isEmpty) {
return QNTocModel.empty;
}
var networkData = await BUtils.qn_Network(tocUrl, headers: {
'User-Agent':
'laobai_tv/1.1.3(Mozilla/5.0 (Linux; Android 10; Redmi Note 8 Build/PKQ1.190616.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36)',
'Referer': "https://apk.jqkan.com/",
});
final dataList = BUtils.qn_queryList(
withRule: "\$.data.play_data[*]", node: networkData);
final List innerList = [];
// var bId = qnGet(model.bookId, 'bId');
for (var s in dataList) {
final data = json.decode(s);
var playId = data['play_id'];
final url =
'https://apk-lb-play.fodexin.com/api2/web/index.php/?r=api,,,$playId';
innerList.add(
QNTocInnerModel.init(title: data['name'], url: url, needVip: false));
}
return QNTocModel.init(
bookId: model.bookId,
updateTime: DateTime.now().millisecondsSinceEpoch,
dataList: innerList);
}
/// 获取正文内容(音乐、漫画)
@override
Future queryBookContent(
QNBookModel model, String contentKey) async {
String bId = BUtils.qn_Get(model.bookId, 'bId');
var split = contentKey.split(',,,');
String requestData = _laobai(split[1], bId );
var networkData = await BUtils.qn_Network(
split[0],
method: 'POST',
requestData: requestData,
headers: {
"Referer": 'https://apk-lb.iiszg.com/',
'User-Agent':
'laobai_tv/1.1.3(Mozilla/5.0 (Linux; Android 10; Redmi Note 8 Build/PKQ1.190616.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36)',
'content-type': 'application/x-www-form-urlencoded'
},
);
var content =
BUtils.qn_querySingle(withRule: "\$.data.url", node: networkData);
return QNContentModel.init(
contentKey: contentKey,
musicContent: MusicContentModel.init(url: content, headers: {
'Referer': 'https://apk-lb.iiszg.com/',
'User-Agent':
'laobai_tv/1.1.3(Mozilla/5.0 (Linux; Android 10; Redmi Note 8 Build/PKQ1.190616.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36)'
}));
}
/// -------------------- 以下为 站点校验 -------------------------
///
///
/// 站点校验配置
@override
Map verifyBookConfig() => {
"searchKey": '元尊',
"detailPrefix":
'https://apk-lb-json.fodexin.com/json/v1/cont'
};
// -------------
static String laobaiEncode(List bArr) {
var bArr2 = BUtils.qn_UtilsSync('utf8_bytes',
["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"]);
final length = bArr.length - (bArr.length % 3);
List bArr3 = [];
for(int i = 0 ;i < length * 3; i++ ){
bArr3.add(-1234);
}
var i = 0;
var i2 = 0;
while (i < length) {
var i3 = i + 1;
var b2 = bArr[i];
var i4 = i3 + 1;
var b3 = bArr[i3];
i = i4 + 1;
var b4 = bArr[i4];
var i5 = i2 + 1;
bArr3[i2] = bArr2[(b2 & 255) >> 2];
var i6 = i5 + 1;
bArr3[i5] = bArr2[((b2 & 3) << 4) | ((b3 & 255) >> 4)];
var i7 = i6 + 1;
// LogUtil.debug(() => 'i6 ->$i6 i ->$i');
bArr3[i6] = bArr2[((b3 & 15) << 2) | ((b4 & 255) >> 6)];
i2 = i7 + 1;
bArr3[i7] = bArr2[b4 & 63];
// LogUtil.debug(() => 'i7 ->$i7 i ->$i');
}
var length2 = bArr.length - length;
if (length2 == 1) {
var b5 = bArr[i];
var i8 = i2 + 1;
bArr3[i2] = bArr2[(b5 & 255) >> 2];
var i9 = i8 + 1;
bArr3[i8] = bArr2[(b5 & 3) << 4];
var b6 = 61;
bArr3[i9] = b6;
bArr3[i9 + 1] = b6;
} else if (length2 == 2) {
var i10 = i + 1;
var b7 = bArr[i];
var b8 = bArr[i10];
var i11 = i2 + 1;
bArr3[i2] = bArr2[(b7 & 255) >> 2];
var i12 = i11 + 1;
bArr3[i11] = bArr2[((b7 & 3) << 4) | ((b8 & 255) >> 4)];
bArr3[i12] = bArr2[(b8 & 15) << 2];
bArr3[i12 + 1] = 61;
}
final List newList = [];
for (int x in bArr3) {
if (x == -1234) {
}else {
newList.add(x);
}
}
var decodeStr = BUtils.qn_UtilsSync("utf8_decode",[newList]);
return decodeStr;
}
static final jmkey = [
48,
-126,
2,
34,
48,
13,
6,
9,
42,
-122,
72,
-122,
-9,
13,
1,
1,
1,
5,
0,
3,
-126,
2,
15,
0,
48,
-126,
2,
10,
2,
-126,
2,
1,
0,
-40,
104,
121,
-84,
-28,
-99,
-13,
-100,
21,
12,
95,
14,
1,
110,
44,
31,
75,
-116,
107,
42,
-39,
52,
59,
-91,
-80,
-15,
28,
102,
-4,
-4,
-91,
-98,
-92,
-80,
-37,
-63,
68,
35,
45,
-42,
75,
52,
-54,
61,
40,
-67,
-36,
79,
-56,
107,
2,
34,
-20,
67,
123,
-126,
-107,
37,
46,
-3,
51,
64,
-17,
64,
-126,
-98,
-45,
-51,
28,
-12,
78,
-87,
38,
-62,
87,
-55,
-102,
27,
-27,
-111,
1,
75,
86,
30,
-20,
-5,
-44,
-17,
-87,
-32,
-107,
42,
77,
116,
-71,
-124,
-109,
-117,
-28,
-56,
-107,
59,
-106,
-56,
118,
82,
-76,
-128,
56,
75,
15,
15,
-9,
-15,
-101,
53,
-47,
-117,
-71,
-112,
-43,
-100,
61,
67,
-113,
49,
-82,
87,
104,
-34,
-74,
15,
-24,
-113,
37,
109,
-90,
-115,
52,
6,
-95,
23,
111,
125,
-106,
42,
-122,
74,
-15,
71,
-23,
-116,
32,
25,
83,
-19,
8,
72,
99,
-54,
47,
-112,
22,
-71,
76,
94,
122,
-77,
99,
46,
-74,
-118,
-100,
106,
58,
-63,
-106,
30,
23,
-84,
90,
23,
-45,
-85,
124,
-56,
8,
-101,
42,
-33,
90,
-32,
29,
-70,
-103,
93,
28,
1,
-45,
-81,
-98,
68,
69,
-107,
-75,
92,
-95,
-123,
11,
29,
76,
-103,
-15,
-23,
-4,
-16,
107,
122,
52,
6,
50,
-29,
52,
-25,
-123,
-33,
59,
-78,
94,
49,
-2,
70,
18,
85,
37,
99,
21,
-103,
-12,
97,
-11,
61,
-28,
37,
100,
33,
119,
51,
94,
-113,
127,
5,
-49,
-47,
-60,
17,
122,
-23,
117,
17,
-100,
-72,
54,
-37,
16,
-116,
-99,
35,
-122,
-86,
85,
87,
-58,
107,
-13,
-61,
-93,
-78,
-88,
113,
14,
-2,
-66,
52,
105,
-44,
24,
43,
79,
-114,
-19,
73,
-97,
-10,
-20,
-48,
90,
18,
-59,
-2,
-20,
89,
2,
115,
45,
-11,
-47,
-13,
69,
-122,
-2,
-7,
-30,
-91,
14,
-38,
-29,
62,
41,
-103,
107,
127,
-77,
-59,
-83,
20,
118,
-17,
123,
-10,
-119,
123,
32,
79,
104,
-75,
-112,
-11,
-88,
92,
0,
-57,
116,
-95,
-26,
96,
115,
84,
43,
-90,
-84,
124,
-104,
121,
-116,
-122,
106,
24,
112,
-19,
120,
49,
-26,
-11,
66,
17,
60,
-24,
69,
-122,
-113,
-45,
9,
58,
-83,
-14,
57,
72,
-67,
115,
-34,
-48,
32,
-34,
53,
31,
-59,
-102,
-102,
-49,
100,
9,
18,
-61,
-35,
69,
18,
-71,
-97,
-22,
77,
-109,
22,
-9,
-75,
-73,
104,
-55,
77,
37,
16,
63,
-101,
83,
-59,
46,
93,
121,
-124,
59,
120,
34,
-10,
45,
-92,
24,
-78,
98,
-3,
93,
-82,
15,
-5,
53,
-17,
-17,
127,
89,
-76,
-19,
53,
62,
112,
95,
102,
114,
-82,
37,
-68,
-56,
-40,
-25,
-22,
-108,
41,
101,
-94,
67,
110,
10,
-119,
115,
-85,
-36,
-103,
0,
14,
105,
-60,
37,
-127,
-107,
-2,
125,
-3,
125,
-113,
-61,
-25,
-43,
-8,
79,
13,
101,
-103,
32,
-54,
-68,
121,
16,
-79,
113,
-118,
-100,
39,
-3,
-90,
-24,
-51,
-108,
-78,
120,
-109,
-86,
-116,
99,
0,
69,
-72,
97,
-65,
-15,
2,
3,
1,
0,
1
];
String _laobai(String pid, String bid) {
double ti = DateTime.now().millisecondsSinceEpoch * 1.0 / 1000;
double me = ti % 60;
final int _5 = BUtils.roundToInt(ti - me);
var md = BUtils.qn_UtilsSync('md5', ['play$bid$pid']);
var md_5 = '${md}1449682949$_5';
final String t = BUtils.qn_UtilsSync('md5', [md_5]);
final str = '{"m":"play","t":"$t","aid":"$bid","pid":"$pid"}';
final rsaEncrpyt = BUtils.qn_UtilsSync('rsaEncrypt', [str, jmkey, 'PKCS1']);
var encodeStrr = laobaiEncode(rsaEncrpyt);
final bm = BUtils.qn_UtilsSync('urlEncode', [encodeStrr]);
return 'params=$bm&version=1.1.3';
}
}
```