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

966 lines
32 KiB

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