bilibili support avid

This commit is contained in:
cxfksword
2023-02-23 10:46:51 +08:00
parent 65e1f9586e
commit 96a8a1bf9b
9 changed files with 17270 additions and 9 deletions

View File

@@ -103,5 +103,46 @@ namespace Jellyfin.Plugin.Danmu.Test
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetVideoByAvidAsync()
{
var _bilibiliApi = new BilibiliApi(loggerFactory);
Task.Run(async () =>
{
try
{
var avid = "av5048623";
var result = await _bilibiliApi.GetVideoByAvidAsync(avid, CancellationToken.None);
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
[TestMethod]
public void TestGetDanmuContentByProtoAsync()
{
var _bilibiliApi = new BilibiliApi(loggerFactory);
Task.Run(async () =>
{
try
{
var aid = 5048623;
var cid = 9708007;
var result = await _bilibiliApi.GetDanmuContentByProtoAsync(aid, cid, CancellationToken.None);
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
}
}

View File

@@ -31,7 +31,7 @@ namespace Jellyfin.Plugin.Danmu.Test
public void TestSearch()
{
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
scraperManager.register(new Bilibili(loggerFactory));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
@@ -64,7 +64,7 @@ namespace Jellyfin.Plugin.Danmu.Test
public void TestAddMovie()
{
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
scraperManager.register(new Bilibili(loggerFactory));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
@@ -98,7 +98,7 @@ namespace Jellyfin.Plugin.Danmu.Test
public void TestUpdateMovie()
{
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Bilibili(loggerFactory));
scraperManager.register(new Bilibili(loggerFactory));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
@@ -128,5 +128,39 @@ namespace Jellyfin.Plugin.Danmu.Test
}
[TestMethod]
public void TestUpdateMovieWithAvid()
{
var scraperManager = new ScraperManager(loggerFactory);
scraperManager.register(new Bilibili(loggerFactory));
var fileSystemStub = new Mock<Jellyfin.Plugin.Danmu.Core.IFileSystem>();
var directoryServiceStub = new Mock<IDirectoryService>();
var libraryManagerStub = new Mock<ILibraryManager>();
var libraryManagerEventsHelper = new LibraryManagerEventsHelper(libraryManagerStub.Object, loggerFactory, fileSystemStub.Object, scraperManager);
var item = new Movie
{
Name = "新龙门客栈",
ProviderIds = new Dictionary<string, string>() { { Bilibili.ScraperProviderId, "av5921024" } },
};
var list = new List<LibraryEvent>();
list.Add(new LibraryEvent { Item = item, EventType = EventType.Update });
Task.Run(async () =>
{
try
{
await libraryManagerEventsHelper.ProcessQueuedMovieEvents(list, EventType.Update);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}).GetAwaiter().GetResult();
}
}
}

View File

@@ -6,6 +6,7 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.22.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />

View File

@@ -14,6 +14,7 @@
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.22.0" />
<PackageReference Include="Jellyfin.Controller" Version="10.8.0" />
<PackageReference Include="Jellyfin.Model" Version="10.8.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />

View File

@@ -245,9 +245,9 @@ public class LibraryManagerEventsHelper : IDisposable
{
// item所在的媒体库不启用弹幕插件忽略处理
var libraryOptions = _libraryManager.GetLibraryOptions(item);
if (libraryOptions == null || libraryOptions.DisabledSubtitleFetchers.Contains(Plugin.Instance?.Name))
if (libraryOptions != null && libraryOptions.DisabledSubtitleFetchers.Contains(Plugin.Instance?.Name))
{
// Console.WriteLine($"ignore {item.Path} {item.Name}");
this._logger.LogInformation($"媒体库已关闭danmu插件, 忽略处理[{item.Name}].");
return true;
}
@@ -807,12 +807,11 @@ public class LibraryManagerEventsHelper : IDisposable
var bytes = danmaku.ToXml();
if (bytes.Length < 10 * 1024)
{
_memoryCache.Remove(checkDownloadedKey);
_logger.LogInformation("[{0}]弹幕内容少于10KB忽略处理{1}.{2}", scraper.Name, item.IndexNumber, item.Name);
return;
}
await this.SaveDanmu(item, bytes);
this._logger.LogInformation("[{0}]弹幕下载成功name={1}.{2} commentId={3}", scraper.Name, item.IndexNumber, item.Name, commentId);
this._logger.LogInformation("[{0}]弹幕下载成功name={1}.{2} commentId={3}", scraper.Name, item.IndexNumber ?? 1, item.Name, commentId);
}
else
{

View File

@@ -95,7 +95,7 @@ public class Bilibili : AbstractScraper
var media = new ScraperMedia();
var isMovieItemType = item is MediaBrowser.Controller.Entities.Movies.Movie;
if (id.StartsWith("BV", StringComparison.CurrentCulture))
if (id.StartsWith("BV", StringComparison.CurrentCultureIgnoreCase))
{
var video = await _api.GetVideoByBvidAsync(id, CancellationToken.None).ConfigureAwait(false);
if (video == null)
@@ -134,6 +134,37 @@ public class Bilibili : AbstractScraper
return media;
}
if (id.StartsWith("av", StringComparison.CurrentCultureIgnoreCase))
{
var biliplusVideo = await _api.GetVideoByAvidAsync(id, CancellationToken.None).ConfigureAwait(false);
if (biliplusVideo == null)
{
log.LogInformation("获取不到b站视频信息avid={0}", id);
return null;
}
var aid = id.Substring(2);
// 分P
foreach (var (page, idx) in biliplusVideo.List.WithIndex())
{
media.Episodes.Add(new ScraperEpisode() { Id = "", CommentId = $"{aid},{page.Cid}" });
}
if (isMovieItemType)
{
media.Id = id;
media.CommentId = media.Episodes.Count > 0 ? $"{aid},{media.Episodes[0].CommentId}" : "";
}
else
{
media.Id = id;
}
return media;
}
var seasonId = id.ToLong();
if (seasonId <= 0)
{
@@ -168,7 +199,7 @@ public class Bilibili : AbstractScraper
public override async Task<ScraperEpisode?> GetMediaEpisode(BaseItem item, string id)
{
var episode = new ScraperEpisode();
if (id.StartsWith("BV", StringComparison.CurrentCulture))
if (id.StartsWith("BV", StringComparison.CurrentCultureIgnoreCase))
{
var video = await _api.GetVideoByBvidAsync(id, CancellationToken.None).ConfigureAwait(false);
if (video == null)
@@ -186,6 +217,25 @@ public class Bilibili : AbstractScraper
return null;
}
if (id.StartsWith("av", StringComparison.CurrentCultureIgnoreCase))
{
var biliplusVideo = await _api.GetVideoByAvidAsync(id, CancellationToken.None).ConfigureAwait(false);
if (biliplusVideo == null)
{
log.LogInformation("获取不到b站视频信息avid={0}", id);
return null;
}
if (biliplusVideo.List.Length > 0)
{
var aid = id.Substring(2);
return new ScraperEpisode() { Id = "", CommentId = $"{aid},{biliplusVideo.List[0].Cid}" };
}
return null;
}
var epId = id.ToLong();
if (epId <= 0)
{
@@ -209,6 +259,22 @@ public class Bilibili : AbstractScraper
public override async Task<ScraperDanmaku?> GetDanmuContent(BaseItem item, string commentId)
{
if (commentId.Contains(","))
{
var arr = commentId.Split(",");
if (arr.Length == 2)
{
var aid = arr[0].ToLong();
var cmid = arr[1].ToLong();
if (aid > 0 && cmid > 0)
{
return await _api.GetDanmuContentByProtoAsync(aid, cmid, CancellationToken.None).ConfigureAwait(false);
}
}
return null;
}
var cid = commentId.ToLong();
if (cid > 0)
{

View File

@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -23,6 +24,8 @@ using Jellyfin.Plugin.Danmu.Core.Http;
using Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity;
using RateLimiter;
using ComposableAsync;
using Jellyfin.Plugin.Danmu.Scrapers.Entity;
using Jellyfin.Plugin.Danmu.Core.Extensions;
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili;
@@ -30,6 +33,9 @@ public class BilibiliApi : AbstractApi
{
private static readonly object _lock = new object();
private TimeLimiter _timeConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(1000));
private TimeLimiter _delayExecuteConstraint = TimeLimiter.GetFromMaxCountByInterval(1, TimeSpan.FromMilliseconds(100));
private static readonly Regex regBiliplusVideoInfo = new Regex(@"view\((.+?)\);", RegexOptions.Compiled);
/// <summary>
/// Initializes a new instance of the <see cref="BilibiliApi"/> class.
@@ -253,6 +259,102 @@ public class BilibiliApi : AbstractApi
return null;
}
public async Task<BiliplusVideo?> GetVideoByAvidAsync(string avid, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(avid))
{
return null;
}
var cacheKey = $"video_{avid}";
var expiredOption = new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) };
BiliplusVideo? videoData;
if (_memoryCache.TryGetValue<BiliplusVideo?>(cacheKey, out videoData))
{
return videoData;
}
var url = $"https://www.biliplus.com/video/{avid}/";
var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var videoJson = regBiliplusVideoInfo.FirstMatchGroup(body);
if (!string.IsNullOrEmpty(videoJson))
{
var videoInfo = videoJson.FromJson<BiliplusVideo>();
_memoryCache.Set<BiliplusVideo?>(cacheKey, videoInfo, expiredOption);
return videoInfo;
}
_memoryCache.Set<BiliplusVideo?>(cacheKey, null, expiredOption);
return null;
}
/// <summary>
/// 下载实时弹幕,返回弹幕列表
/// protobuf定义https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/grpc_api/bilibili/community/service/dm/v1/dm.proto
/// </summary>
/// <param name="aid">稿件avID</param>
/// <param name="cid">视频CID</param>
public async Task<ScraperDanmaku?> GetDanmuContentByProtoAsync(long aid, long cid, CancellationToken cancellationToken)
{
var danmaku = new ScraperDanmaku();
danmaku.ChatId = cid;
danmaku.ChatServer = "api.bilibili.com";
danmaku.Items = new List<ScraperDanmakuText>();
try
{
var segmentIndex = 1; // 分包每6分钟一包
while (true)
{
var url = $"https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&pid={aid}&segment_index={segmentIndex}";
var bytes = await httpClient.GetByteArrayAsync(url, cancellationToken).ConfigureAwait(false);
var danmuReply = Biliproto.Community.Service.Dm.V1.DmSegMobileReply.Parser.ParseFrom(bytes);
if (danmuReply == null || danmuReply.Elems == null || danmuReply.Elems.Count <= 0)
{
break;
}
var segmentList = new List<ScraperDanmakuText>();
foreach (var dm in danmuReply.Elems)
{
// <d p="944.95400,5,25,16707842,1657598634,0,ece5c9d1,1094775706690331648,11">今天的风儿甚是喧嚣</d>
// time, mode, size, color, create, pool, sender, id, weight(屏蔽等级)
segmentList.Add(new ScraperDanmakuText()
{
Id = dm.Id,
Progress = dm.Progress,
Mode = dm.Mode,
Fontsize = dm.Fontsize,
Color = dm.Color,
MidHash = dm.MidHash,
Content = dm.Content,
Ctime = dm.Ctime,
Weight = dm.Weight,
Pool = dm.Pool,
});
}
// 每段有6分钟弹幕为避免弹幕太大从中间隔抽取最大60秒200条弹幕
danmaku.Items.AddRange(segmentList.ExtractToNumber(1200));
segmentIndex += 1;
// 等待一段时间避免api请求太快
await _delayExecuteConstraint;
}
}
catch (Exception ex)
{
}
return danmaku;
}
private async Task EnsureSessionCookie(CancellationToken cancellationToken)
{
var url = "https://www.bilibili.com";

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Jellyfin.Plugin.Danmu.Scrapers.Bilibili.Entity
{
public class BiliplusVideo
{
[JsonPropertyName("aid")]
public long AId { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("list")]
public VideoPart[] List { get; set; }
}
}

File diff suppressed because it is too large Load Diff