feat: add import detection toggles
This commit is contained in:
@@ -8,11 +8,20 @@ using LiveRecorder.Application.Models.LiveRooms;
|
||||
using LiveRecorder.Application.Models.RecordTasks;
|
||||
using LiveRecorder.Domain.Entities;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LiveRecorder.Application.Services;
|
||||
|
||||
public sealed class LiveRoomService
|
||||
{
|
||||
private static readonly Regex DouyinRoomIdRegex = new(
|
||||
@"(?<id>\d{8,})",
|
||||
RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly Regex BilibiliRoomIdRegex = new(
|
||||
"""^(?:(?:https?:\/\/)?live\.bilibili\.com\/(?:blanc\/|h5\/)?)?(?<id>\d+)\/?(?:[#\?].*)?$""",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
private readonly ILiveRoomRepository _liveRoomRepository;
|
||||
private readonly IRecordSessionRepository _recordSessionRepository;
|
||||
private readonly ILivePlatformAdapterFactory _livePlatformAdapterFactory;
|
||||
@@ -83,7 +92,7 @@ public sealed class LiveRoomService
|
||||
request.Url,
|
||||
request.PlatformOverride,
|
||||
request.AnchorName,
|
||||
cancellationToken);
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var hasActiveSession = await _recordSessionRepository.GetActiveByLiveRoomIdAsync(liveRoom.Id, cancellationToken) is not null;
|
||||
return Map(liveRoom, effectiveSettings, hasActiveSession);
|
||||
@@ -95,6 +104,9 @@ public sealed class LiveRoomService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var detectRoomMetadata = request.DetectRoomMetadata;
|
||||
var detectLiveStatus = request.DetectLiveStatus;
|
||||
|
||||
var results = new List<ImportLiveRoomItemResultDto>();
|
||||
var lines = request.Content
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
@@ -129,7 +141,9 @@ public sealed class LiveRoomService
|
||||
url,
|
||||
request.PlatformOverride,
|
||||
anchorName,
|
||||
cancellationToken);
|
||||
detectRoomMetadata,
|
||||
detectLiveStatus,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
results.Add(new ImportLiveRoomItemResultDto
|
||||
{
|
||||
@@ -190,7 +204,9 @@ public sealed class LiveRoomService
|
||||
string rawInput,
|
||||
LivePlatformType? platformOverride,
|
||||
string? fallbackAnchorName,
|
||||
CancellationToken cancellationToken)
|
||||
bool detectRoomMetadata = true,
|
||||
bool detectLiveStatus = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var input = rawInput.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
@@ -198,12 +214,30 @@ public sealed class LiveRoomService
|
||||
throw new InvalidOperationException("Live room URL is required.");
|
||||
}
|
||||
|
||||
var adapter = platformOverride.HasValue && platformOverride.Value != LivePlatformType.Unknown
|
||||
? _livePlatformAdapterFactory.GetByPlatform(platformOverride.Value)
|
||||
: _livePlatformAdapterFactory.GetByInput(input);
|
||||
ILivePlatformAdapter? adapter = null;
|
||||
ParsedLiveRoom parsedRoom;
|
||||
if (detectRoomMetadata || detectLiveStatus)
|
||||
{
|
||||
adapter = platformOverride.HasValue && platformOverride.Value != LivePlatformType.Unknown
|
||||
? _livePlatformAdapterFactory.GetByPlatform(platformOverride.Value)
|
||||
: _livePlatformAdapterFactory.GetByInput(input);
|
||||
|
||||
parsedRoom = await adapter.ParseRoomAsync(input, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedRoom = ParseRoomLocally(input, platformOverride);
|
||||
}
|
||||
|
||||
LiveStatusSnapshot? liveStatus = null;
|
||||
if (detectLiveStatus)
|
||||
{
|
||||
adapter ??= platformOverride.HasValue && platformOverride.Value != LivePlatformType.Unknown
|
||||
? _livePlatformAdapterFactory.GetByPlatform(platformOverride.Value)
|
||||
: _livePlatformAdapterFactory.GetByInput(input);
|
||||
liveStatus = await adapter.GetLiveStatusAsync(parsedRoom.RoomId, cancellationToken);
|
||||
}
|
||||
|
||||
var parsedRoom = await adapter.ParseRoomAsync(input, cancellationToken);
|
||||
var liveStatus = await adapter.GetLiveStatusAsync(parsedRoom.RoomId, cancellationToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var liveRoom = await _liveRoomRepository.GetByPlatformRoomIdAsync(parsedRoom.PlatformType, parsedRoom.RoomId, cancellationToken);
|
||||
@@ -211,7 +245,16 @@ public sealed class LiveRoomService
|
||||
if (liveRoom is null)
|
||||
{
|
||||
liveRoom = new LiveRoom(parsedRoom.PlatformType, parsedRoom.SourceUrl, parsedRoom.RoomId, parsedRoom.NormalizedUrl, now);
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(liveRoom, liveStatus, now, cancellationToken);
|
||||
if (liveStatus is not null)
|
||||
{
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(
|
||||
liveRoom,
|
||||
liveStatus,
|
||||
now,
|
||||
updateMetadata: detectRoomMetadata,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
liveRoom.UpdateMetadata(null, fallbackAnchorName, null, null, null, now);
|
||||
|
||||
await _liveRoomRepository.AddAsync(liveRoom, cancellationToken);
|
||||
@@ -220,7 +263,16 @@ public sealed class LiveRoomService
|
||||
{
|
||||
liveRoom.UpdateSource(parsedRoom.SourceUrl, parsedRoom.NormalizedUrl, now);
|
||||
liveRoom.UpdateRoomId(parsedRoom.RoomId, now);
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(liveRoom, liveStatus, now, cancellationToken);
|
||||
if (liveStatus is not null)
|
||||
{
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(
|
||||
liveRoom,
|
||||
liveStatus,
|
||||
now,
|
||||
updateMetadata: detectRoomMetadata,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
liveRoom.UpdateMetadata(null, fallbackAnchorName, null, null, null, now);
|
||||
}
|
||||
|
||||
@@ -231,7 +283,10 @@ public sealed class LiveRoomService
|
||||
$"Live room resolved: {liveRoom.RoomId} ({liveRoom.Platform}).",
|
||||
liveRoomId: liveRoom.Id,
|
||||
cancellationToken: cancellationToken);
|
||||
await TryAutoStartRecordingAsync(liveRoom, liveStatus, cancellationToken);
|
||||
if (liveStatus is not null)
|
||||
{
|
||||
await TryAutoStartRecordingAsync(liveRoom, liveStatus, cancellationToken);
|
||||
}
|
||||
|
||||
var effectiveSettings = await _liveRoomRecordingSettingsResolver.ResolveAsync(liveRoom, cancellationToken);
|
||||
return (liveRoom, effectiveSettings, created);
|
||||
@@ -246,7 +301,7 @@ public sealed class LiveRoomService
|
||||
var liveStatus = await adapter.GetLiveStatusAsync(room.RoomId, cancellationToken);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(room, liveStatus, now, cancellationToken);
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(room, liveStatus, now, cancellationToken: cancellationToken);
|
||||
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
await _systemLogService.WriteAsync(
|
||||
@@ -445,6 +500,99 @@ public sealed class LiveRoomService
|
||||
return BuildBatchResult(request.LiveRoomIds.Count, results);
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParseRoomLocally(string input, LivePlatformType? platformOverride)
|
||||
{
|
||||
var platform = ResolvePlatformForLocalImport(input, platformOverride);
|
||||
return platform switch
|
||||
{
|
||||
LivePlatformType.Douyin => ParseDouyinRoomLocally(input),
|
||||
LivePlatformType.Bilibili => ParseBilibiliRoomLocally(input),
|
||||
_ => throw new NotSupportedException(
|
||||
"This platform still requires live room detection during import. Please enable detection or choose a supported platform.")
|
||||
};
|
||||
}
|
||||
|
||||
private static LivePlatformType ResolvePlatformForLocalImport(string input, LivePlatformType? platformOverride)
|
||||
{
|
||||
if (platformOverride.HasValue && platformOverride.Value != LivePlatformType.Unknown)
|
||||
{
|
||||
return platformOverride.Value;
|
||||
}
|
||||
|
||||
if (input.Contains("douyin.com", StringComparison.OrdinalIgnoreCase) ||
|
||||
input.Contains("iesdouyin.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Douyin;
|
||||
}
|
||||
|
||||
if (input.Contains("live.bilibili.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LivePlatformType.Bilibili;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(
|
||||
"Unable to infer the platform without detection. Please specify a platform override or keep detection enabled.");
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParseDouyinRoomLocally(string input)
|
||||
{
|
||||
var roomId = ExtractDouyinRoomId(input);
|
||||
if (string.IsNullOrWhiteSpace(roomId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to extract the Douyin room id locally. Please keep detection enabled for this link.");
|
||||
}
|
||||
|
||||
return new ParsedLiveRoom(
|
||||
LivePlatformType.Douyin,
|
||||
roomId,
|
||||
input.Trim(),
|
||||
$"https://live.douyin.com/{roomId}");
|
||||
}
|
||||
|
||||
private static ParsedLiveRoom ParseBilibiliRoomLocally(string input)
|
||||
{
|
||||
var roomId = ExtractBilibiliRoomId(input);
|
||||
if (string.IsNullOrWhiteSpace(roomId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Unable to extract the Bilibili room id locally. Please keep detection enabled for this link.");
|
||||
}
|
||||
|
||||
return new ParsedLiveRoom(
|
||||
LivePlatformType.Bilibili,
|
||||
roomId,
|
||||
input.Trim(),
|
||||
$"https://live.bilibili.com/{roomId}");
|
||||
}
|
||||
|
||||
private static string? ExtractDouyinRoomId(string input)
|
||||
{
|
||||
var trimmedInput = input.Trim();
|
||||
var match = DouyinRoomIdRegex.Match(trimmedInput);
|
||||
return match.Success ? match.Groups["id"].Value : null;
|
||||
}
|
||||
|
||||
private static string? ExtractBilibiliRoomId(string input)
|
||||
{
|
||||
var trimmedInput = input.Trim();
|
||||
var directMatch = BilibiliRoomIdRegex.Match(trimmedInput);
|
||||
if (directMatch.Success)
|
||||
{
|
||||
return directMatch.Groups["id"].Value;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(trimmedInput, UriKind.Absolute, out var uri) ||
|
||||
!uri.Host.Contains("live.bilibili.com", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return uri.Segments
|
||||
.Select(static item => item.Trim('/'))
|
||||
.FirstOrDefault(static item => !string.IsNullOrWhiteSpace(item) && item.All(char.IsDigit));
|
||||
}
|
||||
|
||||
private async Task<Dictionary<Guid, RecordingExecutionSettings>> BuildEffectiveSettingsLookupAsync(
|
||||
IReadOnlyCollection<LiveRoom> rooms,
|
||||
CancellationToken cancellationToken)
|
||||
|
||||
@@ -26,6 +26,7 @@ public sealed class LiveRoomStatusService
|
||||
LiveRoom liveRoom,
|
||||
LiveStatusSnapshot liveStatus,
|
||||
DateTimeOffset observedAt,
|
||||
bool updateMetadata = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(liveRoom);
|
||||
@@ -33,13 +34,17 @@ public sealed class LiveRoomStatusService
|
||||
|
||||
var wasLive = liveRoom.AvailabilityStatus == LiveRoomAvailabilityStatus.Live;
|
||||
|
||||
liveRoom.UpdateMetadata(
|
||||
liveStatus.Title,
|
||||
liveStatus.AnchorName,
|
||||
liveStatus.AnchorId,
|
||||
liveStatus.AvatarUrl,
|
||||
liveStatus.CoverUrl,
|
||||
observedAt);
|
||||
if (updateMetadata)
|
||||
{
|
||||
liveRoom.UpdateMetadata(
|
||||
liveStatus.Title,
|
||||
liveStatus.AnchorName,
|
||||
liveStatus.AnchorId,
|
||||
liveStatus.AvatarUrl,
|
||||
liveStatus.CoverUrl,
|
||||
observedAt);
|
||||
}
|
||||
|
||||
liveRoom.UpdateAvailability(
|
||||
liveStatus.IsLive ? LiveRoomAvailabilityStatus.Live : LiveRoomAvailabilityStatus.Offline,
|
||||
observedAt);
|
||||
|
||||
@@ -247,7 +247,7 @@ public sealed class RecordService
|
||||
try
|
||||
{
|
||||
var liveStatus = await adapter.GetLiveStatusAsync(liveRoom.RoomId, cancellationToken);
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(liveRoom, liveStatus, now, cancellationToken);
|
||||
await _liveRoomStatusService.ApplySnapshotAsync(liveRoom, liveStatus, now, cancellationToken: cancellationToken);
|
||||
|
||||
if (!liveStatus.IsLive)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user