Greasy Fork is available in English.

HistogramHeatGraph_html5.user.js

ニコニコ動画のコメントをグラフで表示(html5版)※コメントをリロードすることでグラフを再描画します

Fra 03.11.2018. Se den seneste versjonen.

  1. // ==UserScript==
  2. // @name HistogramHeatGraph_html5.user.js
  3. // @namespace sotoba
  4. // @version 1.1.6.20181103
  5. // @description ニコニコ動画のコメントをグラフで表示(html5版)※コメントをリロードすることでグラフを再描画します
  6. // @homepageURL https://github.com/SotobatoNihu/HistogramHeatGraph_html5
  7. // @match https://www.nicovideo.jp/*
  8. // @match https://www.nicovideo.jp/watch/*
  9. // @require https://code.jquery.com/jquery-3.2.1.min.js
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. // default settings
  17. class NicoHeatGraph {
  18. constructor() {
  19. this.MINIMUMBARNUM = 50;
  20. this.DEFAULTINTERBAL = 10;
  21. this.MAXCOMMENTNUM = 30;
  22. this.GRAPHHEIGHT = 30;
  23. this.GRAPHDEFWIDTH = 856;
  24. this.barIndexNum = 0;
  25. this.$canvas = null;
  26. this.$commentgraph = $('<div>').attr('id', 'comment-graph');
  27. this.$commentlist = $('<div>').attr('id', 'comment-list');
  28. }
  29.  
  30. drawCoordinate() {
  31. const $commentgraph = this.$commentgraph;
  32. const $commentlist = this.$commentlist;
  33. if(!($('#comment-graph').length)) {
  34. $('.PlayerContainer').eq(0).append($commentgraph);
  35. $('.MainContainer').eq(0).append($commentlist);
  36. }
  37. this.$canvas = $("#CommentRenderer").children('canvas').eq(0);
  38. const styleString = `
  39. #comment-graph :hover{
  40. -webkit-filter: hue-rotate(180deg);
  41. filter: hue-rotate(180deg);
  42. }
  43. #comment-list:empty {
  44. display: none;
  45. }
  46. `;
  47. const style = document.createElement('style');
  48. style.appendChild(document.createTextNode(styleString));
  49. document.body.appendChild(style);
  50. const playerWidth = parseFloat(this.$canvas.css('width')) | this.GRAPHDEFWIDTH;
  51. $commentgraph.height(this.GRAPHHEIGHT);
  52. $commentgraph.width(playerWidth);
  53. $commentgraph.css({
  54. background: 'repeating-linear-gradient(to top, #000, #111 5px)',
  55. border: '1px solid #000',
  56. borderTo: 0,
  57. float: 'left',
  58. fontSize: 0,
  59. whiteSpace: 'nowrap',
  60. });
  61. $commentlist.css({
  62. background: '#000',
  63. color: '#fff',
  64. fontSize: '12px',
  65. lineHeight: 1.25,
  66. padding: '4px 4px 0',
  67. pointerEvents: 'none',
  68. position: 'absolute',
  69. zIndex: 9999,
  70. });
  71. }
  72.  
  73.  
  74. drowgraph(commentData, $canvas) {
  75. const $commentgraph = this.$commentgraph;
  76. const $commentlist = this.$commentlist;
  77. const ApiJsonData = JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'))
  78. const playerWidth = parseFloat($canvas.css("width"));
  79. const videoTotalTime = ApiJsonData.video.dmcInfo !== null ? ApiJsonData.video.dmcInfo.video.length_seconds : ApiJsonData.video.duration;
  80. let barTimeInterval;
  81.  
  82. //TODO 非常に長い(2,3時間以上)動画の処理
  83. //長い動画
  84. if (videoTotalTime > this.MINIMUMBARNUM * this.DEFAULTINTERBAL) {
  85. barTimeInterval = this.DEFAULTINTERBAL;
  86. this.barIndexNum = Math.ceil(videoTotalTime / barTimeInterval);
  87. //普通の動画
  88. } else if (videoTotalTime > this.MINIMUMBARNUM) {
  89. this.barIndexNum = this.MINIMUMBARNUM;
  90. barTimeInterval = videoTotalTime / this.MINIMUMBARNUM;
  91. } else {
  92. //MINIMUMBARNUM秒以下の短い動画
  93. this.barIndexNum = Math.floor(videoTotalTime);
  94. barTimeInterval = 1;
  95. }
  96.  
  97. $commentgraph.width(playerWidth);
  98. const barColors = [
  99. '003165', '00458f', '0058b5', '005fc4', '006adb',
  100. '0072ec', '007cff', '55a7ff', '3d9bff'
  101. ];
  102. let listCounts = (new Array(this.barIndexNum + 1)).fill(0);
  103. const listMessages = (new Array(this.barIndexNum + 1)).fill("");
  104. const listTimes = (new Array(this.barIndexNum + 1)).fill("");
  105. const lastBarTimeIntervalGap = Math.floor(videoTotalTime - (this.barIndexNum * barTimeInterval));
  106. const barWidth = playerWidth / this.barIndexNum;
  107.  
  108. const MAXCOMMENTNUM = this.MAXCOMMENTNUM;
  109.  
  110. $(commentData).find('chat').each(function (index) {
  111. let vpos = $(this).attr('vpos') / 100;
  112. //動画長を超えた時間のpostがあるため対処
  113. if (videoTotalTime <= vpos) {
  114. vpos = videoTotalTime;
  115. }
  116. const section = Math.floor(vpos / barTimeInterval);
  117. listCounts[section]++;
  118. if (listCounts[section] <= MAXCOMMENTNUM) {
  119. const comment = $(this).text().replace(/"|<|&lt;/g, ' ').replace(/\n/g, '<br>');
  120. listMessages[section] += comment + '<br>';
  121. }
  122. });
  123.  
  124. let starttime = 0;
  125. let nexttime = 0;
  126. for (let i = 0; i < this.barIndexNum; i++) {
  127. starttime = nexttime;
  128. nexttime += barTimeInterval;
  129. if (i == this.barIndexNum - 1) {
  130. nexttime += lastBarTimeIntervalGap;
  131. }
  132. const startmin = Math.floor(starttime / 60);
  133. const startsec = Math.floor(starttime - startmin * 60);
  134. let endmin = Math.floor(nexttime / 60);
  135. let endsec = Math.ceil(nexttime - endmin * 60);
  136. if (59 < endsec) {
  137. endmin += 1;
  138. endsec -= 60;
  139. }
  140. listTimes[i] += `${("0" + startmin).slice(-2)}:${("0" + startsec).slice(-2)}-${("0" + endmin).slice(-2)}:${("0" + endsec).slice(-2)}`;
  141. }
  142.  
  143. // TODO なぜかthis.barIndexNum以上の配列ができる
  144. listCounts = listCounts.slice(0, this.barIndexNum);
  145. const listCountMax = Math.max.apply(null, listCounts);
  146. const barColorRatio = (barColors.length - 1) / listCountMax;
  147.  
  148. $commentgraph.empty();
  149. $commentgraph.height(this.GRAPHHEIGHT);
  150.  
  151. for (let i = 0; i < this.barIndexNum; i++) {
  152. const barColor = barColors[Math.floor(listCounts[i] * barColorRatio)];
  153. const barBackground = `linear-gradient(to top, #${barColor}, #${barColor} ` +
  154. `${listCounts[i]}px, transparent ${listCounts[i]}px, transparent)`;
  155. const barText = listCounts[i] ?
  156. `${listMessages[i]}<br><br>${listTimes[i]} コメ ${listCounts[i]}` : '';
  157. $('<div>')
  158. .css('background-image', barBackground)
  159. .css('float', 'left')
  160. .data('text', barText)
  161. .height(this.GRAPHHEIGHT)
  162. .width(barWidth)
  163. .addClass("commentbar")
  164. .appendTo($commentgraph);
  165. }
  166. }
  167.  
  168. addMousefunc($canvas) {
  169. const $commentgraph = this.$commentgraph;
  170. const $commentlist = this.$commentlist;
  171.  
  172. function mouseOverFunc() {
  173. $commentlist.css({
  174. 'left': $(this).offset().left,
  175. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  176. }).html($(this).data('text'));
  177. }
  178.  
  179. function mouseOutFunc() {
  180. $commentlist.empty();
  181. }
  182.  
  183. $commentgraph.children().on({
  184. 'mouseenter': function (val) {
  185. $commentlist.css({
  186. 'left': $(this).offset().left,
  187. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  188. }).html($(this).data('text'));
  189. },
  190. 'mousemove': function (val) {
  191. $commentlist.offset({
  192. 'left': $(this).offset().left,
  193. 'top': $commentgraph.offset().top - $commentlist.height() - 10
  194. });
  195. },
  196. 'mouseleave': function () {
  197. $commentlist.empty();
  198. }
  199. });
  200.  
  201. /* 1 Dom Style Watcher本体 監視する側*/
  202. const domStyleWatcher = {
  203. Start: function (tgt, styleobj) {
  204. function eventHappen(data1, data2) {
  205. const throwval = tgt.css(styleobj);
  206. tgt.trigger('domStyleChange', [throwval]);
  207. }
  208.  
  209. const filter = ['style'];
  210. const options = {
  211. attributes: true,
  212. attributeFilter: filter
  213. };
  214. const mutOb = new MutationObserver(eventHappen);
  215. mutOb.observe(tgt, options);
  216. return mutOb;
  217. },
  218. Stop: function (mo) {
  219. mo.disconnect();
  220. }
  221. };
  222.  
  223. function catchEvent(event, value) {
  224. const playerWidth = parseFloat(value);
  225. const barIndexNum = $('.commentbar').length;
  226. $commentgraph.width(playerWidth);
  227. $('.commentbar').width(playerWidth / barIndexNum);
  228. }
  229. const target = document.getElementById('CommentRenderer').firstChild
  230. target.addEventListener('domStyleChange', catchEvent);//イベントを登録
  231. domStyleWatcher.Start(target, 'width');//監視開始
  232. //domStyleWatcher.Stop(dsw);//監視終了
  233. }
  234.  
  235. async getCommentData() {
  236. const ApiJsonData = await JSON.parse(document.getElementById('js-initial-watch-data').getAttribute('data-api-data'));
  237. let thread_id;
  238. let video_id;
  239. let user_id;
  240. if (ApiJsonData.video.dmcInfo !== null) {
  241. thread_id = ApiJsonData.video.dmcInfo.thread.thread_id;
  242. video_id = ApiJsonData.video.dmcInfo.video.video_id;
  243. user_id = ApiJsonData.video.dmcInfo.user.user_id;
  244. } else {
  245. thread_id = ApiJsonData.thread.ids.default;
  246. video_id = ApiJsonData.video.id;
  247. user_id = ApiJsonData.viewer.id;
  248. }
  249. if (video_id.startsWith('sm') || video_id.startsWith('nm')) {
  250. const url = `https://nmsg.nicovideo.jp/api/thread?thread=${thread_id}&version=20061206&res_from=-1000&scores=1`
  251. const data = await fetch(url, {mode: 'cors'})
  252. .then(response => response.text())
  253. .then(str => (new window.DOMParser()).parseFromString(str, "text/xml"))
  254. return data
  255. } else {
  256.  
  257. const url = `https://flapi.nicovideo.jp/api/getthreadkey?thread=${thread_id}`
  258. const response1 = await fetch(url, {mode: 'cors'})
  259. .then(response => response.text())
  260. const url2 = `https://nmsg.nicovideo.jp/api/thread?thread=${thread_id}&version=20061206&res_from=-1000&scores=1&user=${user_id}&${response1}`
  261. const data = await fetch(url2, {mode: 'cors'})
  262. .then(response => response.text())
  263. .then(str => (new window.DOMParser()).parseFromString(str, "text/xml"))
  264. return data
  265. }
  266. }
  267.  
  268. load() {
  269. const self = this;
  270. this.getCommentData().then(data => {
  271. this.canvas = $('#CommentRenderer').children('canvas').eq(0);
  272. self.drowgraph(data, this.canvas)
  273. self.addMousefunc(this.canvas)
  274. }
  275. )//.catch(console.log("load failed"))
  276. }
  277.  
  278. reload() {
  279. this.load()
  280. }
  281. }
  282.  
  283. // Main
  284. const heatgraph = new NicoHeatGraph();
  285. heatgraph.drawCoordinate();
  286.  
  287. window.onload = () =>{
  288. heatgraph.load();
  289. //reload when start button pushed
  290. const startButtons = document.getElementsByClassName('VideoStartButtonContainer')
  291. for (let startbutton of startButtons ) {
  292. startbutton.addEventListener('click', ()=>{
  293. console.log("comment reload.")
  294. heatgraph.reload()
  295. }, false)
  296. }
  297.  
  298. // reload when reload button pushed
  299. const reloadButtons = document.getElementsByClassName('ReloadButton')
  300. for (let reloadButton of reloadButtons) {
  301. reloadButton.addEventListener('click', ()=>{
  302. console.log("comment reload.")
  303. heatgraph.reload()
  304. }, false)
  305. }
  306. const links = document.getElementsByTagName('a');
  307. for (const link of links) {
  308. link.addEventListener('click', () => {
  309. heatgraph.reload()
  310. });
  311. }
  312.  
  313. }
  314. })();