直播流下载功能移步至LiveRecord模块下,删除了Creeper模块下的视频下载功能

This commit is contained in:
suifeng
2023-05-21 22:07:40 +08:00
parent 79ae63c176
commit 7c9dc7cf4f
95 changed files with 61 additions and 554 deletions

View File

@@ -63,12 +63,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>BarrageModule</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>

View File

@@ -1,7 +1,7 @@
package org.example;
import org.example.danmaku.core.manager.LoadTaskManager;
import org.example.danmaku.pojo.download.assign.BilibiliLiveLoadConfig;
import org.example.core.manager.LoadTaskManager;
import org.example.pojo.download.assign.BilibiliLiveLoadConfig;
public class Test {

View File

@@ -1,4 +1,4 @@
package org.example.danmaku.core.control;
package org.example.core.control;
/**
* 弹幕下载任务

View File

@@ -1,12 +1,12 @@
package org.example.danmaku.core.control.impl;
package org.example.core.control.impl;
import org.example.constpool.ConstPool;
import org.example.danmaku.core.control.LoadTask;
import org.example.danmaku.core.factory.ProcessorFactory;
import org.example.danmaku.core.pipeline.PipelineWriteJson;
import org.example.danmaku.core.processor.BilibiliLiveProcessor;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.danmaku.utils.CreeperConfig;
import org.example.core.control.LoadTask;
import org.example.core.factory.ProcessorFactory;
import org.example.core.pipeline.PipelineWriteJson;
import org.example.core.processor.BilibiliLiveProcessor;
import org.example.pojo.download.LoadConfig;
import org.example.utils.CreeperConfig;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Spider;

View File

@@ -1,12 +1,12 @@
package org.example.danmaku.core.control.impl;
package org.example.core.control.impl;
import org.example.constpool.ConstPool;
import org.example.danmaku.core.control.LoadTask;
import org.example.danmaku.core.factory.ProcessorFactory;
import org.example.danmaku.core.pipeline.PipelineWriteJson;
import org.example.danmaku.core.processor.DouyuRecordProcessor;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.danmaku.utils.CreeperConfig;
import org.example.core.control.LoadTask;
import org.example.core.factory.ProcessorFactory;
import org.example.core.pipeline.PipelineWriteJson;
import org.example.core.processor.DouyuRecordProcessor;
import org.example.pojo.download.LoadConfig;
import org.example.utils.CreeperConfig;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Spider;

View File

@@ -1,12 +1,12 @@
package org.example.danmaku.core.factory;
package org.example.core.factory;
import org.example.danmaku.core.control.LoadTask;
import org.example.danmaku.core.control.impl.BilibiliLiveLoadTask;
import org.example.danmaku.core.control.impl.DouyuRecordLoadTask;
import org.example.core.control.LoadTask;
import org.example.core.control.impl.BilibiliLiveLoadTask;
import org.example.core.control.impl.DouyuRecordLoadTask;
import org.example.exception.FileCacheException;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.danmaku.pojo.download.assign.BilibiliLiveLoadConfig;
import org.example.danmaku.pojo.download.assign.DouyuRecordLoadConfig;
import org.example.pojo.download.LoadConfig;
import org.example.pojo.download.assign.BilibiliLiveLoadConfig;
import org.example.pojo.download.assign.DouyuRecordLoadConfig;
/**
* 弹幕下载任务工厂

View File

@@ -1,12 +1,12 @@
package org.example.danmaku.core.factory;
package org.example.core.factory;
import org.example.danmaku.core.processor.AbstractProcessor;
import org.example.danmaku.core.processor.BilibiliLiveProcessor;
import org.example.danmaku.core.processor.DouyuRecordProcessor;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.danmaku.pojo.download.assign.BilibiliLiveLoadConfig;
import org.example.danmaku.pojo.download.assign.DouyuRecordLoadConfig;
import org.example.danmaku.utils.CreeperConfig;
import org.example.core.processor.AbstractProcessor;
import org.example.core.processor.BilibiliLiveProcessor;
import org.example.core.processor.DouyuRecordProcessor;
import org.example.pojo.download.LoadConfig;
import org.example.pojo.download.assign.BilibiliLiveLoadConfig;
import org.example.pojo.download.assign.DouyuRecordLoadConfig;
import org.example.utils.CreeperConfig;
/**
* 处理器工厂

View File

@@ -1,9 +1,9 @@
package org.example.danmaku.core.manager;
package org.example.core.manager;
import org.example.danmaku.core.control.LoadTask;
import org.example.danmaku.core.factory.LoadTaskFactory;
import org.example.core.control.LoadTask;
import org.example.core.factory.LoadTaskFactory;
import org.example.exception.FileCacheException;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.pojo.download.LoadConfig;
import java.util.Collections;
import java.util.Map;

View File

@@ -1,10 +1,10 @@
package org.example.danmaku.core.pipeline;
package org.example.core.pipeline;
import org.example.cache.FileCache;
import org.example.exception.FileCacheException;
import org.example.danmaku.pojo.Barrage;
import org.example.danmaku.pojo.configfile.BarrageSaveFile;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.pojo.Barrage;
import org.example.pojo.configfile.BarrageSaveFile;
import org.example.pojo.download.LoadConfig;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

View File

@@ -1,4 +1,4 @@
package org.example.danmaku.core.processor;
package org.example.core.processor;
import us.codecraft.webmagic.Page;

View File

@@ -1,10 +1,10 @@
package org.example.danmaku.core.processor;
package org.example.core.processor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.example.danmaku.pojo.Barrage;
import org.example.danmaku.pojo.download.assign.BilibiliLiveLoadConfig;
import org.example.pojo.Barrage;
import org.example.pojo.download.assign.BilibiliLiveLoadConfig;
import us.codecraft.webmagic.Page;
import java.util.ArrayList;

View File

@@ -1,10 +1,10 @@
package org.example.danmaku.core.processor;
package org.example.core.processor;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.example.danmaku.pojo.Barrage;
import org.example.danmaku.pojo.download.assign.DouyuRecordLoadConfig;
import org.example.pojo.Barrage;
import org.example.pojo.download.assign.DouyuRecordLoadConfig;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.utils.HttpConstant;

View File

@@ -1,4 +1,4 @@
package org.example.danmaku.pojo;
package org.example.pojo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;

View File

@@ -1,9 +1,9 @@
package org.example.danmaku.pojo.configfile;
package org.example.pojo.configfile;
import org.example.common.ConfigFile;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.pojo.download.LoadConfig;
import org.example.exception.FileCacheException;
import org.example.danmaku.pojo.Barrage;
import org.example.pojo.Barrage;
import org.example.util.FileUtil;
import org.example.util.JsonFileUtil;

View File

@@ -1,7 +1,7 @@
package org.example.danmaku.pojo.download;
package org.example.pojo.download;
import lombok.Data;
import org.example.danmaku.utils.FormatUtil;
import org.example.utils.FormatUtil;
/**
* 单次弹幕爬取信息配置基类

View File

@@ -1,8 +1,8 @@
package org.example.danmaku.pojo.download.assign;
package org.example.pojo.download.assign;
import lombok.Data;
import org.example.constpool.ConstPool;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.pojo.download.LoadConfig;
/**
* (B站直播)配置信息

View File

@@ -1,8 +1,8 @@
package org.example.danmaku.pojo.download.assign;
package org.example.pojo.download.assign;
import lombok.Data;
import org.example.constpool.ConstPool;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.pojo.download.LoadConfig;
/**
* (斗鱼录播)配置信息

View File

@@ -1,4 +1,4 @@
package org.example.danmaku.utils;
package org.example.utils;
import java.io.IOException;
import java.io.InputStream;

View File

@@ -1,4 +1,4 @@
package org.example.danmaku.utils;
package org.example.utils;
import java.text.SimpleDateFormat;
import java.util.Date;

View File

@@ -1,86 +0,0 @@
package org.example.video;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import org.example.video.core.handle.FlvHandle;
import org.example.video.core.monitor.StatusMonitor;
import org.example.video.core.parser.BilibiliFlvUrlParse;
/**
*
* @author 燧枫
* @date 2023/5/18 22:23
*/
public class BilibiliLiveStreamTest {
static String videoPath = "F:\\";
public void startFlvStreamParse(String url, StatusMonitor statusMonitor, OutputStream fileIO, Map<String, String> headers) {
new Thread(() -> {
FlvHandle f = new FlvHandle();
try {
URLConnection conn = new URL(url).openConnection();
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
}
InputStream in = conn.getInputStream();
f.parseStream(in, statusMonitor, fileIO);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public void startFlvStreamParse(String url, StatusMonitor statusMonitor, OutputStream fileIO) {
Map<String, String> defaultHeaders = new HashMap<>();
defaultHeaders.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36");
defaultHeaders.put("Origin", "https://live.bilibili.com");
defaultHeaders.put("Referer", "https://live.bilibili.com/");
startFlvStreamParse(url, statusMonitor, fileIO, defaultHeaders);
}
public static void main(String[] args) {
String roomId = "732";
int qn = 10000;
StatusMonitor statusMonitor = new StatusMonitor();
BilibiliLiveStreamTest bilibiliLiveStreamTest = new BilibiliLiveStreamTest();
BilibiliFlvUrlParse bilibiliFlvUrlParse = new BilibiliFlvUrlParse();
try {
String url = bilibiliFlvUrlParse.getFlvUrl(roomId, qn);
System.out.println(url);
OutputStream fileIO = new FileOutputStream(videoPath + roomId + ".flv");
bilibiliLiveStreamTest.startFlvStreamParse(url, statusMonitor, fileIO);
while (true) {
// 注意我们在这里没有包含 isStopFlag 和 getVideoTagSpeed 方法,
// 因为这些方法的实现可能需要更复杂的逻辑
if (statusMonitor.isConnectionClosed()) {
System.out.println("连接中断,已停止录制...");
break;
}
// 输出实时的下载状态
System.out.println("平均下载速度:" + statusMonitor.getDownloadSpeedAvg() + " B/s");
System.out.println("瞬时下载速度:" + statusMonitor.getDownloadSpeed() + " B/s");
System.out.println("已写入数据量:" + statusMonitor.getDownloadedBytes() + " bytes");
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -1,36 +0,0 @@
package org.example.video.core.handle;
import org.example.video.core.monitor.StatusMonitor;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Flv下载器
* @author 燧枫
* @date 2023/5/19 0:22
*/
public class FlvHandle {
public void parseStream(InputStream in, StatusMonitor statusMonitor, OutputStream out) {
try {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
statusMonitor.addDownloadedBytes(bytesRead);
}
} catch (Exception e) {
statusMonitor.setConnectionClosed(true);
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@@ -1,46 +0,0 @@
package org.example.video.core.monitor;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author 燧枫
* @date 2023/5/19 0:00
*/
public class StatusMonitor {
private long downloadedBytes = 0;
private boolean connectionClosed = false;
private long startTime = System.currentTimeMillis();
private Queue<Long> recentBytes = new LinkedList<>();
private static final int QUEUE_SIZE = 10; // 储存最近10秒的下载字节数
public synchronized void addDownloadedBytes(int bytes) {
downloadedBytes += bytes;
recentBytes.offer((long) bytes);
if (recentBytes.size() > QUEUE_SIZE) {
recentBytes.poll();
}
}
public synchronized long getDownloadedBytes() {
return downloadedBytes;
}
public synchronized double getDownloadSpeed() { // 瞬时下载速度,单位:字节每秒
return recentBytes.stream().mapToLong(Long::longValue).sum();
}
public synchronized double getDownloadSpeedAvg() { // 平均下载速度,单位:字节每秒
long elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000;
return (double) downloadedBytes / Math.max(1, elapsedSeconds);
}
public synchronized void setConnectionClosed(boolean connectionClosed) {
this.connectionClosed = connectionClosed;
}
public synchronized boolean isConnectionClosed() {
return connectionClosed;
}
}

View File

@@ -1,55 +0,0 @@
package org.example.video.core.parser;
import org.example.video.utils.HttpClientUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
/**
* b站flv链接解析器
*
* @author 燧枫
* @date 2023/5/16 20:42
*/
public class BilibiliFlvUrlParse {
String urlFormat = "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=%s&protocol=0,1&format=0,1,2&codec=0,1&qn=%d&platform=web&ptype=8";
// 通过房间号roomId,qn(画质,10000为原画画质),得到flv链接
public String getFlvUrl(String roomId, int qn) throws Exception {
String url = String.format(urlFormat, roomId, qn);
String response = HttpClientUtil.get(url);
JSONObject js = new JSONObject(response);
if (js.getInt("code") != 0) {
throw new Exception(js.getString("message"));
}
if (js.getJSONObject("data").getInt("live_status") != 1) {
throw new Exception("主播未开播或已下播");
}
JSONObject data = js.getJSONObject("data");
JSONArray streamList = data.getJSONObject("playurl_info").getJSONObject("playurl").getJSONArray("stream");
for (int i = 0; i < streamList.length(); i++) {
JSONObject stream = streamList.getJSONObject(i);
if ("http_stream".equals(stream.getString("protocol_name"))) {
JSONArray formatList = stream.getJSONArray("format");
for (int j = 0; j < formatList.length(); j++) {
JSONObject format = formatList.getJSONObject(j);
if ("flv".equals(format.getString("format_name"))) {
String host = format.getJSONArray("codec").getJSONObject(0).getJSONArray("url_info").getJSONObject(0).getString("host");
String extra = format.getJSONArray("codec").getJSONObject(0).getJSONArray("url_info").getJSONObject(0).getString("extra");
String baseUrl = format.getJSONArray("codec").getJSONObject(0).getString("base_url");
return host + baseUrl + extra;
}
}
}
}
throw new Exception("没有找到直播流地址");
}
}

View File

@@ -1,36 +0,0 @@
package org.example.video.pool;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
/**
* http请求链接池
* @author 燧枫
* @date 2023/5/16 19:22
*/
public class HttpClientPool {
private static final int MAX_TOTAL_CONNECTIONS = 100;
private static final int DEFAULT_MAX_PER_ROUTE = 20;
private static HttpClientPool instance;
private final CloseableHttpClient httpClient;
private HttpClientPool() {
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager();
connManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
connManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE);
this.httpClient = HttpClients.custom().setConnectionManager(connManager).build();
}
public static synchronized HttpClientPool getInstance() {
if (instance == null) {
instance = new HttpClientPool();
}
return instance;
}
public CloseableHttpClient getHttpClient() {
return httpClient;
}
}

View File

@@ -1,83 +0,0 @@
package org.example.video.utils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.example.video.pool.HttpClientPool;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* 简单的get,post请求工具类
* @author 燧枫
* @date 2023/5/16 19:24
*/
public class HttpClientUtil {
private static final CloseableHttpClient httpClient = HttpClientPool.getInstance().getHttpClient();
// get请求
public static String get(String url) {
return get(url, null);
}
// post请求
public static String post(String url, String json) {
return post(url, json, null);
}
// get请求带请求头
public static String get(String url, Map<String, String> headers) {
HttpGet httpGet = new HttpGet(url);
addHeaders(httpGet, headers);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// post请求带请求头
public static String post(String url, String json, Map<String, String> headers) {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
addHeaders(httpPost, headers);
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
return handleResponse(response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 统一处理请求头
private static void addHeaders(HttpRequest httpRequest, Map<String, String> headers) {
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
httpRequest.addHeader(entry.getKey(), entry.getValue());
}
}
}
// 统一封装成response
private static String handleResponse(HttpResponse response) {
HttpEntity entity = response.getEntity();
if (entity != null) {
try {
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Response entity is null");
}
}
}

View File

@@ -1,8 +1,8 @@
package org.example;
import org.example.exception.FileCacheException;
import org.example.danmaku.pojo.configfile.BarrageSaveFile;
import org.example.danmaku.pojo.download.LoadConfig;
import org.example.pojo.configfile.BarrageSaveFile;
import org.example.pojo.download.LoadConfig;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ConcurrentLinkedQueue;

View File

@@ -1,15 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="us.codecraft.webmagic" level="error" additivity="false">
<appender-ref ref="STDOUT" />
</logger>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@@ -1,13 +0,0 @@
dy.userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.48
dy.retryTimes=3
dy.retrySleepTime=100
dy.threadCnt=5
dy.emptySleepTime=100
dy.sleepTime=100
bi.userAgent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.48
bi.retryTimes=1
bi.retrySleepTime=10
bi.threadCnt=1
bi.emptySleepTime=10
bi.sleepTime=10