Skip to content

Instantly share code, notes, and snippets.

@tomo0611
Created December 16, 2020 16:37
Show Gist options
  • Save tomo0611/68bda43be6574182b2f58473eb577c78 to your computer and use it in GitHub Desktop.
Save tomo0611/68bda43be6574182b2f58473eb577c78 to your computer and use it in GitHub Desktop.
NikoNiko Comment SDK
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# RealTime NikoNiko Comment SDK
# *
# * Author:
# * Tomochan <[email protected]>
#
import cv2
from PIL import ImageFont, ImageDraw, Image
import numpy as np
import requests, html, json
import re
import websocket
try:
import thread
except ImportError:
import _thread as thread
import time
import random
class Chat(object):
""" Chat Class """
date = 0
date_usec = 0
user_id = ""
premium = 0
anonymity = 1
content = ""
font_option = []
font_color = (255,255,255)
font_size = 24
font_position_type = 0 # 0=move RtoL, 1=top, 2=center, 3=bottom
x = 0
y = 0
draw = None
def setFontOption(self,options):
for option in options:
## COLORS
if option == "white":
self.font_color = (255,255,255)
elif option == "red":
self.font_color = (255, 0, 0)
elif option == "pink":
self.font_color = (255, 128, 128)
elif option == "orange":
self.font_color = (255, 192, 0)
elif option == "yellow":
self.font_color = (255, 255, 0)
elif option == "green":
self.font_color = (0, 255, 0)
elif option == "cyan":
self.font_color = (0, 255, 255)
elif option == "blue":
self.font_color = (0,0,255)
elif option == "purple":
self.font_color = (192, 0, 255)
elif option == "black":
self.font_color = (0,0,0)
## PREMIUM COLORS
elif option == "white2":
self.font_color = (204, 204, 153)
elif option == "niconicowhite":
self.font_color = (204, 204, 153)
elif option == "red2":
self.font_color = (204, 0, 51)
elif option == "truered":
self.font_color = (204, 0, 51)
elif option == "orange2":
self.font_color = (255, 102, 0)
elif option == "passionorange":
self.font_color = (255, 102, 0)
elif option == "yellow2":
self.font_color = (153, 153, 0)
elif option == "madyellow":
self.font_color = (153, 153, 0)
elif option == "green2":
self.font_color = (0, 204, 102)
elif option == "elementalgreen":
self.font_color = (0, 204, 102)
elif option == "blue2":
self.font_color = (51, 153, 255)
elif option == "marineblue":
self.font_color = (51, 153, 255)
elif option == "purple2":
self.font_color = (102, 51, 204)
elif option == "nobleviolet":
self.font_color = (102, 51, 204)
elif option == "pink2":
self.font_color = (255, 51, 204)
elif option == "cyan2":
self.font_color = (0, 204, 204)
elif option == "black2":
self.font_color = (102, 102, 102)
elif option == "big":
self.font_size = 36
elif option == "medium":
self.font_size = 24
elif option == "small":
self.font_size = 12
elif option == "ue":
self.font_position_type = 1
elif option == "naka":
self.font_position_type = 2
elif option == "shita":
self.font_position_type = 3
else:
if option != "184":
print("Unparsed Font Option : "+option)
liveids = ["ch2646436","ch2646437","ch2646438","ch2646439","ch2646440","ch2646441","ch2646442","ch2646485","ch2646846"]
liveurlname = {}
messages = []
viewers = {}
for liveid in liveids:
j = {}
# https://qiita.com/sqrtxx/items/49beaa3795925e7de666
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0'}
res_text = requests.get('https://live2.nicovideo.jp/watch/'+liveid, headers=headers).text
# NHK総合:https://live.nicovideo.jp/watch/ch2646436 (jk1)
# NHK Eテレ:https://live.nicovideo.jp/watch/ch2646437 (jk2)
# 日本テレビ:https://live.nicovideo.jp/watch/ch2646438 (jk4)
# テレビ朝日:https://live.nicovideo.jp/watch/ch2646439 (jk5)
# TBSテレビ:https://live.nicovideo.jp/watch/ch2646440 (jk6)
# テレビ東京:https://live.nicovideo.jp/watch/ch2646441 (jk7)
# フジテレビ:https://live.nicovideo.jp/watch/ch2646442 (jk8)
# TOKYO MX:https://live.nicovideo.jp/watch/ch2646485 (jk9)
# BS11:https://live.nicovideo.jp/watch/ch2646846 (jk211)
# https://qiita.com/luohao0404/items/7135b2b96f9b0b196bf3
results = re.findall('<script id="embedded-data" data-props="{.*?}"></script>',res_text)
for result in results:
# https://docs.python.org/3/library/html.html
j = html.unescape(result[39:-11])
j = json.loads(j)
#j = json.dumps(j, ensure_ascii=False, allow_nan=True, indent=4)
#print(j)
room_info = {}
print("Title : "+j["socialGroup"]["name"])
def on_message(ws, message):
global room_info, viewers, liveurlname
# {"type":"serverTime","data":{"currentMs":"2020-12-16T15:59:20.450+09:00"}}
# {"type":"seat","data":{"keepIntervalSec":30}}
# {"type":"stream","data":{"uri":"https://XXX.dmc.nico/hlslive/ht2_nicolive/XXX/master.m3u8?ht2_nicolive=XXX","syncUri":"https://pc086544093.dmc.nico/hlslive/ht2_nicolive/nicolive-XXX/stream_sync.json?ht2_nicolive=anonymous-XXX","quality":"high","availableQualities":["abr","high","normal","low","super_low","audio_high"],"protocol":"hls"}}
# {"type":"room","data":{"name":"アリーナ","messageServer":{"uri":"wss://msgd.live2.nicovideo.jp/websocket","type":"niwavided"},"threadId":"M.XXXXX","isFirst":true,"waybackkey":"XXX.ik0CkRw9OrhkIR7fRfP-w-0t1Bs"}}
# {"type":"schedule","data":{"begin":"2020-12-16T11:00:00+09:00","end":"2020-12-17T04:00:00+09:00"}}
# {"type":"statistics","data":{"viewers":7465,"comments":9668,"adPoints":6300,"giftPoints":1270}}
# {"type":"ping"}
print("on_msg : "+message)
js = json.loads(message)
if js["type"] == "room":
room_info = js
elif js["type"] == "ping":
ws.send('{"type":"pong"}')
ws.send('{"type":"keepSeat"}')
elif js["type"] == "statistics":
viewers[liveurlname[ws.url]] = js["data"]["viewers"]
def on_error(ws, error):
print("on_err : "+error)
def on_close(ws):
print("### closed ###")
def on_open(ws):
ws.send('{"type":"startWatching","data":{"stream":{"quality":"high","protocol":"hls","latency":"high","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}')
def startWebSocket(*args):
global j
print(j["site"]["relive"]["webSocketUrl"]+"&frontend_id="+str(j["site"]["frontendId"]))
headers = {'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0'}
ws = websocket.WebSocketApp(j["site"]["relive"]["webSocketUrl"]+"&frontend_id="+str(j["site"]["frontendId"]),on_message = on_message,on_error = on_error,on_close = on_close,header = headers)
ws.on_open = on_open
ws.run_forever()
thread.start_new_thread(startWebSocket, ())
liveurlname[j["site"]["relive"]["webSocketUrl"]+"&frontend_id="+str(j["site"]["frontendId"])] = j["socialGroup"]["name"].replace("(ニコニコ実況)","")
while True:
if room_info != {}:
break
else:
time.sleep(0.2)
print("RoomInfo was found!")
starts_since = time.time()
def on_message2(ws, message):
global messages, starts_since
# {"chat":{"thread":"","no":2998,"vpos":190476,"date":12381493116,"date_usec":1274545,"mail":"184","user_id":"fUOYn-XXX","premium":1,"anonymity":1,"content":"てすと"}}
chat_json = json.loads(message)
print("on_msg2 : "+json.dumps(chat_json, ensure_ascii=False, allow_nan=True))
if chat_json["chat"]:
chat = Chat()
chat.date = chat_json["chat"]["date"]
chat.date_usec = chat_json["chat"]["date_usec"]
chat.user_id = chat_json["chat"]["user_id"]
#chat.premium = chat_json["chat"]["premium"]
#chat.anonymity = chat_json["chat"]["anonymity"]
chat.content = chat_json["chat"]["content"]
chat.setFontOption(chat_json["chat"]["mail"].split(" "))
chat.x = 1600
chat.y = random.randrange(40, 450, 3)
if chat.date > starts_since:
messages.append(chat)
def on_error2(ws, error):
print("on_err2 : "+error)
def on_close2(ws):
print("### closed2 ###")
def on_open2(ws):
global room_info
print("Connected to Messaging Server!")
# time.sleep(1)
ws.send('[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+room_info["data"]["threadId"]+'","version":"20061206","user_id":"guest","res_from":-150,"with_global":1,"scores":1,"nicoru":0}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]')
def startWebSocket2(*args):
global room_info
print("Connect to Messaging Server...")
headers2 = {'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0'}
ws2 = websocket.WebSocketApp(room_info["data"]["messageServer"]["uri"],on_message = on_message2,on_error = on_error2,on_close = on_close2,header = headers2)
ws2.on_open = on_open2
ws2.run_forever()
thread.start_new_thread(startWebSocket2, ())
height = 900
width = 1600
font = ImageFont.truetype("C:/Windows/Fonts/HGRSMP.TTF", 32)
blank_image = np.zeros((height,width,3), np.uint8)
font_viewer_count = ImageFont.truetype("C:/Windows/Fonts/HGRSMP.TTF", 32)
while True:
blank_image[:,:] = (0,255,0)
img_pil = Image.fromarray(blank_image)
draw = ImageDraw.Draw(img_pil)
msgs_to_delete = []
for msg in messages:
if msg.draw == None:
if msg.font_size == 24:
w, h = draw.textsize(msg.content,font=font)
txt = Image.new("RGB", (w+6,h+6), (0,255,0))
d = ImageDraw.Draw(txt)
d.text((0,0), msg.content,font=font, fill=(msg.font_color[2],msg.font_color[1],msg.font_color[0]), stroke_width=3, stroke_fill=(0,0,0))
msg.draw = txt
del d
else:
fontt = ImageFont.truetype("C:/Windows/Fonts/HGRSMP.TTF", msg.font_size)
w, h = draw.textsize(msg.content,font=fontt)
txt = Image.new("RGB", (w+6,h+6), (0,255,0))
d = ImageDraw.Draw(txt)
d.text((0,0), msg.content,font=fontt, fill=(msg.font_color[2],msg.font_color[1],msg.font_color[0]), stroke_width=3, stroke_fill=(0,0,0))
msg.draw = txt
del d
x = msg.x
y = msg.y
img_pil.paste(msg.draw, (x, y))
msg.x = x - 30
if msg.x+msg.draw.width < 0:
msgs_to_delete.append(msg)
for msg in msgs_to_delete:
messages.remove(msg)
view_str = ""
for key in viewers.keys():
view_str = view_str + key + " : " + str(viewers[key]) + "人\n"
draw.text((10,500), view_str,font=font_viewer_count, fill=(255,255,255), stroke_width=3, stroke_fill=(0,0,0))
blank_image = np.array(img_pil)
cv2.imshow("RealTime NikoNiko Comment Viewer for OBS - type \"q\" to quit.", blank_image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
# wss://a.live2.nicovideo.jp/unama/wsapi/v2/watch/32672256623193?audience_token=XXX&frontend_id=12
# {"type":"startWatching","data":{"stream":{"quality":"high","protocol":"hls","latency":"high","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}
# wss://msgd.live2.nicovideo.jp/websocket
# [{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"M.XXX","version":"20061206","user_id":"guest","res_from":-150,"with_global":1,"scores":1,"nicoru":0}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]
@bjctw
Copy link

bjctw commented Feb 5, 2021

    ws.send('[{"ping":{"content":"rs:0"}},{"ping":{"content":"ps:0"}},{"thread":{"thread":"'+room_info["data"]["threadId"]+'","version":"20061206","user_id":"guest","res_from":-150,"with_global":1,"scores":1,"nicoru":0}},{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]')

If send request message without "when" element, the server will return an incorrect last_res value (seems always 157).
This will be a problem when getting time-shift comments.
Also, suggest to split long strings for multiple lines for easy reading and debugging.
eg:

    msg = ('[{"ping":{"content":"rs:0"}}, {"ping":{"content":"ps:0"}}, '
        '{"thread":{"thread":"'+room_info["data"]["threadId"]+'", "version":"20061206","user_id":"guest", '
        '"when": "1893427200", ' #2030/1/1
        '"res_from":-10,"with_global":1,"scores":1,"nicoru":0}}, '
        '{"ping":{"content":"pf:0"}},{"ping":{"content":"rf:0"}}]')
    ws.send(msg)

@tomo0611
Copy link
Author

tomo0611 commented Feb 5, 2021

Thank you for your suggestion.

I think I have to use dict to make json string, not use string object. This script is just for testing so there'd be some mistakes and not good syntax.

Unfortunately, NikoNiko Video announced that those legacy APIs are to be deprecated. Thereby I decided not to update this script.

Moreover, I'm busy on studying to enter a medical school. So I have no time to create new library which supports new APIs. I'm so sorry....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment