Created
December 16, 2020 16:37
-
-
Save tomo0611/68bda43be6574182b2f58473eb577c78 to your computer and use it in GitHub Desktop.
NikoNiko Comment SDK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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"}}] |
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
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: