使用 Netty 重新实现 LiveClient, 使用 guava EventBus 来作为事件机制实现

This commit is contained in:
czp 2018-01-31 10:38:53 +08:00
parent 7b85c8a956
commit a8e3aa15b8
37 changed files with 1014 additions and 259 deletions

View File

@ -24,7 +24,7 @@ dependencies {
// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
// https://mvnrepository.com/artifact/io.netty/netty-all
compile group: 'io.netty', name: 'netty-all', version: '4.1.19.Final'
compile group: 'io.netty', name: 'netty-all', version: '4.1.20.Final'
// https://mvnrepository.com/artifact/com.google.guava/guava
compile group: 'com.google.guava', name: 'guava', version: '23.6-jre'
}

View File

@ -0,0 +1,9 @@
{
"cmd": "ACTIVITY_EVENT",
"data": {
"keyword": "newspring_2018",
"type": "cracker",
"limit": 300000,
"progress": 158912
}
}

View File

@ -1,4 +1,4 @@
{
"cmd": "LIVE",
"roomid": 1110317
"roomid": "1110317"
}

View File

@ -1,4 +1,4 @@
{
"cmd": "PREPARING",
"roomid": 1110317
"roomid": "1110317"
}

View File

@ -55,7 +55,6 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
@Override
public PassportService getPassportService() {
if (passportService == null) {
LOGGER.debug("Init PassportService in BilibiliAPI instance {}", this.hashCode());
OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
.addInterceptor(new AddFixedParamsInterceptor(
"build", bilibiliClientProperties.getBuild(),
@ -84,7 +83,6 @@ public class BilibiliAPI implements BilibiliServiceProvider, LiveClientProvider
@Override
public LiveService getLiveService() {
if (liveService == null) {
LOGGER.debug("Init LiveService in BilibiliAPI instance {}", this.hashCode());
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new AddFixedHeadersInterceptor(
"Buvid", bilibiliClientProperties.getBuvId(),

View File

@ -1,15 +1,32 @@
package com.hiczp.bilibili.api.live.socket;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.hiczp.bilibili.api.BilibiliServiceProvider;
import com.hiczp.bilibili.api.live.entity.LiveRoomInfoEntity;
import com.hiczp.bilibili.api.live.socket.codec.PackageDecoder;
import com.hiczp.bilibili.api.live.socket.codec.PackageEncoder;
import com.hiczp.bilibili.api.live.socket.event.ConnectSucceedEvent;
import com.hiczp.bilibili.api.live.socket.event.ConnectionCloseEvent;
import com.hiczp.bilibili.api.live.socket.handler.LiveClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class LiveClient {
public class LiveClient implements Closeable {
private static final Logger LOGGER = LoggerFactory.getLogger(LiveClient.class);
private final EventBus eventBus = new EventBus("BilibiliLiveClient");
@ -17,42 +34,158 @@ public class LiveClient {
private final long showRoomId;
private final long userId;
private LiveRoomInfoEntity.LiveRoomEntity liveRoomEntity;
private long reconnectLimit = 0;
private long reconnectAttempt = 0;
private long reconnectDelay = 5L;
private boolean userWantClose = false;
private Long roomId;
private EventLoopGroup eventLoopGroup;
private Channel channel;
public LiveClient(BilibiliServiceProvider bilibiliServiceProvider, long showRoomId) {
this.bilibiliServiceProvider = bilibiliServiceProvider;
this.showRoomId = showRoomId;
this.userId = 0;
initEventBus();
}
public LiveClient(BilibiliServiceProvider bilibiliServiceProvider, long showRoomId, long userId) {
this.bilibiliServiceProvider = bilibiliServiceProvider;
this.showRoomId = showRoomId;
this.userId = userId;
initEventBus();
}
private void fetchRoomInfo() throws IOException {
liveRoomEntity = bilibiliServiceProvider.getLiveService()
private void initEventBus() {
eventBus.register(this);
}
public synchronized LiveClient connect() throws IOException {
if (channel != null && channel.isActive()) {
LOGGER.warn("Already connected to server, connect method can not be invoked twice");
return this;
}
reconnectAttempt++;
LOGGER.info("Fetching info of live room {}", showRoomId);
LiveRoomInfoEntity.LiveRoomEntity liveRoomEntity = bilibiliServiceProvider.getLiveService()
.getRoomInfo(showRoomId)
.execute()
.body()
.getData();
roomId = liveRoomEntity.getRoomId();
LOGGER.info("Get actual room id {}", roomId);
if (eventLoopGroup != null) {
eventLoopGroup.shutdownGracefully();
}
eventLoopGroup = new NioEventLoopGroup(1);
LOGGER.debug("Init SocketChannel Bootstrap");
Bootstrap bootstrap = new Bootstrap()
.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(
Integer.MAX_VALUE,
0,
Package.LENGTH_FIELD_LENGTH,
-Package.LENGTH_FIELD_LENGTH,
0
))
.addLast(new IdleStateHandler(40, 0, 0))
//.addLast(new LoggingHandler(LogLevel.DEBUG))
.addLast(new PackageEncoder())
.addLast(new PackageDecoder())
.addLast(new LiveClientHandler(roomId, userId, eventBus));
}
});
String address = liveRoomEntity.getCmt();
int port = liveRoomEntity.getCmtPortGoim();
LOGGER.info("Connecting to Bullet Screen server {}:{}", address, port);
try {
channel = bootstrap.connect(address, port)
.sync()
.channel();
} catch (InterruptedException e) {
e.printStackTrace();
close();
} catch (Exception e) { //有可能在此时出现网络错误
close();
throw new IOException(e);
}
return this;
}
public LiveClient connect() throws IOException {
LOGGER.info("Try to fetch info of live room {}", showRoomId);
fetchRoomInfo();
LOGGER.info("Get actual room id {}", liveRoomEntity.getRoomId());
@Override
public synchronized void close() {
userWantClose = true;
if (eventLoopGroup != null) {
LOGGER.info("Closing connection");
try {
eventLoopGroup.shutdownGracefully().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
eventLoopGroup = null;
}
}
LOGGER.debug("Init NioEventLoop");
@Subscribe
public void onConnectSucceed(ConnectSucceedEvent connectSucceedEvent) {
LOGGER.info("Connect succeed");
if (userWantClose) {
reconnectAttempt = 0;
}
}
throw new UnsupportedOperationException();
@Subscribe
public void onConnectionClose(ConnectionCloseEvent connectionCloseEvent) {
LOGGER.info("Connection closed");
if (!userWantClose && reconnectAttempt <= reconnectLimit) {
LOGGER.info("Reconnect attempt {}, limit {}", reconnectAttempt, reconnectLimit);
LOGGER.info("Auto reconnect after {} second", reconnectDelay);
eventLoopGroup.schedule(
() -> {
try {
connect();
} catch (IOException e) {
e.printStackTrace();
}
},
reconnectDelay,
TimeUnit.SECONDS
);
} else {
eventLoopGroup.shutdownGracefully();
}
if (userWantClose) {
userWantClose = false;
reconnectAttempt = 0;
}
}
public EventBus getEventBus() {
return eventBus;
}
public LiveClient registerListener(Object object) {
eventBus.register(object);
return this;
}
public LiveClient unregisterListeners(Object object) {
eventBus.unregister(object);
return this;
}
public long getShowRoomId() {
return showRoomId;
}
@ -61,7 +194,17 @@ public class LiveClient {
return userId;
}
public Optional<LiveRoomInfoEntity.LiveRoomEntity> getLiveRoomEntity() {
return Optional.of(liveRoomEntity);
public Optional<Long> getRoomId() {
return Optional.of(roomId);
}
public LiveClient setReconnectLimit(long reconnectLimit) {
this.reconnectLimit = reconnectLimit;
return this;
}
public LiveClient setReconnectDelay(long reconnectDelay) {
this.reconnectDelay = reconnectDelay;
return this;
}
}

View File

@ -0,0 +1,115 @@
package com.hiczp.bilibili.api.live.socket;
//数据包结构说明
//00 00 00 28 00 10 00 00 00 00 00 07 00 00 00 00
//00 00 00 28/00 10/00 00 00 00 00 07/00 00 00 00
//1-4 字节: 数据包长度
//5-6 字节: 协议头长度, 固定值 0x10
//7-8 字节: 设备类型, Android 固定为 0
//9-12 字节: 数据包类型
//13-16 字节: 设备类型, 7-8 字节
public class Package {
public static final short LENGTH_FIELD_LENGTH = 4;
private static final short PROTOCOL_HEAD_LENGTH = 16;
private final int packageLength;
private final short protocolHeadLength;
private final DeviceType shortDeviceType;
private final PackageType packageType;
private final DeviceType longDeviceType;
private final byte[] content;
public Package(int packageLength, short protocolHeadLength, DeviceType shortDeviceType, PackageType packageType, DeviceType longDeviceType, byte[] content) {
this.packageLength = packageLength;
this.protocolHeadLength = protocolHeadLength;
this.shortDeviceType = shortDeviceType;
this.packageType = packageType;
this.longDeviceType = longDeviceType;
this.content = content;
}
public Package(PackageType packageType, byte[] content) {
this(PROTOCOL_HEAD_LENGTH + content.length,
PROTOCOL_HEAD_LENGTH,
DeviceType.ANDROID,
packageType,
DeviceType.ANDROID,
content
);
}
public int getPackageLength() {
return packageLength;
}
public short getProtocolHeadLength() {
return protocolHeadLength;
}
public DeviceType getShortDeviceType() {
return shortDeviceType;
}
public PackageType getPackageType() {
return packageType;
}
public DeviceType getLongDeviceType() {
return longDeviceType;
}
public byte[] getContent() {
return content;
}
public enum DeviceType {
ANDROID(0x00);
private final int value;
DeviceType(int value) {
this.value = value;
}
public static DeviceType valueOf(int value) {
for (DeviceType deviceType : DeviceType.values()) {
if (deviceType.value == value) {
return deviceType;
}
}
throw new IllegalArgumentException("No matching constant for [" + value + "]");
}
public int getValue() {
return value;
}
}
public enum PackageType {
HEART_BEAT(0x02),
VIEWER_COUNT(0x03),
DATA(0x05),
ENTER_ROOM(0x07),
ENTER_ROOM_SUCCESS(0x08);
private final int value;
PackageType(int value) {
this.value = value;
}
public static PackageType valueOf(int value) {
for (PackageType packageType : PackageType.values()) {
if (packageType.value == value) {
return packageType;
}
}
throw new IllegalArgumentException("No matching constant for [" + value + "]");
}
public int getValue() {
return value;
}
}
}

View File

@ -0,0 +1,22 @@
package com.hiczp.bilibili.api.live.socket;
import com.google.gson.Gson;
import com.hiczp.bilibili.api.live.socket.entity.EnterRoomEntity;
public class PackageHelper {
private static final Gson GSON = new Gson();
public static Package createEnterRoomPackage(long roomId, long userId) {
return new Package(
Package.PackageType.ENTER_ROOM,
GSON.toJson(new EnterRoomEntity(roomId, userId)).getBytes()
);
}
public static Package createHeatBeatPackage() {
return new Package(
Package.PackageType.HEART_BEAT,
new byte[0]
);
}
}

View File

@ -1,143 +0,0 @@
package com.hiczp.bilibili.api.live.socket;
import com.google.gson.Gson;
import com.hiczp.bilibili.api.live.socket.entity.EnterRoomEntity;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Arrays;
//数据包结构说明
//00 00 00 28 00 10 00 00 00 00 00 07 00 00 00 00
//00 00 00 28/00 10/00 00 00 00 00 07/00 00 00 00
//1-4 字节: 数据包长度
//5-6 字节: 协议头长度, 固定值 0x10
//7-8 字节: 设备类型, Android 固定为 0
//9-12 字节: 数据包类型
//13-16 字节: 设备类型, 7-8 字节
public class PackageRepository {
//数据包总长标识占用的 bytes 长度
static final short PACKAGE_LENGTH_BYTES_LENGTH = 4;
//协议头长度标识占用的 bytes 长度
static final short PROTOCOL_HEAD_LENGTH_BYTES_LENGTH = 2;
//设备类型短标识 bytes
static final byte[] SHORT_DEVICE_TYPE_BYTES = {0x00, 0x00};
//数据包类型标识 bytes
static final short PACKAGE_TYPE_BYTES_LENGTH = 4;
static final byte[] HEART_BEAT_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x02}; //心跳包
static final byte[] VIEWER_COUNT_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x03}; //观众人数
static final byte[] DATA_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x05}; //弹幕, 礼物, 系统消息 etc
static final byte[] ENTER_ROOM_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x07}; //进入房间
static final byte[] ENTER_ROOM_SUCCESS_PACKAGE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x08}; //进入房间的响应包
//设备类型长标识 bytes
static final byte[] LONG_DEVICE_TYPE_BYTES = {0x00, 0x00, 0x00, 0x00};
//协议头总长度
static final short PROTOCOL_HEAD_LENGTH = (short) (PACKAGE_LENGTH_BYTES_LENGTH + PROTOCOL_HEAD_LENGTH_BYTES_LENGTH + SHORT_DEVICE_TYPE_BYTES.length + PACKAGE_TYPE_BYTES_LENGTH + LONG_DEVICE_TYPE_BYTES.length);
//协议头长度标识 bytes
static final byte[] PROTOCOL_HEAD_LENGTH_BYTES = ByteBuffer.allocate(PROTOCOL_HEAD_LENGTH_BYTES_LENGTH).putShort(PROTOCOL_HEAD_LENGTH).array();
private static final Gson GSON = new Gson();
private static ByteBuffer createPackage(byte[] packageTypeBytes, String content) {
int totalLength = PROTOCOL_HEAD_LENGTH + content.length();
ByteBuffer byteBuffer = ByteBuffer.allocate(totalLength)
.putInt(totalLength)
.put(PROTOCOL_HEAD_LENGTH_BYTES)
.put(SHORT_DEVICE_TYPE_BYTES)
.put(packageTypeBytes)
.put(LONG_DEVICE_TYPE_BYTES)
.put(content.getBytes());
byteBuffer.flip();
return byteBuffer;
}
private static void readDataFromSocketChannel(SocketChannel socketChannel, ByteBuffer byteBuffer) throws IOException {
while (byteBuffer.hasRemaining()) {
socketChannel.read(byteBuffer);
}
}
//userId 0 表示用户未登录, 并不影响弹幕推送, 但是可能和用户统计有关
public static ByteBuffer createEnterRoomPackage(int roomId, int userId) {
return createPackage(ENTER_ROOM_PACKAGE_TYPE_BYTES, GSON.toJson(new EnterRoomEntity(roomId, userId)));
}
public static ByteBuffer createHeartBeatPackage(int roomId, int userId) {
return createPackage(HEART_BEAT_PACKAGE_TYPE_BYTES, GSON.toJson(new EnterRoomEntity(roomId, userId)));
}
public static ByteBuffer readNextPackage(SocketChannel socketChannel) throws IOException {
//获取数据包总长度
ByteBuffer packageLengthByteBuffer = ByteBuffer.allocate(PACKAGE_LENGTH_BYTES_LENGTH);
readDataFromSocketChannel(socketChannel, packageLengthByteBuffer);
packageLengthByteBuffer.flip();
int packageLength = packageLengthByteBuffer.getInt();
packageLengthByteBuffer.rewind();
//获取数据包剩下的部分
ByteBuffer restPackageByteBuffer = ByteBuffer.allocate(packageLength - PACKAGE_LENGTH_BYTES_LENGTH);
readDataFromSocketChannel(socketChannel, restPackageByteBuffer);
restPackageByteBuffer.flip();
//合并 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(packageLengthByteBuffer.limit() + restPackageByteBuffer.limit());
byteBuffer.put(packageLengthByteBuffer).put(restPackageByteBuffer);
byteBuffer.flip();
return byteBuffer;
}
public static ByteBuffer[] readNextPackageSplit(SocketChannel socketChannel) throws IOException {
//获取数据包总长度
ByteBuffer packageLengthByteBuffer = ByteBuffer.allocate(PACKAGE_LENGTH_BYTES_LENGTH);
readDataFromSocketChannel(socketChannel, packageLengthByteBuffer);
packageLengthByteBuffer.flip();
int packageLength = packageLengthByteBuffer.getInt();
packageLengthByteBuffer.rewind();
//获取协议头长度
ByteBuffer protocolHeadLengthByteBuffer = ByteBuffer.allocate(PROTOCOL_HEAD_LENGTH_BYTES_LENGTH);
readDataFromSocketChannel(socketChannel, protocolHeadLengthByteBuffer);
protocolHeadLengthByteBuffer.flip();
int protocolHeadLength = protocolHeadLengthByteBuffer.getShort();
protocolHeadLengthByteBuffer.rewind();
//获取剩余的协议头
ByteBuffer restProtocolHeadByteBuffer = ByteBuffer.allocate(protocolHeadLength - PACKAGE_LENGTH_BYTES_LENGTH - PROTOCOL_HEAD_LENGTH_BYTES_LENGTH);
readDataFromSocketChannel(socketChannel, restProtocolHeadByteBuffer);
restProtocolHeadByteBuffer.flip();
//得到设备类型短标识
ByteBuffer shortDeviceTypeByteBuffer = ByteBuffer.allocate(SHORT_DEVICE_TYPE_BYTES.length);
shortDeviceTypeByteBuffer.putShort(restProtocolHeadByteBuffer.getShort());
shortDeviceTypeByteBuffer.flip();
//得到数据包类型
ByteBuffer packageTypeByteBuffer = ByteBuffer.allocate(PACKAGE_TYPE_BYTES_LENGTH);
packageTypeByteBuffer.putInt(restProtocolHeadByteBuffer.getInt());
packageTypeByteBuffer.flip();
//得到设备类型长标识
ByteBuffer longDeviceTypeByteBuffer = ByteBuffer.allocate(LONG_DEVICE_TYPE_BYTES.length);
longDeviceTypeByteBuffer.putInt(restProtocolHeadByteBuffer.getInt());
longDeviceTypeByteBuffer.flip();
//获取正文
ByteBuffer contentByteBuffer = ByteBuffer.allocate(packageLength - protocolHeadLength);
readDataFromSocketChannel(socketChannel, contentByteBuffer);
contentByteBuffer.flip();
//组成数组
return new ByteBuffer[]{
packageLengthByteBuffer, //0
protocolHeadLengthByteBuffer, //1
shortDeviceTypeByteBuffer, //2
packageTypeByteBuffer, //3
longDeviceTypeByteBuffer, //4
contentByteBuffer //5
};
}
public static boolean isNextPackageIsEnterRoomSuccessPackage(SocketChannel socketChannel) throws IOException {
return Arrays.equals(readNextPackageSplit(socketChannel)[3].array(), ENTER_ROOM_SUCCESS_PACKAGE_TYPE_BYTES);
}
}

View File

@ -1,54 +0,0 @@
package com.hiczp.bilibili.api.live.socket;
import java.util.Arrays;
public class Utils {
private static byte[][] splitBytes(byte[] bytes, int n) {
int lineCount = bytes.length % n == 0 ? bytes.length / n : bytes.length / n + 1;
byte[][] result = new byte[lineCount][];
int to;
for (int line = 1; line <= lineCount; line++) {
if (line != lineCount) {
to = line * n;
} else {
to = bytes.length;
}
result[line - 1] = Arrays.copyOfRange(bytes, (line - 1) * n, to);
}
return result;
}
public static void printBytes(byte[] bytes) {
byte[][] data = splitBytes(bytes, 16);
byte[] currentRow;
char c;
for (int i = 0; i < data.length; i++) {
System.out.printf("%08x ", i * 16);
currentRow = data[i];
for (int j = 0; j < currentRow.length; j++) {
System.out.printf("%02x ", currentRow[j]);
if (j == 7) {
System.out.print(" ");
}
}
if (currentRow.length < 16) {
for (int k = 0; k < (48 - currentRow.length * 2 - (currentRow.length - 1)); k++) {
System.out.print(" ");
}
}
System.out.print(" ");
for (int j = 0; j < currentRow.length; j++) {
if (currentRow[j] < ' ') {
c = '.';
} else {
c = (char) currentRow[j];
}
System.out.print(c);
if (j == 7) {
System.out.print(" ");
}
}
System.out.println();
}
}
}

View File

@ -0,0 +1,30 @@
package com.hiczp.bilibili.api.live.socket.codec;
import com.hiczp.bilibili.api.live.socket.Package;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class PackageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int packageLength = in.readInt();
short protocolHeadLength = in.readShort();
Package.DeviceType shortDeviceType = Package.DeviceType.valueOf(in.readShort());
Package.PackageType packageType = Package.PackageType.valueOf(in.readInt());
Package.DeviceType longDeviceType = Package.DeviceType.valueOf(in.readInt());
byte[] content = new byte[packageLength - protocolHeadLength];
in.readBytes(content);
out.add(new Package(
packageLength,
protocolHeadLength,
shortDeviceType,
packageType,
longDeviceType,
content
));
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.codec;
import com.hiczp.bilibili.api.live.socket.Package;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class PackageEncoder extends MessageToByteEncoder<Package> {
@Override
protected void encode(ChannelHandlerContext ctx, Package msg, ByteBuf out) throws Exception {
out.writeInt(msg.getPackageLength())
.writeShort(msg.getProtocolHeadLength())
.writeShort(msg.getShortDeviceType().getValue())
.writeInt(msg.getPackageType().getValue())
.writeInt(msg.getLongDeviceType().getValue())
.writeBytes(msg.getContent());
}
}

View File

@ -0,0 +1,81 @@
package com.hiczp.bilibili.api.live.socket.entity;
import com.google.gson.annotations.SerializedName;
public class ActivityEventEntity {
/**
* cmd : ACTIVITY_EVENT
* data : {"keyword":"newspring_2018","type":"cracker","limit":300000,"progress":158912}
*/
@SerializedName("cmd")
private String cmd;
@SerializedName("data")
private Data data;
public String getCmd() {
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
public Data getData() {
return data;
}
public void setData(Data data) {
this.data = data;
}
public static class Data {
/**
* keyword : newspring_2018
* type : cracker
* limit : 300000
* progress : 158912
*/
@SerializedName("keyword")
private String keyword;
@SerializedName("type")
private String type;
@SerializedName("limit")
private int limit;
@SerializedName("progress")
private int progress;
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
public int getProgress() {
return progress;
}
public void setProgress(int progress) {
this.progress = progress;
}
}
}

View File

@ -9,7 +9,7 @@ import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
public class DanMuMSGEntity {
public class DanMuMsgEntity {
private static final Gson GSON = new Gson();
private static final Type STRING_LIST_TYPE = new TypeToken<List<String>>() {
}.getType();
@ -76,8 +76,8 @@ public class DanMuMSGEntity {
}
//得到发送者的用户 ID
public int getUserId() {
return info.get(2).getAsJsonArray().get(0).getAsInt();
public long getUserId() {
return info.get(2).getAsJsonArray().get(0).getAsLong();
}
//得到发送者的用户名

View File

@ -4,28 +4,28 @@ import com.google.gson.annotations.SerializedName;
public class EnterRoomEntity {
@SerializedName("roomid")
private int roomId;
private long roomId;
@SerializedName("uid")
private int userId;
private long userId;
public EnterRoomEntity(int roomId, int userId) {
public EnterRoomEntity(long roomId, long userId) {
this.roomId = roomId;
this.userId = userId;
}
public int getRoomId() {
public long getRoomId() {
return roomId;
}
public void setRoomId(int roomId) {
public void setRoomId(long roomId) {
this.roomId = roomId;
}
public int getUserId() {
public long getUserId() {
return userId;
}
public void setUserId(int userId) {
public void setUserId(long userId) {
this.userId = userId;
}
}

View File

@ -75,9 +75,9 @@ public class SendGiftEntity {
@SerializedName("rcost")
private int rCost;
@SerializedName("uid")
private int uid;
private long uid;
@SerializedName("timestamp")
private int timestamp;
private long timestamp;
@SerializedName("giftId")
private int giftId;
@SerializedName("giftType")
@ -157,19 +157,19 @@ public class SendGiftEntity {
this.rCost = rCost;
}
public int getUid() {
public long getUid() {
return uid;
}
public void setUid(int uid) {
public void setUid(long uid) {
this.uid = uid;
}
public int getTimestamp() {
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}

View File

@ -26,9 +26,9 @@ public class SysGiftEntity {
@SerializedName("url")
private String url;
@SerializedName("roomid")
private int roomId;
private long roomId;
@SerializedName("real_roomid")
private int realRoomId;
private long realRoomId;
@SerializedName("giftId")
private int giftId;
@SerializedName("msgTips")
@ -74,19 +74,19 @@ public class SysGiftEntity {
this.url = url;
}
public int getRoomId() {
public long getRoomId() {
return roomId;
}
public void setRoomId(int roomId) {
public void setRoomId(long roomId) {
this.roomId = roomId;
}
public int getRealRoomId() {
public long getRealRoomId() {
return realRoomId;
}
public void setRealRoomId(int realRoomId) {
public void setRealRoomId(long realRoomId) {
this.realRoomId = realRoomId;
}

View File

@ -2,7 +2,7 @@ package com.hiczp.bilibili.api.live.socket.entity;
import com.google.gson.annotations.SerializedName;
public class SysMSGEntity {
public class SysMsgEntity {
/**
* cmd : SYS_MSG
* msg : 瑾然-:?在直播间:?3939852:?赠送 小电视一个请前往抽奖
@ -30,11 +30,11 @@ public class SysMSGEntity {
@SerializedName("url")
private String url;
@SerializedName("roomid")
private int roomId;
private long roomId;
@SerializedName("real_roomid")
private int realRoomId;
private long realRoomId;
@SerializedName("rnd")
private int rnd;
private long rnd;
@SerializedName("tv_id")
private String tvId;
@ -86,27 +86,27 @@ public class SysMSGEntity {
this.url = url;
}
public int getRoomId() {
public long getRoomId() {
return roomId;
}
public void setRoomId(int roomId) {
public void setRoomId(long roomId) {
this.roomId = roomId;
}
public int getRealRoomId() {
public long getRealRoomId() {
return realRoomId;
}
public void setRealRoomId(int realRoomId) {
public void setRealRoomId(long realRoomId) {
this.realRoomId = realRoomId;
}
public int getRnd() {
public long getRnd() {
return rnd;
}
public void setRnd(int rnd) {
public void setRnd(long rnd) {
this.rnd = rnd;
}

View File

@ -38,7 +38,7 @@ public class WelcomeEntity {
*/
@SerializedName("uid")
private int uid;
private long uid;
@SerializedName("uname")
private String userName;
@SerializedName("is_admin")
@ -46,11 +46,11 @@ public class WelcomeEntity {
@SerializedName("vip")
private int vip;
public int getUid() {
public long getUid() {
return uid;
}
public void setUid(int uid) {
public void setUid(long uid) {
this.uid = uid;
}
@ -62,11 +62,11 @@ public class WelcomeEntity {
this.userName = userName;
}
public boolean isIsAdmin() {
public boolean isAdmin() {
return isAdmin;
}
public void setIsAdmin(boolean isAdmin) {
public void setAdmin(boolean isAdmin) {
this.isAdmin = isAdmin;
}

View File

@ -14,7 +14,7 @@ public class WelcomeGuardEntity {
@SerializedName("data")
private DataEntity data;
@SerializedName("roomid")
private int roomId;
private long roomId;
public String getCmd() {
return cmd;
@ -32,11 +32,11 @@ public class WelcomeGuardEntity {
this.data = data;
}
public int getRoomId() {
public long getRoomId() {
return roomId;
}
public void setRoomId(int roomId) {
public void setRoomId(long roomId) {
this.roomId = roomId;
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.ActivityEventEntity;
import java.util.EventObject;
public class ActivityEventPackageEvent extends EventObject {
private ActivityEventEntity activityEventEntity;
public ActivityEventPackageEvent(Object source, ActivityEventEntity activityEventEntity) {
super(source);
this.activityEventEntity = activityEventEntity;
}
public ActivityEventEntity getActivityEventEntity() {
return activityEventEntity;
}
}

View File

@ -0,0 +1,9 @@
package com.hiczp.bilibili.api.live.socket.event;
import java.util.EventObject;
public class ConnectSucceedEvent extends EventObject {
public ConnectSucceedEvent(Object source) {
super(source);
}
}

View File

@ -0,0 +1,9 @@
package com.hiczp.bilibili.api.live.socket.event;
import java.util.EventObject;
public class ConnectionCloseEvent extends EventObject {
public ConnectionCloseEvent(Object source) {
super(source);
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.DanMuMsgEntity;
import java.util.EventObject;
public class DanMuMsgPackageEvent extends EventObject {
private DanMuMsgEntity danMuMsgEntity;
public DanMuMsgPackageEvent(Object source, DanMuMsgEntity danMuMsgEntity) {
super(source);
this.danMuMsgEntity = danMuMsgEntity;
}
public DanMuMsgEntity getDanMuMsgEntity() {
return danMuMsgEntity;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.LiveEntity;
import java.util.EventObject;
public class LivePackageEvent extends EventObject {
private LiveEntity liveEntity;
public LivePackageEvent(Object source, LiveEntity liveEntity) {
super(source);
this.liveEntity = liveEntity;
}
public LiveEntity getLiveEntity() {
return liveEntity;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.PreparingEntity;
import java.util.EventObject;
public class PreparingPackageEvent extends EventObject {
private PreparingEntity preparingEntity;
public PreparingPackageEvent(Object source, PreparingEntity preparingEntity) {
super(source);
this.preparingEntity = preparingEntity;
}
public PreparingEntity getPreparingEntity() {
return preparingEntity;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.SendGiftEntity;
import java.util.EventObject;
public class SendGiftPackageEvent extends EventObject {
private SendGiftEntity sendGiftEntity;
public SendGiftPackageEvent(Object source, SendGiftEntity sendGiftEntity) {
super(source);
this.sendGiftEntity = sendGiftEntity;
}
public SendGiftEntity getSendGiftEntity() {
return sendGiftEntity;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.SysGiftEntity;
import java.util.EventObject;
public class SysGiftPackageEvent extends EventObject {
private SysGiftEntity sysGiftEntity;
public SysGiftPackageEvent(Object source, SysGiftEntity sysGiftEntity) {
super(source);
this.sysGiftEntity = sysGiftEntity;
}
public SysGiftEntity getSysGiftEntity() {
return sysGiftEntity;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.SysMsgEntity;
import java.util.EventObject;
public class SysMsgPackageEvent extends EventObject {
private SysMsgEntity sysMsgEntity;
public SysMsgPackageEvent(Object source, SysMsgEntity sysMsgEntity) {
super(source);
this.sysMsgEntity = sysMsgEntity;
}
public SysMsgEntity getSysMsgEntity() {
return sysMsgEntity;
}
}

View File

@ -0,0 +1,16 @@
package com.hiczp.bilibili.api.live.socket.event;
import java.util.EventObject;
public class UnknownPackageEvent extends EventObject {
private String json;
public UnknownPackageEvent(Object source, String json) {
super(source);
this.json = json;
}
public String getJson() {
return json;
}
}

View File

@ -0,0 +1,16 @@
package com.hiczp.bilibili.api.live.socket.event;
import java.util.EventObject;
public class ViewerCountPackageEvent extends EventObject {
private int viewerCount;
public ViewerCountPackageEvent(Object source, int viewerCount) {
super(source);
this.viewerCount = viewerCount;
}
public int getViewerCount() {
return viewerCount;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.WelcomeGuardEntity;
import java.util.EventObject;
public class WelcomeGuardPackageEvent extends EventObject {
private WelcomeGuardEntity welcomeGuardEntity;
public WelcomeGuardPackageEvent(Object source, WelcomeGuardEntity welcomeGuardEntity) {
super(source);
this.welcomeGuardEntity = welcomeGuardEntity;
}
public WelcomeGuardEntity getWelcomeGuardEntity() {
return welcomeGuardEntity;
}
}

View File

@ -0,0 +1,18 @@
package com.hiczp.bilibili.api.live.socket.event;
import com.hiczp.bilibili.api.live.socket.entity.WelcomeEntity;
import java.util.EventObject;
public class WelcomePackageEvent extends EventObject {
private WelcomeEntity welcomeEntity;
public WelcomePackageEvent(Object source, WelcomeEntity welcomeEntity) {
super(source);
this.welcomeEntity = welcomeEntity;
}
public WelcomeEntity getWelcomeEntity() {
return welcomeEntity;
}
}

View File

@ -0,0 +1,146 @@
package com.hiczp.bilibili.api.live.socket.handler;
import com.google.common.eventbus.EventBus;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.hiczp.bilibili.api.live.socket.Package;
import com.hiczp.bilibili.api.live.socket.PackageHelper;
import com.hiczp.bilibili.api.live.socket.entity.*;
import com.hiczp.bilibili.api.live.socket.event.*;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class LiveClientHandler extends SimpleChannelInboundHandler<Package> {
private static final Logger LOGGER = LoggerFactory.getLogger(LiveClientHandler.class);
private static final Gson GSON = new Gson();
private static final JsonParser JSON_PARSER = new JsonParser();
private long roomId;
private long userId;
private EventBus eventBus;
public LiveClientHandler(long roomId, long userId, EventBus eventBus) {
this.roomId = roomId;
this.userId = userId;
this.eventBus = eventBus;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
LOGGER.debug("Sending Enter Room package");
ctx.writeAndFlush(PackageHelper.createEnterRoomPackage(roomId, userId));
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
eventBus.post(new ConnectionCloseEvent(this));
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.READER_IDLE) {
ctx.close();
}
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Package msg) throws Exception {
switch (msg.getPackageType()) {
case VIEWER_COUNT: {
eventBus.post(new ViewerCountPackageEvent(this, ByteBuffer.wrap(msg.getContent()).getInt()));
}
break;
case ENTER_ROOM_SUCCESS: {
eventBus.post(new ConnectSucceedEvent(this));
ctx.executor().scheduleAtFixedRate(
() -> ctx.writeAndFlush(PackageHelper.createHeatBeatPackage()),
0L,
30L,
TimeUnit.SECONDS
);
}
break;
case DATA: {
String content = new String(msg.getContent());
String cmd = JSON_PARSER.parse(content)
.getAsJsonObject()
.get("cmd")
.getAsString();
Supplier<Object> eventCreationExpression; //try 不能写在 switch 外面, lambda 来延迟执行
switch (cmd) {
case "DANMU_MSG": {
eventCreationExpression = () -> new DanMuMsgPackageEvent(this, GSON.fromJson(content, DanMuMsgEntity.class));
}
break;
case "SEND_GIFT": {
eventCreationExpression = () -> new SendGiftPackageEvent(this, GSON.fromJson(content, SendGiftEntity.class));
}
break;
case "SYS_GIFT": {
eventCreationExpression = () -> new SysGiftPackageEvent(this, GSON.fromJson(content, SysGiftEntity.class));
}
break;
case "SYS_MSG": {
eventCreationExpression = () -> new SysMsgPackageEvent(this, GSON.fromJson(content, SysMsgEntity.class));
}
break;
case "WELCOME": {
eventCreationExpression = () -> new WelcomePackageEvent(this, GSON.fromJson(content, WelcomeEntity.class));
}
break;
case "WELCOME_GUARD": {
eventCreationExpression = () -> new WelcomeGuardPackageEvent(this, GSON.fromJson(content, WelcomeGuardEntity.class));
}
break;
case "LIVE": {
eventCreationExpression = () -> new LivePackageEvent(this, GSON.fromJson(content, LiveEntity.class));
}
break;
case "PREPARING": {
eventCreationExpression = () -> new PreparingPackageEvent(this, GSON.fromJson(content, PreparingEntity.class));
}
break;
case "ACTIVITY_EVENT": {
eventCreationExpression = () -> new ActivityEventPackageEvent(this, GSON.fromJson(content, ActivityEventEntity.class));
}
break;
default: {
LOGGER.error("Received unknown json below: \n{}", formatJson(content));
eventCreationExpression = () -> new UnknownPackageEvent(this, content);
}
break;
}
try {
eventBus.post(eventCreationExpression.get());
} catch (JsonParseException e) {
LOGGER.error("Json parse error: {}, json below: \n{}", e.getMessage(), formatJson(content));
}
}
break;
}
}
private String formatJson(String json) {
return new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(JSON_PARSER.parse(json));
}
}

View File

@ -0,0 +1,178 @@
package com.hiczp.bilibili.api.test;
import com.google.common.eventbus.Subscribe;
import com.hiczp.bilibili.api.BilibiliAPI;
import com.hiczp.bilibili.api.live.socket.LiveClient;
import com.hiczp.bilibili.api.live.socket.entity.*;
import com.hiczp.bilibili.api.live.socket.event.*;
import org.junit.FixMethodOrder;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LiveClientTest {
private static final Logger LOGGER = LoggerFactory.getLogger(LiveClientTest.class);
private static final BilibiliAPI BILIBILI_API = Config.getBilibiliAPI();
private static final int ROOM_ID = 3;
private static final long TEST_TIME = 70 * 1000;
@Ignore
@Test
public void _0_duplicateConnectAndCloseTest() throws Exception {
LiveClient liveClient = BILIBILI_API
.getLiveClient(ROOM_ID)
.setReconnectLimit(5)
.setReconnectDelay(1);
LOGGER.debug("Connecting!");
liveClient.connect();
Thread.sleep(5000);
LOGGER.debug("Connecting!");
liveClient.connect();
Thread.sleep(5000);
LOGGER.debug("Disconnecting!");
liveClient.close();
Thread.sleep(5000);
LOGGER.debug("Disconnecting!");
liveClient.close();
Thread.sleep(5000);
LOGGER.debug("Connecting!");
liveClient.connect();
Thread.sleep(5000);
LOGGER.debug("Disconnecting!");
liveClient.close();
Thread.sleep(5000);
}
@Test
public void _1_longTimeTest() throws Exception {
LiveClient liveClient = BILIBILI_API
.getLiveClient(ROOM_ID)
.setReconnectLimit(5)
.setReconnectDelay(1)
.registerListener(new Listener());
LOGGER.debug("Start long-time test");
LOGGER.debug("Connecting!");
liveClient.connect();
Thread.sleep(TEST_TIME);
LOGGER.debug("Disconnecting!");
liveClient.close();
Thread.sleep(5000);
}
private class Listener {
private final Logger LOGGER = LoggerFactory.getLogger(Listener.class);
@Subscribe
public void activityEvent(ActivityEventPackageEvent activityEventPackageEvent) {
ActivityEventEntity.Data data = activityEventPackageEvent.getActivityEventEntity().getData();
LOGGER.info("[ActivityEvent] keyword: {}, type: {}, progress: {}%",
data.getKeyword(),
data.getType(),
((float) data.getProgress() / data.getLimit()) * 100
);
}
@Subscribe
public void connectionClose(ConnectionCloseEvent connectionCloseEvent) {
LOGGER.info("[ConnectionClose] Connection closed");
}
@Subscribe
public void connectSucceed(ConnectSucceedEvent connectSucceedEvent) {
LOGGER.info("[ConnectSucceed] Connect succeed");
}
@Subscribe
public void danMuMsg(DanMuMsgPackageEvent danMuMsgPackageEvent) {
DanMuMsgEntity danMuMsgEntity = danMuMsgPackageEvent.getDanMuMsgEntity();
StringBuilder stringBuilder = new StringBuilder("[DanMuMsg] ");
danMuMsgEntity.getFansMedalName().ifPresent(fansMedalName ->
stringBuilder.append(String.format("[%s %d] ", fansMedalName, danMuMsgEntity.getFansMedalLevel().get()))
);
List<String> userTitles = danMuMsgEntity.getUserTitles();
if (!userTitles.isEmpty()) {
stringBuilder.append(userTitles.get(0))
.append(" ");
}
stringBuilder.append(String.format("[UL %d] ", danMuMsgEntity.getUserLevel()));
stringBuilder.append(String.format("%s: ", danMuMsgEntity.getUsername()));
stringBuilder.append(danMuMsgEntity.getMessage());
LOGGER.info(stringBuilder.toString());
}
@Subscribe
public void live(LivePackageEvent livePackageEvent) {
LOGGER.info("[Live] Room {} start live", livePackageEvent.getLiveEntity().getRoomId());
}
@Subscribe
public void preparing(PreparingPackageEvent preparingPackageEvent) {
LOGGER.info("[Preparing] Room {} stop live", preparingPackageEvent.getPreparingEntity().getRoomId());
}
@Subscribe
public void sendGift(SendGiftPackageEvent sendGiftPackageEvent) {
SendGiftEntity.DataEntity dataEntity = sendGiftPackageEvent.getSendGiftEntity().getData();
LOGGER.info("[SendGift] {} give {}*{}",
dataEntity.getUserName(),
dataEntity.getGiftName(),
dataEntity.getNum()
);
}
@Subscribe
public void SysGift(SysGiftPackageEvent sysGiftPackageEvent) {
SysGiftEntity sysGiftEntity = sysGiftPackageEvent.getSysGiftEntity();
LOGGER.info("[SysGift] {}: {}",
sysGiftEntity.getMsg(),
sysGiftEntity.getUrl()
);
}
@Subscribe
public void SysMsg(SysMsgPackageEvent sysMsgPackageEvent) {
SysMsgEntity sysMsgEntity = sysMsgPackageEvent.getSysMsgEntity();
LOGGER.info("[SysMsg] {}: {}",
sysMsgEntity.getMsg(),
sysMsgEntity.getUrl()
);
}
@Subscribe
public void ViewerCount(ViewerCountPackageEvent viewerCountPackageEvent) {
LOGGER.info("[ViewerCount] {}", viewerCountPackageEvent.getViewerCount());
}
@Subscribe
public void WelcomeGuard(WelcomeGuardPackageEvent welcomeGuardPackageEvent) {
WelcomeGuardEntity.DataEntity dataEntity = welcomeGuardPackageEvent.getWelcomeGuardEntity().getData();
LOGGER.info("[WelcomeGuard] [GL {}] {}",
dataEntity.getGuardLevel(),
dataEntity.getUsername()
);
}
@Subscribe
public void Welcome(WelcomePackageEvent welcomePackageEvent) {
WelcomeEntity.DataEntity dataEntity = welcomePackageEvent.getWelcomeEntity().getData();
StringBuilder stringBuilder = new StringBuilder("[Welcome] ");
if (dataEntity.isAdmin()) {
stringBuilder.append("[ADMIN] ");
}
stringBuilder.append(String.format("[VIP %d] ", dataEntity.getVip()))
.append(dataEntity.getUserName());
LOGGER.info(stringBuilder.toString());
}
}
}

View File

@ -14,7 +14,8 @@ import java.io.InputStreamReader;
@RunWith(Suite.class)
@Suite.SuiteClasses({
UserInfoTest.class
UserInfoTest.class,
LiveClientTest.class
})
public class RuleSuite {
@ClassRule

View File

@ -11,13 +11,12 @@ import org.slf4j.LoggerFactory;
public class UserInfoTest {
private static final Logger LOGGER = LoggerFactory.getLogger(UserInfoTest.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final BilibiliAPI bilibiliAPI = Config.getBilibiliAPI();
private static final BilibiliAPI BILIBILI_API = Config.getBilibiliAPI();
@Test
public void getUserInfo() throws Exception {
InfoEntity infoEntity = bilibiliAPI.getPassportService()
.getInfo(bilibiliAPI.getBilibiliAccount().getAccessToken())
InfoEntity infoEntity = BILIBILI_API.getPassportService()
.getInfo(BILIBILI_API.getBilibiliAccount().getAccessToken())
.execute()
.body();
LOGGER.debug("UserInfo below: \n{}", GSON.toJson(infoEntity));