Skip to content

Commit c62a552

Browse files
committed
✨ [nico] ニコ生から直接コメントを取得できるように (#5)
1 parent 0da5c75 commit c62a552

File tree

9 files changed

+1341
-186
lines changed

9 files changed

+1341
-186
lines changed

src/miraktest-nico/NicoRenderer.tsx

+211
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import axios from "axios"
2+
import React, { useEffect, useState } from "react"
3+
import { atom, useRecoilValue, useRecoilState } from "recoil"
4+
import YAML from "yaml"
5+
import { InitPlugin } from "../@types/plugin"
6+
import { SayaDefinition } from "../miraktest-annict/types"
7+
import tailwind from "../tailwind.scss"
8+
import { KakologStream } from "./streams/Kakolog"
9+
import { NicoLiveList } from "./streams/NicoLiveList"
10+
import { NicoSetting } from "./types"
11+
12+
const _id = "io.github.ci7lus.miraktest-plugins.nico"
13+
const prefix = "plugins.ci7lus.nico"
14+
const meta = {
15+
id: _id,
16+
name: "ニコニコ実況",
17+
author: "ci7lus",
18+
version: "0.2.0",
19+
description:
20+
"ニコニコ実況からコメントを取得するプラグインです。対応するコメントレンダラープラグインが必要です。",
21+
}
22+
23+
export const NicoRenderer: InitPlugin["renderer"] = ({ atoms }) => {
24+
const settingAtom = atom<NicoSetting>({
25+
key: `${prefix}.setting`,
26+
default: {
27+
isLiveEnabled: true,
28+
isTimeshiftEnabled: true,
29+
},
30+
})
31+
32+
let isDplayerFound = false
33+
let isPkrFound = false
34+
35+
return {
36+
...meta,
37+
exposedAtoms: [],
38+
sharedAtoms: [
39+
{
40+
type: "atom",
41+
atom: settingAtom,
42+
},
43+
],
44+
storedAtoms: [
45+
{
46+
type: "atom",
47+
atom: settingAtom,
48+
},
49+
],
50+
setup({ plugins }) {
51+
isDplayerFound = !!plugins.find(
52+
(plugin) => plugin.id === "io.github.ci7lus.miraktest-plugins.dplayer"
53+
)
54+
isPkrFound = !!plugins.find(
55+
(plugin) => plugin.id === "io.github.ci7lus.miraktest-plugins.pecore"
56+
)
57+
},
58+
components: [
59+
{
60+
id: `${prefix}.onPlayer`,
61+
position: "onPlayer",
62+
component: () => {
63+
const setting = useRecoilValue(settingAtom)
64+
const service = useRecoilValue(atoms.contentPlayerServiceSelector)
65+
const program = useRecoilValue(atoms.contentPlayerProgramSelector)
66+
const isSeekable = useRecoilValue(
67+
atoms.contentPlayerIsSeekableSelector
68+
)
69+
const time = useRecoilValue(atoms.contentPlayerPlayingTimeSelector)
70+
71+
const [sayaDefinition, setSayaDefinition] =
72+
useState<SayaDefinition | null>(null)
73+
useEffect(() => {
74+
if (!isPkrFound && !isDplayerFound) {
75+
console.warn("コメント送信先の取得に失敗しています")
76+
return
77+
}
78+
axios
79+
.get<string>(
80+
"https://cdn.jsdelivr.net/gh/SlashNephy/saya@dev/docs/definitions.yml",
81+
{
82+
responseType: "text",
83+
}
84+
)
85+
.then((r) => {
86+
const parsed: SayaDefinition = YAML.parse(r.data)
87+
setSayaDefinition(parsed)
88+
})
89+
.catch(console.error)
90+
}, [])
91+
92+
return (
93+
<>
94+
{setting.isLiveEnabled && sayaDefinition && (
95+
<NicoLiveList
96+
sayaDefinition={sayaDefinition}
97+
service={service}
98+
program={program}
99+
setting={setting}
100+
isSeekable={isSeekable}
101+
isDplayerFound={isDplayerFound}
102+
isPkrFound={isPkrFound}
103+
/>
104+
)}
105+
{setting.isTimeshiftEnabled && sayaDefinition && (
106+
<KakologStream
107+
sayaDefinition={sayaDefinition}
108+
service={service}
109+
program={program}
110+
isSeekable={isSeekable}
111+
time={time}
112+
setting={setting}
113+
isDplayerFound={isDplayerFound}
114+
isPkrFound={isPkrFound}
115+
/>
116+
)}
117+
</>
118+
)
119+
},
120+
},
121+
{
122+
id: `${prefix}.settings`,
123+
position: "onSetting",
124+
label: meta.name,
125+
component: () => {
126+
const [setting, setSetting] = useRecoilState(settingAtom)
127+
const [mail /*, setMail*/] = useState(setting.mail)
128+
const [pass /*, _setPass*/] = useState(setting.pass)
129+
const [isLiveEnabled, setIsLiveEnabled] = useState(
130+
setting.isLiveEnabled
131+
)
132+
const [isTimeshiftEnabled, setIsTimeshiftEnabled] = useState(
133+
setting.isTimeshiftEnabled
134+
)
135+
//const [isHidden, setIsHidden] = useState(true)
136+
137+
return (
138+
<>
139+
<style>{tailwind}</style>
140+
<form
141+
className="m-4"
142+
onSubmit={(e) => {
143+
e.preventDefault()
144+
setSetting({
145+
mail,
146+
pass,
147+
isLiveEnabled,
148+
isTimeshiftEnabled,
149+
})
150+
}}
151+
>
152+
<label className="block mt-4">
153+
<span>ニコニコ実況</span>
154+
<input
155+
type="checkbox"
156+
className="block mt-2 form-checkbox"
157+
checked={isLiveEnabled || false}
158+
onChange={() => setIsLiveEnabled((enabled) => !enabled)}
159+
/>
160+
</label>
161+
<label className="block mt-4">
162+
<span>タイムシフト有効</span>
163+
<input
164+
type="checkbox"
165+
className="block mt-2 form-checkbox"
166+
checked={isTimeshiftEnabled || false}
167+
onChange={() =>
168+
setIsTimeshiftEnabled((enabled) => !enabled)
169+
}
170+
/>
171+
</label>
172+
{/*<label className="mt-4 block">
173+
<span>モリタポのメールアドレス</span>
174+
<input
175+
type="text"
176+
placeholder="miyou@miyou.tv"
177+
className="block mt-2 form-input rounded-md w-full text-gray-900"
178+
value={mail || ""}
179+
onChange={(e) => setMail(e.target.value)}
180+
/>
181+
</label>
182+
<label className="mt-4 block">
183+
<span>モリタポのパスワード</span>
184+
<input
185+
type={isHidden ? "password" : "text"}
186+
placeholder="*****"
187+
className="block mt-2 form-input rounded-md w-full text-gray-900"
188+
value={pass || ""}
189+
onChange={(e) => setPass(e.target.value)}
190+
onFocus={() => setIsHidden(false)}
191+
onBlur={() => setIsHidden(true)}
192+
/>
193+
</label>*/}
194+
<button
195+
type="submit"
196+
className="bg-gray-100 text-gray-800 p-2 px-2 my-4 rounded-md focus:outline-none cursor-pointer"
197+
>
198+
保存
199+
</button>
200+
</form>
201+
</>
202+
)
203+
},
204+
},
205+
],
206+
destroy() {
207+
return
208+
},
209+
windows: {},
210+
}
211+
}

src/miraktest-nico/casAPI.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import axios from "axios"
2+
import { LiveProgram } from "./types/cas"
3+
import { EmbeddedData } from "./types/embedded-data"
4+
5+
// https://github.com/SlashNephy/saya/blob/3dfd347a745dff751c8cdd17dc933943b73a1b0f/src/main/kotlin/blue/starry/saya/services/nicolive/LiveNicoliveCommentProvider.kt
6+
7+
export const casClient = axios.create({
8+
baseURL: "https://api.cas.nicovideo.jp/v2",
9+
headers: {
10+
"x-frontend-id": "89",
11+
},
12+
})
13+
14+
export type CasMeta = {
15+
status: number
16+
totalCount?: number
17+
ssId: string
18+
}
19+
20+
export type LivePrograms = {
21+
meta: CasMeta
22+
data?: LiveProgram[]
23+
}
24+
25+
export const getLivePrograms = async ({
26+
searchWord,
27+
}: {
28+
searchWord: string
29+
}) => {
30+
return await casClient
31+
.get<LivePrograms>("/search/programs.json", {
32+
params: {
33+
liveStatus: "onair",
34+
sort: "startTime",
35+
limit: 20,
36+
searchWord,
37+
searchTargets: "tagsExact",
38+
order: "desc",
39+
},
40+
})
41+
.then((data) => data.data?.data ?? [])
42+
}
43+
44+
export const getEmbeddedData = async ({ liveId }: { liveId: string }) => {
45+
const livePage = await axios.get<Document>(
46+
`https://live.nicovideo.jp/watch/${liveId}`,
47+
{ responseType: "document" }
48+
)
49+
const embeddedDataDom = livePage.data.getElementById("embedded-data")
50+
if (!embeddedDataDom) {
51+
throw new Error("#embedded-data not found on " + liveId)
52+
}
53+
const data = embeddedDataDom.getAttribute("data-props")
54+
if (!data) {
55+
throw new Error("data-props not found on " + liveId)
56+
}
57+
return JSON.parse(data) as EmbeddedData
58+
}

0 commit comments

Comments
 (0)