diff --git a/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs b/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs
index 52cbd24..c9d2cd0 100644
--- a/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs
+++ b/Jellyfin.Plugin.Danmu.Test/IqiyiApiTest.cs
@@ -96,11 +96,16 @@ namespace Jellyfin.Plugin.Danmu.Test
{
var vid = "132987200";
var result = await api.GetDanmuContentByMatAsync(vid, 1, CancellationToken.None);
- Console.WriteLine(result);
+ Console.WriteLine($"获取到 {result.Count} 条弹幕");
+ if (result.Count > 0)
+ {
+ Console.WriteLine($"第一条弹幕:{result[0].Content} (时间:{result[0].ShowTime}s)");
+ }
}
catch (Exception ex)
{
- Console.WriteLine(ex.Message);
+ Console.WriteLine($"错误:{ex.Message}");
+ Console.WriteLine($"堆栈:{ex.StackTrace}");
}
}).GetAwaiter().GetResult();
}
@@ -114,17 +119,29 @@ namespace Jellyfin.Plugin.Danmu.Test
{
try
{
- var vid = "132987200";
+ var vid = "2569036400194800";
var result = await api.GetDanmuContentAsync(vid, CancellationToken.None);
- Console.WriteLine(result);
+ Console.WriteLine($"获取到 {result.Count} 条弹幕");
+ if (result.Count > 0)
+ {
+ Console.WriteLine($"第一条弹幕:{result[0].Content} (时间:{result[0].ShowTime}s)");
+ }
}
catch (Exception ex)
{
- Console.WriteLine(ex.Message);
+ Console.WriteLine($"错误:{ex.Message}");
+ Console.WriteLine($"堆栈:{ex.StackTrace}");
}
}).GetAwaiter().GetResult();
}
+ [TestMethod]
+ public void TestRemoveInvalidXmlChars()
+ {
+ // 测试包含垂直制表符和换页符
+ var textWithVtFf = "挽星🔥";
+ Assert.AreEqual("挽星🔥", IqiyiApi.RemoveInvalidXmlChars(textWithVtFf));
+ }
}
}
diff --git a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs
index 4401ecd..61593ad 100644
--- a/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs
+++ b/Jellyfin.Plugin.Danmu/Scrapers/Iqiyi/IqiyiApi.cs
@@ -1,9 +1,11 @@
-using System.IO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
+using System.Globalization;
+using System.Text;
using System.Text.RegularExpressions;
+using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
@@ -64,7 +66,7 @@ public class IqiyiApi : AbstractApi
keyword = HttpUtility.UrlEncode(keyword);
var url = $"https://search.video.iqiyi.com/o?if=html5&key={keyword}&pageNum=1&pageSize=20";
- var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var result = new List();
@@ -147,7 +149,7 @@ public class IqiyiApi : AbstractApi
using (var request = new HttpRequestMessage(HttpMethod.Get, url))
{
request.Headers.Add("user-agent", MOBILE_USER_AGENT);
- var response = await this.httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ using var response = await this.httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
@@ -180,7 +182,7 @@ public class IqiyiApi : AbstractApi
}
var url = $"https://pcw-api.iqiyi.com/albums/album/avlistinfo?aid={albumId}&page=1&size={size}";
- var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var albumResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -203,7 +205,7 @@ public class IqiyiApi : AbstractApi
}
var url = $"https://pcw-api.iqiyi.com/album/album/baseinfo/{albumId}";
- var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var albumResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -224,10 +226,10 @@ public class IqiyiApi : AbstractApi
var year = begin.Year;
var month = begin.ToString("MM");
url = $"https://pub.m.iqiyi.com/h5/main/videoList/source/month/?sourceId={albumId}&year={year}&month={month}";
- response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
+ using var monthResponse = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ monthResponse.EnsureSuccessStatusCode();
- var videoListResult = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
+ var videoListResult = await monthResponse.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (videoListResult != null && videoListResult.Data != null && videoListResult.Data.Videos != null && videoListResult.Data.Videos.Count > 0)
{
list.AddRange(videoListResult.Data.Videos.Where(x => !x.ShortTitle.Contains("精编版") && !x.ShortTitle.Contains("会员版")));
@@ -271,6 +273,11 @@ public class IqiyiApi : AbstractApi
// 每段有300秒弹幕,为避免弹幕太大,从中间隔抽取最大60秒200条弹幕
danmuList.AddRange(comments.ExtractToNumber(1000));
}
+ catch (InvalidOperationException ex)
+ {
+ _logger.LogError("获取爱奇艺弹幕({0})出错:{1}", tvId, ex.Message);
+ break;
+ }
catch (Exception ex)
{
break;
@@ -298,7 +305,7 @@ public class IqiyiApi : AbstractApi
var s2 = tvId.Substring(tvId.Length - 2);
// 一次拿300秒的弹幕
var url = $"http://cmts.iqiyi.com/bullet/{s1}/{s2}/{tvId}_300_{mat}.z";
- var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
using (var zipStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false))
@@ -317,19 +324,45 @@ public class IqiyiApi : AbstractApi
}
memoryStream.Position = 0;
- using (var reader = new StreamReader(memoryStream))
+ using (var reader = new StreamReader(memoryStream, leaveOpen: true))
{
var serializer = new XmlSerializer(typeof(IqiyiCommentDocument));
- var result = serializer.Deserialize(reader) as IqiyiCommentDocument;
- if (result != null && result.Data != null)
+ try
{
- var comments = new List();
- foreach (var entry in result.Data)
+ var result = serializer.Deserialize(reader) as IqiyiCommentDocument;
+ if (result != null && result.Data != null)
{
- comments.AddRange(entry.List);
+ var comments = new List();
+ foreach (var entry in result.Data)
+ {
+ comments.AddRange(entry.List);
+ }
+ return comments;
+ }
+ }
+ catch (InvalidOperationException ex)
+ {
+ // 重置 MemoryStream 位置并创建新的 StreamReader
+ memoryStream.Position = 0;
+ using (var cleanReader = new StreamReader(memoryStream, leaveOpen: true))
+ {
+ var xmlContent = cleanReader.ReadToEnd();
+ var cleanXml = RemoveInvalidXmlChars(xmlContent);
+ using (var stringReader = new StringReader(cleanXml))
+ {
+ var result = serializer.Deserialize(stringReader) as IqiyiCommentDocument;
+ if (result != null && result.Data != null)
+ {
+ var comments = new List();
+ foreach (var entry in result.Data)
+ {
+ comments.AddRange(entry.List);
+ }
+ return comments;
+ }
+ }
}
- return comments;
}
}
}
@@ -339,10 +372,30 @@ public class IqiyiApi : AbstractApi
return new List();
}
+ ///
+ /// 移除 XML 字符串中的无效字符(控制字符和零宽字符).
+ ///
+ /// 需要清理的 XML 字符串.
+ /// 清理后的 XML 字符串.
+ public static string RemoveInvalidXmlChars(string xml)
+ {
+ if (string.IsNullOrEmpty(xml))
+ {
+ return xml;
+ }
+
+ // 移除 XML 非法字符:
+ // \u0000-\u0008: NULL 及其他控制字符
+ // \u000B-\u000C: 垂直制表符和换页符
+ // \u000E-\u001F: 其他控制字符
+ // \u200B-\u200D: 零宽字符(零宽空格、零宽不连字符、零宽连字符)
+ // \uFEFF: 零宽非断空格(BOM)
+ string pattern = @"[\u0000-\u0008\u000B\u000C\u000E-\u001F\u200B-\u200D\uFEFF]|";
+ return Regex.Replace(xml, pattern, string.Empty);
+ }
+
protected async Task LimitRequestFrequently()
{
await this._timeConstraint;
}
-
}
-