ぼざろクリーチャーシリーズ DB 兼 API(自分用)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

830 lines
27 KiB

  1. import json
  2. import os
  3. import random
  4. import string
  5. import time
  6. from dataclasses import dataclass
  7. from datetime import datetime
  8. from typing import TypedDict, cast
  9. import mysql.connector
  10. import requests
  11. DbNull = (None,)
  12. DbNullType = tuple[None]
  13. class VideoResult (TypedDict):
  14. contentId: str
  15. title: str
  16. tags: list[str]
  17. description: str
  18. viewCounter: int
  19. startTime: str
  20. class VideoSearchParam (TypedDict):
  21. q: str
  22. targets: str
  23. _sort: str
  24. fields: str
  25. _limit: int
  26. jsonFilter: str
  27. class CommentResult (TypedDict):
  28. pass
  29. def main (
  30. ) -> None:
  31. conn = mysql.connector.connect (host = os.environ['MYSQL_HOST'],
  32. user = os.environ['MYSQL_USER'],
  33. password = os.environ['MYSQL_PASS'])
  34. now = datetime.now ()
  35. video_dao = VideoDao (conn)
  36. tag_dao = TagDao (conn)
  37. video_tag_dao = VideoTagDao (conn)
  38. video_history_dao = VideoHistoryDao (conn)
  39. api_data = search_nico_by_tags (['伊地知ニジカ', 'ぼざろクリーチャーシリーズ'])
  40. update_tables (video_dao, tag_dao, video_tag_dao, video_history_dao, api_data, now)
  41. # TODO: 書くこと
  42. def update_tables (
  43. video_dao: VideoDao,
  44. tag_dao: TagDao,
  45. video_tag_dao: VideoTagDao,
  46. video_history_dao: VideoHistoryDao,
  47. api_data: list[VideoResult],
  48. now: datetime,
  49. ) -> None:
  50. for datum in api_data:
  51. video = VideoDto (code = datum['contentId'],
  52. title = datum['title'],
  53. description = datum['description'],
  54. uploaded_at = datum['startTime'])
  55. video_dao.upsert (video, False)
  56. if video.id_ is not None:
  57. video_history = VideoHistoryDto (video_id = video.id_,
  58. fetched_at = now,
  59. views_count = datum['viewCounter'])
  60. video_history_dao.insert (video_history)
  61. tag_ids: list[int] = []
  62. video_tags = video_tag_dao.fetch_alive_by_video_id (video.id_, False)
  63. for vt in video_tags:
  64. tag = tag_dao.find (vt.tag_id)
  65. if (tag is not None
  66. and (tag.name not in datum['tags'])
  67. and (tag.id_ is not None)):
  68. tag_ids.append (tag.id_)
  69. video_tag_dao.untag_all (video.id_, tag_ids, now)
  70. tags: list[TagDto] = []
  71. for tag_name in datum['tags']:
  72. tag = tag_dao.fetch_by_name (tag_name)
  73. if tag is None:
  74. tag = TagDto (name = tag_name)
  75. tag_dao.insert (tag)
  76. if video.id_ is not None and tag.id_ is not None:
  77. video_tag = video_tag_dao.fetch_alive_by_ids (video.id_, tag.id_, False)
  78. if video_tag is None:
  79. video_tag = VideoTagDto (video_id = video.id_,
  80. tag_id = tag.id_,
  81. tagged_at = now)
  82. video_tag_dao.insert (video_tag, False)
  83. # TODO: コメント取得すること
  84. # TODO: 削除処理,存在しなぃ動画リストを取得した上で行ふこと
  85. # video_dao.delete (video_ids, now)
  86. # TODO: 書くこと
  87. def fetch_comments (
  88. video_id: str,
  89. ) -> list:
  90. headers = { 'X-Frontend-Id': '6',
  91. 'X-Frontend-Version': '0' }
  92. action_track_id = (
  93. ''.join (random.choice (string.ascii_letters + string.digits)
  94. for _ in range (10))
  95. + '_'
  96. + str (random.randrange (10 ** 12, 10 ** 13)))
  97. url = (f"https://www.nicovideo.jp/api/watch/v3_guest/{ video_id }"
  98. + f"?actionTrackId={ action_track_id }")
  99. res = requests.post (url, headers = headers, timeout = 60).json ()
  100. try:
  101. nv_comment = res['data']['comment']['nvComment']
  102. except KeyError:
  103. return []
  104. if nv_comment is None:
  105. return []
  106. headers = { 'X-Frontend-Id': '6',
  107. 'X-Frontend-Version': '0',
  108. 'Content-Type': 'application/json' }
  109. params = { 'params': nv_comment['params'],
  110. 'additionals': { },
  111. 'threadKey': nv_comment['threadKey'] }
  112. url = nv_comment['server'] + '/v1/threads'
  113. res = (requests.post (url, json.dumps (params),
  114. headers = headers,
  115. timeout = 60)
  116. .json ())
  117. try:
  118. return res['data']['threads'][1]['comments']
  119. except (IndexError, KeyError):
  120. return []
  121. def search_nico_by_tag (
  122. tag: str,
  123. ) -> list[VideoResult]:
  124. return search_nico_by_tags ([tag])
  125. def search_nico_by_tags (
  126. tags: list[str],
  127. ) -> list[VideoResult]:
  128. url = ('https://snapshot.search.nicovideo.jp'
  129. + '/api/v2/snapshot/video/contents/search')
  130. # TODO: 年月日の設定ができてゐなぃのと,100 件までしか取得できなぃので何とかすること
  131. query_filter = json.dumps ({ 'type': 'or',
  132. 'filters': [
  133. { 'type': 'range',
  134. 'field': 'startTime',
  135. 'from': f"{year}-{start}T00:00:00+09:00",
  136. 'to': f"{year}-{end}T23:59:59+09:00",
  137. 'include_lower': True }] })
  138. params: VideoSearchParam
  139. params = { 'q': ' OR '.join (tags),
  140. 'targets': 'tagsExact',
  141. '_sort': '-viewCounter',
  142. 'fields': 'contentId,title,tags,description,viewCounter,startTime',
  143. '_limit': 100,
  144. 'jsonFilter': query_filter }
  145. res = requests.get (url, params = cast (dict[str, int | str], params), timeout = 60).json ()
  146. return res['data']
  147. class VideoDao:
  148. def __init__ (
  149. self,
  150. conn
  151. ):
  152. self.conn = conn
  153. def find (
  154. self,
  155. video_id: int,
  156. with_relation_tables: bool,
  157. ) -> VideoDto | None:
  158. with self.conn.cursor () as c:
  159. c.execute ("""
  160. SELECT
  161. id,
  162. code,
  163. title,
  164. description,
  165. uploaded_at,
  166. deleted_at
  167. FROM
  168. videos
  169. WHERE
  170. id = %s
  171. ORDER BY
  172. id""", video_id)
  173. row = c.fetchone ()
  174. if row is None:
  175. return None
  176. return self._create_dto_from_row (row, with_relation_tables)
  177. def fetch_all (
  178. self,
  179. with_relation_tables: bool,
  180. ) -> list[VideoDto]:
  181. with self.conn.cursor () as c:
  182. c.execute ("""
  183. SELECT
  184. id,
  185. code,
  186. title,
  187. description,
  188. uploaded_at,
  189. deleted_at
  190. FROM
  191. videos
  192. ORDER BY
  193. id""")
  194. videos: list[VideoDto] = []
  195. for row in c.fetchall ():
  196. videos.append (self._create_dto_from_row (row, with_relation_tables))
  197. return videos
  198. def upsert (
  199. self,
  200. video: VideoDto,
  201. with_relation_tables: bool,
  202. ) -> None:
  203. with self.conn.cursor () as c:
  204. c.execute ("""
  205. INSERT INTO
  206. videos(
  207. code,
  208. title,
  209. description,
  210. uploaded_at,
  211. deleted_at)
  212. VALUES
  213. (
  214. %s,
  215. %s,
  216. %s,
  217. %s,
  218. %s)
  219. ON DUPLICATE KEY UPDATE
  220. code = VALUES(code),
  221. title = VALUES(title),
  222. description = VALUES(description),
  223. uploaded_at = VALUES(uploaded_at),
  224. deleted_at = VALUES(deleted_at)""", (video.code,
  225. video.title,
  226. video.description,
  227. video.uploaded_at,
  228. video.deleted_at))
  229. video.id_ = c.lastrowid
  230. if with_relation_tables:
  231. if video.video_tags is not None:
  232. VideoTagDao (self.conn).upsert_all (video.video_tags, False)
  233. if video.comments is not None:
  234. CommentDao (self.conn).upsert_all (video.comments, False)
  235. if video.video_histories is not None:
  236. VideoHistoryDao (self.conn).upsert_all (video.video_histories)
  237. def upsert_all (
  238. self,
  239. videos: list[VideoDto],
  240. with_relation_tables: bool,
  241. ) -> None:
  242. for video in videos:
  243. self.upsert (video, with_relation_tables)
  244. def delete (
  245. self,
  246. video_ids: list[int],
  247. at: datetime,
  248. ) -> None:
  249. with self.conn.cursor () as c:
  250. c.execute ("""
  251. UPDATE
  252. videos
  253. SET
  254. deleted_at = %s
  255. WHERE
  256. id IN (%s)""", (at, (*video_ids,)))
  257. def _create_dto_from_row (
  258. self,
  259. row,
  260. with_relation_tables: bool,
  261. ) -> VideoDto:
  262. video = VideoDto (id_ = row['id'],
  263. code = row['code'],
  264. title = row['title'],
  265. description = row['description'],
  266. uploaded_at = row['uploaded_at'],
  267. deleted_at = row['deleted_at'])
  268. if with_relation_tables and video.id_ is not None:
  269. video.video_tags = VideoTagDao (self.conn).fetch_by_video_id (video.id_, False)
  270. for i in range (len (video.video_tags)):
  271. video.video_tags[i].video = video
  272. video.comments = CommentDao (self.conn).fetch_by_video_id (video.id_, False)
  273. for i in range (len (video.comments)):
  274. video.comments[i].video = video
  275. video.video_histories = VideoHistoryDao (self.conn).fetch_by_video_id (video.id_, False)
  276. for i in range (len (video.video_histories)):
  277. video.video_histories[i].video = video
  278. return video
  279. @dataclass (slots = True)
  280. class VideoDto:
  281. code: str
  282. title: str
  283. description: str
  284. uploaded_at: datetime
  285. id_: int | None = None
  286. deleted_at: datetime | DbNullType = DbNull
  287. video_tags: list[VideoTagDto] | None = None
  288. comments: list[CommentDto] | None = None
  289. video_histories: list[VideoHistoryDto] | None = None
  290. class VideoTagDao:
  291. def __init__ (
  292. self,
  293. conn,
  294. ):
  295. self.conn = conn
  296. def fetch_by_video_id (
  297. self,
  298. video_id: int,
  299. with_relation_tables: bool,
  300. ) -> list[VideoTagDto]:
  301. with self.conn.cursor () as c:
  302. c.execute ("""
  303. SELECT
  304. id,
  305. video_id,
  306. tag_id,
  307. tagged_at,
  308. untagged_at
  309. FROM
  310. video_tags
  311. WHERE
  312. video_id = %s
  313. ORDER BY
  314. id""", video_id)
  315. video_tags: list[VideoTagDto] = []
  316. for row in c.fetchall ():
  317. video_tags.append (self._create_dto_from_row (row, with_relation_tables))
  318. return video_tags
  319. def fetch_alive_by_video_id (
  320. self,
  321. video_id: int,
  322. with_relation_tables: bool,
  323. ) -> list[VideoTagDto]:
  324. with self.conn.cursor () as c:
  325. c.execute ("""
  326. SELECT
  327. id,
  328. video_id,
  329. tag_id,
  330. tagged_at,
  331. untagged_at
  332. FROM
  333. video_tags
  334. WHERE
  335. video_id = %s
  336. AND (untagged_at IS NULL)
  337. ORDER BY
  338. id""", video_id)
  339. video_tags: list[VideoTagDto] = []
  340. for row in c.fetchall ():
  341. video_tags.append (self._create_dto_from_row (row, with_relation_tables))
  342. return video_tags
  343. def fetch_alive_by_ids (
  344. self,
  345. video_id: int,
  346. tag_id: int,
  347. with_relation_tables: bool,
  348. ) -> VideoTagDto | None:
  349. with self.conn.cursor () as c:
  350. c.execute ("""
  351. SELECT
  352. id,
  353. video_id,
  354. tag_id,
  355. tagged_at,
  356. untagged_at
  357. FROM
  358. video_tags
  359. WHERE
  360. video_id = %s
  361. AND tag_id = %s""", (video_id, tag_id))
  362. row = c.fetchone ()
  363. if row is None:
  364. return None
  365. return self._create_dto_from_row (row, with_relation_tables)
  366. def insert (
  367. self,
  368. video_tag: VideoTagDto,
  369. with_relation_tables: bool,
  370. ) -> None:
  371. with self.conn.cursor () as c:
  372. c.execute ("""
  373. INSERT INTO
  374. video_tags(
  375. video_id,
  376. tag_id,
  377. tagged_at,
  378. untagged_at)
  379. VALUES
  380. (
  381. %s,
  382. %s,
  383. %s,
  384. %s)""", (video_tag.video_id, video_tag.tag_id,
  385. video_tag.tagged_at, video_tag.untagged_at))
  386. video_tag.id_ = c.lastrowid
  387. if with_relation_tables:
  388. if video_tag.video is not None:
  389. VideoDao (self.conn).upsert (video_tag.video, True)
  390. if video_tag.tag is not None:
  391. TagDao (self.conn).upsert (video_tag.tag)
  392. def upsert (
  393. self,
  394. video_tag: VideoTagDto,
  395. with_relation_tables: bool,
  396. ) -> None:
  397. with self.conn.cursor () as c:
  398. c.execute ("""
  399. INSERT INTO
  400. video_tags(
  401. video_id,
  402. tag_id,
  403. tagged_at,
  404. untagged_at)
  405. VALUES
  406. (
  407. %s,
  408. %s,
  409. %s,
  410. %s)
  411. ON DUPLICATE KEY UPDATE
  412. video_id = VALUES(video_id),
  413. tag_id = VALUES(tag_id),
  414. tagged_at = VALUES(tagged_at),
  415. untagged_at = VALUES(untagged_at)""", (video_tag.video_id,
  416. video_tag.tag_id,
  417. video_tag.tagged_at,
  418. video_tag.untagged_at))
  419. video_tag.id_ = c.lastrowid
  420. if with_relation_tables:
  421. if video_tag.video is not None:
  422. VideoDao (self.conn).upsert (video_tag.video, True)
  423. if video_tag.tag is not None:
  424. TagDao (self.conn).upsert (video_tag.tag)
  425. def upsert_all (
  426. self,
  427. video_tags: list[VideoTagDto],
  428. with_relation_tables: bool,
  429. ) -> None:
  430. for video_tag in video_tags:
  431. self.upsert (video_tag, with_relation_tables)
  432. def untag_all (
  433. self,
  434. video_id: int,
  435. tag_ids: list[int],
  436. now: datetime
  437. ) -> None:
  438. with self.conn.cursor () as c:
  439. c.execute ("""
  440. UPDATE
  441. video_tags
  442. SET
  443. untagged_at = %s
  444. WHERE
  445. video_id = %s
  446. AND tag_ids IN (%s)""", (now, video_id, (*tag_ids,)))
  447. def _create_dto_from_row (
  448. self,
  449. row,
  450. with_relation_tables: bool,
  451. ) -> VideoTagDto:
  452. video_tag = VideoTagDto (id_ = row['id'],
  453. video_id = row['video_id'],
  454. tag_id = row['tag_id'],
  455. tagged_at = row['tagged_at'],
  456. untagged_at = row['untagged_at'])
  457. if with_relation_tables:
  458. video_tag.video = VideoDao (self.conn).find (video_tag.video_id, True)
  459. video_tag.tag = TagDao (self.conn).find (video_tag.tag_id)
  460. return video_tag
  461. @dataclass (slots = True)
  462. class VideoTagDto:
  463. video_id: int
  464. tag_id: int
  465. tagged_at: datetime
  466. id_: int | None = None
  467. untagged_at: datetime | DbNullType = DbNull
  468. video: VideoDto | None = None
  469. tag: TagDto | None = None
  470. class TagDao:
  471. def __init__ (
  472. self,
  473. conn,
  474. ):
  475. self.conn = conn
  476. def find (
  477. self,
  478. tag_id: int,
  479. ) -> TagDto | None:
  480. with self.conn.cursor () as c:
  481. c.execute ("""
  482. SELECT
  483. id,
  484. name)
  485. FROM
  486. tags
  487. WHERE
  488. id = %s""", tag_id)
  489. row = c.fetchone ()
  490. if row is None:
  491. return None
  492. return self._create_dto_from_row (row)
  493. def fetch_by_name (
  494. self,
  495. tag_name: str,
  496. ) -> TagDto | None:
  497. with self.conn.cursor () as c:
  498. c.execute ("""
  499. SELECT
  500. id,
  501. name
  502. FROM
  503. tags
  504. WHERE
  505. name = %s""", tag_name)
  506. row = c.fetchone ()
  507. if row is None:
  508. return None
  509. return self._create_dto_from_row (row)
  510. def insert (
  511. self,
  512. tag: TagDto,
  513. ) -> None:
  514. with self.conn.cursor () as c:
  515. c.execute ("""
  516. INSERT INTO
  517. tags(name)
  518. VALUES
  519. (%s)""", tag.name)
  520. tag.id_ = c.lastrowid
  521. def upsert (
  522. self,
  523. tag: TagDto,
  524. ) -> None:
  525. with self.conn.cursor () as c:
  526. c.execute ("""
  527. INSERT INTO
  528. tags(name)
  529. VALUES
  530. (%s)
  531. ON DUPLICATE KEY UPDATE
  532. name = VALUES(name)""", tag.name)
  533. tag.id_ = c.lastrowid
  534. def _create_dto_from_row (
  535. self,
  536. row,
  537. ) -> TagDto:
  538. return TagDto (id_ = row['id'],
  539. name = row['name'])
  540. @dataclass (slots = True)
  541. class TagDto:
  542. name: str
  543. id_: int | None = None
  544. class VideoHistoryDao:
  545. def __init__ (
  546. self,
  547. conn,
  548. ):
  549. self.conn = conn
  550. def fetch_by_video_id (
  551. self,
  552. video_id: int,
  553. with_relation_tables: bool,
  554. ) -> list[VideoHistoryDto]:
  555. with self.conn.cursor () as c:
  556. c.execute ("""
  557. SELECT
  558. id,
  559. video_id,
  560. fetched_at,
  561. views_count
  562. FROM
  563. video_histories
  564. WHERE
  565. video_id = %s""", video_id)
  566. video_histories: list[VideoHistoryDto] = []
  567. for row in c.fetchall ():
  568. video_histories.append (self._create_dto_from_row (row, with_relation_tables))
  569. return video_histories
  570. def insert (
  571. self,
  572. video_history: VideoHistoryDto,
  573. ) -> None:
  574. with self.conn.cursor () as c:
  575. c.execute ("""
  576. INSERT INTO
  577. video_histories(
  578. video_id,
  579. fetched_at,
  580. views_count)
  581. VALUES
  582. (
  583. %s,
  584. %s,
  585. %s)""", (video_history.video_id,
  586. video_history.fetched_at,
  587. video_history.views_count))
  588. def upsert (
  589. self,
  590. video_history: VideoHistoryDto,
  591. ) -> None:
  592. with self.conn.cursor () as c:
  593. c.execute ("""
  594. INSERT INTO
  595. video_histories(
  596. video_id,
  597. fetched_at,
  598. views_count)
  599. VALUES
  600. (
  601. %s,
  602. %s,
  603. %s)
  604. ON DUPLICATE KEY UPDATE
  605. video_id,
  606. fetched_at,
  607. views_count""", (video_history.video_id,
  608. video_history.fetched_at,
  609. video_history.views_count))
  610. def upsert_all (
  611. self,
  612. video_histories: list[VideoHistoryDto],
  613. ) -> None:
  614. for video_history in video_histories:
  615. self.upsert (video_history)
  616. def _create_dto_from_row (
  617. self,
  618. row,
  619. with_relation_tables: bool,
  620. ) -> VideoHistoryDto:
  621. video_history = VideoHistoryDto (id_ = row['id'],
  622. video_id = row['video_id'],
  623. fetched_at = row['fetched_at'],
  624. views_count = row['views_count'])
  625. if with_relation_tables:
  626. video_history.video = VideoDao (self.conn).find (video_history.video_id, True)
  627. return video_history
  628. @dataclass (slots = True)
  629. class VideoHistoryDto:
  630. video_id: int
  631. fetched_at: datetime
  632. views_count: int
  633. id_: int | None = None
  634. video: VideoDto | None = None
  635. class CommentDao:
  636. def __init__ (
  637. self,
  638. conn,
  639. ):
  640. self.conn = conn
  641. def fetch_by_video_id (
  642. self,
  643. video_id: int,
  644. with_relation_tables: bool,
  645. ) -> list[CommentDto]:
  646. with self.conn.cursor () as c:
  647. c.execute ("""
  648. SELECT
  649. id,
  650. video_id,
  651. comment_no,
  652. user_id,
  653. content,
  654. posted_at,
  655. nico_count,
  656. vpos_ms
  657. FROM
  658. comments
  659. WHERE
  660. video_id = %s""", video_id)
  661. comments: list[CommentDto] = []
  662. for row in c.fetchall ():
  663. comments.append (self._create_dto_from_row (row, with_relation_tables))
  664. return comments
  665. def upsert (
  666. self,
  667. comment: CommentDto,
  668. with_relation_tables: bool,
  669. ) -> None:
  670. with self.conn.cursor () as c:
  671. c.execute ("""
  672. INSERT INTO
  673. comments(
  674. video_id,
  675. comment_no,
  676. user_id,
  677. content,
  678. posted_at,
  679. nico_count,
  680. vpos_ms)
  681. VALUES
  682. (
  683. %s,
  684. %s,
  685. %s,
  686. %s,
  687. %s,
  688. %s,
  689. %s)
  690. ON DUPLICATE KEY UPDATE
  691. video_id = VALUES(video_id),
  692. comment_no = VALUES(comment_no),
  693. user_id = VALUES(user_id),
  694. content = VALUES(content),
  695. posted_at = VALUES(posted_at),
  696. nico_count = VALUES(nico_count),
  697. vpos_ms = VALUES(vpos_ms)""", (comment.video_id,
  698. comment.comment_no,
  699. comment.user_id,
  700. comment.content,
  701. comment.posted_at,
  702. comment.nico_count,
  703. comment.vpos_ms))
  704. def upsert_all (
  705. self,
  706. comments: list[CommentDto],
  707. with_relation_tables: bool,
  708. ) -> None:
  709. for comment in comments:
  710. self.upsert (comment, with_relation_tables)
  711. def _create_dto_from_row (
  712. self,
  713. row,
  714. with_relation_tables: bool,
  715. ) -> CommentDto:
  716. comment = CommentDto (id_ = row['id'],
  717. video_id = row['video_id'],
  718. comment_no = row['comment_no'],
  719. user_id = row['user_id'],
  720. content = row['content'],
  721. posted_at = row['posted_at'],
  722. nico_count = row['nico_count'],
  723. vpos_ms = row['vpos_ms'])
  724. if with_relation_tables:
  725. comment.video = VideoDao (self.conn).find (comment.video_id, True)
  726. return comment
  727. @dataclass (slots = True)
  728. class CommentDto:
  729. video_id: int
  730. comment_no: int
  731. user_id: int
  732. content: str
  733. posted_at: datetime
  734. id_: int | None = None
  735. nico_count: int = 0
  736. vpos_ms: int | DbNullType = DbNull
  737. video: VideoDto | None = None
  738. if __name__ == '__main__':
  739. main ()