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

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
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
1 month 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
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
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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. def main (
  28. ) -> None:
  29. conn = mysql.connector.connect (host = os.environ['MYSQL_HOST'],
  30. user = os.environ['MYSQL_USER'],
  31. password = os.environ['MYSQL_PASS'])
  32. now = datetime.now ()
  33. video_dao = VideoDao (conn)
  34. tag_dao = TagDao (conn)
  35. api_data = search_nico_by_tags (['伊地知ニジカ', 'ぼざろクリーチャーシリーズ'])
  36. update_video_and_tag_table (video_dao, tag_dao, api_data, now)
  37. # TODO: 書くこと
  38. def update_video_and_tag_table (
  39. video_dao: VideoDao,
  40. tag_dao: TagDao,
  41. api_data: list[VideoResult],
  42. now: datetime,
  43. ) -> None:
  44. videos: list[VideoDto] = []
  45. for datum in api_data:
  46. tags: list[TagDto] = []
  47. for tag_name in datum['tags']:
  48. tags.append (TagDto (name = tag_name))
  49. tag_dao.upsert_all (tags)
  50. # TODO: タグの対応づけすること
  51. # TODO: コメント取得すること
  52. video = VideoDto (code = datum['contentId'],
  53. title = datum['title'],
  54. description = datum['description'],
  55. uploaded_at = datum['startTime'],
  56. deleted_at = DbNull)
  57. videos.append (video)
  58. video_dao.upsert_all (videos)
  59. video_dao.delete_nonexistent_data (video_id_list)
  60. # TODO: 書くこと
  61. def fetch_comments (
  62. video_id: str,
  63. ) -> list:
  64. headers = { 'X-Frontend-Id': '6',
  65. 'X-Frontend-Version': '0' }
  66. action_track_id = (
  67. ''.join (random.choice (string.ascii_letters + string.digits)
  68. for _ in range (10))
  69. + '_'
  70. + str (random.randrange (10 ** 12, 10 ** 13)))
  71. url = (f"https://www.nicovideo.jp/api/watch/v3_guest/{ video_id }"
  72. + f"?actionTrackId={ action_track_id }")
  73. res = requests.post (url, headers = headers, timeout = 60).json ()
  74. try:
  75. nv_comment = res['data']['comment']['nvComment']
  76. except KeyError:
  77. return []
  78. if nv_comment is None:
  79. return []
  80. headers = { 'X-Frontend-Id': '6',
  81. 'X-Frontend-Version': '0',
  82. 'Content-Type': 'application/json' }
  83. params = { 'params': nv_comment['params'],
  84. 'additionals': { },
  85. 'threadKey': nv_comment['threadKey'] }
  86. url = nv_comment['server'] + '/v1/threads'
  87. res = (requests.post (url, json.dumps (params),
  88. headers = headers,
  89. timeout = 60)
  90. .json ())
  91. try:
  92. return res['data']['threads'][1]['comments']
  93. except (IndexError, KeyError):
  94. return []
  95. def search_nico_by_tag (
  96. tag: str,
  97. ) -> list[VideoResult]:
  98. return search_nico_by_tags ([tag])
  99. def search_nico_by_tags (
  100. tags: list[str],
  101. ) -> list[VideoResult]:
  102. url = ('https://snapshot.search.nicovideo.jp'
  103. + '/api/v2/snapshot/video/contents/search')
  104. # TODO: 年月日の設定ができてゐなぃのと,100 件までしか取得できなぃので何とかすること
  105. query_filter = json.dumps ({ 'type': 'or',
  106. 'filters': [
  107. { 'type': 'range',
  108. 'field': 'startTime',
  109. 'from': f"{year}-{start}T00:00:00+09:00",
  110. 'to': f"{year}-{end}T23:59:59+09:00",
  111. 'include_lower': True }] })
  112. params: VideoSearchParam
  113. params = { 'q': ' OR '.join (tags),
  114. 'targets': 'tagsExact',
  115. '_sort': '-viewCounter',
  116. 'fields': 'contentId,title,tags,description,viewCounter,startTime',
  117. '_limit': 100,
  118. 'jsonFilter': query_filter }
  119. res = requests.get (url, params = cast (dict[str, int | str], params), timeout = 60).json ()
  120. return res['data']
  121. class VideoDao:
  122. def __init__ (
  123. self,
  124. conn
  125. ):
  126. self.conn = conn
  127. def fetch (
  128. self,
  129. video_id: int,
  130. with_relation_tables: bool = True,
  131. ) -> VideoDto | None:
  132. with self.conn.cursor () as c:
  133. c.execute ("""
  134. SELECT
  135. id,
  136. code,
  137. title,
  138. description,
  139. uploaded_at,
  140. deleted_at
  141. FROM
  142. videos
  143. WHERE
  144. id = %s
  145. ORDER BY
  146. id""", video_id)
  147. row = c.fetchone ()
  148. if row is None:
  149. return None
  150. return self._create_dto_from_row (row, with_relation_tables)
  151. def fetch_all (
  152. self,
  153. with_relation_tables: bool = True,
  154. ) -> list[VideoDto]:
  155. with self.conn.cursor () as c:
  156. c.execute ("""
  157. SELECT
  158. id,
  159. code,
  160. title,
  161. description,
  162. uploaded_at,
  163. deleted_at
  164. FROM
  165. videos
  166. ORDER BY
  167. id""")
  168. videos: list[VideoDto] = []
  169. for row in c.fetchall ():
  170. videos.append (self._create_dto_from_row (row, with_relation_tables))
  171. return videos
  172. def upsert (
  173. self,
  174. video: VideoDto,
  175. with_relation_tables: bool = True,
  176. ) -> None:
  177. with self.conn.cursor () as c:
  178. c.execute ("""
  179. INSERT INTO
  180. videos(
  181. code,
  182. title,
  183. description,
  184. uploaded_at,
  185. deleted_at)
  186. VALUES
  187. (
  188. %s,
  189. %s,
  190. %s,
  191. %s,
  192. %s)
  193. ON DUPLICATE KEY UPDATE
  194. code = VALUES(code),
  195. title = VALUES(title),
  196. description = VALUES(description),
  197. uploaded_at = VALUES(uploaded_at),
  198. deleted_at = VALUES(deleted_at)""", (video.code,
  199. video.title,
  200. video.description,
  201. video.uploaded_at,
  202. video.deleted_at))
  203. video.id_ = c.lastrowid
  204. if with_relation_tables:
  205. VideoTagDao (self.conn).upsert_all (video.video_tags, False)
  206. CommentDao (self.conn).upsert_all (video.comments, False)
  207. VideoHistoryDao (self.conn).upsert_all (video.video_histories, False)
  208. def upsert_all (
  209. self,
  210. videos: list[VideoDto],
  211. with_relation_tables: bool = True,
  212. ) -> None:
  213. with self.conn.cursor () as c:
  214. for video in videos:
  215. self.upsert (video, with_relation_tables)
  216. def _create_dto_from_row (
  217. self,
  218. row,
  219. with_relation_tables: bool,
  220. ) -> VideoDto:
  221. video = VideoDto (id_ = row['id'],
  222. code = row['code'],
  223. title = row['title'],
  224. description = row['description'],
  225. uploaded_at = row['uploaded_at'],
  226. deleted_at = row['deleted_at'])
  227. if with_relation_tables:
  228. video.video_tags = VideoTagDao (self.conn).fetch_by_video_id (cast (int, video.id_), False)
  229. for i in range (len (video.video_tags)):
  230. video.video_tags[i].video = video
  231. video.comments = CommentDao (self.conn).fetch_by_video_id (video.id_, False)
  232. for i in range (len (video.comments)):
  233. video.comments[i].video = video
  234. video.video_histories = VideoHistoryDao (self.conn).fetch_by_video_id (video.id_, False)
  235. for i in range (len (video.video_histories)):
  236. video.video_histories[i].video = video
  237. return video
  238. @dataclass (slots = True)
  239. class VideoDto:
  240. code: str
  241. title: str
  242. description: str
  243. uploaded_at: datetime
  244. id_: int | None = None
  245. deleted_at: datetime | DbNullType = DbNull
  246. video_tags: list[VideoTagDto] | None = None
  247. comments: list[CommentDto] | None = None
  248. video_histories: list[VideoHistoryDto] | None = None
  249. class VideoTagDao:
  250. def __init__ (
  251. self,
  252. conn,
  253. ):
  254. self.conn = conn
  255. def fetch_by_video_id (
  256. self,
  257. video_id: int,
  258. with_relation_tables: bool = True,
  259. ) -> list[VideoTagDto]:
  260. with self.conn.cursor () as c:
  261. c.execute ("""
  262. SELECT
  263. id,
  264. video_id,
  265. tag_id,
  266. tagged_at,
  267. untagged_at
  268. FROM
  269. video_tags
  270. WHERE
  271. video_id = %s
  272. ORDER BY
  273. id""", video_id)
  274. video_tags: list[VideoTagDto] = []
  275. for row in c.fetchall ():
  276. video_tags.append (self._create_dto_from_row (row, with_relation_tables))
  277. return video_tags
  278. def _create_dto_from_row (
  279. self,
  280. row,
  281. with_relation_tables: bool,
  282. ) -> VideoTagDto:
  283. video_tag = VideoTagDto (id_ = row['id'],
  284. video_id = row['video_id'],
  285. tag_id = row['tag_id'],
  286. tagged_at = row['tagged_at'],
  287. untagged_at = row['untagged_at'])
  288. if with_relation_tables:
  289. video_tag.video = VideoDao (self.conn).fetch (video_tag.video_id, True)
  290. video_tag.tag = TagDao (self.conn).fetch (video_tag.tag_id, True)
  291. return video_tag
  292. @dataclass (slots = True)
  293. class VideoTagDto:
  294. tagged_at: datetime
  295. id_: int | None = None
  296. video_id: int | None = None
  297. tag_id: int | None = None
  298. untagged_at: datetime | DbNullType = DbNull
  299. video: VideoDto | None = None
  300. tag: TagDto | None = None
  301. @dataclass (slots = True)
  302. class TagDto:
  303. name: str
  304. id_: int | None = None
  305. @dataclass (slots = True)
  306. class CommentDto:
  307. video_id: int
  308. comment_no: int
  309. user_id: int
  310. content: str
  311. posted_at: datetime
  312. id_: int | None = None
  313. nico_count: int = 0
  314. vpos_ms: int | DbNullType = DbNull
  315. if __name__ == '__main__':
  316. main ()