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

update_db.py 35 KiB

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