9 コミット

作成者 SHA1 メッセージ 日付
みてるぞ ac17cb2bbf #17 2026-03-05 20:56:40 +09:00
みてるぞ 442097f037 #17 2026-03-05 20:50:00 +09:00
みてるぞ a3d9d0bfd7 feat: タグと関係なしに追跡する動画リスト追加 (#16)
#15 タグ関係なく追跡する動画リスト

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #16
2026-01-01 14:00:11 +09:00
みてるぞ f44637d274 コメント取得追加 2025-10-26 05:07:07 +09:00
みてるぞ f809e9faae キリ番修正 2025-10-26 01:19:15 +09:00
みてるぞ de8fd8634a キリ番修正 2025-10-26 01:07:15 +09:00
みてるぞ 88be511f6e キリ番修正 2025-10-26 01:06:41 +09:00
みてるぞ ea339f1ec9 キリ番修正 2025-10-26 01:05:48 +09:00
みてるぞ 06328a89b2 キリ番追加 2025-10-26 00:45:29 +09:00
7個のファイルの変更193行の追加20行の削除
+16
ファイルの表示
@@ -58,6 +58,13 @@ class Tag (Model):
return self.has_many (VideoTag)
class TrackedVideo (Model):
id: int
code: str
__timestamps__ = False
class User (Model):
id: int
code: str
@@ -74,6 +81,7 @@ class User (Model):
class Video (Model):
id: int
code: str
user_id: int | None
title: str
description: str
uploaded_at: datetime
@@ -81,6 +89,14 @@ class Video (Model):
__timestamps__ = False
@property
def user (
self,
) -> User | None:
if self.user_id is None:
return None
return self.belongs_to (User)
@property
def video_histories (
self,
+58
ファイルの表示
@@ -0,0 +1,58 @@
# pylint: disable = missing-class-docstring
# pylint: disable = missing-function-docstring
"""
動画コードからコメントのリストを取得し,JSON 形式で出力する.
"""
from __future__ import annotations
import json
import os
import sys
from datetime import date, datetime
from typing import TypedDict, cast
from eloquent import DatabaseManager, Model
from db.config import DB
from db.models import Video
DB
def main (
video_code: str,
) -> None:
video = Video.where ('code', video_code).first ()
if video:
comments: list[CommentDict] = []
for row in video.comments:
comment: CommentDict = {
'id': row.id,
'video_id': row.video_id,
'comment_no': row.comment_no,
'user_id': row.user_id,
'content': row.content,
'posted_at': row.posted_at,
'nico_count': row.nico_count,
'vpos_ms': row.vpos_ms }
comments.append (comment)
print (json.dumps (comments, default = str))
else:
print ('[]')
class CommentDict (TypedDict):
id: int
video_id: int
comment_no: int
user_id: int
content: str
posted_at: datetime
nico_count: int
vpos_ms: int
if __name__ == '__main__':
main (sys.argv[1])
+67
ファイルの表示
@@ -0,0 +1,67 @@
# pylint: disable = missing-class-docstring
# pylint: disable = missing-function-docstring
"""
動画履歴の情報を取得し,JSON 形式で出力する.
"""
from __future__ import annotations
import json
import os
import sys
from datetime import date, datetime
from typing import TypedDict, cast
from eloquent import DatabaseManager, Model
from db.config import DB
from db.models import Video, VideoHistory
DB
def main (
views_counts: list[int],
base_date: date,
) -> None:
if not base_date:
base_date = datetime.now ().date ()
kiriban_list: list[tuple[int, str, str]] = []
latest_fetched_at = cast (date, (VideoHistory
.where ('fetched_at', '<=', base_date)
.max ('fetched_at')))
for views_count in views_counts:
targets = { vh.video.code for vh in (
VideoHistory
.where ('fetched_at', latest_fetched_at)
.where ('views_count', '>=', views_count)
.get ()) }
for code in targets:
if code in [kiriban[1] for kiriban in kiriban_list]:
continue
previous_views_count: int | None = (
VideoHistory
.where_has ('video', lambda q, code = code: q.where ('code', code))
.where ('fetched_at', '<', latest_fetched_at)
.max ('views_count'))
if previous_views_count is None:
previous_views_count = 0
if previous_views_count >= views_count:
continue
kiriban_list.append ((views_count, code,
(cast (Video, Video.where ('code', code).first ())
.uploaded_at)))
print (json.dumps (kiriban_list, default = str))
if __name__ == '__main__':
main (map (int, sys.argv[2:]),
datetime.strptime (sys.argv[1], '%Y-%m-%d').date ())
+5 -9
ファイルの表示
@@ -14,25 +14,20 @@ from typing import TypedDict
from eloquent import DatabaseManager, Model
from db.config import DB
from db.models import Video
DB
def main (
) -> None:
config: dict[str, DbConfig] = { 'mysql': { 'driver': 'mysql',
'host': 'localhost',
'database': 'nizika_nico',
'user': os.environ['MYSQL_USER'],
'password': os.environ['MYSQL_PASS'],
'prefix': '' } }
db = DatabaseManager (config)
Model.set_connection_resolver (db)
videos: list[VideoDict] = []
for row in Video.all ():
deleted_at = row.deleted_at.date () if row.deleted_at else None
video: VideoDict = { 'id': row.id,
'code': row.code,
'user': getattr (row.user, 'code', None),
'title': row.title,
'description': row.description,
'tags': [],
@@ -58,6 +53,7 @@ class DbConfig (TypedDict):
class VideoDict (TypedDict):
id: int
code: str
user: str | None
title: str
description: str
tags: list[str]
+2
ファイルの表示
@@ -0,0 +1,2 @@
CREATE TABLE `nizika_nico`.`tracked_videos` (`id` BIGINT NOT NULL AUTO_INCREMENT , `code` VARCHAR(16) NOT NULL COMMENT '動画コード' , PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = '追跡対象動画';
ALTER TABLE `tracked_videos` ADD UNIQUE(`code`);
+3
ファイルの表示
@@ -0,0 +1,3 @@
ALTER TABLE `videos` ADD `user_id` BIGINT NULL DEFAULT NULL COMMENT 'ユーザ Id.' AFTER `code`;
ALTER TABLE `videos` ADD INDEX(`user_id`);
ALTER TABLE `videos` ADD FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
+42 -11
ファイルの表示
@@ -21,7 +21,13 @@ import requests
from eloquent import DatabaseManager, Model
from db.config import DB
from db.models import Comment, Tag, User, Video, VideoHistory, VideoTag
from db.models import (Comment,
Tag,
TrackedVideo,
User,
Video,
VideoHistory,
VideoTag)
def main (
@@ -49,8 +55,16 @@ def update_tables (
for datum in api_data:
tag_names: list[str] = datum['tags'].split ()
user: User | None = None
if datum['userId']:
user = User.where('code', str (datum['userId'])).first ()
if user is None:
user = User ()
user.code = str (datum['userId'])
user.save ()
video = Video ()
video.code = datum['contentId']
video.user_id = user.id if user else None
video.title = datum['title']
video.description = datum['description'] or ''
video.uploaded_at = datetime.fromisoformat (datum['startTime'])
@@ -115,9 +129,9 @@ def update_tables (
video.save ()
def fetch_comments (
def fetch_video_data (
video_code: str,
) -> list[CommentResult]:
) -> dict[str, Any]:
time.sleep (1.2)
headers = { 'X-Frontend-Id': '6',
@@ -132,10 +146,14 @@ def fetch_comments (
url = (f"https://www.nicovideo.jp/api/watch/v3_guest/{ video_code }"
+ f"?actionTrackId={ action_track_id }")
res = requests.post (url, headers = headers, timeout = 60).json ()
return requests.post (url, headers = headers, timeout = 60).json ()
def fetch_comments (
video_code: str,
) -> list[CommentResult]:
try:
nv_comment = res['data']['comment']['nvComment']
nv_comment = fetch_video_data (video_code)['data']['comment']['nvComment']
except KeyError:
return []
if nv_comment is None:
@@ -162,12 +180,6 @@ def fetch_comments (
return []
def search_nico_by_tag (
tag: str,
) -> list[VideoResult]:
return search_nico_by_tags ([tag])
def search_nico_by_tags (
tags: list[str],
) -> list[VideoResult]:
@@ -195,6 +207,7 @@ def search_nico_by_tags (
'targets': 'tagsExact',
'_sort': '-viewCounter',
'fields': ('contentId,'
'userId,'
'title,'
'tags,'
'description,'
@@ -209,6 +222,23 @@ def search_nico_by_tags (
pass
to = until + timedelta (days = 1)
for video in TrackedVideo.get ():
if video.code in map (lambda v: v['contentId'], result_data):
continue
try:
video_data = fetch_video_data (video.code)['data']
result_data.append ({
'contentId': video.code,
'userId': video_data['video']['userId'],
'title': video_data['video']['title'],
'tags': ' '.join (map (lambda t: t['name'],
video_data['tag']['items'])),
'description': video_data['video']['description'],
'viewCounter': video_data['video']['count']['view'],
'startTime': video_data['video']['registeredAt'] })
except Exception:
pass
return result_data
@@ -223,6 +253,7 @@ class VideoSearchParam (TypedDict):
class VideoResult (TypedDict):
contentId: str
userId: int | None
title: str
tags: str
description: str | None