ぼざろクリーチャーシリーズ 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.
 
 
 

843 lines
28 KiB

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