mirror of
https://github.com/cxfksword/jellyfin-plugin-danmu.git
synced 2026-04-23 18:12:00 +08:00
bilibili support avid
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
16995
Jellyfin.Plugin.Danmu/Scrapers/Bilibili/protobuf/Dm.cs
Normal file
16995
Jellyfin.Plugin.Danmu/Scrapers/Bilibili/protobuf/Dm.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user