From bcf793cd9248c8b0bf635cba1a46d4d25a2da112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=98=89=E7=A5=A5=20=E8=A9=B9?= Date: Fri, 3 May 2024 17:19:05 +0800 Subject: [PATCH] update youtube dashboard --- Controllers/ApiController.cs | 36 +++ Models/DbTableClass.cs | 68 +++++ Models/youtubeDetailClass.cs | 63 +++++ Models/youtubeUpdateClass.cs | 235 ++++++++++++++++++ Program.cs | 1 + Views/Home/Dashboard.cshtml | 29 ++- Views/Shared/_LooperLayout.cshtml | 1 + appsettings.json | 6 +- wwwroot/assets/javascript/custom/dashboard.js | 98 +++++++- wwwroot/assets/javascript/custom/kollist.js | 7 + .../assets/javascript/custom/projectlist.js | 2 +- 11 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 Models/youtubeDetailClass.cs create mode 100644 Models/youtubeUpdateClass.cs diff --git a/Controllers/ApiController.cs b/Controllers/ApiController.cs index 19f8f73..2368d4d 100644 --- a/Controllers/ApiController.cs +++ b/Controllers/ApiController.cs @@ -59,6 +59,33 @@ namespace Journeys_WantHome.Controllers this._httpContextAccessor = httpContextAccessor; } + [Route("youtubeList")] + public ActionResult YoutubeList(IFormCollection obj) { + youtubeListResult ret = new youtubeListResult(); + + authToken token = new authToken(this._httpContextAccessor); + if (token.user_isLogin == false) + { + HttpContext.Response.Cookies.Delete("token_key"); + ret.ret = "no"; + ret.err_code = "99999"; + ret.message = "非登入狀態!"; + return Content(JsonConvert.SerializeObject(ret), "application/json;charset=utf-8"); + } + + List youtubes = conn.Query("select top 15 * from youtube where youtube_revoke = 'N' order by youtube_subscriberCount desc").ToList(); + + foreach (youtube objItem in youtubes) { + youtubeDetailClass detail = new youtubeDetailClass(objItem); + + ret.youtubeList.Add(detail); + } + + + ret.ret = "yes"; + return Content(JsonConvert.SerializeObject(ret), "application/json;charset=utf-8"); + } + [Route("projectKolViewList")] public ActionResult ProjectKolViewList(IFormCollection obj) { projectKolViewResult ret = new projectKolViewResult(); @@ -1900,6 +1927,15 @@ namespace Journeys_WantHome.Controllers } + + public class youtubeListResult + { + public string ret { get; set; } = "no"; + public string err_code { get; set; } = "0000"; + public string message { get; set; } = ""; + + public List youtubeList = new List(); + } public class projectKolViewResult { public string ret { get; set; } = "no"; diff --git a/Models/DbTableClass.cs b/Models/DbTableClass.cs index 4f38671..c68ff55 100644 --- a/Models/DbTableClass.cs +++ b/Models/DbTableClass.cs @@ -8,6 +8,74 @@ using Newtonsoft.Json.Linq; public class DbTableClass { + [Table("youtubeDetail")] + public class youtubeDetail + { + [JsonIgnore] + [Key] + public int youtubeDetail_sn { get; set; } + public string youtube_uid { get; set; } = ""; + public string youtubeDetail_uid { get; set; } = ""; + public DateTime? youtubeDetail_publishedAt { get; set; } + public string youtubeDetail_title { get; set; } = ""; + public string youtubeDetail_description { get; set; } = ""; + public string youtubeDetail_thumbnails { get; set; } = ""; + public string youtubeDetail_videoId { get; set; } = ""; + public int youtubeDetail_viewCount { get; set; } = 0; + public int youtubeDetail_likeCount { get; set; } = 0; + public int youtubeDetail_favoriteCount { get; set; } = 0; + public int youtubeDetail_commentCount { get; set; } = 0; + + [JsonIgnore] + public string youtubeDetail_json { get; set; } = ""; + public DateTime youtubeDetail_createdate { get; set; } = DateTime.Now; + } + + + + [Table("youtube")] + public class youtube + { + [JsonIgnore] + [Key] + public int youtube_sn { get; set; } + public string youtube_uid { get; set; } = ""; + public string kol_uid { get; set; } = ""; + public string kolMedia_uid { get; set; } = ""; + public string youtube_name { get; set; } = ""; + public string youtube_account { get; set; } = ""; + public string youtube_revoke { get; set; } = "N"; + public int youtube_subscriberCount { get; set; } = 0; + public int youtube_viewCount { get; set; } = 0; + public int youtube_avgViewCount { get; set; } = 0; + public string youtube_photo { get; set; } = ""; + + [JsonIgnore] + public string youtube_json { get; set; } = ""; + public DateTime youtube_updateTime { get; set; } = DateTime.Now; + } + + + [Table("updateLog")] + public class updateLog + { + [JsonIgnore] + [Key] + public int updateLog_sn { get; set; } + public string updateLog_type { get; set; } = ""; + public string updateLog_kol_uid { get; set; } = ""; + public string updateLog_kolMedia_uid { get; set; } = ""; + public string updateLog_kolMedia_accountName { get; set; } = ""; + public string updateLog_kolMedia_displayName { get; set; } = ""; + public string updateLog_result { get; set; } = "faild"; + public string updateLog_uid { get; set; } = ""; + + [JsonIgnore] + public string updateLog_text { get; set; } = ""; + public DateTime updateLog_updateTime { get; set; } = DateTime.Now; + } + + [Table("kolProjectView")] public class kolProjectView { diff --git a/Models/youtubeDetailClass.cs b/Models/youtubeDetailClass.cs new file mode 100644 index 0000000..01760ac --- /dev/null +++ b/Models/youtubeDetailClass.cs @@ -0,0 +1,63 @@ +using Dapper; +using Dapper.Contrib.Extensions; +using NPOI.SS.Formula.Functions; +using System.Data.SqlClient; +using System.Security.Cryptography.X509Certificates; +using static DbTableClass; + +public class youtubeDetailClass : youtube +{ + DbConn dbConn = new DbConn(); + SqlConnection conn = new SqlConnection(GlobalClass.appsettings("ConnectionStrings:SQLConnectionString")); + + List youtubeDetails = new List(); + private youtube _youtube; + + public youtubeDetailClass() + { + _youtube = new youtube(); + } + + public youtubeDetailClass(youtube youtubeObj) + { + Type youtubeType = youtubeObj.GetType(); + + foreach (var prop in youtubeType.GetProperties()) + { + string propName = prop.Name; + var valueProperty = youtubeType.GetProperty(propName); + object propValue = valueProperty.GetValue(youtubeObj, null); + + this.GetType().GetProperty(propName).SetValue(this, propValue); + } + + _youtube = youtubeObj; + loadList(); + } + + public youtubeDetailClass(string youtube_uid) { + _youtube = conn.QueryFirstOrDefault("select * from youtube where youtube_uid = @youtube_uid", new { youtube_uid = youtube_uid }); + + if (_youtube != null) + { + youtube youtubeObj = _youtube; + Type youtubeType = youtubeObj.GetType(); + + foreach (var prop in youtubeType.GetProperties()) + { + string propName = prop.Name; + var valueProperty = youtubeType.GetProperty(propName); + object propValue = valueProperty.GetValue(youtubeObj, null); + + this.GetType().GetProperty(propName).SetValue(this, propValue); + } + + loadList(); + } + } + + private void loadList() + { + youtubeDetails = conn.Query("select * from youtubeDetail where youtube_uid = @youtube_uid", new { youtube_uid = _youtube.youtube_uid }).ToList(); + } + } diff --git a/Models/youtubeUpdateClass.cs b/Models/youtubeUpdateClass.cs new file mode 100644 index 0000000..397aae9 --- /dev/null +++ b/Models/youtubeUpdateClass.cs @@ -0,0 +1,235 @@ +using Dapper; +using Dapper.Contrib.Extensions; +using Newtonsoft.Json; +using NPOI.XSSF.UserModel; +using Org.BouncyCastle.Asn1.Ocsp; +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Text; +using System.Web; +using static DbTableClass; +using Org.BouncyCastle.Crypto.Operators; +using System.Globalization; + +public class youtubeUpdateClass : IHostedService, IDisposable +{ + static Timer _timer; + private int execCount = 0; + DbConn dbConn = new DbConn(); + SqlConnection conn = new SqlConnection(GlobalClass.appsettings("ConnectionStrings:SQLConnectionString")); + + public youtubeUpdateClass() { } + + public Task StartAsync(CancellationToken cancellationToken) + { + _timer = new Timer(DoWork, null, + TimeSpan.Zero, + TimeSpan.FromSeconds(5 * 60)); + return Task.CompletedTask; + } + + public async void DoWork(object state) { + conn.Execute("insert into schedule (schedule_type, schedule_log) values ('YouTube', '執行DoWork')"); + + //利用 Interlocked 計數防止重複執行 + Interlocked.Increment(ref execCount); + if (execCount == 1) { + conn.Execute("insert into schedule (schedule_type, schedule_log) values ('YouTube', '執行DoWork,execCount == 1時執行更新工作')"); + + //撈出Youtube頻道清單 + List kolMedias = conn.Query("select * from kolMedia where option_uid = 'media' and optionItem_uid = 'media003' ").ToList(); + + foreach(kolMedia mediaItem in kolMedias) + { + string overDate = DateTime.Now.AddDays(int.Parse(GlobalClass.appsettings("UpdateByDay")) * -1).ToString("yyyy/MM/dd HH:mm:ss"); + + updateLog objLog = conn.QueryFirstOrDefault("select * from updateLog where updateLog_type = 'YouTube' and updateLog_result = 'success' and updateLog_kol_uid = @kol_uid and updateLog_kolMedia_uid = @kolMedia_uid and updateLog_updateTime >= @overDate ", new { kol_uid = mediaItem.kol_uid, kolMedia_uid = mediaItem.kolMedia_uid, overDate = overDate }); + + if (objLog == null) { + + + //撈取youtube帳號訂閱數 + string youtubeChannelsApi = "https://www.googleapis.com/youtube/v3/channels?part=statistics,snippet,contentDetails&forHandle=" + mediaItem.kolMedia_accountName + "&key=" + GlobalClass.appsettings("GoogleKey"); + string channelsStatus = ""; + string channel_id = ""; + using (var httpClient = new HttpClient()) + { + using (var response = await httpClient.GetAsync(youtubeChannelsApi)) + { + channelsStatus = await response.Content.ReadAsStringAsync(); + } + } + + dynamic channelsObj; + channelsObj = JsonConvert.DeserializeObject(channelsStatus); + + if (channelsObj.items != null) + { + channel_id = channelsObj.items[0].id; + + string subscriberCount = channelsObj.items[0].statistics.subscriberCount; + + mediaItem.kolMedia_fansNum = (int)channelsObj.items[0].statistics.subscriberCount; + + string youtube_uid = GlobalClass.CreateRandomCode(32); + + youtube newYoutube = new youtube(); + newYoutube.youtube_uid = youtube_uid; + newYoutube.kol_uid = mediaItem.kol_uid; + newYoutube.kolMedia_uid = mediaItem.kolMedia_uid; + newYoutube.youtube_account = mediaItem.kolMedia_accountName; + newYoutube.youtube_name = channelsObj.items[0].snippet.title; + newYoutube.youtube_photo = channelsObj.items[0].snippet.thumbnails.high.url; + newYoutube.youtube_revoke = "N"; + newYoutube.youtube_subscriberCount = (int)channelsObj.items[0].statistics.subscriberCount; + newYoutube.youtube_viewCount = (int)channelsObj.items[0].statistics.viewCount; + newYoutube.youtube_json = channelsStatus; + + //取得上傳影片的playlist id + string playlist_id = channelsObj.items[0].contentDetails.relatedPlaylists.uploads; + + string youtubePlaylistApi = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=5&key=" + GlobalClass.appsettings("GoogleKey") + "&playlistId=" + playlist_id; + string youtubePlaylistJson = ""; + + using (var httpClient = new HttpClient()) + { + using (var response = await httpClient.GetAsync(youtubePlaylistApi)) + { + youtubePlaylistJson = await response.Content.ReadAsStringAsync(); + } + } + + dynamic youtubePlaylistObj; + youtubePlaylistObj = JsonConvert.DeserializeObject(youtubePlaylistJson); + string videoIdList = ""; + + foreach (var item in youtubePlaylistObj.items) { + videoIdList += item.snippet.resourceId.videoId + ","; + } + + videoIdList = videoIdList.TrimEnd(','); + + if (videoIdList == "") + { + updateLog failedLog = new updateLog(); + failedLog.updateLog_text = "此Youtube帳號 " + mediaItem.kolMedia_accountName + "(" + mediaItem.kolMedia_displayName + ") Google Youtube Data Api 的Video數量 回傳0個!"; + failedLog.updateLog_kolMedia_uid = mediaItem.kolMedia_uid; + failedLog.updateLog_kolMedia_accountName = mediaItem.kolMedia_accountName; + failedLog.updateLog_kolMedia_displayName = mediaItem.kolMedia_displayName; + failedLog.updateLog_kol_uid = mediaItem.kol_uid; + failedLog.updateLog_type = "YouTube"; + failedLog.updateLog_result = "failed"; + + conn.Insert(failedLog); + } + else + { + string youtubeVideoApi = "https://www.googleapis.com/youtube/v3/videos?part=statistics,snippet&key=" + GlobalClass.appsettings("GoogleKey") + "&id=" + videoIdList; + string youtubeVideoJson = ""; + + using (var httpClient = new HttpClient()) + { + using (var response = await httpClient.GetAsync(youtubeVideoApi)) + { + youtubeVideoJson = await response.Content.ReadAsStringAsync(); + } + } + + dynamic youtubeVideoObj; + youtubeVideoObj = JsonConvert.DeserializeObject(youtubeVideoJson); + int totalViewNum = 0; + int videosNum = 0; + List youtubeDetailList = new List(); + + foreach (var item in youtubeVideoObj.items) { + totalViewNum += (int)item.statistics.viewCount; + videosNum++; + youtubeDetail objDetail = new youtubeDetail(); + objDetail.youtube_uid = youtube_uid; + objDetail.youtubeDetail_uid = "ytd_" + GlobalClass.CreateRandomCode(16); + objDetail.youtubeDetail_title = item.snippet.title; + objDetail.youtubeDetail_description = item.snippet.description; + + string publishedAt = item.snippet.publishedAt; + + var published = DateTime.Parse(publishedAt, null, DateTimeStyles.None); + + objDetail.youtubeDetail_publishedAt = published; + objDetail.youtubeDetail_commentCount = (int)item.statistics.commentCount; + objDetail.youtubeDetail_viewCount = (int)item.statistics.viewCount; + objDetail.youtubeDetail_likeCount = (int)item.statistics.likeCount; + objDetail.youtubeDetail_favoriteCount = (int)item.statistics.favoriteCount; + objDetail.youtubeDetail_thumbnails = item.snippet.thumbnails.maxres.url; + objDetail.youtubeDetail_videoId = item.id; + objDetail.youtubeDetail_json = JsonConvert.SerializeObject(item); + + youtubeDetailList.Add(objDetail); + } + + double avg = ((totalViewNum / videosNum) + 0.5); + + newYoutube.youtube_avgViewCount = (int)System.Math.Floor(avg); + + conn.Execute("update youtube set youtube_revoke = 'Y' where kol_uid = @kol_uid and kolMedia_uid = @kolMedia_uid", new { kol_uid = mediaItem.kol_uid, kolMedia_uid = mediaItem.kolMedia_uid }); + + conn.Insert(youtubeDetailList); + conn.Insert(newYoutube); + + } + + + updateLog successLog = new updateLog(); + successLog.updateLog_text = "此Youtube帳號 " + mediaItem.kolMedia_accountName + "(" + mediaItem.kolMedia_displayName + ") Youtube資料更新成功"; + successLog.updateLog_kolMedia_uid = mediaItem.kolMedia_uid; + successLog.updateLog_kolMedia_accountName = mediaItem.kolMedia_accountName; + successLog.updateLog_kolMedia_displayName = mediaItem.kolMedia_displayName; + successLog.updateLog_kol_uid = mediaItem.kol_uid; + successLog.updateLog_type = "YouTube"; + successLog.updateLog_result = "success"; + successLog.updateLog_uid = channel_id; + + conn.Insert(successLog); + } + else { + updateLog failedLog = new updateLog(); + failedLog.updateLog_text = "此Youtube帳號 " + mediaItem.kolMedia_accountName + "(" + mediaItem.kolMedia_displayName + ") Google Youtube Data Api回傳Null值!"; + failedLog.updateLog_kolMedia_uid = mediaItem.kolMedia_uid; + failedLog.updateLog_kolMedia_accountName = mediaItem.kolMedia_accountName; + failedLog.updateLog_kolMedia_displayName = mediaItem.kolMedia_displayName; + failedLog.updateLog_kol_uid = mediaItem.kol_uid; + failedLog.updateLog_type = "YouTube"; + failedLog.updateLog_result = "failed"; + + conn.Insert(failedLog); + } + + } + + } + + conn.Update(kolMedias); + + conn.Execute("insert into schedule (schedule_type, schedule_log) values ('YouTube', '執行DoWork,執行更新工作結束')"); + } + + Interlocked.Decrement(ref execCount); + conn.Execute("insert into schedule (schedule_type, schedule_log) values ('YouTube', '執行DoWork,execCount減1')"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + //調整Timer為永不觸發,停用定期排程 + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 5092d37..cb4c596 100644 --- a/Program.cs +++ b/Program.cs @@ -5,6 +5,7 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.TryAddSingleton(); +builder.Services.AddHostedService(); var app = builder.Build(); diff --git a/Views/Home/Dashboard.cshtml b/Views/Home/Dashboard.cshtml index 863d754..1266a6b 100644 --- a/Views/Home/Dashboard.cshtml +++ b/Views/Home/Dashboard.cshtml @@ -6,10 +6,12 @@ } @section Style { - + } @section Script { + + } + +
+ +
+ +

社群平台排行榜

+
+ +
+ + + +
+
+
+
diff --git a/Views/Shared/_LooperLayout.cshtml b/Views/Shared/_LooperLayout.cshtml index 3d8b26c..e49ff30 100644 --- a/Views/Shared/_LooperLayout.cshtml +++ b/Views/Shared/_LooperLayout.cshtml @@ -23,6 +23,7 @@ + diff --git a/appsettings.json b/appsettings.json index ff63115..19881bc 100644 --- a/appsettings.json +++ b/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "SQLConnectionString": "Data Source=sql.bremen.com.tw;Initial Catalog=journeys_wanthome;User ID=journeys_wanthome;Password=2icR52n@9;Max Pool Size=250;", + "SQLConnectionString": "Data Source=sql.bremen.com.tw;Initial Catalog=journeys_wanthome;User ID=journeys_wanthome;Password=2icR52n@9;Max Pool Size=500;", "ElabConnectionString": "Data Source=sql.bremen.com.tw;database=elab;uid=elab;pwd=2#2k9Vfg", "DBConnectionString": "Data Source=sql.bremen.com.tw;Initial Catalog=prm;User ID=prm;Password=y6U6x?t5;Max Pool Size=250;" }, @@ -16,5 +16,7 @@ "id": "admin", "pwd": "?Bremen!", "perm": "system" - } + }, + "GoogleKey": "AIzaSyBGUAe21kB_qRq2XmenGgWu_gUvDYL39PE", + "UpdateByDay": "1" } diff --git a/wwwroot/assets/javascript/custom/dashboard.js b/wwwroot/assets/javascript/custom/dashboard.js index 0497358..7ed946a 100644 --- a/wwwroot/assets/javascript/custom/dashboard.js +++ b/wwwroot/assets/javascript/custom/dashboard.js @@ -1,5 +1,101 @@  $(document).ready(function () { + loadYoutubeCard(); +}); -}); \ No newline at end of file +function loadYoutubeCard() { + $.ajax({ + url: "/Api/youtubeList", + type: "post", + data: null, + success: function (data, textStatus, jqXHR) { + if (data.ret == "yes") { + var obj = data.youtubeList; + $('#card_group').append(youtubeCardHtml(obj)); + + + + + } else { + alert(data.message); + + if (data.err_code == "99999") { + location.href = "/Root/Login"; + } + } + }, + error: function (jqXHR, textStatus, errorThrown) { + alert('網路或伺服器發生錯誤,請稍後重試!'); + } + }); +} + +function youtubeCardHtml(objItem) { + var html = ""; + html += ""; + html += "
"; + html += " "; + html += "
"; + html += " "; + html += "
"; + html += " "; + html += "
"; + html += " Youtube 頻道排行 "; + html += "
"; + html += "
"; + html += "
"; + html += "
"; + html += " "; + html += "
"; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + html += " "; + + $.each(objItem, function (index, item) { + html += youtubeSubHtml(item); + }); + + html += " "; + html += "
頻道名稱 帳號 訂閱數 平均觀看數(近5)
"; + html += "
"; + html += " "; + html += "
"; + html += "
"; + html += "
"; + html += "
"; + + return html; +} + +function youtubeSubHtml(item) { + var html = ""; + + html += " "; + html += " "; + html += " "; + html += "  "; + html += " " + item.youtube_name + ""; + html += " "; + html += " " + item.youtube_account + " "; + html += " " + AppendComma(item.youtube_subscriberCount) + " "; + html += " " + AppendComma(item.youtube_avgViewCount) + " "; + html += " "; + html += " "; + html += " "; + html += " "; + + return html; +} \ No newline at end of file diff --git a/wwwroot/assets/javascript/custom/kollist.js b/wwwroot/assets/javascript/custom/kollist.js index b3a329b..1a98082 100644 --- a/wwwroot/assets/javascript/custom/kollist.js +++ b/wwwroot/assets/javascript/custom/kollist.js @@ -98,6 +98,13 @@ $(document).ready(function () { kolMedia_fansNum: RemoveComma($(this).find('td').eq(4).text().trim()) } + if ($(this).find('td').eq(0).text().trim() == 'YouTube') { + if (kolMedia_accountName.substring(0, 1) != '@') { + err_msg += $(this).find('td').eq(3).text().trim() + " 此YouTube頻道資料有誤,YouTube的帳號名稱第一碼應該為@字元\n"; + + } + } + mediaArray.push(item); }); diff --git a/wwwroot/assets/javascript/custom/projectlist.js b/wwwroot/assets/javascript/custom/projectlist.js index bf0a529..bd9ccc8 100644 --- a/wwwroot/assets/javascript/custom/projectlist.js +++ b/wwwroot/assets/javascript/custom/projectlist.js @@ -492,7 +492,7 @@ $(document).ready(function () { function kolProjectPreview(obj) { var uid = obj.getAttribute('data-uid'); - alert(uid); + //alert(uid); var formData = { method: "get",