diff --git a/BLiveIF/BLiveIF.csproj b/BLiveIF/BLiveIF.csproj
new file mode 100644
index 00000000..c91b685b
--- /dev/null
+++ b/BLiveIF/BLiveIF.csproj
@@ -0,0 +1,41 @@
+
+
+ net462
+ 8.0
+ Release;Beta;Alpha;Debug
+
+
+ bin\Release\
+ TRACE
+ 4
+ pdbonly
+ true
+ prompt
+ 4
+
+
+ bin\Beta\
+ TRACE;BETA
+ 4
+ true
+ pdbonly
+ AnyCPU
+ prompt
+
+
+ TRACE;DEBUG;ALPHA
+ 4
+ full
+ true
+ false
+
+
+ DEBUG;TRACE
+ 4
+ full
+ true
+
+
+
+
+
\ No newline at end of file
diff --git a/BLiveIF/Message.cs b/BLiveIF/Message.cs
new file mode 100644
index 00000000..8892d4a6
--- /dev/null
+++ b/BLiveIF/Message.cs
@@ -0,0 +1,70 @@
+using SitePlugin;
+using System;
+using System.Collections.Generic;
+
+namespace BLiveSitePlugin
+{
+ public enum BLiveMessageType
+ {
+ Unknown,
+ Comment,
+ //Item,
+ Stamp,
+ Yell,
+ Connected,
+ Disconnected,
+ }
+
+
+ public interface IBLiveMessage : ISiteMessage
+ {
+ BLiveMessageType BLiveMessageType { get; }
+ }
+ public interface IBLiveConnected : IBLiveMessage
+ {
+ string Text { get; }
+ }
+ public interface IBLiveDisconnected : IBLiveMessage
+ {
+ string Text { get; }
+ }
+ public interface IBLiveComment : IBLiveMessage
+ {
+ IEnumerable NameItems { get; }
+ IEnumerable MessageItems { get; }
+ string Id { get; }
+ DateTime PostTime { get; }
+ string UserId { get; }
+ }
+ public interface IBLiveStamp : IBLiveMessage
+ {
+ IMessageImage Stamp { get; }
+ string Message { get; }
+ IEnumerable NameItems { get; set; }
+ IMessageImage UserIcon { get; }
+ DateTime PostTime { get; }
+ string Id { get; }
+ }
+ public interface IBLiveYell : IBLiveMessage
+ {
+ string YellPoints { get; }
+ string Message { get; }
+ IEnumerable NameItems { get; }
+ IMessageImage UserIcon { get; }
+ DateTime PostTime { get; }
+ string Id { get; }
+ }
+ //public interface IBLiveItem : IBLiveMessage
+ //{
+ // string ItemName { get; }
+ // int ItemCount { get; }
+ // //string Comment { get; }
+ // long Id { get; }
+ // //string UserName { get; }
+ // string UserPath { get; }
+ // long UserId { get; }
+ // string AccountName { get; }
+ // long PostedAt { get; }
+ // string UserIconUrl { get; }
+ //}
+}
\ No newline at end of file
diff --git a/BLiveIF/Properties/AssemblyInfo.cs b/BLiveIF/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..5f282702
--- /dev/null
+++ b/BLiveIF/Properties/AssemblyInfo.cs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/BLiveSitePlugin/API.cs b/BLiveSitePlugin/API.cs
new file mode 100644
index 00000000..4fbd7cc8
--- /dev/null
+++ b/BLiveSitePlugin/API.cs
@@ -0,0 +1,135 @@
+using Codeplex.Data;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace BLiveSitePlugin.Low.BanList
+{
+ public class Item
+ {
+ public string banned_user_id { get; set; }
+ }
+
+ public class Data
+ {
+ public List- items { get; set; }
+ }
+
+ public class RootObject
+ {
+ public int status { get; set; }
+ public Data data { get; set; }
+ }
+}
+namespace BLiveSitePlugin
+{
+ class Me
+ {
+ public string DisplayName { get; set; }
+ public string UserId { get; set; }
+ }
+ static class API
+ {
+ public static async Task GetMeAsync(IDataSource server, CookieContainer cc)
+ {
+ var me = new Me();
+ var url = "https://live.carol-i.com";
+ var res = await server.GetAsync(url, cc);
+ var match0 = Regex.Match(res, "
([^<]*)
");
+ if (match0.Success)
+ {
+ var displayName = match0.Groups[1].Value;
+ me.DisplayName = displayName;
+ }
+ var match1 = Regex.Match(res, "([^<]*)
");
+ if (match1.Success)
+ {
+ me.UserId = match1.Groups[1].Value;
+ }
+ return me;
+ }
+ public static async Task GetMovieInfo(IDataSource dataSource, string liveId, CookieContainer cc)
+ {
+ //https://public.blive.tv/external/api/v5/movies/pC8n3HQX5gh
+ var url = "https://public.blive.tv/external/api/v5/movies/" + liveId;
+ var ret = await dataSource.GetAsync(url, cc);
+ var obj = Tools.Deserialize(ret);
+ return new MovieInfo(obj);
+ }
+ public static async Task GetChannelMovies(IDataSource dataSource, string channelId)
+ {
+ //https://public.blive.tv/external/api/v5/movies?channel_id=rainbow6jp
+ var url = "https://public.blive.tv/external/api/v5/movies?channel_id=" + channelId;
+ var ret = await dataSource.GetAsync(url);
+ var obj = Tools.Deserialize(ret);
+ return obj;
+ }
+ public static async Task GetMovies(IDataSource dataSource, string channelId)
+ {
+ var url = $"https://public.blive.tv/external/api/v5/movies?channel_id={channelId}&sort=onair_status";
+ var res = await dataSource.GetAsync(url);
+ var obj = Tools.Deserialize(res);
+ return obj;
+ }
+ public static async Task<(Low.Chats.RootObject[], string raw)> GetChats(IDataSource dataSource, string liveId, DateTime toCreatedAt, CookieContainer cc)
+ {
+ //https://public.blive.tv/external/api/v5/movies/9PgmVnlqtMz/chats?to_created_at=2018-07-24T19:32:50.395Z
+ var url = "https://public.blive.tv/external/api/v5/movies/" + liveId + "/chats?to_created_at=" + toCreatedAt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
+ var res = await dataSource.GetAsync(url, cc);
+ var obj = Tools.Deserialize(res);
+ return (obj, res);
+ }
+ }
+}
+namespace BLiveSitePlugin.Low
+{
+ public class WebsocketContext2
+ {
+ public string sid { get; set; }
+ public List upgrades { get; set; }
+ public int pingInterval { get; set; }
+ public int pingTimeout { get; set; }
+ }
+ public class Item
+ {
+ public string user_id { get; set; }
+ public string user_name { get; set; }
+ public string user_type { get; set; }
+ public string user_key { get; set; }
+ public int user_rank { get; set; }
+ public string user_icon { get; set; }
+ public string room_id { get; set; }
+ public string chat_id { get; set; }
+ public string message { get; set; }
+ public string item { get; set; }
+ public int supporter_rank { get; set; }
+ public int is_creaters { get; set; }
+ public string golds { get; set; }
+ public string cre_dt { get; set; }
+ public int is_fresh { get; set; }
+ public int is_warned { get; set; }
+ public int has_banned_word { get; set; }
+ public int is_moderator { get; set; }
+ public int is_premium { get; set; }
+ public int is_premium_hidden { get; set; }
+ public string user_color { get; set; }
+ public string display_dt { get; set; }
+ public string del_flg { get; set; }
+ public string quality_type { get; set; }
+ }
+ public class Data
+ {
+ public List- items { get; set; }
+ }
+
+ public class ChatList
+ {
+ public int status { get; set; }
+ public Data data { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/BLiveSitePlugin/BLiveCommentData.cs b/BLiveSitePlugin/BLiveCommentData.cs
new file mode 100644
index 00000000..50a663ca
--- /dev/null
+++ b/BLiveSitePlugin/BLiveCommentData.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using SitePlugin;
+
+namespace BLiveSitePlugin
+{
+ class BLiveCommentData : IBLiveCommentData
+ {
+ public bool IsYell => !string.IsNullOrEmpty(YellPoints);
+ public string YellPoints { get; set; }
+ public string Message { get; set; }
+ public string Id { get; set; }
+ public string UserId { get; set; }
+ public DateTime PostTime { get; set; }
+ public string UserKey { get; set; }
+ public string UserType { get; set; }
+ public IMessageImage Stamp { get; set; }
+ public string Name { get; set; }
+ public List NameIcons { get; set; }
+ public TimeSpan Elapsed { get; set; }
+ public string UserIconUrl { get; set; }
+ }
+}
diff --git a/BLiveSitePlugin/BLiveCommentViewModel.cs b/BLiveSitePlugin/BLiveCommentViewModel.cs
new file mode 100644
index 00000000..ec1cf8ec
--- /dev/null
+++ b/BLiveSitePlugin/BLiveCommentViewModel.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Common;
+using SitePlugin;
+
+namespace BLiveSitePlugin
+{
+ public interface IBLiveCommentViewModel : ICommentViewModel
+ {
+ string PostDate { get; }
+ string Elapsed { get; }
+ bool IsStamp { get; }
+ bool IsYell { get; }
+ }
+ class BLiveCommentViewModel : CommentViewModelBase, IBLiveCommentViewModel
+ {
+ public override MessageType MessageType { get; protected set; }
+ private ICommentOptions _options;
+ private readonly IBLiveSiteOptions _siteOptions;
+
+ public string PostDate { get; }
+ public string Elapsed { get; }
+ public override string UserId { get; }
+ public bool IsStamp { get; }
+ public bool IsYell { get; }
+ public BLiveCommentViewModel(IBLiveCommentData commentData, ICommentOptions options, IBLiveSiteOptions siteOptions, ICommentProvider commentProvider, bool isFirstComment, IUser user)
+ : base(options, user, commentProvider, isFirstComment)
+ {
+ MessageType = MessageType.Comment;
+ _options = options;
+ _siteOptions = siteOptions;
+ UserId = commentData.UserId;
+ Id = commentData.Id;
+ PostDate = commentData.PostTime.ToString("HH:mm:ss");
+ var elapsed = commentData.Elapsed;
+ Elapsed = Tools.ElapsedToString(elapsed);
+ IsStamp = commentData.Stamp != null;
+ IsYell = commentData.IsYell;
+ if (!string.IsNullOrEmpty(commentData.UserIconUrl))
+ {
+ Thumbnail = new MessageImage { Url = commentData.UserIconUrl };
+ }
+ if (siteOptions.IsAutoSetNickname)
+ {
+ var nick = ExtractNickname(commentData.Message);
+ if (!string.IsNullOrEmpty(nick))
+ {
+ user.Nickname = nick;
+ }
+ }
+ //Name
+ {
+ var nameItems = new List();
+ nameItems.Add(MessagePartFactory.CreateMessageText(commentData.Name));
+ nameItems.AddRange(commentData.NameIcons);
+ NameItemsInternal = nameItems;
+ }
+ //Message
+ {
+ var messageItems = new List();
+ if (commentData.IsYell)
+ {
+ MessageType = MessageType.BroadcastInfo;
+ messageItems.Add(MessagePartFactory.CreateMessageText("エールポイント:" + commentData.YellPoints + Environment.NewLine));
+ }
+ messageItems.Add(MessagePartFactory.CreateMessageText(commentData.Message));
+ if (commentData.Stamp != null)
+ {
+ MessageType = MessageType.BroadcastInfo;
+ messageItems.Add(commentData.Stamp);
+ }
+ MessageItems = messageItems;
+ }
+ Init();
+ }
+ protected virtual void PlaySound(string filePath)
+ {
+ var player = new System.Media.SoundPlayer(filePath);
+ player.Play();
+ }
+ public override async Task AfterCommentAdded()
+ {
+ await Task.Yield();
+ try
+ {
+ if (IsStamp)
+ {
+ if (_siteOptions.IsPlayStampMusic && !string.IsNullOrEmpty(_siteOptions.StampMusicFilePath))
+ {
+ PlaySound(_siteOptions.StampMusicFilePath);
+ }
+ }
+ if (IsYell)
+ {
+ if (_siteOptions.IsPlayYellMusic && !string.IsNullOrEmpty(_siteOptions.YellMusicFilePath))
+ {
+ PlaySound(_siteOptions.YellMusicFilePath);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex.Message);
+ }
+ }
+ }
+}
diff --git a/BLiveSitePlugin/BLiveOptionsPanel.xaml b/BLiveSitePlugin/BLiveOptionsPanel.xaml
new file mode 100644
index 00000000..1a5ac23f
--- /dev/null
+++ b/BLiveSitePlugin/BLiveOptionsPanel.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/BLiveSitePlugin/BLiveOptionsPanel.xaml.cs b/BLiveSitePlugin/BLiveOptionsPanel.xaml.cs
new file mode 100644
index 00000000..c094de82
--- /dev/null
+++ b/BLiveSitePlugin/BLiveOptionsPanel.xaml.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace BLiveSitePlugin
+{
+ ///
+ /// Interaction logic for BLiveOptionsPanel.xaml
+ ///
+ public partial class BLiveOptionsPanel : UserControl
+ {
+ public BLiveOptionsPanel()
+ {
+ InitializeComponent();
+ }
+ public void SetViewModel(BLiveOptionsViewModel vm)
+ {
+ this.DataContext = vm;
+ }
+ public BLiveOptionsViewModel GetViewModel()
+ {
+ return (BLiveOptionsViewModel)this.DataContext;
+ }
+ }
+}
diff --git a/BLiveSitePlugin/BLiveOptionsTabPage.cs b/BLiveSitePlugin/BLiveOptionsTabPage.cs
new file mode 100644
index 00000000..1fbd04d1
--- /dev/null
+++ b/BLiveSitePlugin/BLiveOptionsTabPage.cs
@@ -0,0 +1,28 @@
+using SitePlugin;
+using System.Windows.Controls;
+
+namespace BLiveSitePlugin
+{
+ public class BLiveOptionsTabPage : IOptionsTabPage
+ {
+ public string HeaderText { get; }
+
+ public UserControl TabPagePanel => _panel;
+
+ public void Apply()
+ {
+ var optionsVm = _panel.GetViewModel();
+ optionsVm.OriginOptions.Set(optionsVm.ChangedOptions);
+ }
+
+ public void Cancel()
+ {
+ }
+ private readonly BLiveOptionsPanel _panel;
+ public BLiveOptionsTabPage(string displayName, BLiveOptionsPanel panel)
+ {
+ HeaderText = displayName;
+ _panel = panel;
+ }
+ }
+}
diff --git a/BLiveSitePlugin/BLiveOptionsViewModel.cs b/BLiveSitePlugin/BLiveOptionsViewModel.cs
new file mode 100644
index 00000000..a3e8f816
--- /dev/null
+++ b/BLiveSitePlugin/BLiveOptionsViewModel.cs
@@ -0,0 +1,121 @@
+using GalaSoft.MvvmLight.CommandWpf;
+using System;
+using System.ComponentModel;
+using System.Windows.Input;
+
+namespace BLiveSitePlugin
+{
+ public class BLiveOptionsViewModel : INotifyPropertyChanged
+ {
+ public ICommand ShowOpenStampMusicSelectorCommand { get; }
+ public ICommand ShowOpenYellMusicSelectorCommand { get; }
+ private void ShowOpenStampMusicSelector()
+ {
+ var filename = OpenFileDialog("", "音声ファイルを指定して下さい", "waveファイル|*.wav");
+ if (!string.IsNullOrEmpty(filename))
+ {
+ StampMusicFilePath = filename;
+ }
+ }
+ private void ShowOpenYellMusicSelector()
+ {
+ var filename = OpenFileDialog("", "音声ファイルを指定して下さい", "waveファイル|*.wav");
+ if (!string.IsNullOrEmpty(filename))
+ {
+ YellMusicFilePath = filename;
+ }
+ }
+ protected virtual string OpenFileDialog(string defaultPath, string title, string filter)
+ {
+ string ret = null;
+ var fileDialog = new Microsoft.Win32.OpenFileDialog();
+ fileDialog.Title = title;
+ fileDialog.Filter = filter;
+ var result = fileDialog.ShowDialog();
+ if (result == true)
+ {
+ ret = fileDialog.FileName;
+ }
+ return ret;
+ }
+ public int StampSize
+ {
+ get { return _changed.StampSize; }
+ set { _changed.StampSize = value; }
+ }
+ public bool IsPlayStampMusic
+ {
+ get { return _changed.IsPlayStampMusic; }
+ set
+ {
+ _changed.IsPlayStampMusic = value;
+ RaisePropertyChanged();
+ }
+ }
+ public string StampMusicFilePath
+ {
+ get { return _changed.StampMusicFilePath; }
+ set
+ {
+ _changed.StampMusicFilePath = value;
+ RaisePropertyChanged();
+ }
+ }
+ public bool IsPlayYellMusic
+ {
+ get { return _changed.IsPlayYellMusic; }
+ set
+ {
+ _changed.IsPlayYellMusic = value;
+ RaisePropertyChanged();
+ }
+ }
+ public string YellMusicFilePath
+ {
+ get { return _changed.YellMusicFilePath; }
+ set
+ {
+ _changed.YellMusicFilePath = value;
+ RaisePropertyChanged();
+ }
+ }
+ public bool IsAutoSetNickname
+ {
+ get { return ChangedOptions.IsAutoSetNickname; }
+ set { ChangedOptions.IsAutoSetNickname = value; }
+ }
+ private readonly BLiveSiteOptions _origin;
+ private readonly BLiveSiteOptions _changed;
+ internal BLiveSiteOptions OriginOptions { get { return _origin; } }
+ internal BLiveSiteOptions ChangedOptions { get { return _changed; } }
+
+ internal BLiveOptionsViewModel(BLiveSiteOptions siteOptions)
+ {
+ _origin = siteOptions;
+ _changed = siteOptions.Clone();
+ ShowOpenStampMusicSelectorCommand = new RelayCommand(ShowOpenStampMusicSelector);
+ ShowOpenYellMusicSelectorCommand = new RelayCommand(ShowOpenYellMusicSelector);
+ }
+
+ #region INotifyPropertyChanged
+ [NonSerialized]
+ private System.ComponentModel.PropertyChangedEventHandler _propertyChanged;
+ ///
+ ///
+ ///
+ public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged
+ {
+ add { _propertyChanged += value; }
+ remove { _propertyChanged -= value; }
+ }
+ ///
+ ///
+ ///
+ ///
+ protected void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
+ {
+ _propertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
+ }
+ #endregion
+ }
+}
diff --git a/BLiveSitePlugin/BLiveSiteContext.cs b/BLiveSitePlugin/BLiveSiteContext.cs
new file mode 100644
index 00000000..bf8ebb6e
--- /dev/null
+++ b/BLiveSitePlugin/BLiveSiteContext.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Linq;
+using System.Text;
+using SitePlugin;
+using Common;
+using System.Windows.Controls;
+using System.Diagnostics;
+using SitePluginCommon;
+
+namespace BLiveSitePlugin
+{
+ public class BLiveSiteContext : SiteContextBase
+ {
+ public override Guid Guid => new Guid("F4434012-3E68-4DD9-B2A8-F2BD7D601725");
+
+ public override string DisplayName => "B-LIVE";
+ protected override SiteType SiteType => SiteType.BLive;
+ public override IOptionsTabPage TabPanel
+ {
+ get
+ {
+ var panel = new BLiveOptionsPanel();
+ panel.SetViewModel(new BLiveOptionsViewModel(_siteOptions));
+ return new BLiveOptionsTabPage(DisplayName, panel);
+ }
+ }
+
+ public override ICommentProvider CreateCommentProvider()
+ {
+ return new CommentProvider(_options, _siteOptions, _logger, _userStoreManager)
+ {
+ SiteContextGuid = Guid,
+ };
+ }
+
+ public override UserControl GetCommentPostPanel(ICommentProvider commentProvider)
+ {
+ var nicoCommentProvider = commentProvider as CommentProvider;
+ Debug.Assert(nicoCommentProvider != null);
+ if (nicoCommentProvider == null)
+ return null;
+
+ var vm = new CommentPostPanelViewModel(nicoCommentProvider, _logger);
+ var panel = new CommentPostPanel
+ {
+ //IsEnabled = false,
+ DataContext = vm
+ };
+ return panel;
+ }
+
+ public override bool IsValidInput(string input)
+ {
+ return Tools.IsValidUrl(input);
+ }
+
+ public override void LoadOptions(string path, IIo io)
+ {
+ _siteOptions = new BLiveSiteOptions();
+ try
+ {
+ var s = io.ReadFile(path);
+
+ _siteOptions.Deserialize(s);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex.Message);
+ _logger.LogException(ex, "", $"path={path}");
+ }
+ }
+
+ public override void SaveOptions(string path, IIo io)
+ {
+ try
+ {
+ var s = _siteOptions.Serialize();
+ io.WriteFile(path, s);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex.Message);
+ _logger.LogException(ex, "", path);
+ }
+ }
+ private BLiveSiteOptions _siteOptions;
+ private ICommentOptions _options;
+ private ILogger _logger;
+
+ public BLiveSiteContext(ICommentOptions options, ILogger logger, IUserStoreManager userStoreManager)
+ : base(options,userStoreManager, logger)
+ {
+ _options = options;
+ _logger = logger;
+ }
+ }
+}
diff --git a/BLiveSitePlugin/BLiveSiteOptions.cs b/BLiveSitePlugin/BLiveSiteOptions.cs
new file mode 100644
index 00000000..ef9275e7
--- /dev/null
+++ b/BLiveSitePlugin/BLiveSiteOptions.cs
@@ -0,0 +1,100 @@
+using Common;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
+
+namespace BLiveSitePlugin
+{
+ public interface IBLiveSiteOptions: INotifyPropertyChanged
+ {
+ int StampSize { get; }
+ bool IsPlayStampMusic { get; }
+ string StampMusicFilePath { get; }
+ bool IsPlayYellMusic { get; }
+ string YellMusicFilePath { get; }
+ bool IsAutoSetNickname { get; }
+ }
+ internal class BLiveSiteOptions : DynamicOptionsBase, IBLiveSiteOptions
+ {
+ public int StampSize { get => GetValue(); set => SetValue(value); }
+ public bool IsPlayStampMusic { get => GetValue(); set => SetValue(value); }
+ public string StampMusicFilePath { get => GetValue(); set => SetValue(value); }
+ public bool IsPlayYellMusic { get => GetValue(); set => SetValue(value); }
+ public string YellMusicFilePath { get => GetValue(); set => SetValue(value); }
+ public bool IsAutoSetNickname { get => GetValue(); set => SetValue(value); }
+ protected override void Init()
+ {
+ Dict.Add(nameof(StampSize), new Item { DefaultValue = 64, Predicate = n => n > 0, Serializer = n => n.ToString(), Deserializer = s => int.Parse(s) });
+ Dict.Add(nameof(IsPlayStampMusic), new Item { DefaultValue = false, Predicate = b => true, Serializer = b => b.ToString(), Deserializer = s => bool.Parse(s) });
+ Dict.Add(nameof(StampMusicFilePath), new Item { DefaultValue = "", Predicate = s => !string.IsNullOrEmpty(s), Serializer = s => s, Deserializer = s => s });
+ Dict.Add(nameof(IsPlayYellMusic), new Item { DefaultValue = false, Predicate = b => true, Serializer = b => b.ToString(), Deserializer = s => bool.Parse(s) });
+ Dict.Add(nameof(YellMusicFilePath), new Item { DefaultValue = "", Predicate = s => !string.IsNullOrEmpty(s), Serializer = s => s, Deserializer = s => s });
+ Dict.Add(nameof(IsAutoSetNickname), new Item { DefaultValue = false, Predicate = b => true, Serializer = b => b.ToString(), Deserializer = s => bool.Parse(s) });
+ }
+ internal BLiveSiteOptions Clone()
+ {
+ return (BLiveSiteOptions)this.MemberwiseClone();
+ }
+ internal void Set(BLiveSiteOptions changedOptions)
+ {
+ foreach (var src in changedOptions.Dict)
+ {
+ var v = src.Value;
+ SetValue(v.Value, src.Key);
+ }
+ }
+ #region Converters
+ private FontFamily FontFamilyFromString(string str)
+ {
+ return new FontFamily(str);
+ }
+ private string FontFamilyToString(FontFamily family)
+ {
+ return family.FamilyNames.Values.First();
+ }
+ private FontStyle FontStyleFromString(string str)
+ {
+ return (FontStyle)new FontStyleConverter().ConvertFromString(str);
+ }
+ private string FontStyleToString(FontStyle style)
+ {
+ return new FontStyleConverter().ConvertToString(style);
+ }
+ private FontWeight FontWeightFromString(string str)
+ {
+ return (FontWeight)new FontWeightConverter().ConvertFromString(str);
+ }
+ private string FontWeightToString(FontWeight weight)
+ {
+ return new FontWeightConverter().ConvertToString(weight);
+ }
+ private Color ColorFromArgb(string argb)
+ {
+ if (argb == null)
+ throw new ArgumentNullException("argb");
+ var pattern = "#(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})";
+ var match = System.Text.RegularExpressions.Regex.Match(argb, pattern, System.Text.RegularExpressions.RegexOptions.Compiled);
+
+ if (!match.Success)
+ {
+ throw new ArgumentException("形式が不正");
+ }
+ else
+ {
+ var a = byte.Parse(match.Groups["a"].Value, System.Globalization.NumberStyles.HexNumber);
+ var r = byte.Parse(match.Groups["r"].Value, System.Globalization.NumberStyles.HexNumber);
+ var g = byte.Parse(match.Groups["g"].Value, System.Globalization.NumberStyles.HexNumber);
+ var b = byte.Parse(match.Groups["b"].Value, System.Globalization.NumberStyles.HexNumber);
+ return Color.FromArgb(a, r, g, b);
+ }
+ }
+ private string ColorToArgb(Color color)
+ {
+ var argb = color.ToString();
+ return argb;
+ }
+ #endregion
+ }
+}
diff --git a/BLiveSitePlugin/BLiveSitePlugin.csproj b/BLiveSitePlugin/BLiveSitePlugin.csproj
new file mode 100644
index 00000000..7ae15299
--- /dev/null
+++ b/BLiveSitePlugin/BLiveSitePlugin.csproj
@@ -0,0 +1,78 @@
+
+
+ true
+ net462
+ 8.0
+ Release;Beta;Alpha;Debug
+ true
+
+
+ bin\Release\
+ TRACE
+ 4
+ pdbonly
+ true
+ prompt
+ 4
+
+
+ bin\Beta\
+ TRACE;BETA
+ 4
+ true
+ pdbonly
+ AnyCPU
+ prompt
+
+
+ TRACE;DEBUG;ALPHA
+ 4
+ full
+ true
+ false
+
+
+ DEBUG;TRACE
+ 4
+ full
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
\ No newline at end of file
diff --git a/BLiveSitePlugin/BLiveWebsocket.cs b/BLiveSitePlugin/BLiveWebsocket.cs
new file mode 100644
index 00000000..fa4c300d
--- /dev/null
+++ b/BLiveSitePlugin/BLiveWebsocket.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Threading.Tasks;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Net;
+using Common;
+
+namespace BLiveSitePlugin
+{
+ class BLiveWebsocket : IBLiveWebsocket
+ {
+ private Websocket _websocket;
+ private readonly ILogger _logger;
+ //public event EventHandler CommentReceived;
+ public event EventHandler Received;
+
+ public async Task ReceiveAsync(string liveId, string userAgent, List cookies)
+ {
+ var cookieList = new List>();
+
+ var origin = "https://live.carol-i.com";
+
+ _websocket = new Websocket();
+ _websocket.Received += Websocket_Received;
+ // liveIdを渡すためのラムダ式でOpenedイベントを設定
+ _websocket.Opened += (sender, e) => Websocket_Opened(sender, e, liveId);
+
+ var url = $"wss://live.carol-i.com:6001/socket.io/?EIO=3&transport=websocket";
+ await _websocket.ReceiveAsync(url, cookieList, userAgent, origin);
+ //切断後処理
+ _heartbeatTimer.Enabled = false;
+
+ }
+
+ private void Websocket_Opened(object sender, EventArgs e, string liveId)
+ {
+ _heartbeatTimer.Enabled = true;
+
+ // WebSocketが開かれた後にチャンネルにサブスクライブする
+ string subscribeMessage = $"42[\"subscribe\", {{\"channel\": \"message.received.{liveId}\"}}]";
+ Task.Run(async () => await SendAsync(subscribeMessage));
+ }
+
+ private void Websocket_Received(object sender, string e)
+ {
+ Debug.WriteLine(e);
+ IPacket packet = null;
+ try
+ {
+ packet = Packet.Parse(e);
+ }
+ catch (ParseException ex)
+ {
+ _logger.LogException(ex);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex.Message);
+ }
+ if (packet == null)
+ return;
+ Received?.Invoke(this, packet);
+ }
+
+ public async Task SendAsync(IPacket packet)
+ {
+ if (packet is PacketPing ping)
+ {
+ await SendAsync(ping.Raw);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+ public async Task SendAsync(string s)
+ {
+ await _websocket.SendAsync(s);
+ }
+ System.Timers.Timer _heartbeatTimer = new System.Timers.Timer();
+ public BLiveWebsocket(ILogger logger)
+ {
+ _logger = logger;
+ _heartbeatTimer.Interval = 25 * 1000;
+ _heartbeatTimer.Elapsed += _heartBeatTimer_Elapsed;
+ _heartbeatTimer.AutoReset = true;
+ }
+ private async void _heartBeatTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ try
+ {
+ await SendAsync(new PacketPing());
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine(ex.Message);
+ }
+ }
+
+ public void Disconnect()
+ {
+ _websocket.Disconnect();
+ }
+ }
+}
diff --git a/BLiveSitePlugin/CommentPostPanel.xaml b/BLiveSitePlugin/CommentPostPanel.xaml
new file mode 100644
index 00000000..5609c5dc
--- /dev/null
+++ b/BLiveSitePlugin/CommentPostPanel.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/BLiveSitePlugin/CommentPostPanel.xaml.cs b/BLiveSitePlugin/CommentPostPanel.xaml.cs
new file mode 100644
index 00000000..611af82c
--- /dev/null
+++ b/BLiveSitePlugin/CommentPostPanel.xaml.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace BLiveSitePlugin
+{
+ ///
+ /// Interaction logic for CommentPostPanel.xaml
+ ///
+ public partial class CommentPostPanel : UserControl
+ {
+ public CommentPostPanel()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/BLiveSitePlugin/CommentPostPanelViewModel.cs b/BLiveSitePlugin/CommentPostPanelViewModel.cs
new file mode 100644
index 00000000..843cd4b8
--- /dev/null
+++ b/BLiveSitePlugin/CommentPostPanelViewModel.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using Common;
+using GalaSoft.MvvmLight;
+using GalaSoft.MvvmLight.Command;
+namespace BLiveSitePlugin
+{
+ class CommentPostPanelViewModel : ViewModelBase
+ {
+ public ICommand PostCommentCommand { get; }
+
+ #region Input
+ private string _input;
+ public string Input
+ {
+ get { return _input; }
+ set
+ {
+ if (_input == value) return;
+ _input = value;
+ RaisePropertyChanged();
+ }
+ }
+ #endregion //Input
+
+ #region CanPostComment
+ private bool _canPostComment;
+ public bool CanPostComment
+ {
+ get { return _canPostComment; }
+ set
+ {
+ if (_canPostComment == value) return;
+ _canPostComment = value;
+ RaisePropertyChanged();
+ }
+ }
+ #endregion //CanPostComment
+ private void PostComment(string str)
+ {
+
+ }
+
+ private readonly CommentProvider _commentProvider;
+ private readonly ILogger _logger;
+
+
+ #region Ctors
+ public CommentPostPanelViewModel()
+ {
+ if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
+ {
+
+ }
+ else
+ {
+ throw new NotSupportedException();
+ }
+ }
+ public CommentPostPanelViewModel(CommentProvider commentProvider, ILogger logger)
+ {
+ _commentProvider = commentProvider;
+ _logger = logger;
+ PostCommentCommand = new RelayCommand(()=> PostComment(Input));
+
+ Input = "コメント投稿は未対応です";
+ CanPostComment = false;
+ }
+ #endregion
+ }
+}
diff --git a/BLiveSitePlugin/CommentProvider.cs b/BLiveSitePlugin/CommentProvider.cs
new file mode 100644
index 00000000..7602a6d4
--- /dev/null
+++ b/BLiveSitePlugin/CommentProvider.cs
@@ -0,0 +1,453 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using SitePlugin;
+using ryu_s.BrowserCookie;
+using Common;
+using System.Threading;
+using System.Net;
+using System.Drawing;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Collections.Concurrent;
+using SitePluginCommon;
+
+namespace BLiveSitePlugin
+{
+ [Serializable]
+ public class InvalidInputException : Exception
+ {
+ public InvalidInputException() { }
+ }
+ class CommentProvider : ICommentProvider
+ {
+ string _liveId;
+ Context _context;
+ #region ICommentProvider
+ #region Events
+ public event EventHandler MetadataUpdated;
+ public event EventHandler CanConnectChanged;
+ public event EventHandler CanDisconnectChanged;
+ public event EventHandler Connected;
+ public event EventHandler MessageReceived;
+ #endregion //Events
+
+ #region CanConnect
+ private bool _canConnect;
+ public bool CanConnect
+ {
+ get { return _canConnect; }
+ set
+ {
+ if (_canConnect == value)
+ return;
+ _canConnect = value;
+ CanConnectChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ #endregion //CanConnect
+
+ #region CanDisconnect
+ private bool _canDisconnect;
+ public bool CanDisconnect
+ {
+ get { return _canDisconnect; }
+ set
+ {
+ if (_canDisconnect == value)
+ return;
+ _canDisconnect = value;
+ CanDisconnectChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+ #endregion //CanDisconnect
+ protected virtual Task GetLiveId(string input)
+ {
+ return Tools.GetLiveId(_dataSource, input);
+ }
+ private void BeforeConnecting()
+ {
+ CanConnect = false;
+ CanDisconnect = true;
+ _isExpectedDisconnect = false;
+ }
+ private void AfterDisconnected()
+ {
+ _500msTimer.Enabled = false;
+ _ws = null;
+ CanConnect = true;
+ CanDisconnect = false;
+ }
+ protected virtual List GetCookies(IBrowserProfile browserProfile)
+ {
+ List cookies = null;
+ try
+ {
+ cookies = browserProfile.GetCookieCollection("blive.tv");
+ }
+ catch { }
+ return cookies ?? new List();
+ }
+ protected virtual CookieContainer CreateCookieContainer(IBrowserProfile browserProfile)
+ {
+ var cc = new CookieContainer();
+ try
+ {
+ var cookies = browserProfile.GetCookieCollection("blive.tv");
+ foreach (var cookie in cookies)
+ {
+ cc.Add(cookie);
+ }
+ }
+ catch { }
+ return cc;
+ }
+ private async Task ConnectInternalAsync(string input, IBrowserProfile browserProfile)
+ {
+ if (_ws != null)
+ {
+ throw new InvalidOperationException("");
+ }
+ var cookies = GetCookies(browserProfile);
+ _cc = CreateCookieContainer(cookies);
+ _context = Tools.GetContext(cookies);
+ string liveId;
+ try
+ {
+ liveId = await GetLiveId(input);
+ _liveId = liveId;
+ }
+ catch (InvalidInputException ex)
+ {
+ _logger.LogException(ex, "無効な入力値", $"input={input}");
+ SendSystemInfo("無効な入力値です", InfoType.Error);
+ AfterDisconnected();
+ return;
+ }
+
+ Reconnect:
+ _ws = CreateBLiveWebsocket();
+ _ws.Received += WebSocket_Received;
+
+ var userAgent = GetUserAgent(browserProfile.Type);
+ var wsTask = _ws.ReceiveAsync(liveId, userAgent, cookies);
+
+ var tasks = new List
+ {
+ wsTask,
+ };
+
+ while (tasks.Count > 0)
+ {
+ var t = await Task.WhenAny(tasks);
+ try
+ {
+ await wsTask;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogException(ex);
+ }
+ tasks.Remove(wsTask);
+ SendSystemInfo("wsタスク終了", InfoType.Debug);
+ }
+ _ws.Received -= WebSocket_Received;
+
+ // TODO: 意図的ではない切断の場合は再接続する
+ }
+ public async Task ConnectAsync(string input, IBrowserProfile browserProfile)
+ {
+ BeforeConnecting();
+ try
+ {
+ await ConnectInternalAsync(input, browserProfile);
+ }
+ finally
+ {
+ AfterDisconnected();
+ }
+ }
+
+ protected virtual IBLiveWebsocket CreateBLiveWebsocket()
+ {
+ return new BLiveWebsocket(_logger);
+ }
+
+ protected virtual async Task<(Low.Chats.RootObject[], string raw)> GetChats(MovieInfo movieContext2)
+ {
+ return await API.GetChats(_dataSource, movieContext2.Id, DateTime.Now, _cc);
+ }
+
+ protected virtual async Task GetMovieInfo(string liveId)
+ {
+ return await API.GetMovieInfo(_dataSource, liveId, _cc);
+ }
+
+ private CookieContainer CreateCookieContainer(List cookies)
+ {
+ var cc = new CookieContainer();
+ try
+ {
+ foreach (var cookie in cookies)
+ {
+ cc.Add(cookie);
+ }
+ }
+ catch { }
+ return cc;
+ }
+
+ private BLiveMessageContext CreateMessageContext(Tools.IComment comment, IBLiveCommentData commentData, bool isInitialComment)
+ {
+ var userId = commentData.UserId;
+ var user = GetUser(userId) as IUser2;
+ if (!_userDict.ContainsKey(userId))
+ {
+ _userDict.AddOrUpdate(user.UserId, user, (id, u) => u);
+ }
+ bool isFirstComment;
+ if (_userCommentCountDict.ContainsKey(userId))
+ {
+ _userCommentCountDict[userId]++;
+ isFirstComment = false;
+ }
+ else
+ {
+ _userCommentCountDict.Add(userId, 1);
+ isFirstComment = true;
+ }
+
+ var nameItems = new List();
+ nameItems.Add(MessagePartFactory.CreateMessageText(commentData.Name));
+ nameItems.AddRange(commentData.NameIcons);
+ user.Name = nameItems;
+
+ var messageItems = new List();
+ if (commentData.Message != null)
+ {
+ messageItems.Add(MessagePartFactory.CreateMessageText(commentData.Message));
+ }
+
+ BLiveMessageContext messageContext = null;
+ IBLiveMessage message;
+ if (commentData.IsYell)
+ {
+ message = new BLiveYell("")
+ {
+ YellPoints = commentData.YellPoints,
+ Id = commentData.Id,
+ NameItems = nameItems,
+ PostTime = commentData.PostTime,
+ UserId = commentData.UserId,
+ Message = commentData.Message,
+ };
+ }
+ else if (commentData.Stamp != null)
+ {
+ message = new BLiveStamp("")
+ {
+ Stamp = commentData.Stamp,
+ Id = commentData.Id,
+ NameItems = nameItems,
+ PostTime = commentData.PostTime,
+ UserId = commentData.UserId,
+ };
+ }
+ else
+ {
+ message = new BLiveComment("")
+ {
+ MessageItems = messageItems,
+ Id = commentData.Id,
+ NameItems = nameItems,
+ PostTime = commentData.PostTime,
+ UserId = commentData.UserId,
+ };
+
+ }
+ var metadata = new MessageMetadata(message, _options, _siteOptions, user, this, isFirstComment)
+ {
+ IsInitialComment = isInitialComment,
+ SiteContextGuid = SiteContextGuid,
+ };
+ var methods = new BLiveMessageMethods();
+ messageContext = new BLiveMessageContext(message, metadata, methods);
+ return messageContext;
+ }
+
+ IBLiveWebsocket _ws;
+ ///
+ /// ユーザが意図した切断か
+ ///
+ bool _isExpectedDisconnect;
+
+ public void Disconnect()
+ {
+ _isExpectedDisconnect = true;
+ if (_ws != null)
+ {
+ _ws.Disconnect();
+ }
+ }
+ public IUser GetUser(string userId)
+ {
+ return _userStoreManager.GetUser(SiteType.BLive, userId);
+ }
+ #endregion //ICommentProvider
+
+
+ #region Fields
+ private ICommentOptions _options;
+ private BLiveSiteOptions _siteOptions;
+ private ILogger _logger;
+ private IUserStoreManager _userStoreManager;
+ private readonly IDataSource _dataSource;
+ private CookieContainer _cc;
+ #endregion //Fields
+
+ #region ctors
+ System.Timers.Timer _500msTimer = new System.Timers.Timer();
+ public CommentProvider(ICommentOptions options, BLiveSiteOptions siteOptions, ILogger logger, IUserStoreManager userStoreManager)
+ {
+ _options = options;
+ _siteOptions = siteOptions;
+ _logger = logger;
+ _userStoreManager = userStoreManager;
+ _dataSource = new DataSource();
+ _500msTimer.Interval = 500;
+ _500msTimer.Elapsed += _500msTimer_Elapsed;
+ _500msTimer.AutoReset = true;
+
+ CanConnect = true;
+ CanDisconnect = false;
+ }
+
+ private void _500msTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
+ {
+ var elapsed = DateTime.Now - _startAt;
+ MetadataUpdated?.Invoke(this, new Metadata
+ {
+ Elapsed = Tools.ElapsedToString(elapsed),
+ });
+ }
+ #endregion //ctors
+
+ #region Events
+ #endregion //Events
+ private void SendSystemInfo(string message, InfoType type)
+ {
+ var context = InfoMessageContext.Create(new InfoMessage
+ {
+ Text = message,
+ SiteType = SiteType.BLive,
+ Type = type,
+ }, _options);
+ MessageReceived?.Invoke(this, context);
+ }
+ public Guid SiteContextGuid { get; set; }
+ Dictionary _userCommentCountDict = new Dictionary();
+ [Obsolete]
+ Dictionary _userViewModelDict = new Dictionary();
+ ConcurrentDictionary _userDict = new ConcurrentDictionary();
+ DateTime _startAt;
+ private static string GetUserAgent(BrowserType browser)
+ {
+ string userAgent;
+ switch (browser)
+ {
+ case BrowserType.Chrome:
+ userAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36";
+ break;
+ case BrowserType.Firefox:
+ userAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0";
+ break;
+ case BrowserType.IE:
+ userAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; rv:11.0) like Gecko";
+ break;
+ case BrowserType.Opera:
+ userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.1144";
+ break;
+ default:
+ throw new Exception("未対応のブラウザ");
+ }
+ return userAgent;
+ }
+
+ private void WebSocket_Received(object sender, IPacket e)
+ {
+ try
+ {
+ if (e is PacketMessageEventMessageChat chat)
+ {
+ var comment = Tools.Parse(chat.Comment);
+ var commentData = Tools.CreateCommentData(comment, _startAt, _siteOptions);
+ var messageContext = CreateMessageContext(comment, commentData, false);
+ if (messageContext != null)
+ {
+ MessageReceived?.Invoke(this, messageContext);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogException(ex);
+ }
+ }
+ public async Task PostCommentAsync(string str)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task GetCurrentUserInfo(IBrowserProfile browserProfile)
+ {
+ var cc = CreateCookieContainer(browserProfile);
+ var me = await API.GetMeAsync(_dataSource, cc);
+ return new CurrentUserInfo
+ {
+ IsLoggedIn = !string.IsNullOrEmpty(me.UserId),
+ UserId = me.UserId,
+ Username = me.DisplayName,
+ };
+ }
+
+ public void SetMessage(string raw)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ class CurrentUserInfo : ICurrentUserInfo
+ {
+ public string Username { get; set; }
+ public string UserId { get; set; }
+ public bool IsLoggedIn { get; set; }
+ }
+ public class MovieContext2
+ {
+ public string Title { get; set; }
+ //"0":予約?
+ //"1":放送中
+ //"2":放送終了
+ public string OnairStatus { get; set; }
+ public string MovieId { get; set; }
+ public string RecxuserId { get; set; }
+ public string Id { get; set; }
+ public DateTime StartAt { get; set; }
+ }
+ class MessageImagePortion : IMessageImagePortion
+ {
+ public int SrcX { get; set; }
+
+ public int SrcY { get; set; }
+
+ public int SrcWidth { get; set; }
+
+ public int SrcHeight { get; set; }
+
+ public int Width { get; set; }
+
+ public int Height { get; set; }
+
+ public Image Image { get; set; }
+ public string Alt { get; set; }
+ }
+}
diff --git a/BLiveSitePlugin/Context.cs b/BLiveSitePlugin/Context.cs
new file mode 100644
index 00000000..60ab5e70
--- /dev/null
+++ b/BLiveSitePlugin/Context.cs
@@ -0,0 +1,17 @@
+namespace BLiveSitePlugin
+{
+ public class Context
+ {
+ public string Uuid { get; }
+ public string AccessToken { get; }
+ public Context(string uuid, string accessToken)
+ {
+ Uuid = uuid;
+ AccessToken = accessToken;
+ }
+ public override string ToString()
+ {
+ return $"Uuid={Uuid}, AccessToken={AccessToken}";
+ }
+ }
+}
diff --git a/BLiveSitePlugin/DataSource.cs b/BLiveSitePlugin/DataSource.cs
new file mode 100644
index 00000000..a1f9256d
--- /dev/null
+++ b/BLiveSitePlugin/DataSource.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Threading.Tasks;
+using System.Net;
+using System.Net.Http;
+using System.Collections.Generic;
+using SitePluginCommon;
+
+namespace BLiveSitePlugin
+{
+ interface IDataSource
+ {
+ Task GetAsync(string url);
+ Task GetAsync(string url, Dictionary headers);
+ Task GetAsync(string url, CookieContainer cc);
+ Task GetByteArrayAsync(string url, CookieContainer cc);
+ Task PostJsonAsync(string url, Dictionary headers, string json);
+ }
+ class DataSource : ServerBase, IDataSource
+ {
+ public async Task GetAsync(string url, CookieContainer cc)
+ {
+ var result = await GetInternalAsync(new HttpOptions
+ {
+ Url = url,
+ Cc = cc,
+ });
+ var str = await result.Content.ReadAsStringAsync();
+ return str;
+ }
+ public Task GetAsync(string url)
+ {
+ return GetAsync(url, (CookieContainer)null);
+ }
+ public async Task GetAsync(string url, Dictionary headers)
+ {
+ var result = await GetInternalAsync(new HttpOptions
+ {
+ Url = url,
+ Headers = headers,
+ });
+ var str = await result.Content.ReadAsStringAsync();
+ return str;
+ }
+ public async Task PostJsonAsync(string url, Dictionary headers, string json)
+ {
+ var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
+ var result = await PostInternalAsync(new HttpOptions
+ {
+ Url = url,
+ Headers = headers,
+ }, content);
+ var str = await result.Content.ReadAsStringAsync();
+ return str;
+ }
+
+ public async Task GetByteArrayAsync(string url, CookieContainer cc)
+ {
+ var result = await GetInternalAsync(new HttpOptions
+ {
+ Url = url,
+ Cc = cc,
+ });
+ var arr = await result.Content.ReadAsByteArrayAsync();
+ return arr;
+ }
+
+ public DataSource()
+ {
+
+ }
+ }
+}
diff --git a/BLiveSitePlugin/DynamicJson.cs b/BLiveSitePlugin/DynamicJson.cs
new file mode 100644
index 00000000..45771ed6
--- /dev/null
+++ b/BLiveSitePlugin/DynamicJson.cs
@@ -0,0 +1,431 @@
+/*--------------------------------------------------------------------------
+* DynamicJson
+* ver 1.2.0.0 (May. 21th, 2010)
+*
+* created and maintained by neuecc
+* licensed under Microsoft Public License(Ms-PL)
+* http://neue.cc/
+* http://dynamicjson.codeplex.com/
+*--------------------------------------------------------------------------*/
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Dynamic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization.Json;
+using System.Text;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace Codeplex.Data
+{
+ public class DynamicJson : DynamicObject
+ {
+ private enum JsonType
+ {
+ @string, number, boolean, @object, array, @null
+ }
+
+ // public static methods
+
+ /// from JsonSring to DynamicJson
+ public static dynamic Parse(string json)
+ {
+ return Parse(json, Encoding.Unicode);
+ }
+
+ /// from JsonSring to DynamicJson
+ public static dynamic Parse(string json, Encoding encoding)
+ {
+ using (var reader = JsonReaderWriterFactory.CreateJsonReader(encoding.GetBytes(json), XmlDictionaryReaderQuotas.Max))
+ {
+ return ToValue(XElement.Load(reader));
+ }
+ }
+
+ /// from JsonSringStream to DynamicJson
+ public static dynamic Parse(Stream stream)
+ {
+ using (var reader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max))
+ {
+ return ToValue(XElement.Load(reader));
+ }
+ }
+
+ /// from JsonSringStream to DynamicJson
+ public static dynamic Parse(Stream stream, Encoding encoding)
+ {
+ using (var reader = JsonReaderWriterFactory.CreateJsonReader(stream, encoding, XmlDictionaryReaderQuotas.Max, _ => { }))
+ {
+ return ToValue(XElement.Load(reader));
+ }
+ }
+
+ /// create JsonSring from primitive or IEnumerable or Object({public property name:property value})
+ public static string Serialize(object obj)
+ {
+ return CreateJsonString(new XStreamingElement("root", CreateTypeAttr(GetJsonType(obj)), CreateJsonNode(obj)));
+ }
+
+ // private static methods
+
+ private static dynamic ToValue(XElement element)
+ {
+ var type = (JsonType)Enum.Parse(typeof(JsonType), element.Attribute("type").Value);
+ switch (type)
+ {
+ case JsonType.boolean:
+ return (bool)element;
+ case JsonType.number:
+ return (double)element;
+ case JsonType.@string:
+ return (string)element;
+ case JsonType.@object:
+ case JsonType.array:
+ return new DynamicJson(element, type);
+ case JsonType.@null:
+ default:
+ return null;
+ }
+ }
+
+ private static JsonType GetJsonType(object obj)
+ {
+ if (obj == null) return JsonType.@null;
+
+ switch (Type.GetTypeCode(obj.GetType()))
+ {
+ case TypeCode.Boolean:
+ return JsonType.boolean;
+ case TypeCode.String:
+ case TypeCode.Char:
+ case TypeCode.DateTime:
+ return JsonType.@string;
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.UInt16:
+ case TypeCode.UInt32:
+ case TypeCode.UInt64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ case TypeCode.SByte:
+ case TypeCode.Byte:
+ return JsonType.number;
+ case TypeCode.Object:
+ return (obj is IEnumerable) ? JsonType.array : JsonType.@object;
+ case TypeCode.DBNull:
+ case TypeCode.Empty:
+ default:
+ return JsonType.@null;
+ }
+ }
+
+ private static XAttribute CreateTypeAttr(JsonType type)
+ {
+ return new XAttribute("type", type.ToString());
+ }
+
+ private static object CreateJsonNode(object obj)
+ {
+ var type = GetJsonType(obj);
+ switch (type)
+ {
+ case JsonType.@string:
+ case JsonType.number:
+ return obj;
+ case JsonType.boolean:
+ return obj.ToString().ToLower();
+ case JsonType.@object:
+ return CreateXObject(obj);
+ case JsonType.array:
+ return CreateXArray(obj as IEnumerable);
+ case JsonType.@null:
+ default:
+ return null;
+ }
+ }
+
+ private static IEnumerable CreateXArray(T obj) where T : IEnumerable
+ {
+ return obj.Cast