You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
5.8 KiB
142 lines
5.8 KiB
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<Song> crawl(int limit) throws NetworkException, ParseException {
|
|
logger.info("开始爬取网易云热歌榜,限制 {} 首", limit);
|
|
List<Song> 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<Song> 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();
|
|
}
|
|
}
|
|
}
|