From c306458c7ecf4119621e2677f4d84fbaf6000331 Mon Sep 17 00:00:00 2001 From: Guoyiting <654525901@qq.com> Date: Sat, 30 May 2026 00:11:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=9F=B3=E4=B9=90=E7=88=AC?= =?UTF-8?q?=E8=99=AB=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logs/crawler.log | 65 ++++++++ pom.xml | 59 ++++++++ src/main/java/com/music/App.java | 19 +++ .../com/music/command/AnalyzeCommand.java | 36 +++++ src/main/java/com/music/command/Command.java | 8 + .../java/com/music/command/CrawlCommand.java | 52 +++++++ .../java/com/music/command/ExitCommand.java | 23 +++ .../java/com/music/command/HelpCommand.java | 22 +++ .../com/music/command/HistoryCommand.java | 32 ++++ .../java/com/music/command/ListCommand.java | 22 +++ .../java/com/music/command/SaveCommand.java | 38 +++++ .../music/controller/CrawlerController.java | 59 ++++++++ .../com/music/exception/CrawlerException.java | 10 ++ .../com/music/exception/NetworkException.java | 10 ++ .../com/music/exception/ParseException.java | 10 ++ src/main/java/com/music/model/Song.java | 55 +++++++ .../com/music/repository/SongRepository.java | 36 +++++ .../com/music/service/AnalyzerService.java | 55 +++++++ .../com/music/strategy/CrawlStrategy.java | 11 ++ .../com/music/strategy/KuGouStrategy.java | 120 +++++++++++++++ .../com/music/strategy/NetEaseStrategy.java | 142 ++++++++++++++++++ .../java/com/music/strategy/QQStrategy.java | 104 +++++++++++++ .../com/music/strategy/StrategyFactory.java | 23 +++ src/main/java/com/music/util/CsvUtil.java | 28 ++++ src/main/java/com/music/util/RetryUtils.java | 26 ++++ src/main/java/com/music/view/ConsoleView.java | 115 ++++++++++++++ src/main/resources/logback.xml | 23 +++ target/classes/com/music/App.class | Bin 0 -> 1129 bytes .../com/music/command/AnalyzeCommand.class | Bin 0 -> 2144 bytes .../classes/com/music/command/Command.class | Bin 0 -> 234 bytes .../com/music/command/CrawlCommand.class | Bin 0 -> 3381 bytes .../com/music/command/ExitCommand.class | Bin 0 -> 929 bytes .../com/music/command/HelpCommand.class | Bin 0 -> 838 bytes .../com/music/command/HistoryCommand.class | Bin 0 -> 1910 bytes .../com/music/command/ListCommand.class | Bin 0 -> 948 bytes .../com/music/command/SaveCommand.class | Bin 0 -> 2309 bytes .../music/controller/CrawlerController.class | Bin 0 -> 4013 bytes .../music/exception/CrawlerException.class | Bin 0 -> 554 bytes .../music/exception/NetworkException.class | Bin 0 -> 660 bytes .../com/music/exception/ParseException.class | Bin 0 -> 565 bytes target/classes/com/music/model/Song.class | Bin 0 -> 2449 bytes .../com/music/repository/SongRepository.class | Bin 0 -> 1865 bytes .../com/music/service/AnalyzerService.class | Bin 0 -> 5627 bytes .../com/music/strategy/CrawlStrategy.class | Bin 0 -> 380 bytes .../com/music/strategy/KuGouStrategy.class | Bin 0 -> 6716 bytes .../com/music/strategy/NetEaseStrategy.class | Bin 0 -> 8616 bytes .../com/music/strategy/QQStrategy.class | Bin 0 -> 6239 bytes .../com/music/strategy/StrategyFactory.class | Bin 0 -> 1298 bytes target/classes/com/music/util/CsvUtil.class | Bin 0 -> 2134 bytes .../util/RetryUtils$ThrowingAction.class | Bin 0 -> 468 bytes .../classes/com/music/util/RetryUtils.class | Bin 0 -> 1779 bytes .../classes/com/music/view/ConsoleView.class | Bin 0 -> 7148 bytes target/classes/logback.xml | 23 +++ 53 files changed, 1226 insertions(+) create mode 100644 logs/crawler.log create mode 100644 pom.xml create mode 100644 src/main/java/com/music/App.java create mode 100644 src/main/java/com/music/command/AnalyzeCommand.java create mode 100644 src/main/java/com/music/command/Command.java create mode 100644 src/main/java/com/music/command/CrawlCommand.java create mode 100644 src/main/java/com/music/command/ExitCommand.java create mode 100644 src/main/java/com/music/command/HelpCommand.java create mode 100644 src/main/java/com/music/command/HistoryCommand.java create mode 100644 src/main/java/com/music/command/ListCommand.java create mode 100644 src/main/java/com/music/command/SaveCommand.java create mode 100644 src/main/java/com/music/controller/CrawlerController.java create mode 100644 src/main/java/com/music/exception/CrawlerException.java create mode 100644 src/main/java/com/music/exception/NetworkException.java create mode 100644 src/main/java/com/music/exception/ParseException.java create mode 100644 src/main/java/com/music/model/Song.java create mode 100644 src/main/java/com/music/repository/SongRepository.java create mode 100644 src/main/java/com/music/service/AnalyzerService.java create mode 100644 src/main/java/com/music/strategy/CrawlStrategy.java create mode 100644 src/main/java/com/music/strategy/KuGouStrategy.java create mode 100644 src/main/java/com/music/strategy/NetEaseStrategy.java create mode 100644 src/main/java/com/music/strategy/QQStrategy.java create mode 100644 src/main/java/com/music/strategy/StrategyFactory.java create mode 100644 src/main/java/com/music/util/CsvUtil.java create mode 100644 src/main/java/com/music/util/RetryUtils.java create mode 100644 src/main/java/com/music/view/ConsoleView.java create mode 100644 src/main/resources/logback.xml create mode 100644 target/classes/com/music/App.class create mode 100644 target/classes/com/music/command/AnalyzeCommand.class create mode 100644 target/classes/com/music/command/Command.class create mode 100644 target/classes/com/music/command/CrawlCommand.class create mode 100644 target/classes/com/music/command/ExitCommand.class create mode 100644 target/classes/com/music/command/HelpCommand.class create mode 100644 target/classes/com/music/command/HistoryCommand.class create mode 100644 target/classes/com/music/command/ListCommand.class create mode 100644 target/classes/com/music/command/SaveCommand.class create mode 100644 target/classes/com/music/controller/CrawlerController.class create mode 100644 target/classes/com/music/exception/CrawlerException.class create mode 100644 target/classes/com/music/exception/NetworkException.class create mode 100644 target/classes/com/music/exception/ParseException.class create mode 100644 target/classes/com/music/model/Song.class create mode 100644 target/classes/com/music/repository/SongRepository.class create mode 100644 target/classes/com/music/service/AnalyzerService.class create mode 100644 target/classes/com/music/strategy/CrawlStrategy.class create mode 100644 target/classes/com/music/strategy/KuGouStrategy.class create mode 100644 target/classes/com/music/strategy/NetEaseStrategy.class create mode 100644 target/classes/com/music/strategy/QQStrategy.class create mode 100644 target/classes/com/music/strategy/StrategyFactory.class create mode 100644 target/classes/com/music/util/CsvUtil.class create mode 100644 target/classes/com/music/util/RetryUtils$ThrowingAction.class create mode 100644 target/classes/com/music/util/RetryUtils.class create mode 100644 target/classes/com/music/view/ConsoleView.class create mode 100644 target/classes/logback.xml diff --git a/logs/crawler.log b/logs/crawler.log new file mode 100644 index 0000000..0c1d27c --- /dev/null +++ b/logs/crawler.log @@ -0,0 +1,65 @@ +2026-05-29 23:18:06.182 [main] INFO com.music.strategy.NetEaseStrategy - 开始爬取网易云热歌榜,限制 50 首 +2026-05-29 23:18:07.033 [main] ERROR com.music.strategy.NetEaseStrategy - 网易云爬取失败 +java.lang.NullPointerException: Cannot invoke "com.google.gson.JsonObject.getAsJsonArray(String)" because "result" is null + at com.music.strategy.NetEaseStrategy.crawl(NetEaseStrategy.java:35) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +2026-05-29 23:18:07.036 [main] ERROR com.music.command.CrawlCommand - 爬取异常 +com.music.exception.ParseException: 解析网易云数据失败: Cannot invoke "com.google.gson.JsonObject.getAsJsonArray(String)" because "result" is null + at com.music.strategy.NetEaseStrategy.crawl(NetEaseStrategy.java:79) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +Caused by: java.lang.NullPointerException: Cannot invoke "com.google.gson.JsonObject.getAsJsonArray(String)" because "result" is null + at com.music.strategy.NetEaseStrategy.crawl(NetEaseStrategy.java:35) + ... 3 common frames omitted +2026-05-29 23:19:31.271 [main] INFO com.music.strategy.NetEaseStrategy - 开始爬取网易云热歌榜,限制 50 首 +2026-05-29 23:19:56.780 [main] INFO com.music.strategy.NetEaseStrategy - 网易云爬取完成,共 50 首 +2026-05-29 23:19:56.805 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=netease, 数量=50 +2026-05-29 23:21:00.898 [main] INFO com.music.command.AnalyzeCommand - 分析报告已生成,共 50 首歌曲 +2026-05-29 23:21:21.127 [main] INFO com.music.command.SaveCommand - 数据已保存到文件: result.csv +2026-05-29 23:25:29.304 [main] INFO com.music.strategy.QQStrategy - 开始爬取 QQ 音乐热歌榜,限制 50 首 +2026-05-29 23:25:30.367 [main] INFO com.music.strategy.QQStrategy - QQ音乐爬取完成,共 20 首 +2026-05-29 23:25:30.368 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=qq, 数量=20 +2026-05-29 23:26:13.206 [main] INFO com.music.strategy.KuGouStrategy - 开始爬取酷狗热歌榜,限制 50 首 +2026-05-29 23:26:13.691 [main] ERROR com.music.strategy.KuGouStrategy - 酷狗爬取失败,使用模拟数据 +com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.JsonParser.parseReader(JsonParser.java:76) + at com.google.gson.JsonParser.parseString(JsonParser.java:51) + at com.music.strategy.KuGouStrategy.crawl(KuGouStrategy.java:39) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1659) + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1465) + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:551) + at com.google.gson.stream.JsonReader.peek(JsonReader.java:433) + at com.google.gson.JsonParser.parseReader(JsonParser.java:71) + ... 5 common frames omitted +2026-05-29 23:26:13.695 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=kugou, 数量=8 +2026-05-29 23:27:34.126 [main] INFO com.music.strategy.KuGouStrategy - 开始爬取酷狗热歌榜,限制 50 首 +2026-05-29 23:27:34.611 [main] ERROR com.music.strategy.KuGouStrategy - 酷狗爬取失败,使用模拟数据 +com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.JsonParser.parseReader(JsonParser.java:76) + at com.google.gson.JsonParser.parseString(JsonParser.java:51) + at com.music.strategy.KuGouStrategy.crawl(KuGouStrategy.java:39) + at com.music.command.CrawlCommand.execute(CrawlCommand.java:40) + at com.music.controller.CrawlerController.start(CrawlerController.java:52) + at com.music.App.main(App.java:17) +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 12 path $ + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1659) + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1465) + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:551) + at com.google.gson.stream.JsonReader.peek(JsonReader.java:433) + at com.google.gson.JsonParser.parseReader(JsonParser.java:71) + ... 5 common frames omitted +2026-05-29 23:27:34.613 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=kugou, 数量=8 +2026-05-29 23:28:00.192 [main] INFO com.music.strategy.KuGouStrategy - 开始爬取酷狗热歌榜,限制 50 首 +2026-05-29 23:28:00.937 [main] INFO com.music.strategy.KuGouStrategy - 酷狗爬取完成,真实数据 22 首 +2026-05-29 23:28:00.939 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=kugou, 数量=22 +2026-05-29 23:28:29.618 [main] INFO com.music.command.AnalyzeCommand - 分析报告已生成,共 22 首歌曲 +2026-05-29 23:33:25.068 [main] INFO com.music.strategy.NetEaseStrategy - 开始爬取网易云热歌榜,限制 50 首 +2026-05-29 23:33:31.039 [main] INFO com.music.strategy.NetEaseStrategy - 网易云爬取完成,共 50 首 +2026-05-29 23:33:31.042 [main] INFO com.music.command.CrawlCommand - 爬取完成,平台=netease, 数量=50 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..69ca0ff --- /dev/null +++ b/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.musiccrawler + music-crawler + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + + org.jsoup + jsoup + 1.16.1 + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + com.google.code.gson + gson + 2.10.1 + + + + ch.qos.logback + logback-classic + 1.4.14 + + + org.jsoup + jsoup + 1.16.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + \ No newline at end of file diff --git a/src/main/java/com/music/App.java b/src/main/java/com/music/App.java new file mode 100644 index 0000000..ff4d454 --- /dev/null +++ b/src/main/java/com/music/App.java @@ -0,0 +1,19 @@ +package com.music; + +import com.music.controller.CrawlerController; +import com.music.repository.SongRepository; +import com.music.service.AnalyzerService; +import com.music.strategy.StrategyFactory; +import com.music.view.ConsoleView; + +public class App { + public static void main(String[] args) { + ConsoleView view = new ConsoleView(); + SongRepository repository = new SongRepository(); + StrategyFactory factory = new StrategyFactory(); + AnalyzerService analyzer = new AnalyzerService(); + + CrawlerController controller = new CrawlerController(view, repository, factory, analyzer); + controller.start(); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/AnalyzeCommand.java b/src/main/java/com/music/command/AnalyzeCommand.java new file mode 100644 index 0000000..81dca40 --- /dev/null +++ b/src/main/java/com/music/command/AnalyzeCommand.java @@ -0,0 +1,36 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.service.AnalyzerService; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AnalyzeCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(AnalyzeCommand.class); + private final ConsoleView view; + private final AnalyzerService analyzerService; + + public AnalyzeCommand(ConsoleView view, AnalyzerService analyzerService) { + this.view = view; + this.analyzerService = analyzerService; + } + + @Override + public String getName() { + return "analyze"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + var songs = repository.getAll(); + if (songs.isEmpty()) { + view.printError("暂无数据,请先执行 crawl 命令爬取歌曲。"); + return; + } + view.printInfo("正在分析数据..."); + var stats = analyzerService.analyze(songs); + view.displayAnalysis(stats); + logger.info("分析报告已生成,共 {} 首歌曲", songs.size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/Command.java b/src/main/java/com/music/command/Command.java new file mode 100644 index 0000000..7f0cbeb --- /dev/null +++ b/src/main/java/com/music/command/Command.java @@ -0,0 +1,8 @@ +package com.music.command; + +import com.music.repository.SongRepository; + +public interface Command { + String getName(); + void execute(String[] args, SongRepository repository); +} diff --git a/src/main/java/com/music/command/CrawlCommand.java b/src/main/java/com/music/command/CrawlCommand.java new file mode 100644 index 0000000..e0a865c --- /dev/null +++ b/src/main/java/com/music/command/CrawlCommand.java @@ -0,0 +1,52 @@ +package com.music.command; + +import com.music.exception.CrawlerException; +import com.music.repository.SongRepository; +import com.music.strategy.CrawlStrategy; +import com.music.strategy.StrategyFactory; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CrawlCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(CrawlCommand.class); + private final ConsoleView view; + private final StrategyFactory factory; + + public CrawlCommand(ConsoleView view, StrategyFactory factory) { + this.view = view; + this.factory = factory; + } + + @Override + public String getName() { + return "crawl"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + if (args.length < 2) { + view.printError("用法: crawl (netease/qq/kugou)"); + return; + } + String platform = args[1]; + CrawlStrategy strategy = factory.getStrategy(platform); + if (strategy == null) { + view.printError("不支持的平台: " + platform + ",可选:netease, qq, kugou"); + return; + } + view.printInfo("正在爬取 " + platform + " 热歌榜..."); + try { + var songs = strategy.crawl(50); // 爬取前50首 + repository.addAll(songs); + view.printSuccess(String.format("成功爬取 %d 首歌曲", songs.size())); + logger.info("爬取完成,平台={}, 数量={}", platform, songs.size()); + } catch (CrawlerException e) { + view.printError("爬取失败: " + e.getMessage()); + logger.error("爬取异常", e); + } catch (Exception e) { + view.printError("未知错误: " + e.getMessage()); + logger.error("未知异常", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/ExitCommand.java b/src/main/java/com/music/command/ExitCommand.java new file mode 100644 index 0000000..b0f1fe1 --- /dev/null +++ b/src/main/java/com/music/command/ExitCommand.java @@ -0,0 +1,23 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; + +public class ExitCommand implements Command { + private final ConsoleView view; + + public ExitCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "exit"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + view.printSuccess("再见!"); + System.exit(0); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/HelpCommand.java b/src/main/java/com/music/command/HelpCommand.java new file mode 100644 index 0000000..e73b2ad --- /dev/null +++ b/src/main/java/com/music/command/HelpCommand.java @@ -0,0 +1,22 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; + +public class HelpCommand implements Command { + private final ConsoleView view; + + public HelpCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "help"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + view.printHelp(); + } +} diff --git a/src/main/java/com/music/command/HistoryCommand.java b/src/main/java/com/music/command/HistoryCommand.java new file mode 100644 index 0000000..75ceb60 --- /dev/null +++ b/src/main/java/com/music/command/HistoryCommand.java @@ -0,0 +1,32 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; +import java.util.List; + +public class HistoryCommand implements Command { + private final ConsoleView view; + private final List history; + + public HistoryCommand(ConsoleView view, List history) { + this.view = view; + this.history = history; + } + + @Override + public String getName() { + return "history"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + if (history.isEmpty()) { + view.println("没有命令历史。"); + return; + } + view.println("最近输入的命令:"); + for (int i = 0; i < history.size(); i++) { + view.println(" " + (i + 1) + ". " + history.get(i)); + } + } +} diff --git a/src/main/java/com/music/command/ListCommand.java b/src/main/java/com/music/command/ListCommand.java new file mode 100644 index 0000000..4e6ef22 --- /dev/null +++ b/src/main/java/com/music/command/ListCommand.java @@ -0,0 +1,22 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.view.ConsoleView; + +public class ListCommand implements Command { + private final ConsoleView view; + + public ListCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "list"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + view.displaySongs(repository.getAll()); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/command/SaveCommand.java b/src/main/java/com/music/command/SaveCommand.java new file mode 100644 index 0000000..4f745e7 --- /dev/null +++ b/src/main/java/com/music/command/SaveCommand.java @@ -0,0 +1,38 @@ +package com.music.command; + +import com.music.repository.SongRepository; +import com.music.util.CsvUtil; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SaveCommand implements Command { + private static final Logger logger = LoggerFactory.getLogger(SaveCommand.class); + private final ConsoleView view; + + public SaveCommand(ConsoleView view) { + this.view = view; + } + + @Override + public String getName() { + return "save"; + } + + @Override + public void execute(String[] args, SongRepository repository) { + if (args.length < 2) { + view.printError("用法: save <文件名>"); + return; + } + String filename = args[1]; + try { + CsvUtil.saveToCsv(repository.getAll(), filename); + view.printSuccess("已保存到 " + filename); + logger.info("数据已保存到文件: {}", filename); + } catch (Exception e) { + view.printError("保存失败: " + e.getMessage()); + logger.error("保存CSV失败", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/controller/CrawlerController.java b/src/main/java/com/music/controller/CrawlerController.java new file mode 100644 index 0000000..4ab4bf1 --- /dev/null +++ b/src/main/java/com/music/controller/CrawlerController.java @@ -0,0 +1,59 @@ +package com.music.controller; + +import com.music.command.*; +import com.music.repository.SongRepository; +import com.music.service.AnalyzerService; +import com.music.strategy.StrategyFactory; +import com.music.view.ConsoleView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class CrawlerController { + private static final Logger logger = LoggerFactory.getLogger(CrawlerController.class); + private final Map commands = new HashMap<>(); + private final ConsoleView view; + private final SongRepository repository; + private final List history = new ArrayList<>(); + + public CrawlerController(ConsoleView view, SongRepository repository, + StrategyFactory factory, AnalyzerService analyzer) { + this.view = view; + this.repository = repository; + registerCommand(new HelpCommand(view)); + registerCommand(new ExitCommand(view)); + registerCommand(new ListCommand(view)); + registerCommand(new CrawlCommand(view, factory)); + registerCommand(new SaveCommand(view)); + registerCommand(new AnalyzeCommand(view, analyzer)); + registerCommand(new HistoryCommand(view, history)); + } + + private void registerCommand(Command cmd) { + commands.put(cmd.getName(), cmd); + } + + public void start() { + view.printSuccess("欢迎使用音乐爬虫系统 (CLI)"); + view.println("输入 help 查看所有命令。\n"); + while (true) { + String input = view.readLine().trim(); + if (input.isEmpty()) continue; + history.add(input); + String[] parts = input.split("\\s+"); + String cmdName = parts[0].toLowerCase(); + Command cmd = commands.get(cmdName); + if (cmd == null) { + view.printError("未知命令: " + cmdName + ",输入 help 查看帮助"); + continue; + } + try { + cmd.execute(parts, repository); + } catch (Exception e) { + view.printError("命令执行出错: " + e.getMessage()); + logger.error("命令执行异常", e); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/exception/CrawlerException.java b/src/main/java/com/music/exception/CrawlerException.java new file mode 100644 index 0000000..95bb01e --- /dev/null +++ b/src/main/java/com/music/exception/CrawlerException.java @@ -0,0 +1,10 @@ +package com.music.exception; + +public class CrawlerException extends Exception { + public CrawlerException(String message) { + super(message); + } + public CrawlerException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/exception/NetworkException.java b/src/main/java/com/music/exception/NetworkException.java new file mode 100644 index 0000000..b40b835 --- /dev/null +++ b/src/main/java/com/music/exception/NetworkException.java @@ -0,0 +1,10 @@ +package com.music.exception; + +public class NetworkException extends CrawlerException { + private final String url; + public NetworkException(String url, String message, Throwable cause) { + super(message, cause); + this.url = url; + } + public String getUrl() { return url; } +} diff --git a/src/main/java/com/music/exception/ParseException.java b/src/main/java/com/music/exception/ParseException.java new file mode 100644 index 0000000..00050ba --- /dev/null +++ b/src/main/java/com/music/exception/ParseException.java @@ -0,0 +1,10 @@ +package com.music.exception; + +public class ParseException extends CrawlerException { + public ParseException(String message) { + super(message); + } + public ParseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/music/model/Song.java b/src/main/java/com/music/model/Song.java new file mode 100644 index 0000000..335b59a --- /dev/null +++ b/src/main/java/com/music/model/Song.java @@ -0,0 +1,55 @@ +package com.music.model; + +public class Song { + private String platform; // netease, qq, kugou + private String name; + private String artist; + private String album; + private Integer duration; // 秒 + private Integer popularity; + private String chartType; + private Integer rank; + + public Song() {} + + // 全参构造器(方便测试) + public Song(String platform, String name, String artist, String album, Integer duration, Integer rank) { + this.platform = platform; + this.name = name; + this.artist = artist; + this.album = album; + this.duration = duration; + this.rank = rank; + this.chartType = "热歌榜"; + } + + // Getters and Setters + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getArtist() { return artist; } + public void setArtist(String artist) { this.artist = artist; } + + public String getAlbum() { return album; } + public void setAlbum(String album) { this.album = album; } + + public Integer getDuration() { return duration; } + public void setDuration(Integer duration) { this.duration = duration; } + + public Integer getPopularity() { return popularity; } + public void setPopularity(Integer popularity) { this.popularity = popularity; } + + public String getChartType() { return chartType; } + public void setChartType(String chartType) { this.chartType = chartType; } + + public Integer getRank() { return rank; } + public void setRank(Integer rank) { this.rank = rank; } + + @Override + public String toString() { + return String.format("%d. %s - %s [%s]", rank, name, artist, platform); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/repository/SongRepository.java b/src/main/java/com/music/repository/SongRepository.java new file mode 100644 index 0000000..8db53be --- /dev/null +++ b/src/main/java/com/music/repository/SongRepository.java @@ -0,0 +1,36 @@ +package com.music.repository; + +import com.music.model.Song; +import java.util.*; + +public class SongRepository { + private final List songs = new ArrayList<>(); + + public void add(Song song) { + if (song == null) { + throw new IllegalArgumentException("歌曲不能为 null"); + } + if (song.getName() == null || song.getName().trim().isEmpty()) { + throw new IllegalArgumentException("歌曲名不能为空"); + } + songs.add(song); + } + + public void addAll(List songList) { + for (Song s : songList) { + add(s); + } + } + + public List getAll() { + return Collections.unmodifiableList(songs); + } + + public int size() { + return songs.size(); + } + + public void clear() { + songs.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/service/AnalyzerService.java b/src/main/java/com/music/service/AnalyzerService.java new file mode 100644 index 0000000..b68be77 --- /dev/null +++ b/src/main/java/com/music/service/AnalyzerService.java @@ -0,0 +1,55 @@ +package com.music.service; + +import com.music.model.Song; +import java.util.*; +import java.util.stream.Collectors; + +public class AnalyzerService { + + public Map analyze(List songs) { + Map result = new HashMap<>(); + + // 基础统计 + long uniqueSongs = songs.stream() + .map(s -> s.getName() + "|" + s.getArtist()) + .distinct() + .count(); + long duplicateCount = songs.size() - uniqueSongs; + long artistCount = songs.stream().map(Song::getArtist).distinct().count(); + + result.put("totalSongs", songs.size()); + result.put("uniqueSongs", uniqueSongs); + result.put("duplicateCount", duplicateCount); + result.put("artistCount", artistCount); + + // 热门歌手排行 + Map artistCountMap = songs.stream() + .collect(Collectors.groupingBy(Song::getArtist, Collectors.counting())); + List> topArtists = new ArrayList<>(artistCountMap.entrySet()); + topArtists.sort((a, b) -> b.getValue().compareTo(a.getValue())); + result.put("topArtists", topArtists); + + // 时长分析 + double avgDuration = songs.stream().mapToInt(Song::getDuration).average().orElse(0); + result.put("avgDuration", avgDuration); + + Song shortest = songs.stream().min(Comparator.comparingInt(Song::getDuration)).orElse(null); + Song longest = songs.stream().max(Comparator.comparingInt(Song::getDuration)).orElse(null); + result.put("shortestSong", shortest == null ? "无" : String.format("%s (%d秒)", shortest.getName(), shortest.getDuration())); + result.put("longestSong", longest == null ? "无" : String.format("%s (%d秒)", longest.getName(), longest.getDuration())); + + // 时长分布 + Map durationDist = songs.stream() + .collect(Collectors.groupingBy(s -> { + int min = s.getDuration() / 60; + if (min < 3) return "3分钟以下"; + else if (min < 4) return "3-4分钟"; + else if (min < 5) return "4-5分钟"; + else if (min < 6) return "5-6分钟"; + else return "6分钟以上"; + }, Collectors.counting())); + result.put("durationDistribution", durationDist); + + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/CrawlStrategy.java b/src/main/java/com/music/strategy/CrawlStrategy.java new file mode 100644 index 0000000..1eb558c --- /dev/null +++ b/src/main/java/com/music/strategy/CrawlStrategy.java @@ -0,0 +1,11 @@ +package com.music.strategy; + +import com.music.model.Song; +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import java.util.List; + +public interface CrawlStrategy { + boolean supports(String platform); + List crawl(int limit) throws NetworkException, ParseException; +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/KuGouStrategy.java b/src/main/java/com/music/strategy/KuGouStrategy.java new file mode 100644 index 0000000..3eb74f0 --- /dev/null +++ b/src/main/java/com/music/strategy/KuGouStrategy.java @@ -0,0 +1,120 @@ +package com.music.strategy; + +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import com.music.model.Song; +import com.music.util.RetryUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class KuGouStrategy implements CrawlStrategy { + private static final Logger logger = LoggerFactory.getLogger(KuGouStrategy.class); + + @Override + public boolean supports(String platform) { + return "kugou".equalsIgnoreCase(platform); + } + + @Override + public List crawl(int limit) throws NetworkException, ParseException { + logger.info("开始爬取酷狗热歌榜,限制 {} 首", limit); + try { + // 使用酷狗网页版排行榜(相对稳定) + String url = "https://www.kugou.com/yy/rank/home/1-6666.html"; + Document doc = RetryUtils.retry(() -> fetchDocument(url), 3, 1000); + + // 解析歌曲列表(根据酷狗网页结构调整选择器) + Elements songItems = doc.select("#rankWrap .pc_temp_songlist li"); + if (songItems.isEmpty()) { + // 备用选择器 + songItems = doc.select(".song-list li"); + } + if (songItems.isEmpty()) { + logger.warn("未找到歌曲列表,可能网页结构已变化,使用模拟数据"); + return getMockSongs(limit); + } + + List songs = new ArrayList<>(); + int rank = 1; + for (Element item : songItems) { + if (rank > limit) break; + + // 歌曲名和歌手:通常在 a 标签内,格式如 "歌曲名 - 歌手" + Element nameLink = item.select(".pc_temp_songname a").first(); + if (nameLink == null) nameLink = item.select("a").first(); + if (nameLink == null) continue; + + String fullText = nameLink.text(); + String name = fullText; + String artist = "未知歌手"; + if (fullText.contains("-")) { + String[] parts = fullText.split("-", 2); + if (parts.length == 2) { + name = parts[0].trim(); + artist = parts[1].trim(); + } + } + + // 时长(格式如 03:45) + String durationStr = item.select(".pc_temp_time").text(); + int durationSeconds = parseDuration(durationStr); + + Song song = new Song(); + song.setPlatform("kugou"); + song.setRank(rank); + song.setChartType("热歌榜"); + song.setName(name); + song.setArtist(artist); + song.setAlbum("酷狗热歌榜"); + song.setDuration(durationSeconds); + songs.add(song); + + rank++; + } + logger.info("酷狗爬取完成,真实数据 {} 首", songs.size()); + return songs.isEmpty() ? getMockSongs(limit) : songs; + } catch (Exception e) { + logger.error("酷狗爬取失败,使用模拟数据", e); + return getMockSongs(limit); + } + } + + private Document fetchDocument(String url) throws Exception { + return Jsoup.connect(url) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + .header("Referer", "https://www.kugou.com/") + .timeout(10000) + .get(); + } + + private int parseDuration(String durationStr) { + if (durationStr == null || durationStr.isEmpty()) return 0; + try { + String[] parts = durationStr.split(":"); + if (parts.length == 2) { + return Integer.parseInt(parts[0]) * 60 + Integer.parseInt(parts[1]); + } + } catch (NumberFormatException e) { + logger.warn("时长解析失败: {}", durationStr); + } + return 0; + } + + private List getMockSongs(int limit) { + List songs = new ArrayList<>(); + String[] names = {"海阔天空", "老男孩", "逆战", "夜曲", "青花瓷", "演员", "消愁", "童话"}; + String[] artists = {"Beyond", "筷子兄弟", "张杰", "周杰伦", "周杰伦", "薛之谦", "毛不易", "光良"}; + for (int i = 0; i < Math.min(limit, names.length); i++) { + Song song = new Song("kugou", names[i], artists[i], "酷狗精选", 220 + i * 10, i + 1); + songs.add(song); + } + return songs; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/NetEaseStrategy.java b/src/main/java/com/music/strategy/NetEaseStrategy.java new file mode 100644 index 0000000..8c4a80f --- /dev/null +++ b/src/main/java/com/music/strategy/NetEaseStrategy.java @@ -0,0 +1,142 @@ +package com.music.strategy; // 注意你的包名是 com.music + +import com.google.gson.*; +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import com.music.model.Song; +import com.music.util.RetryUtils; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class NetEaseStrategy implements CrawlStrategy { + private static final Logger logger = LoggerFactory.getLogger(NetEaseStrategy.class); + + @Override + public boolean supports(String platform) { + return "netease".equalsIgnoreCase(platform); + } + + @Override + public List crawl(int limit) throws NetworkException, ParseException { + logger.info("开始爬取网易云热歌榜,限制 {} 首", limit); + List songs = new ArrayList<>(); + try { + // 使用更稳定的接口:歌单详情 API (歌单 ID: 3778678 是官方热歌榜) + String jsonData = RetryUtils.retry(() -> fetchJsonFromUrl(), 3, 1000); + + // 调试:打印前500字符,查看返回结构 + if (jsonData.length() > 500) { + logger.debug("API返回预览: {}", jsonData.substring(0, 500)); + } else { + logger.debug("API返回: {}", jsonData); + } + + JsonObject root = JsonParser.parseString(jsonData).getAsJsonObject(); + int code = root.get("code").getAsInt(); + if (code != 200) { + throw new ParseException("网易云API返回错误码: " + code); + } + + // 新版网易云API返回的数据在 "playlist" -> "tracks" 下 + JsonObject playlist = root.getAsJsonObject("playlist"); + if (playlist == null) { + // 兼容旧版结构:直接 result.tracks + JsonObject result = root.getAsJsonObject("result"); + if (result == null) { + throw new ParseException("JSON中既没有 playlist 也没有 result 字段,请检查API返回"); + } + parseTracks(result.getAsJsonArray("tracks"), songs, limit); + } else { + JsonArray tracks = playlist.getAsJsonArray("tracks"); + parseTracks(tracks, songs, limit); + } + + logger.info("网易云爬取完成,共 {} 首", songs.size()); + return songs; + } catch (Exception e) { + logger.error("网易云爬取失败", e); + if (e instanceof NetworkException) throw (NetworkException) e; + if (e instanceof ParseException) throw (ParseException) e; + throw new ParseException("解析网易云数据失败: " + e.getMessage(), e); + } + } + + private void parseTracks(JsonArray tracks, List songs, int limit) { + if (tracks == null) { + logger.warn("tracks 数组为空"); + return; + } + int count = 0; + for (int i = 0; i < tracks.size() && count < limit; i++) { + JsonObject track = tracks.get(i).getAsJsonObject(); + Song song = new Song(); + song.setPlatform("netease"); + song.setRank(i + 1); + song.setChartType("热歌榜"); + song.setName(track.get("name").getAsString()); + + // 歌手 + if (track.has("artists")) { + JsonArray artists = track.getAsJsonArray("artists"); + StringBuilder sb = new StringBuilder(); + for (int j = 0; j < artists.size(); j++) { + sb.append(artists.get(j).getAsJsonObject().get("name").getAsString()); + if (j < artists.size() - 1) sb.append("/"); + } + song.setArtist(sb.toString()); + } else { + song.setArtist("未知歌手"); + } + + // 专辑 + if (track.has("album")) { + JsonObject album = track.getAsJsonObject("album"); + song.setAlbum(album.has("name") ? album.get("name").getAsString() : "未知专辑"); + } else { + song.setAlbum("未知专辑"); + } + + // 时长(毫秒转秒) + song.setDuration(track.get("duration").getAsInt() / 1000); + songs.add(song); + count++; + try { Thread.sleep(100); } catch (InterruptedException ignored) {} + } + } + + private String fetchJsonFromUrl() throws Exception { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .addInterceptor(chain -> { + Request original = chain.request(); + Request request = original.newBuilder() + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .header("Cookie", "os=pc; appver=2.0.2;") + .header("Referer", "https://music.163.com/") + .method(original.method(), original.body()) + .build(); + return chain.proceed(request); + }) + .build(); + // 使用官方热歌榜歌单 ID: 3778678 的详情接口 + String url = "https://music.163.com/api/playlist/detail?id=3778678"; + Request request = new Request.Builder() + .url(url) + .get() + .build(); + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new NetworkException(url, "HTTP " + response.code(), null); + } + return response.body().string(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/QQStrategy.java b/src/main/java/com/music/strategy/QQStrategy.java new file mode 100644 index 0000000..8c79586 --- /dev/null +++ b/src/main/java/com/music/strategy/QQStrategy.java @@ -0,0 +1,104 @@ +package com.music.strategy; + +import com.music.exception.NetworkException; +import com.music.exception.ParseException; +import com.music.model.Song; +import com.music.util.RetryUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class QQStrategy implements CrawlStrategy { + private static final Logger logger = LoggerFactory.getLogger(QQStrategy.class); + + @Override + public boolean supports(String platform) { + return "qq".equalsIgnoreCase(platform); + } + + @Override + public List crawl(int limit) throws NetworkException, ParseException { + logger.info("开始爬取 QQ 音乐热歌榜,限制 {} 首", limit); + List songs = new ArrayList<>(); + try { + // 使用重试工具包装网络请求 + String url = "https://y.qq.com/n/ryqq/toplist/4"; // QQ音乐热歌榜 + Document doc = RetryUtils.retry(() -> fetchDocument(url), 3, 1000); + + // 解析歌曲列表:选择器基于 QQ 音乐网页结构 + Elements songItems = doc.select(".songlist__list li"); + if (songItems.isEmpty()) { + logger.warn("未找到歌曲列表,网页结构可能已变化"); + return songs; // 返回空列表,不抛异常 + } + + int rank = 1; + for (Element item : songItems) { + if (rank > limit) break; + + // 歌曲名 + String name = item.select(".songlist__songname").text(); + if (name.isEmpty()) { + // 备用选择器 + name = item.select(".songlist__songname_txt").text(); + } + // 歌手 + String artist = item.select(".songlist__artist").text(); + if (artist.isEmpty()) { + artist = item.select(".songlist__artist_name").text(); + } + // 时长(格式如 03:45) + String durationStr = item.select(".songlist__time").text(); + int durationSeconds = parseDuration(durationStr); + + Song song = new Song(); + song.setPlatform("qq"); + song.setRank(rank); + song.setChartType("热歌榜"); + song.setName(name.isEmpty() ? "未知歌曲" : name); + song.setArtist(artist.isEmpty() ? "未知歌手" : artist); + song.setAlbum("QQ音乐专辑"); // 网页上未直接展示专辑,可留空或后续补充 + song.setDuration(durationSeconds); + songs.add(song); + + logger.debug("QQ音乐: 排名{} {} - {}", rank, name, artist); + rank++; + } + logger.info("QQ音乐爬取完成,共 {} 首", songs.size()); + return songs; + } catch (Exception e) { + logger.error("QQ音乐爬取失败", e); + if (e instanceof NetworkException) throw (NetworkException) e; + if (e instanceof ParseException) throw (ParseException) e; + throw new ParseException("解析QQ音乐数据失败: " + e.getMessage(), e); + } + } + + private Document fetchDocument(String url) throws Exception { + return Jsoup.connect(url) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + .header("Referer", "https://y.qq.com/") + .timeout(10000) + .get(); + } + + private int parseDuration(String durationStr) { + if (durationStr == null || durationStr.isEmpty()) return 0; + // 格式: "03:45" -> 225秒 + try { + String[] parts = durationStr.split(":"); + if (parts.length == 2) { + return Integer.parseInt(parts[0]) * 60 + Integer.parseInt(parts[1]); + } + } catch (NumberFormatException e) { + logger.warn("时长解析失败: {}", durationStr); + } + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/strategy/StrategyFactory.java b/src/main/java/com/music/strategy/StrategyFactory.java new file mode 100644 index 0000000..c6ea8c2 --- /dev/null +++ b/src/main/java/com/music/strategy/StrategyFactory.java @@ -0,0 +1,23 @@ +package com.music.strategy; + +import java.util.ArrayList; +import java.util.List; + +public class StrategyFactory { + private final List strategies = new ArrayList<>(); + + public StrategyFactory() { + strategies.add(new NetEaseStrategy()); + strategies.add(new QQStrategy()); + strategies.add(new KuGouStrategy()); + } + + public CrawlStrategy getStrategy(String platform) { + for (CrawlStrategy s : strategies) { + if (s.supports(platform)) { + return s; + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/music/util/CsvUtil.java b/src/main/java/com/music/util/CsvUtil.java new file mode 100644 index 0000000..859fc08 --- /dev/null +++ b/src/main/java/com/music/util/CsvUtil.java @@ -0,0 +1,28 @@ +package com.music.util; + +import com.music.model.Song; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.List; + +public class CsvUtil { + public static void saveToCsv(List songs, String filename) throws Exception { + try (PrintWriter out = new PrintWriter(new FileWriter(filename))) { + out.println("排名,歌曲名称,歌手,专辑,时长(秒),平台"); + for (Song s : songs) { + out.printf("%d,\"%s\",\"%s\",\"%s\",%d,%s\n", + s.getRank(), + escape(s.getName()), + escape(s.getArtist()), + escape(s.getAlbum()), + s.getDuration(), + s.getPlatform()); + } + } + } + + private static String escape(String str) { + if (str == null) return ""; + return str.replace("\"", "\"\""); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/util/RetryUtils.java b/src/main/java/com/music/util/RetryUtils.java new file mode 100644 index 0000000..2b9bec3 --- /dev/null +++ b/src/main/java/com/music/util/RetryUtils.java @@ -0,0 +1,26 @@ +package com.music.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RetryUtils { + private static final Logger logger = LoggerFactory.getLogger(RetryUtils.class); + + @FunctionalInterface + public interface ThrowingAction { + T call() throws Exception; + } + + public static T retry(ThrowingAction action, int maxRetries, long waitMillis) throws Exception { + for (int i = 0; i < maxRetries; i++) { + try { + return action.call(); + } catch (Exception e) { + if (i == maxRetries - 1) throw e; + logger.warn("重试 {}/{},等待 {}ms,异常: {}", i + 1, maxRetries, waitMillis, e.getMessage()); + Thread.sleep(waitMillis); + } + } + throw new IllegalStateException("Unreachable"); + } +} \ No newline at end of file diff --git a/src/main/java/com/music/view/ConsoleView.java b/src/main/java/com/music/view/ConsoleView.java new file mode 100644 index 0000000..b210120 --- /dev/null +++ b/src/main/java/com/music/view/ConsoleView.java @@ -0,0 +1,115 @@ +package com.music.view; + +import com.music.model.Song; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +public class ConsoleView { + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_RESET = "\u001B[0m"; + + private final Scanner scanner = new Scanner(System.in); + + public void printSuccess(String msg) { + System.out.println(ANSI_GREEN + msg + ANSI_RESET); + } + + public void printError(String msg) { + System.out.println(ANSI_RED + msg + ANSI_RESET); + } + + public void printInfo(String msg) { + System.out.println(ANSI_CYAN + msg + ANSI_RESET); + } + + public void printHelp() { + println("\n可用命令:"); + println(" crawl - 爬取歌曲 (platform: netease, qq, kugou)"); + println(" list - 显示已爬取的所有歌曲"); + println(" save - 保存到 CSV 文件 (例如 save data.csv)"); + println(" analyze - 显示数据分析报告"); + println(" history - 显示本次会话输入的命令历史"); + println(" help - 显示本帮助"); + println(" exit - 退出程序\n"); + } + + public void displaySongs(List songs) { + if (songs.isEmpty()) { + println("暂无歌曲数据,请先执行 crawl 命令。"); + return; + } + System.out.printf("%-4s %-30s %-20s %-10s %-10s%n", + "排名", "歌曲名", "歌手", "时长(秒)", "平台"); + for (Song s : songs) { + System.out.printf("%-4d %-30s %-20s %-10d %-10s%n", + s.getRank(), + truncate(s.getName(), 28), + truncate(s.getArtist(), 18), + s.getDuration(), + s.getPlatform()); + } + } + + public void displayAnalysis(Map stats) { + println("\n" + "=".repeat(60)); + println("📊 音乐数据分析报告"); + println("=".repeat(60)); + + // 基础统计 + println("\n📋 【基础统计】"); + println("-".repeat(40)); + println(" 总歌曲数: " + stats.get("totalSongs") + " 首"); + println(" 去重后: " + stats.get("uniqueSongs") + " 首"); + println(" 重复歌曲: " + stats.get("duplicateCount") + " 首"); + println(" 涉及歌手: " + stats.get("artistCount") + " 位"); + + // 热门歌手排行 + println("\n🎤 【热门歌手上榜次数排行】"); + println("-".repeat(40)); + @SuppressWarnings("unchecked") + List> topArtists = (List>) stats.get("topArtists"); + int rank = 1; + for (Map.Entry entry : topArtists) { + System.out.printf(" %d. %s: 上榜 %d 次\n", rank++, entry.getKey(), entry.getValue()); + if (rank > 15) break; + } + + // 时长分析 + println("\n⏱️ 【歌曲时长分析】"); + println("-".repeat(40)); + System.out.printf(" 平均时长: %.1f 秒 (%.1f 分钟)\n", stats.get("avgDuration"), (Double)stats.get("avgDuration") / 60); + System.out.println(" 最短歌曲: " + stats.get("shortestSong")); + System.out.println(" 最长歌曲: " + stats.get("longestSong")); + + // 时长分布 + println("\n📈 【歌曲时长分布】"); + println("-".repeat(40)); + @SuppressWarnings("unchecked") + Map durationDist = (Map) stats.get("durationDistribution"); + durationDist.forEach((range, count) -> { + double percentage = count * 100.0 / (Integer)stats.get("totalSongs"); + System.out.printf(" %s: %d 首 (%.1f%%) ", range, count, percentage); + int bar = (int)(percentage / 2); + for (int i = 0; i < bar; i++) System.out.print("█"); + System.out.println(); + }); + println(""); + } + + private String truncate(String str, int maxLen) { + if (str == null) return ""; + if (str.length() <= maxLen) return str; + return str.substring(0, maxLen - 3) + "..."; + } + + public void println(String msg) { + System.out.println(msg); + } + + public String readLine() { + return scanner.nextLine(); + } +} \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..4dea16e --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + UTF-8 + %highlight(%level) %d{HH:mm:ss} %logger{20} - %msg%n + + + + logs/crawler.log + + logs/crawler.%d{yyyy-MM-dd}.log + 7 + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/target/classes/com/music/App.class b/target/classes/com/music/App.class new file mode 100644 index 0000000000000000000000000000000000000000..951bac3ab7017fd762e0562154f06b20816082db GIT binary patch literal 1129 zcmZ`&-EI<55dIEbSlH5bTPi}ORr)9NZ`Ei_ifPi2^a^VthNSW8upV)nWtW^?XzDBY zBwl(Wi6lON4`rMK#NGaIbLPyV$kaTCt+X?nJkh>F)TG4Pn`F=Z6QDKwksG` z8otfl7MG6Fr$#OO;sgqPBWaBu>~hCr$X7l#qU6m`I$oz%ZRuEMSc_r?j<~Ywz8Cne zXwlt(iHwd_hOG!BMb8hM(3jU%)Au^>|EmUa3jL5_GeQTUT&n_tB}o!ZlI{J>kPXQ*7m(n`mQUaRhRsVBoUul43zL#{ooUZZkq#>0*0G0usjwjK_HXAPQAl@cIM%HzWan@*i*=b??kj;OFMaM zO2ta75(s8=r-D>qb2M2?Jz;9L9q(S8P!M#gLO~NjCykswT+EK@))8&gBxo{_*31#j zGUVC2Z0z?C?u+_zT10|RcDH=HM?$E_Iu)I0kw7-62qGkR8&xcW%JiKRhAq(Lb4K2m zs5QA$nEbGoCG3^aZeQ*x$1-x6coVi#$Eyo>`m1`n=ukTzJ-%4m7tqoRdE1aI$5Q!R z=5X~Gm-x4#SHX7Hx7H7LHBixq7#oa0_nIc@UBFp%3^SHAY$v`9_v1kY4+yll)vHQW z?7&XKHtd0H!I^UV<5&}R3G6C=c&7aRXXSS6{%8lnkcmn$s46xd@ z_;+a)6uZlTS)M~Gp2jomP1`ul_Tf7n3L%AO6&w** zUVC#xIY;LlQ89w&Shz{eEb52G1=^!S{`U0LSoxzkuHYC))}4=B#S3_m0S&SyuyqOg zeGOcha?Fednw4SJE0*YQmbBKU92Wnp-vt_Mz5u#x^U?11trR($Ls6?wBq?UXw@;C& z!ono+S3`Q~_o2vg?wRg`i|?V2ows}K_a*Fgpzp?`K-x8@94&p4IK1H%O67}ITHkNT z@wUS4jOzbIpR64=a{Scu+rZyWIT-nu6P9zyT{)Gw26)?y7HYNf)Xi0Xf?;tDZ9k&@ z3J-Ny!{0_%6heo41D-l@k7st2I`zD7?U~2go^4mLzGwSYL~6!i#sQ-Q7$t}{?v}fb zA{8@qVH4$a;G&ZlTIJNFTY-DA*$dOlRm$qaTP}0)<67mqQ!8T69Up_6kWB0b12u3R z+&+h%;oj@GPv8SI_xyzQ;fLn%@D+ErXAX~Z6X~5pVs}7Zbp+-xIE(ghk~YHv*Kqhc zp09^YE#UI9g^)om8puWq)?zi*@qPozk8q-HgOcp zrR)1AWu+#UFfy>GrLWoO_3iZC*8g2WlP0M9l*cmMzZ literal 0 HcmV?d00001 diff --git a/target/classes/com/music/command/CrawlCommand.class b/target/classes/com/music/command/CrawlCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..4c08c3f53949c3131a6a666c13fe12abf0aeb8c7 GIT binary patch literal 3381 zcmaJ@YjYb{8GhE1yt2HBZ8;b0rbf9|zKW)$C6;SQ)r1<91nl6FmfJ>N%Nt8td3R+e zEv2`Rrp3htlC(_&ftCgjL&&WSb|!R&4}=+h2bP;p{m3U6p0m4_HnEImwC9}nyyrRZ z{XPHu@2yV(JdO`#)Jf>b82LoLWa*g%XL+?SmPncEOfKavWCSHNA5*8*L{2Sa6Nio- z(=xV3;(}tN%Sh?{>k0sI$Eh!;5t!pz9wxp{Ff|5uX1jmv#DSuQm535IW znxlF~&5f$2F4i8ge!!D3756E%N_*elnhJN0Mi9V*3fi$=Nb*Gm4Ty-@Rt1evsJ}g- zTM}YEv|2tEwx;d})jXx%5byMerI|b3FAG2FUxpT!nP{hTEtpdYmR=Da$oG3+y{{fur;hy<|Jf=#FGstH~V zO2P@xMCtB5!ZL#jzA7|ta5Nt(j2n&EiX?ImSw>r$rS{cs|YIQ9k|l(wi5TPycCY?p$o= zSnTelbLGpk<@c{Q;vl{y&)8#z4@GboqcXlDp`~h=p#sZ; zW-53F-(|2)tGSYPXk5akigHdN^Vq20v-qBj=kBR;u5lH7A3vZnA|h3rcCO(Pe*(C* zk%givjOQicpdTqvg@Y8wK_jJ1MzgFiGBm7fg{42w(kP$bo?TkJu{3w}i5Fh%iIvY@ zz5Dtcr;X5%l|en4@2_GCCZN+UI(=SSNS{ESjH@XkAtsTRL9gwodRjZ4(TcWi6x^<$ znfq1<3JTcCOk+wMpeAZoi=qyD!kD6to<_G{U0PTWqFV}VlvqPFHzm7jn9?WH;R(~2 z5ml3gB7zwq)^P??)vgsOqAY>D^99~nzVOH8iytjHchVfHnPnQa`W@tXgF;sWn6=)Q zvQ_&16^rdq+2TM`3&mk(oY zFOYeoPne2bNZ*t;{Qc)dais^{pOXW=oAT_KIY;;8ATidB*lK3-fLiq0S7gK}nHg=r zE*e~mQ$td2&+ii!p)yYzhAmVs9?oHfiXf0bU` zk6%f6*B>7qw+r|*Unn`v+L1Pl$x<=t)7?e$Q|>u+1{SJYN!C^8EhVXCHq=%rGvH=tlS5Gw*$No3^S)%^)N zx_UlAta>=gdw^zsTX=tz_q94eG4~j@VLSH;;7BKAXcDg=!ZP%*)4QgZqu2{XcU|S+ zmus^Fr?}f5oP@Ze5EYaDM)~V`=1%vgp%1XG>tl38yKbWAT6b{wL9SxGH?g}v*cSW; zTEu4WEqnzK4Bl!$te$8KHt$}<*Xqz8>S+tz#6Ab;o58;#a3s(cTEqbf3m6$$z>%By z;dN|^9__7?7cmyZAMjLvNE{RAkKKadTq9VEg3}C;V_TJD`W8-9IG*=7u2J_vyn>Ug z`eMAqG8=TfG|cBPu!XulNE3F@;%u9ilgq}$9U1P_8t5LFHxQixQ>^J6X0F*conZvq9Gdb zB!0>OJVkj<;dRdDo!M#5-XN~OK0obUViHTNUj`wf1J5772Mx%I)C literal 0 HcmV?d00001 diff --git a/target/classes/com/music/command/ExitCommand.class b/target/classes/com/music/command/ExitCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..6717265aece00544214b5a9d9ba5eadbf9c41de4 GIT binary patch literal 929 zcma)4O>fgc5Ph43I5uul(gaFrY56GFDXotjg1AIfLW+t+DqnhX+*NQD+m-E<_Sg$I zeg{{O_!2_=0R9IOH~s(6{_i?70?C zz7ym+A63`hk}Ygt?LDHXXN)~wCnVT zQlQ=qU1w-Jk(b(IXDNB+#Y{Mrj}p&UO>N3RCOeK#o`zv|Ck^N&krxcw6)aMeWTOa` z$I=}ogtm>nS?E-%NFIi@qDClBzIZW*MqzVS|hU J_c9%uzW~iu(;ol; literal 0 HcmV?d00001 diff --git a/target/classes/com/music/command/HelpCommand.class b/target/classes/com/music/command/HelpCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..756d18cf3debca0dd1bd43e12c2b4ace3f892962 GIT binary patch literal 838 zcma)4%Wl&^6g`uKI5wtf)09VPX?e6tQWY;*1hI)!AruvfAa7VX?#P|W_C$_T;IBZU zMPk7R@KJ~}cGN@^5VGcRXU;w6-0S)A^V@d-&#-TyBCs9m#7kyV6?)tyL3-l7l<{Ox z?k&^=R?mX-z>9-4@?MY6WSCp13*5V&I8PF&oh$iXU~9O*m=kZH)2WW7&wXE@{#>Oh ze<85b`4_P3lW?FCwi)KoqhC1I*s03h5fGYU<(f{Yzu7uy=g9XO!4-FCUWzeXd=iWR(vn-TH%5-sc=`lPtZNNizy~=Teqh*@U$7Es~4(FSUj!@wTIK8iMdj}V2 zer8m`8s`;aL2Qj^o9Gs!+a((7XftA7!79kM!Ml?4kgMQbwRPtcmy)br95o9Px4fQ{ z@OfgBC&tbzu12)c`-0{r?k~(Zg>aj48wJO1iMuk=z;014bMXj|i57TLsPFv(PVu@U literal 0 HcmV?d00001 diff --git a/target/classes/com/music/command/HistoryCommand.class b/target/classes/com/music/command/HistoryCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..7fa34d629d419999c94d20c557ee485edf541aaf GIT binary patch literal 1910 zcmb7FO>Y}j6g|&28QU2r#Yvr{G-(=AYP)TQ?*a}1Y8qm2(xwhBf(;XAdd?`h3i<@b zx@}uV0-+7lcuQb^epg3Iw5)A8cGW17&Il;$ek?}Fdm);xS=I6s@-b2{%a-mo8-~F2 zUC*;V?`2eQ8>Usx%+aK8*0fA_PC!fEEAU;;GO40K_WxM)3@^Qevi6!0MG$cnF$@U= zlc{0^0((^`2uo^2MFdfS!Mtf1OU>G<(OA(}t1L2}w@Z4ps5eZ>w}c^=7zs?Z)k8>o zG>YXXIvs4*|8b&}4R=YeF~)E*)eT)35%$7(+bA_%>SU5{bnCRmZy0skG3AVC1=}j0 zxx*63emsan3Z?}HIvc9uAsl8D(>YnIyPF;|uZNK$G&|R??p*m``{H-Me)H+}hwpEH zbp4m}??!P1#}phD*!S-vq+$jdoz^)4Zq=f%WOwGNVi=DIjP6`HfBXB3w{LyC{oZH4 zUB2MQ`~BO?GUdlqJdP)r6oHana_uPjBcD-lyyHvo=Pu5V>1h?ufcTHp^oqd=FX`@C z(_NRd?&_Ay_8m^GXl}wjtCt3Pn{AF zCV%uCE4X^8vZ&Wv0~amW%|^+XH|0nUbuNP$*;9gW!nR$ z)ZI;CS{ru7(0rJDROa=P92*JOlE5eJ&1z};4i~tGs)jS2x9v)^p6SM^P10ew!kO=( z-i}(Uo9zv5xyb6(YQc1k%wQj04r7`7WO7o>ObP)tXIVxgTh$%Ma1^W%=B`3}rLKTW zVdCDZ4CB{{gIo_m{_aBnak(!zTXDH9DC;L3fa<-+FhJUu3}O#oD&&K_2h(5U{`8R> z7)~F(fy6b6f*9o)M~G)XPq}`ig8__DGU=OPA08k_2VPj=Q8I7<`$?-zK)yvt1xSx$ zf~O38jJJFX#t(eKE8qp|^F(nkfNvUX1>hf!SEV%lk;%xUmeXG%v4tstONfykjwioD z`kF^QOe#3{Ii@5x9(q%%j&I@70Di!eH}Moeg$qG3E+4iqD{%F%pLeUC@*o^x)}z?V zbP}xc04thi1<&(7O-`ba_7k~-!ZKf9@QPT*31nH;L1=gpCyCZP^}`4z{y5sWZMdO6=yN_H`0mlyZ`_I literal 0 HcmV?d00001 diff --git a/target/classes/com/music/command/ListCommand.class b/target/classes/com/music/command/ListCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..ddc7ecfc626f24df4999a3a8e556d64f34d49c1a GIT binary patch literal 948 zcma)5%Wl&^6g`uKxG|Wy9J%|OdQQS00qZ=;I(nm1+Dbp1srUYlg8l_>)aPtcvTbNb81|g=p7*@xd7t-r&+x}T_wEBY z3SU8sz%Jb>XUbL2)H9rwHG3{o(3Xu{ZLT0H(6y*7YZ*(kOPO=Ci-yjFZ34Z`2kVCf z+AOD3GF*W~-f>GA&zc`y%;W=GRv@};8kYq6^Nk47lF2!?=U7IOb9%+bb<4C(|F}RT zl`isF&Y3gf=)^M$x&(GS9(h93eaF3`B7ts!_LAY(ZWP#`$~RKTS(@i%)6KxzA+Sw_ zf|x*O-n5OWYI)Xhr?pwj2r|(%tEjoAT-S|l<8_3Csi!5AF2)hTvnqB%mAC9t(T+GX z@)t}`U@&x+lssMoa-lGpDXmNxJ*jjkmxAw_b}1XfUIz16Ho6*@4ZZ4fmQ7u3b`4$S z8WqPgC1sg{W0%fvEVJoi3o>2CC1vE~`1&`WKlp8R?W-@3$C1Xcf+2yv zCznvgA&dybD}?Bua$SdV*cL8C9lRtbgXa}IC$P5>?uqDB9Kj2e7%4enSuD&$MykGP zW%8!yXXAJgM-{vzu)Ps5*v@&&7wD$qWgMeMWMb0}jSY=7qBN72E+*JHSry}Wg#s*V zOY9R{*ZdjNUy$ncHQQ%F4{VlZb2v%kt14cTG^qhig{rO_o)^POw#WL_>+4_svUdB% zgWtbh`}x}17uRbQ7)yTo*%rKx2?eLvoR7DGiZ^hYaxm?AN8sQlP(ny*{mg>LaTa+6 zZwhobTIJMb-KhAcW2>0NZq|~VPZFP2G7^+Ab@vuqr3NooD6W0K^4F~&B((D?3YZpX zHEMMqdOC{f1=qPG+loMlTysk#v3nEs5<){4tl9rkGoO-q(=zO!e-feO>jD%I?3Qhz zjg7q7dRq&=rY}uum7wJn6cv2@$dg!$LSzM}>gvV>ui}2_4LvLgrRk*O_@3`-l}W>2 zaOS)?43rei3+!u9ZraPvl98zet%+;nT^q-Oz_l=yb?;ih(|o%v!`qj4oTX|d8_K7i zXvp%&!Rad%V-xo<{||N293rP#R>Aa*Y-bzhVptOB2un68VkmEjq^6Luq@du>*tX#Y zkFnt?sIZz%ENqsS0=^>yPj}_(P$L1mDboo5TG4_&`Lwft6Y}xp80BgURPJu&D$P;; zv?s(JblgPG3KuQd&Tm_=6~~Ui09X!uw3H_mnNYA|>d9lWwL zIKRE-jR6dDkLLm~lpvA;SP5l>D+wl&*mIpj7({mvL}KQ4JqY!K{j|uVf79B^ac1~_ z>vz~XbPN571FJ}_3`dVl9a_cVkvkZb){%P{16FbT26_@FM(!dP!H?K87L}X5(N&ze zhbifhk`Cv3qj&Lk1S`Cz2c!Jw;Vdtb+#Du_ARldHq>~hPp`RWD{0@@RBzjE$1&`}MZ8ZAKj3~JB3=LD0HO+nf)5pB k6nrG7X9wu3(W3>k&^dN+W-{ht@U>vm7U$Z*C-BhwANGf3*Z=?k literal 0 HcmV?d00001 diff --git a/target/classes/com/music/controller/CrawlerController.class b/target/classes/com/music/controller/CrawlerController.class new file mode 100644 index 0000000000000000000000000000000000000000..e894173b03bbbb2d968cff955247a5501d390366 GIT binary patch literal 4013 zcma)9`&$&}6@CX62AA=+cnKOKQG+ZCu3BOeAXcRqOaa?0#bVOSxQxrdva_9;1xzoY zwz(K>uG+@dmd3`Ci%n__cQ5HRefn=y!2Htw1ASuOZ)VvYVBzWhuzSAqo$p-UbH4MP z{pS~xw*WkXf5^y_P#!gU!@Vg>kA|a0!ZwX~Tr;TAQ?%@q+n zmQd}KsY%#Bf4AOUI+h}~sV8D}&0aqprQrrwdEI_0^Y!Ukzl8feVuC}Ngk{9FcG8sc zn_AMablWgbNqER3o8^ZiMk4l9MvRB#F5PlZtnql+OeQyXEMVpET)0_Lri=V1w3mxW2VF5wXckK!?=z|>-NLfj~)b|pOEJx!b9nq(?E*e-)rlf#$B0o~5{ zQi~cHEU6rFBIJzR<3jErZPHHWuBR=Ei=GiYfCdrVS0pre-CM2(mQ1gR;c!mhB5I$O zG1(}5J0hWMv2W>W$cQ%KtHQfx35Wh~@0Khq8Lc^uHW4^pyo}Knd`%erx`goZMzb~N zvYyyuqdv7*nehtmcwf-`f)M7169U%C9KRw&ikm~1fuM6$rN9QN4>queKP7i zs?r><_B4wMqP?9GHhW@P;3m;3ux!<|i_k^5X0Beod;66;H*e3LfA{mD>vtwzn|tH)w;U#mU2~an1>k!Za(uVdcDn3-~#o$zHXG zts@at?PI##B_4NMP1wxOJY{qF@hHo9*NwLnToj4$JBesCO~Vw|;#UeT;n&1d8_=RD zn{{7Tkt04&K`#CLiRM3qw`Kfx?lE#knZ|*rmb7&vq2RK>$4?wuSYB#O3-GZxUxX`s zo87XRIWv6s?Q_#NPEU`I7h@Rj$#|Dlv2adM@IF3ZmNXHDgi0@_>6kqzv~`(Azj*Tb zvht9gaBgY9BauGC%_OfDfi1~_0YT#Jben@jpw@d7WMbs~Tp6*|Xb+o$GhxX1h#^Oe zlo{0y>tYPquxQNOC5o0|9x@Eu;=GV-(d;gx)AC~iQ!*xb(Ven(;M5a+Mvo@u3N|FS zDl%i!1>BJEmKTk*w5za*uWekjb~GDCPb%r{NsB?7cd+eLQp+Lk1t~`F5oL|mbnfIU~j#%cha(5O$s$+`4M zZPY80e1Uqa1F=!)4q=Sqdx98+J&OM3&?tUz5rxC|{!M0xh6nR-EEITQ6fd<@A-wTOBy4r~(-_#+xDN5!LD-9@TE$(Crg5*__3Slp*2vs!>U>A-e zOel9V343_DhUXtf3j6Q^{|~>xe+UAa{cg}{Xtr?wD*ixgkMWWC5FZh^1Kj%~dHK}W z-~i{N)D~RB$4usR>L`UT@L#NxG4KWIWqd+HMn|jcI6FdtJcd2SUyyF$%_6y1=j@nwU)7K-ibDftvX-of+}9T#>l&0emRq z?SN>IgZF5G}r&_(geXVh>2!cyyHLM*=E&aZSh)acjhR+=I0L2PF^kM z&K=gRg$otzvSL5>P-TW;)t0O~?qGV@?asv<`|GeWU)qL&9W87tn7xGYRWxwGt@s`6 Hv(WkiDZO|I literal 0 HcmV?d00001 diff --git a/target/classes/com/music/exception/NetworkException.class b/target/classes/com/music/exception/NetworkException.class new file mode 100644 index 0000000000000000000000000000000000000000..0801760a4bcf653657b60af62c7ef5a9e2c92979 GIT binary patch literal 660 zcma)3%T59@6g}lN0)zMfUpo_HV0_Hd1;h=}6=4Iia#O|zs`D_N8S%3;(U`dK1Nh*M(S^3FQmfBFW`-5IL985cE_H9!pTBafEygYl zl%@}*=JiyRr6@PBM0=i2mvoz+Zi)qjF%y+-}{CfWYaD^ia2E(Zzhi;f9(s#wHFP>8wNA3exiMaWR7D^0fMZKZo zi$JJNlfk@}kxT~+?Q`#mzi>C;(aasCN=CE(#e|_Wj30%K9aL*5!(^y?G7|S$I2CHl zr-5K-da=)g30G3*^=2uZ%Y-z%B9Cp~K7$pCB;m6h_D_SsDUkc6JD#g}p=Xn^R&8q4 z21D8BSwhTqVVA)k#hLQOt<<+@ZA)Eh4V-`xPd^5I(fw()C=DnZ^fsOD5|y|7zfG&1 z@0RoCF6GXug&OuKQ6KxL6GO8aYt|i2&@(Kj+gsw`FRVg`s?f0GRc!;a)-XDX77l3? JU6pzUI^PZDe0~4` literal 0 HcmV?d00001 diff --git a/target/classes/com/music/model/Song.class b/target/classes/com/music/model/Song.class new file mode 100644 index 0000000000000000000000000000000000000000..407ac710c3e058289d1bbc24f063d5946f45cf62 GIT binary patch literal 2449 zcmbu9+in|W5Xb-P^I=0<=K#h{9XEC2Yz}UL(nCs1NhvLgN`$LOQN_i^G42+7H>}r4 zz2=rj;F`NifNFt|-~o7$@)n5y?5^$2>hl${v@`y8X1?E?_V0iH_zS=de4a%@!$RBX zH~YhZ)o%8klTNSMa_nvvDGldenBSSro@sZRUmU;av^@>E)1K*l?YMmnrTyvamgib_ zcUMEoHv1h78PoNw0qxUf?|4Y#+{w^2J~jnF^4r4>!0smy?OP+o1cD^7Y!YA$Y>~bJKn*hR|}1e$mNr6 z6$eqHMMdsX>nuY(@!OiEeW2xec0cTKo~L6yC+A_ya^D~cZ${8^+2!9wjS+frybL$}>| zXvrAoWgT~9)lg%uNp5Zq37Hpa37HmOa#noFN%1A;#Fs3LFF6#xvWNRE+HrmW^_1 zgsM?ak5DtpnGsfvQhkK==^bkvZ4e~ISb3B<)bso;pv+sVysggrHhGsYJYurECu9&! ziXh-yei;(JCWVk9ZOPIwu4qz>w696};xN`pZGMb0PCXfy%bnXurndHhHWK8B@S^(39+MwsKg5#Bv;>ri8ev zhFFe6T%G|T%@T-fN{DJ4;))XDJvBr%4pE;0A#)-S@8bhLFR-1_nW)7f)|C*~@nPtE zW+rNJh{g;E8Bu|_p@g^@hcJ{7x783g;}DxOAmn2q5WC8Dtj8g?l@NE-?T9RWupK)y zAmmFW5FaTku^NZCs)V?!uEc7*64z!x$ag{@?oBFjz_pcc&06W>?4L*;C5_iuZ5^d3 gDy^e5#d7N?Ls4%1j!)v0*CUi`ldtF_#!KMwe;LhScK`qY literal 0 HcmV?d00001 diff --git a/target/classes/com/music/repository/SongRepository.class b/target/classes/com/music/repository/SongRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..d2926d78ad8419c6458c030e584e9ef78c337ade GIT binary patch literal 1865 zcmZ`(U2hvj6g}g(UT3p$9LGsXfs)1)Vmqy&6eu_Z(vU*HNue$&37;Ehaks43)_RQ^ zUU`H{yte!Rgai^G$XCV36G;4yQjqu$lr!UvzerwocJ7_I_uO;O+&})^`x(H~cwa+E zAXTbXa+OBiF6BI{R;}B9)!WP!t8V#HYpWqF5Wi-AWab>xE$1$-U$aWSz>zwS)#)lI zU&FVZT*0pUd4c|-U3N{s;aLJxJ?@!8J7=Z3VL2*YewNoFGq!8{vjTln=@o(SJl#6_ zFleA3x`rWvk#?>*&oeh=LLD&-_Cp|H;0PiDLj~KlmK&9I%e!o@I|Pv^R7<9_VtTgR z2ga~})h7Ov9e@rcf#~|ojSYeEsgC0v_|q#IjtNY*d0KQFt86-RUb#`RTz}zu$*TEw z)s5l-f#lt9KD+zH_jh(~{Qb#KcXswB+=k=m7{_s0>q7#g0dI=(qVL&mIiFt9aRMm~ zlMHYWiv}LXNdc{F`O9X7btk9NP5L{-=y()q4O3lRZKg1gkpaTYSCPl24V=PhMzre- zm72e)TC*0#V+7c&;Lp!*w2JuatGxl7!C4JY2*lc*l1(yj6i@b))l^bGy~d1EOafyE zlPfS-^v%+TOJ*(TU4+e;a~ukyS6^gFAlqv_RCVbU3Hdn#vv^)0YWtRF@(~cqK6Ogi zi-A2qfH}--I8RNr3*7e_co7RM#jvHc8I3nw zs@?uTJ)kDO$zGE4f!I1nLAY*zOd;|`FG`}<3|zsR93CadGCgT|$G}_A1@vOI;gzfl zwtTN6-6QCq3jrflk{!X19 z;Wwt@L=a|#A>K~XW;E|$6l2^8*`Xlbr#u~^?OgVE4Bp20YU2KFJorm?3lqN~E|)Dl zvW4u;z?+la%numO?&9$fuE@N7ka>?W7l<=VoSa#hnne6kb*QCW)XxG$AZ8Mw2b+iw5|T2;GyVX%HIQs!`pn zj%aYP1@|=t_YAGFKT*li4qpG3{~?7*_EfUe?nkuM56XsK+26CleO=z)#j3#9JrRy~ hM9^CiD6=5Kx$X#W3$(SN6AQ>jKT-!qc~|9n?>|u^nlJzW literal 0 HcmV?d00001 diff --git a/target/classes/com/music/service/AnalyzerService.class b/target/classes/com/music/service/AnalyzerService.class new file mode 100644 index 0000000000000000000000000000000000000000..a6835f55906d65cc74f2bd14ca30a97739fe64bb GIT binary patch literal 5627 zcmb_g3wRV)75;BtGuccYED#9cQ6NA<14|7QNDzSpO40yo5Q=E)BpJfM?#_01Hf^w0 ze1NrRRV>?pw6#9#yJ{`|cV;$wHwg&e=SP^ik8{rb z&pH1+_wGFU^6@VMSSTiI$P<{_<-{Vfbjt3Eq^xAG-DO3V$IWQp4lCK=_cas_W~lrUWx*JsM!&HHB*@bh=8kmk6rZ8qE1Qw|=8gA{}*yK7h4a}0x z=-qV8CK5SVXJ9riCEzY69Vcf2vpX#1`t&-2B8s4G>{La*&r}A23`rt z-b9+fGOjBOA0f@E`5^N;mI;)k_D}cNLEgG6L$@VO^(;10(rNHRgHr0%rY0_~8 z#kzD;E;3TWS zbgdQnhXJ7+1GI?D-aDjR1 zYUeVjW=F#&1~VIp<;kSkCpVSM_hticz#AC`EAA%yIxJ>*R8AN;M9;6nHL_T}NuWwy zKETSY)Ks@2j68H2=t8##HY3|xonWr6Osl4cJhG%*MAx`eDuX0*jgv-C6cJ8*-B zH#3k~lesbm-hvxh8=d6JXo~g$rmp^cgzQ9Z;=>wIykH^1|UTeT69s z^WIXS)IskWLOe+;m5vfmxiZHChbNF|W1UxOI!8I!!u^^FGmo*6FD49ZfR9O0F-eV_ zdsSZ|bGK3{Cc&Na&LglO7wZMydITCSM$bF!mNE`kDq23UEUg)nnw7F_pC3bDOIj=K zaLumm)C;bZden?{cAGWx1ZLNc9L*6pAc=X}z}F;)i%CqsCWm=GJZj)EJkCp0%-qhy zInFbBqwQ{GE9IJTmn_J+J_MZdT~w(*!lKMR)iZz_1m>tY8w^Eo7x6B;*}=Nj5Wblk6a)_O4J1ABBl zL(C0x=KF&>o@00ZoCTTg^Xx8|b9tuwTj~cLPyc2@rc0p?C!OrFR@w4L%9tTPf96VN za-Xko9G3;aOsuiotxh+urhmd;H2hg$n#Wx)(RRGo*=|MJJ^oS9++xaGS)YNw;%}@l zvFwn*<7#{ZP2c!xGu|DwQZ?<4vpt@1GBi8^qFfi1JfFo4f=5JaJZ>eMqh=~)r8H3`Fk_gP zBauxL)%?kDeo-EAJ~Md?dX*Yenf84CKn1HM2T{bQ#$JSPAxCE)h5jHLc?j_-r$K=d zJ`G=h?^5=7JffUZ6>R5Wlr)mRpHKG1{U;FKbQELOTzU*+1&*T{IEE>Ce9RDh)C#QN zRK38isn){6jMThgQVX^E@MXT_02+>AaXvQIhhH^y0LwF-Rh5NGZ*`?M*i+07pltxJ zK8E#qIElu_;uE-PQ&sUXyk6iau5Jufg^t6Nt6O=f58G8C-+IQvb~)up_&fs2`q8(z zq(1!C@g)Oz+c3ca>^_F~3gr7>#{llnxcaH8vSG4Bo5+xFadj*+PnBZfQdLljW1v_ zPLpI$Q}YFWuP~VKTFeydF-z>kY_Xf)820ef!7Y5>&i)-}5cgq$$FCh^7P%AfOSV3X z&yi=kIDpTipDdd$c3~MamXEn&2d-eN5N+aWw6dk)fY9k}KZ=RuMQo#Ph&WDRC-YW9 zBzKaCCs0Z}JBjlI%D8$d9->A$yVn zE9iso10&QljZWlMO`&(1wHIQJ9;*W^JEthqZ_GeXn{`|KAyF#I#2B8NW5qb?^Sgx@FD8hI{Kohs8pWid;{O7xbO*iw literal 0 HcmV?d00001 diff --git a/target/classes/com/music/strategy/CrawlStrategy.class b/target/classes/com/music/strategy/CrawlStrategy.class new file mode 100644 index 0000000000000000000000000000000000000000..5624776193d5ad564996f0180f5c20503512ea7b GIT binary patch literal 380 zcmZvY%SyvQ6o&s3ZKkcaT5&JpqD63)E-G$BK?o{j=WZN_Fg3}9xmbNQ7e0UwB~H9F zh_24tZ&&CG=!_h_ExcoPH6(>yl^M`59q&{v=i7wW%G$8sVT* zqJAd)O8JGc4@;C`gRuV}c_XY-OO7zebx{i+tRfu#6>ySuM=Eo#N}ii2`?M97n_#87 u&}Cd5|1#sK{WB#HR-%VM620jmMe7km9|;Dro5ncAIyPGZTag}pX4@ZB>1i|o literal 0 HcmV?d00001 diff --git a/target/classes/com/music/strategy/KuGouStrategy.class b/target/classes/com/music/strategy/KuGouStrategy.class new file mode 100644 index 0000000000000000000000000000000000000000..0048ed618c502e4bf8fca74e8321aeedca04065d GIT binary patch literal 6716 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/music/strategy/NetEaseStrategy.class b/target/classes/com/music/strategy/NetEaseStrategy.class new file mode 100644 index 0000000000000000000000000000000000000000..7737be00c00c1dc0b94d5708f1afd2211f47204a GIT binary patch literal 8616 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 literal 0 HcmV?d00001 diff --git a/target/classes/com/music/strategy/QQStrategy.class b/target/classes/com/music/strategy/QQStrategy.class new file mode 100644 index 0000000000000000000000000000000000000000..7b76eece92ca17136a0df6acf6bd95cbef467532 GIT binary patch literal 6239 zcmcIo349dQ9sXW+v$NSuAS@DCQCSc*_j0QV9vDF*Nf3h|9>vLKNS17NmYrDvt<@@8 zEQqKT6s<=+u!;&E3lRlvtF_kNckN|00cu-YYi}$4-pp=xNEUzo`umk*cIM6he&7Fl z@6GEUJ--*g$zq0rB7tM0MsK7yZN{Pz(@JTU-nAyOaAAA)R)JTbe1&#}7Kv+#uE>Ip z6?)WC;1dWJP;5$RtKvCb0bktc>e5pJ!4@Oc6*1$>XRL^{*kg?X{@KxZED^I#7w}Y8 zEf(-L8J&6nWf-YoguvLLI`g!sWu(@qD90#)k}lnHYzdrD*^-a3DXy7jW7Tk%B@h^` zLV;gkWJ@fex21bK^wc7)Bd*&qMz#22Eftf`*+JiIJHh}w7^k8F0Y+(c$4r3O#`pUExH=DZ> zMoMqeOor=E#x-lXk?NI^=abJG7buR(xnRPT%~j6(v=xg-T4JWf*aCA`NA;u?GZLnP zYJti5Wc1vdNSki0GEzN*q6%sSCKZxAUrU+#pd9C>J=T@btaOTj)(j6|wySo%ob!03 z-C#vdZ}ek|K&1bLYx}pY-@k5q|1F!t3m1kD-uKKqeVg`Q|HQx(8wMV^=e-v;9K35& z|GG^0s;k2XAK4thG!Vr3Oj=;oj z%SxIJk;t0*l`HFISrd`enw2XfmXVB8GBP8`K5bHQHs;bl!O7{otkP9Gb9loxx`N38 z&81<-a)EB8)?7$5n3ETEr;JrZm)e!hn&t7t(hftHfy z1kQ9vHbiu&pXT$jm~@d!qk;tj<7D|)m_|AoNe~%kQN0BvYju?QEEZ%jQ-dLo1h%#uS0j%i0OGB>VE(`I7;OR!YIMNBvEe~6HZi*X5| z6f@`cCM_mR(qao=CNO>Ap2r4m+S|WwhXm-|Pxr6ea^QhS3Df;A-g@wX=l1Wpec;}; z{kJ@I;QAN)cRk&I%boojHcP-QQ*k*oPVFi!l{f-PES8}~Rdho3e9Y2G(FPf{qSD>f z&Dn9{xC=1_-7Hled%3qNR-lLO68dV^=%}29!{>l}ViT`>4zhLfgAFj+H3f1q5?U`g zJC1f4DErL>ZF{W(F0ycxY`4^pRrFK9)G})|r<&Cr<$9^5EKar`S26El!^>m@eq2pX zcfGS>GAw~>R9uVC3X~-!MV*~y`3UqQ6uVi$^(;&t@p*Cz;5v+xmm5`lPEJ~>saxk~ zDOir*q~h~fPkeCv0xiKhSGYl#Bbutke%wrg9Aq=8dy9%q(xyt?rfz1tXibtS-Nq3@ zYyW)@JGhdfUr=#}6qRn;?8LVmCUw(#DRq~Mtx~FlQggD!yGLNe!i5f(zO(!G1AA|k zlJ}~(PfGfwWV|EYD+eD?@g>`Jy6o4VPLKB8cV z_Cq@lJo~T=bdQP`@FD}%Qz_Yr)dd1|#c4CF+);2*2w)#xR`618Ww1MMaM@Jx71=uh z?rU0g)6}~3AOZX}6|dk`HcPM8qc<6esAesRS>3!dEsfkIaAJV~-1IuwcQtrT?qOu? z@mOz+zC<(cWf#*-xf8U_+;U#IOYo#qTyi{}MwHWM@2A|XkYi>FmPi|5KaAGTQ?0!FKGWh@@o zA~Wl!ge#ZC5}n2>Gu*Z)JatO_l*TXzX3b~}ubwrdDm*8djO$DEj`=YwGIRRL_0wmC zE9alPsI{e*OK*=JK1YxC7**jWZX^pqd(b1@jURgyM8Q1Ms%jS%V9wN81=m|dX6t{KlZ z!VyX%E&fBrKk+X*m&=EdW`~62ez8#0Tx#FNhbsPqkJuvYS%G7?qUJ@1%qPzr{M*3Q zPZX)bBY3VVHo4EEUZFi@m7D7ZJ9wJ#sX~$KncrT|xT^R?3Gq9yC3EnOeFwHZIB@T# zew(Tq99|Wra+5Jz5oJ8G?D6&>+R^& zCQsqwD|p!Y4c;Q9wHxVFRG$}<=d6+WLsGpAh|F`AVOS>jvdLE6>NYyffEXu^Rz!%W zjeN+l#1%%59%-@dQ;-OAj)P*n;1Q{`H$Nosh8qc4O~?4TTB0+qo0D4%qbHp#uw8Z~ zd6=SHlilK<{H5da<8|GHoweM=4Szd%vT>wOj4elxtY}yjqwv;9MNBBiM*%TeoT!Ks zczhdHCI>QXGOibc=jK<$NdkBN|4Wm_s~beQ<$8E^-Dw{07uEbE;ovy801w=;W8mf| zbEp>F?lhCnQuZ-lHx*IG%|d?5_VFhV?p_W*@+g8Q&J%`}uMk!gF-_pOj~68wIl#h1 zw!4S_i~0X(`OSgnDtRq|+?JN|sfxeyPf1YhhWZSGJNZzAqj>e%uYwq3kAP47^pI7H z@K-7pSMS2up(>+n71a?yh@PeLcpSs4^w~%UMf_EQ$JS&pVLNZ#9xH713P&WXF2kOT z6KH^24V=WJDyik8OciQSXIrS3#=UbmI)eVTK8;{7GJZFv_Tgj?_TaRoyKqKp@XWUA zy615gm5;)_-8lCpv^97`-r)HeT+mP)D$Zc>T^K2qFYUvpicq`5JIg1BI~x4k@P5eO z2cF8mdGT(?0XTOr5@XM`%-VHqoVB; zJiZFl?ZVghan>quuIS`Ov{^*$U$2e0t(;pBC?g1!3QXYKhf&LqPcu39r*Zbr;rnLJ zcnhl0!FlgNJys!tYcLh-F#}sDc^_usah!@>XrTSsc#WFh!Rh!J&fq$5CO+U-4?oTl zBhe&6I9p7@JaHDz5p($kY60Wv;P-}3%ojRZM3SEtK8-f94hw8dZgh~9!=%`XZ{T$z zvO=uIH}MAZ3W@pn79;iWE7d9ZHa&YWQ$+Axe2-1oBF5qS)bKI-xA6n~kf>fosUK0J zgwa2VAM-B2*f-)Qyenk}H{z$%P?^Wo%;D!KV`MY&CVoL5zht#!n>qe}MgPAh^3TAC zLoAa|Ayq4QNI_UZo5F)J4pF*f=rA`LJiPGTQ3c*9h}&;J#01)Lu7}Ws5yPxFlD0GY z3)Ay;ke5S@?~_RvQJR_y{}-i0oZTa5Rsv;MyiUfR?6?ed--j5>Ybndf4Fj)|V!RB0 zpUqk;Sa8W&lWKP3x4{E__+1f#?*;$dhrf6*>ZI~M{JjW$__x3gN;!mfG^ZN;Eab(7 zw6T~aUP5SJL|$IxBDqO9Kw**%9)6FN-wWtjvgJ1GJ4M-SojhDFw_~ej)bu(STZ0WrZl5t$j9~ka8VRR2MLF*XI|A5SDlSums+JeY z{Q-v$FW?l8$O8JRYce93FOsv)gl(tJqT$Or^#~bkkr*Q?_#5QivG9oF#6&SkR1)1) Uq9!|D%g(L$m57L#DyAd!UxL)V1^@s6 literal 0 HcmV?d00001 diff --git a/target/classes/com/music/strategy/StrategyFactory.class b/target/classes/com/music/strategy/StrategyFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..29b711343dc5c1cc61065aba00f41f95ef5f4476 GIT binary patch literal 1298 zcmah|T~8B16g|@x7TR5Cp@1MrLDaUCRuDlgqENvYOAV>P$lI{2WwGrx+gX)I|AdMD z0beoEkV;~F_ILO@#Jk&V+L}##*pE3g_nvdlz4PnO_a6XOu_eLBFsmAsOvS3}YNjqs zF0|4~rs)1`b5$7TiG%<{_?W-pnKG}IGS3ckR&QVviw^GY+*N{7_`{BPd~zVYtn^ zVVeBJCJ$l&NhDw@fp$sdX)ex*#OWA~1qV5jaRwZO4G0)Ja z3ysFdFd0VUiH>eRaq(;j3&==Flm51`tz8*6u}BTBYVT;WqOA;__DZ5$TB63l2NY+CBx>%3QoM%!=)|BiIq+#xgL< zI{SZ*7?h&m>gyd|a~dUKv+FW7^U-B08kVVQ+q&K0D51_(bQ_ko`7uKS?We0tg6yfL zuS6>!`sktCh9JE|guJIyjJ~<#8RXa{0))hS47S+OskOwMtykE1SSc%(g{ox zFw+FSI0Uowo*+UHNq|TPi7G}^i;%Gi1y^ZDZc<_n*J#CX9XDJ#A4pz+u(VCrK%AlQ z8RH8Lq!@l;X(u(`z_Je?(UUqQ^bW(Pi{I>&BK)wKN2ypKOtTgt3xWKj**^qJRs(W6dQ%_f@B6HFbNw}zyc_qAxC>YJo*c8u0eVL literal 0 HcmV?d00001 diff --git a/target/classes/com/music/util/CsvUtil.class b/target/classes/com/music/util/CsvUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..2c26a3782edef4312c3208ce706296ea89da1c9b GIT binary patch literal 2134 zcmZ`(ZF3V<6n<`-WVhQb)IbVBe4$!sOWL4C(WX|h1w`9YNNEY+%QoGnTaw+dyD9LQ z8D|{d5XW&w<)hP2&J=M1l@a_TGyV=K`V-U{@wuCnCbiDAd+$Bxf4}otV&%*7A79+qE)SqUL;8p+q_8XY+dzGL#UL3 z=crj@!5Zc&`bdzDnM8Ys*Ix6Xq{GuR?86H>Uc>=blQaDjhJ8+AT1_$tan$zBoav^d z?aMm)L0U=MF{8lN*QZlukp^-?#~@xM)lDyJ6luA8qfY&ofilcN9k0tUO$>9;^+{DJ z9oBJ3N>wS%PnQZ(ctpn=GW%uA0XLw^li#XJ$Wvr7rCgn7Tvw6qke4oN;1({W@ z2jEW3xy}VS(K^oHtU!w~Gm|bAi>~Q;<_tSmo82ShU`)dvM~c(mT0^K{s)^7nXT=Ws z6Rjzw+8u5kr$i>jDynrQ2?#oE+jxMbqHYgeDz5ILYNC&p%m1zb7Mb>q?72~+7_2b` zR^5)QWT}Eggo*<5X;n#H`4C9#HUF&dgP+_ZVVWZGG?cH7j+ zYw5iPisy4cwX{=mv!+}&0(y0g_sX%xF0#6jJ0JYAsfh5a@Rj7rToLZR3H3TJ4bb?N zYFR-uzk0=hXA56V*o4h=(#m^?n#Zt(w+3vL?!-6zrF8p)7!_l;5zE}hw#?p}h~LD{ z{!ly=eX5LI-OG4Zpg*#Rlkvzhx&?m5zWy+^&$mW?LSv>e9xh{lIunxTOSdqP&V=Rl zwcr|&*P-C5$m`*BMwRE$bQvR4(&Moh~1;|r3SCW-Lb?ys6C^L zXi4;Z3pLSG##G|&byg9>Wn2j=yM}AQDbeU>%^)r@;<1xB^bv_c$_^8W<3!>tQOF?* zhgg)TyF{DIw7!A10Ksk|MNIHE-r*^4@8UgT7sLB7FimYAePuDj#+cMs5SR9|eC7B* z4D})QKvB@4U{1lQ;4en3_X2<8_(MhtuU85h{RQnHkEW(S8Bdd2ji>C;k$?z5Yxg}w rqVr4L(R|b{qqsy}W55B8cmQ!)@2KHHD}3f&$%<4`=&rh>Wna=z2O=(i4hO7(?iEdzXKc$As$nvs4eueix!bXs>F%Gla@S&#drSstBcS zFOHX;lO#)}8fwF2V(EhE%_n?@*KDd#T|=K}&^gPjM6oh%_h0U<|4 m=F6xuDqxfOT8_59m^vC92VB$Sf16*GF`-GN~UWMPR28WIz;i<`A$%^rsfu<8w+IoTD2-efvjB% zt5)P+8(g&PL{}7O9(H{%@Zu4HRK74HppDk%rGZwo=`aP3?6w(m+_)C5nmB}ZftIR_ zH+%`4%-fCIM|~%XiiN#bwm{&pi8L|-?Y0-l$@B_ix z!mehthbu#SJ&hb3%5bGp98q{Ux8fL{&~aQKzi+_1 zA~A6SCrK3P^aZ-|4+*Oorvy&@`SE9eeDit#&0E&ZTYugC^!KknS-qXMV#R)Fxw_%6ggEAyfdV79P$xK)JquB@iHr6cjB5TVwi-i zIAK5@wg?jy9qz`M#_= zemQnxxs@r+7!zon4(P;PP>F6}6qi(LmjrSTL8b~b!@^_!2@D)On@QP{b3^qAPqJJD z-5V>87f*P;uj)sPb9fx&Gq)}JEu>8SZo9U zlRAx7=kOM#G|JrHpg~zyC{1&8LVLhAKc=Ja0Zjh?ru!6TxYdNW@eXH$uGP!UW?Jwr JWyRRB{{WG~yhZ>3 literal 0 HcmV?d00001 diff --git a/target/classes/com/music/view/ConsoleView.class b/target/classes/com/music/view/ConsoleView.class new file mode 100644 index 0000000000000000000000000000000000000000..a366cc526aab901d6b36c31b5175b246f3e93ac4 GIT binary patch literal 7148 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 K + + + UTF-8 + %highlight(%level) %d{HH:mm:ss} %logger{20} - %msg%n + + + + logs/crawler.log + + logs/crawler.%d{yyyy-MM-dd}.log + 7 + + + UTF-8 + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file