| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- using Application.Abstractions.Cache;
- using Application.Abstractions.Data;
- using Application.Abstractions.Messaging;
- using Application.Abstractions.YouTube;
- using Microsoft.EntityFrameworkCore;
- using SharedKernel.Results;
- namespace Application.Features.Api.Channel.GetLiveList;
- public sealed class Handler(
- IAppDbContext db,
- IYouTubeLiveStateStore liveStateStore,
- IYouTubeChannelCache channelCache,
- ICacheService cache
- ) : IQueryHandler<Query, Result<Response>>
- {
- private const string CacheKey = "channel:live:list:main";
- private const int MaxLimit = 100;
- private const int DefaultLimit = 24;
- private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(30);
- public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
- {
- var limit = request.Limit <= 0 ? DefaultLimit : Math.Min(request.Limit, MaxLimit);
- var offset = Math.Max(request.Offset, 0);
- var fullList = await cache.GetAsync<List<LiveChannelItem>>(CacheKey, ct);
- if (fullList is null)
- {
- fullList = await BuildFullLiveListAsync(ct);
- await cache.SetAsync(CacheKey, fullList, CacheTtl, ct);
- }
- IEnumerable<LiveChannelItem> filtered = fullList;
- if (!string.IsNullOrWhiteSpace(request.Keyword))
- {
- var keyword = request.Keyword.Trim();
- filtered = filtered.Where(c => c.ChannelName.Contains(keyword, StringComparison.OrdinalIgnoreCase) || (c.Handle is not null && c.Handle.Contains(keyword, StringComparison.OrdinalIgnoreCase)));
- }
- filtered = request.Sort switch
- {
- "newest" => filtered.OrderByDescending(c => c.StartedAt ?? DateTime.MinValue),
- "name" => filtered.OrderBy(c => c.ChannelName),
- _ => filtered.OrderByDescending(c => c.ViewerCount)
- };
- var ordered = filtered.ToList();
- var total = ordered.Count;
- var page = ordered.Skip(offset).Take(limit).ToList();
- return new Response(page, total);
- }
- private async Task<List<LiveChannelItem>> BuildFullLiveListAsync(CancellationToken ct)
- {
- var channels = await db.Channel.AsNoTracking().Where(c => c.IsActive).Select(c => new { c.SID, c.Name, c.Handle }).ToListAsync(ct);
- if (channels.Count == 0)
- {
- return [];
- }
- var allLive = await liveStateStore.GetAllLiveAsync();
- var liveMap = allLive.Where(l => l.IsLive).ToDictionary(l => l.ChannelId);
- if (liveMap.Count == 0)
- {
- return [];
- }
- var liveChannels = channels.Where(c => liveMap.ContainsKey(c.SID)).ToList();
- if (liveChannels.Count == 0)
- {
- return [];
- }
- var ytCache = await channelCache.GetManyAsync(liveChannels.Select(c => c.SID));
- // ViewerCount 는 YouTube concurrentViewers 를 poller 가 30초 주기로 Redis 에 갱신한 값
- // (과거 IChatConnectionTracker 는 "사이트 접속자 수" 였음 — YouTube 시청자 수가 아니라서 제거)
- var items = liveChannels.Select(ch => {
- var liveInfo = liveMap[ch.SID];
- var ytInfo = ytCache.GetValueOrDefault(ch.SID);
- return new LiveChannelItem(
- ch.SID,
- ch.Name,
- ch.Handle,
- ytInfo?.ThumbnailUrl,
- liveInfo.VideoId,
- $"https://img.youtube.com/vi/{liveInfo.VideoId}/mqdefault.jpg",
- liveInfo.Title,
- liveInfo.ViewerCount,
- liveInfo.ActualStartTime
- );
- }).ToList();
- items.Sort((a, b) => b.ViewerCount.CompareTo(a.ViewerCount));
- return items;
- }
- }
|