|
|
@@ -0,0 +1,244 @@
|
|
|
+package top.lvzhiqiang.service.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Propagation;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.StopWatch;
|
|
|
+import top.lvzhiqiang.config.InitRunner;
|
|
|
+import top.lvzhiqiang.entity.CoinYoutubeYt2140Live;
|
|
|
+import top.lvzhiqiang.entity.CoinYoutubeYt2140LiveChapter;
|
|
|
+import top.lvzhiqiang.enumeration.ResultCodeEnum;
|
|
|
+import top.lvzhiqiang.exception.BusinessException;
|
|
|
+import top.lvzhiqiang.mapper.CoinYoutubeMapper;
|
|
|
+import top.lvzhiqiang.service.CoinYoutubeService;
|
|
|
+import top.lvzhiqiang.util.DateUtils;
|
|
|
+import top.lvzhiqiang.util.SpringUtils;
|
|
|
+import top.lvzhiqiang.util.StringUtils;
|
|
|
+import top.lvzhiqiang.util.UUIDUtils;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStreamReader;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Coin youtube Service
|
|
|
+ *
|
|
|
+ * @author lvzhiqiang
|
|
|
+ * 2025/5/15 10:50
|
|
|
+ */
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+public class CoinYoutubeServiceImpl implements CoinYoutubeService {
|
|
|
+ @Resource
|
|
|
+ private CoinYoutubeMapper coinYoutubeMapper;
|
|
|
+ @Value("${spring.profiles.active}")
|
|
|
+ private String env;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Async("coinTaskExecutor")
|
|
|
+ public void jsoupYoutubeLive4yt2140() throws IOException, InterruptedException {
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140 开始:");
|
|
|
+ StopWatch stopWatch = new StopWatch();
|
|
|
+ stopWatch.start();
|
|
|
+
|
|
|
+ CoinYoutubeYt2140Live coinYoutubeYt2140Live = coinYoutubeMapper.findLatestYt2140Live();
|
|
|
+ String latestOriginalUrl;
|
|
|
+ if (coinYoutubeYt2140Live == null) {
|
|
|
+ latestOriginalUrl = "";
|
|
|
+ } else {
|
|
|
+ latestOriginalUrl = coinYoutubeYt2140Live.getOriginalUrl();
|
|
|
+ }
|
|
|
+
|
|
|
+ String youtubeYt2140LiveUrl = InitRunner.dicCodeMap.get("youtube_yt2140_live_url").getCodeValue();
|
|
|
+ List<String> liveStreams = getChannelLiveStreams(youtubeYt2140LiveUrl);
|
|
|
+
|
|
|
+ int findCount = 0;
|
|
|
+ for (String originalUrl : liveStreams) {
|
|
|
+ if (originalUrl.equals(latestOriginalUrl)) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ CoinYoutubeYt2140Live live = new CoinYoutubeYt2140Live();
|
|
|
+ live.setId(UUIDUtils.getUUID());
|
|
|
+ live.setOriginalUrl(originalUrl);
|
|
|
+ live.setStatus(3);
|
|
|
+ int count = coinYoutubeMapper.insertIgnoreYt2140Live(live);
|
|
|
+ findCount += count;
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140 live success:originalUrl={},count={}", originalUrl, count);
|
|
|
+ }
|
|
|
+
|
|
|
+ stopWatch.stop();
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140 结束:findCount={},time={}", findCount, stopWatch.getTotalTimeMillis());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<String> getChannelLiveStreams(String channelUrl) throws IOException, InterruptedException {
|
|
|
+ List<String> liveUrls = new ArrayList<>();
|
|
|
+
|
|
|
+ String proxyStr = "";
|
|
|
+ if ("dev".equals(env)) {
|
|
|
+ proxyStr = "--proxy socks5://127.0.0.1:1081/";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建命令
|
|
|
+ String command = "yt-dlp " + proxyStr + " --flat-playlist --print url " + channelUrl + "/streams";
|
|
|
+
|
|
|
+ Process process = Runtime.getRuntime().exec(command);
|
|
|
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
+
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ if (line.startsWith("https://www.youtube.com/watch")) {
|
|
|
+ liveUrls.add(line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ int exitCode = process.waitFor();
|
|
|
+ if (exitCode != 0) {
|
|
|
+ throw new RuntimeException("yt-dlp执行失败,退出码: " + exitCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ return liveUrls;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ //@Async("coinTaskExecutor")
|
|
|
+ public void jsoupYoutubeLive4yt2140Chapter(Integer status, String originalUrl, String id) {
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140Chapter 开始:status={}", status);
|
|
|
+
|
|
|
+ StopWatch stopWatch = new StopWatch();
|
|
|
+ stopWatch.start();
|
|
|
+
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
+ if (StringUtils.isNotEmpty(id)) {
|
|
|
+ params.put("id", id);
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotEmpty(originalUrl)) {
|
|
|
+ params.put("originalUrl", originalUrl);
|
|
|
+ }
|
|
|
+ if (status != null) {
|
|
|
+ params.put("status", status);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<CoinYoutubeYt2140Live> coinYoutubeYt2140LiveList = coinYoutubeMapper.findJsoupYt2140LiveListByParams(params);
|
|
|
+ if (coinYoutubeYt2140LiveList.isEmpty()) {
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140Chapter 结束:coinYoutubeYt2140LiveList is empty");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ int successCount = 0;
|
|
|
+ int failCount = 0;
|
|
|
+ for (CoinYoutubeYt2140Live coinYoutubeYt2140Live : coinYoutubeYt2140LiveList) {
|
|
|
+ try {
|
|
|
+ Thread.sleep(5000L);
|
|
|
+
|
|
|
+ SpringUtils.getBean(CoinYoutubeServiceImpl.class).jsoupYoutubeLive4yt2140ChapterSub(coinYoutubeYt2140Live);
|
|
|
+ coinYoutubeYt2140Live.setStatus(1);
|
|
|
+ successCount++;
|
|
|
+ } catch (Exception e) {
|
|
|
+ coinYoutubeYt2140Live.setFailureCause(e.getMessage().length() > 400 ? e.getMessage().substring(0, 400) : e.getMessage());
|
|
|
+
|
|
|
+ if (e.getMessage().contains("no chapters")) {
|
|
|
+ coinYoutubeYt2140Live.setStatus(4);
|
|
|
+ } else {
|
|
|
+ coinYoutubeYt2140Live.setStatus(2);
|
|
|
+ }
|
|
|
+
|
|
|
+ failCount++;
|
|
|
+ } finally {
|
|
|
+ coinYoutubeMapper.insertOrUpdateYt2140Live(coinYoutubeYt2140Live);
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140Chapter update status:originalUrl={},status={}", coinYoutubeYt2140Live.getOriginalUrl(), coinYoutubeYt2140Live.getStatus());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ stopWatch.stop();
|
|
|
+ log.warn("jsoupFulibaPicDetail 结束:totalSize={},successCount={},failCount={},time={}", coinYoutubeYt2140LiveList.size(), successCount, failCount, stopWatch.getTotalTimeMillis());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
|
|
|
+ public void jsoupYoutubeLive4yt2140ChapterSub(CoinYoutubeYt2140Live coinYoutubeYt2140Live) {
|
|
|
+ List<CoinYoutubeYt2140LiveChapter> yt2140LiveChapterList = new ArrayList<>();
|
|
|
+ try {
|
|
|
+ //log.warn("jsoupYoutubeLive4yt2140ChapterSub start:originalUrl={}", coinYoutubeYt2140Live.getOriginalUrl());
|
|
|
+
|
|
|
+ String videoDetailsJson = getVideoDetails(coinYoutubeYt2140Live.getOriginalUrl());
|
|
|
+ JSONObject result = JSONObject.parseObject(videoDetailsJson);
|
|
|
+
|
|
|
+ coinYoutubeYt2140Live.setFullTitle(result.getString("fulltitle"));
|
|
|
+ coinYoutubeYt2140Live.setPublishTime(LocalDate.parse(result.getString("release_date"), DateUtils.dateFormatter_));
|
|
|
+ coinYoutubeYt2140Live.setDuration(result.getString("duration_string"));
|
|
|
+
|
|
|
+ JSONArray chapterArr = result.getJSONArray("chapters");
|
|
|
+ if (chapterArr == null || chapterArr.isEmpty()) {
|
|
|
+ if (coinYoutubeYt2140Live.getPublishTime().plusDays(7).isBefore(LocalDate.now())){
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140ChapterSub chapters is null or empty,no chapters,originalUrl={}", coinYoutubeYt2140Live.getOriginalUrl());
|
|
|
+ throw new BusinessException(ResultCodeEnum.UNKNOWN_ERROR.getCode(), "chapters is null or empty,no chapters");
|
|
|
+ }else{
|
|
|
+ log.warn("jsoupYoutubeLive4yt2140ChapterSub chapters is null or empty,not updated,originalUrl={}", coinYoutubeYt2140Live.getOriginalUrl());
|
|
|
+ throw new BusinessException(ResultCodeEnum.UNKNOWN_ERROR.getCode(), "chapters is null or empty,not updated");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ coinYoutubeYt2140Live.setChapterCount(chapterArr.size());
|
|
|
+ for (int i = 0; i < chapterArr.size(); i++) {
|
|
|
+ JSONObject chapter = chapterArr.getJSONObject(i);
|
|
|
+
|
|
|
+ CoinYoutubeYt2140LiveChapter yt2140LiveChapter = new CoinYoutubeYt2140LiveChapter();
|
|
|
+ yt2140LiveChapter.setChapterTitle(chapter.getString("title"));
|
|
|
+ yt2140LiveChapter.setLiveId(coinYoutubeYt2140Live.getId());
|
|
|
+ yt2140LiveChapter.setStartTime(chapter.getInteger("start_time"));
|
|
|
+ yt2140LiveChapter.setEndTime(chapter.getInteger("end_time"));
|
|
|
+ yt2140LiveChapter.setSort(i + 1);
|
|
|
+
|
|
|
+ yt2140LiveChapterList.add(yt2140LiveChapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ coinYoutubeMapper.insertIgnoreYt2140LiveChapterList(yt2140LiveChapterList);
|
|
|
+ } catch (Exception e) {
|
|
|
+ if (!(e instanceof BusinessException)) {
|
|
|
+ log.error("jsoupYoutubeLive4yt2140ChapterSub exception,originalUrl={}", coinYoutubeYt2140Live.getOriginalUrl(), e);
|
|
|
+ }
|
|
|
+ throw new BusinessException(30000, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getVideoDetails(String videoUrl) throws IOException, InterruptedException {
|
|
|
+ String proxyStr = "";
|
|
|
+ if ("dev".equals(env)) {
|
|
|
+ proxyStr = "--proxy socks5://127.0.0.1:1081/";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建命令:yt-dlp --dump-json 获取视频元数据(JSON格式)
|
|
|
+ String command = "yt-dlp " + proxyStr + " --dump-json " + videoUrl;
|
|
|
+
|
|
|
+ Process process = Runtime.getRuntime().exec(command);
|
|
|
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ StringBuilder jsonOutput = new StringBuilder();
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ jsonOutput.append(line);
|
|
|
+ }
|
|
|
+
|
|
|
+ int exitCode = process.waitFor();
|
|
|
+ if (exitCode != 0) {
|
|
|
+ throw new RuntimeException("yt-dlp 执行失败,退出码: " + exitCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ return jsonOutput.toString(); // 返回 JSON 格式的视频详情
|
|
|
+ }
|
|
|
+}
|