Wukong/Services/Channel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Wukong.Helpers;
using Wukong.Models;
using static System.Threading.Timeout;
namespace Wukong.Services
{
public class UserSong
{
public delegate void ClientSongChangedHandler(UserSong userSong, ClientSong previousSong);
public readonly string UserId;
private ClientSong song;
public ClientSong Song
{
get => song;
set
{
var previous = song;
song = value;
}
}
public UserSong(string userId)
{
UserId = userId;
}
public UserSong Clone()
{
return new UserSong(UserId)
{
Song = Song
};
}
}
public class Channel
{
public string Id { get; }
private readonly ISocketManager socketManager;
private readonly IProvider provider;
private readonly IUserManager userManager;
private readonly IMemoryCache cache;
private readonly ISet<string> finishedUsers = new HashSet<string>();
private readonly ISet<string> downvoteUsers = new HashSet<string>();
private readonly ChannelUserList list = new ChannelUserList();
private DateTime startTime;
private Timer finishTimeoutTimer;
private double Elapsed => (DateTime.Now - startTime).TotalSeconds;
public bool Empty => list.Empty;
private Song nextServerSong;
public List<string> UserList => list.UserList;
public Channel(string id, ISocketManager socketManager, IProvider provider, IUserManager userManager, IMemoryCache cache)
{
Id = id;
this.socketManager = socketManager;
this.provider = provider;
this.userManager = userManager;
this.cache = cache;
list.CurrentChanged += song =>
{
startTime = DateTime.Now;
finishedUsers.Clear();
downvoteUsers.Clear();
BroadcastPlayCurrentSong(song);
};
list.UserChanged += (add, userId) =>
{
BroadcastUserListUpdated();
};
list.NextChanged += song =>
{
BroadcastNextSongUpdated(song);
};
}
public void Join(string userId)
{
list.AddUser(userId);
if (socketManager.IsConnected(userId))
{
EmitChannelInfo(userId);
}
}
public void Leave(string userId)
{
list.RemoveUser(userId);
}
public void Connect(string userId)
{
EmitChannelInfo(userId);
}
public void UpdateSong(string userId, ClientSong song)
{
list.SetSong(userId, song);
}
public bool ReportFinish(string userId, ClientSong song, bool force = false)
{
if (song != list.CurrentPlaying?.Song)
{
CheckShouldForwardCurrentSong();
// Workaround: told the user the current song
EmitChannelInfo(userId);
return false;
}
if (force) downvoteUsers.Add(userId);
else finishedUsers.Add(userId);
CheckShouldForwardCurrentSong();
return true;
}
public bool HasUser(string userId)
{
return list.UserList.Contains(userId);
}
private void CheckShouldForwardCurrentSong()
{
var userList = list.UserList;
var downVoteUserCount = downvoteUsers.Intersect(userList).Count;
var undeterminedCount = userList.Except(downvoteUsers).Except(finishedUsers).Count();
var connectedUserCount = userList.Select(it => socketManager.IsConnected(it)).Count();
if (!list.IsPlaying || downVoteUserCount >= QueryForceForwardCount(connectedUserCount) || undeterminedCount == 0)
{
ShouldForwardNow();
}
else if (undeterminedCount <= connectedUserCount * 0.5)
{
if (finishTimeoutTimer != null) return;
finishTimeoutTimer = new Timer(ShouldForwardNow, null, 5 * 1000, Infinite);
}
}
private static int QueryForceForwardCount(int total)
{
return Convert.ToInt32(Math.Ceiling((double)total / 2));
}
private void ShouldForwardNow(object state = null)
{
finishTimeoutTimer?.Change(Infinite, Infinite);
finishTimeoutTimer?.Dispose();
finishTimeoutTimer = null;
list.GoNext();
}
private async Task<Song> GetSong(ClientSong song) {
if (song == null) return null;
if (cache.TryGetValue(song, out Song cachedSong)) {
return cachedSong;
}
cachedSong = await provider.GetSong(song, true);
if (cachedSong != null) {
cache.Set(song, cachedSong, TimeSpan.FromMinutes(10));
}
return cachedSong;
}
private void BroadcastUserListUpdated(string userId = null)
{
var users = list.UserList;
socketManager.SendMessage(userId != null ? new[] { userId } : users.ToArray(),
new UserListUpdated
{
ChannelId = Id,
Users = users.Select(it => userManager.GetUser(it)).ToList()
});
}
private async void BroadcastNextSongUpdated(ClientSong next, string userId = null)
{
if (next == null) return;
if (nextServerSong == null || nextServerSong.SongId != next.SongId || nextServerSong.SiteId != next.SiteId)
{
nextServerSong = await GetSong(next);
}
if (nextServerSong == null) return;
socketManager.SendMessage(userId != null ? new[] { userId } : UserList.ToArray(),
new NextSongUpdated
{
ChannelId = Id,
Song = nextServerSong
});
}
private async void BroadcastPlayCurrentSong(UserSong current, string userId = null)
{
if (current?.Song != null)
{
Song song;
if (nextServerSong != null &&
nextServerSong.SiteId == current.Song.SiteId &&
nextServerSong.SongId == current.Song.SongId)
{
song = nextServerSong;
}
else
{
song = await GetSong(current.Song);
}
socketManager.SendMessage(userId != null ? new[] { userId } : list.UserList.ToArray()
, new Play
{
ChannelId = Id,
Downvote = downvoteUsers.Contains(userId),
Song = song ?? new Song
{
SiteId = current.Song.SiteId,
SongId = current.Song.SongId,
Title = "server load error",
Artist = current.Song.SiteId,
Album = current.Song.SongId
}, // Workaround for play song == null problem
Elapsed = Elapsed,
User = current.UserId
});
if (song == null)
{
BroadcastNotification(string.Format("Server error: Failed to get song {0}:{1}", current.Song.SiteId, current.Song.SongId), userId);
}
}
}
private void BroadcastNotification(string message, string userId = null)
{
socketManager.SendMessage(userId != null ? new[] { userId } : list.UserList.ToArray(),
new NotificationEvent
{
ChannelId = Id,
Notification = new Notification
{
Message = message,
Timeout = 10000
}
});
}
private void EmitChannelInfo(string userId)
{
BroadcastUserListUpdated(userId);
BroadcastPlayCurrentSong(list.CurrentPlaying, userId);
BroadcastNextSongUpdated(list.NextSong, userId);
}
public async Task<Song> GetCurrent() {
return await GetSong(list.CurrentPlaying?.Song);
}
}
}