コミットを比較
26 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| ac17cb2bbf | |||
| 442097f037 | |||
| a3d9d0bfd7 | |||
| f44637d274 | |||
| f809e9faae | |||
| de8fd8634a | |||
| 88be511f6e | |||
| ea339f1ec9 | |||
| 06328a89b2 | |||
| 463e8bbec7 | |||
| baa75d68ba | |||
| 48e51f97d0 | |||
| c5204383ed | |||
| bf36d05ed3 | |||
| c9bd6fdfa7 | |||
| b2f5f81ca8 | |||
| 67b76e6dd4 | |||
| 6a5e6dfade | |||
| 6e99da7326 | |||
| 53ba658319 | |||
| 067c90890e | |||
| db14af1a73 | |||
| da17333a80 | |||
| ee971997ad | |||
| b448335851 | |||
| 9f810b23f0 |
@@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from eloquent import DatabaseManager, Model # type: ignore
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class DbConfig (TypedDict):
|
||||||
|
driver: str
|
||||||
|
host: str
|
||||||
|
database: str
|
||||||
|
user: str
|
||||||
|
password: str
|
||||||
|
prefix: str
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
# pylint: disable = missing-class-docstring
|
||||||
|
# pylint: disable = missing-function-docstring
|
||||||
|
# pylint: disable = missing-module-docstring
|
||||||
|
# pylint: disable = unused-argument
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Generic, Type, TypeVar, overload
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
_ModelT = TypeVar ('_ModelT', bound = 'Model')
|
||||||
|
|
||||||
|
|
||||||
|
class Connection:
|
||||||
|
def select (self, query: str, bindings: dict[str, Any] | None = None) -> Any: ...
|
||||||
|
|
||||||
|
def insert (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
||||||
|
|
||||||
|
def update (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
||||||
|
|
||||||
|
def delete (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
||||||
|
|
||||||
|
def transaction (self, callback: Any) -> Any: ...
|
||||||
|
|
||||||
|
def begin_transaction (self) -> None: ...
|
||||||
|
|
||||||
|
def commit (self) -> None: ...
|
||||||
|
|
||||||
|
def rollback (self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionResolver:
|
||||||
|
def connection (self, name: str | None = None) -> Any: ...
|
||||||
|
|
||||||
|
def get_default_connection (self) -> str: ...
|
||||||
|
|
||||||
|
def set_default_connection (self, name: str) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseManager:
|
||||||
|
connections: dict[str, Connection]
|
||||||
|
|
||||||
|
def __init__ (self, config: dict[str, Any]) -> None: ...
|
||||||
|
|
||||||
|
def connection (self, name: str | None = None) -> Connection: ...
|
||||||
|
|
||||||
|
def disconnect (self, name: str | None = None) -> None: ...
|
||||||
|
|
||||||
|
def reconnect (self, name: str | None = None) -> Connection: ...
|
||||||
|
|
||||||
|
def get_connections (self) -> dict[str, Connection]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Model:
|
||||||
|
id: int
|
||||||
|
|
||||||
|
_Model__exists: bool
|
||||||
|
|
||||||
|
def has_one (
|
||||||
|
self,
|
||||||
|
related_model: Type[_ModelT],
|
||||||
|
foreign_key: str | None = None,
|
||||||
|
) -> _ModelT: ...
|
||||||
|
|
||||||
|
def has_many (
|
||||||
|
self,
|
||||||
|
related_model: Type[_ModelT],
|
||||||
|
foreign_key: str | None = None,
|
||||||
|
) -> list[_ModelT]: ...
|
||||||
|
|
||||||
|
def belongs_to (
|
||||||
|
self,
|
||||||
|
related_model: Type[_ModelT],
|
||||||
|
foreign_key: str | None = None,
|
||||||
|
) -> _ModelT: ...
|
||||||
|
|
||||||
|
def belongs_to_many (
|
||||||
|
self,
|
||||||
|
related_model: Type[_ModelT],
|
||||||
|
foreign_key: str | None = None,
|
||||||
|
) -> list[_ModelT]: ...
|
||||||
|
|
||||||
|
def save (self) -> None: ...
|
||||||
|
|
||||||
|
def delete (self) -> None: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find (cls, id_: int) -> Self | None: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def query (
|
||||||
|
cls,
|
||||||
|
) -> QueryBuilder[Self]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def where (
|
||||||
|
cls,
|
||||||
|
field: str,
|
||||||
|
operator: str,
|
||||||
|
value: Any,
|
||||||
|
) -> QueryBuilder[Self]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
@classmethod
|
||||||
|
def where (cls, field: str, value: Any) -> QueryBuilder[Self]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def where_not_in (
|
||||||
|
cls,
|
||||||
|
column: str,
|
||||||
|
values: list[Any] | tuple
|
||||||
|
) -> QueryBuilder[Self]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def where_not_null (cls, field: str) -> QueryBuilder[Self]: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def max (cls, column: str) -> Any: ...
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_connection_resolver (cls, resolver: DatabaseManager) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class QueryBuilder (Generic[_ModelT]):
|
||||||
|
def first (self) -> _ModelT | None: ...
|
||||||
|
|
||||||
|
def get (self) -> list[_ModelT]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def where (
|
||||||
|
self,
|
||||||
|
field: str,
|
||||||
|
operator: str,
|
||||||
|
value: Any,
|
||||||
|
) -> QueryBuilder[_ModelT]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def where (self, field: str, value: Any) -> QueryBuilder[_ModelT]: ...
|
||||||
|
|
||||||
|
def where_null (self, field: str) -> QueryBuilder[_ModelT]: ...
|
||||||
|
|
||||||
|
def max (self, column: str) -> Any: ...
|
||||||
|
|
||||||
|
def _load_relation (self, relation_name: str) -> QueryBuilder[_ModelT]: ...
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
# pylint: disable = missing-class-docstring
|
||||||
|
# pylint: disable = missing-function-docstring
|
||||||
|
|
||||||
|
"""
|
||||||
|
ぼざクリ DB の構成
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from db.my_eloquent import Model
|
||||||
|
|
||||||
|
|
||||||
|
class Comment (Model):
|
||||||
|
# pylint: disable = too-many-instance-attributes
|
||||||
|
|
||||||
|
id: int
|
||||||
|
video_id: int
|
||||||
|
comment_no: int
|
||||||
|
user_id: int
|
||||||
|
content: str
|
||||||
|
posted_at: datetime
|
||||||
|
nico_count: int
|
||||||
|
vpos_ms: int
|
||||||
|
|
||||||
|
__timestamps__ = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video (
|
||||||
|
self,
|
||||||
|
) -> Video:
|
||||||
|
return self.belongs_to (Video)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user (
|
||||||
|
self,
|
||||||
|
) -> User:
|
||||||
|
return self.belongs_to (User)
|
||||||
|
|
||||||
|
def upsert (
|
||||||
|
self,
|
||||||
|
*args: str,
|
||||||
|
) -> None:
|
||||||
|
super ().upsert ('video_id', 'comment_no')
|
||||||
|
|
||||||
|
|
||||||
|
class Tag (Model):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
__timestamps__ = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video_tags (
|
||||||
|
self,
|
||||||
|
) -> list[VideoTag]:
|
||||||
|
return self.has_many (VideoTag)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackedVideo (Model):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
|
||||||
|
__timestamps__ = False
|
||||||
|
|
||||||
|
|
||||||
|
class User (Model):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
|
||||||
|
__timestamps__ = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments (
|
||||||
|
self,
|
||||||
|
) -> list[Comment]:
|
||||||
|
return self.has_many (Comment)
|
||||||
|
|
||||||
|
|
||||||
|
class Video (Model):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
user_id: int | None
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
uploaded_at: datetime
|
||||||
|
deleted_at: datetime | None
|
||||||
|
|
||||||
|
__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,
|
||||||
|
) -> list[VideoHistory]:
|
||||||
|
return self.has_many (VideoHistory)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video_tags (
|
||||||
|
self,
|
||||||
|
) -> list[VideoTag]:
|
||||||
|
return self.has_many (VideoTag)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments (
|
||||||
|
self,
|
||||||
|
) -> list[Comment]:
|
||||||
|
return self.has_many (Comment)
|
||||||
|
|
||||||
|
def upsert (
|
||||||
|
self,
|
||||||
|
*args: str,
|
||||||
|
) -> None:
|
||||||
|
super ().upsert ('code')
|
||||||
|
|
||||||
|
|
||||||
|
class VideoHistory (Model):
|
||||||
|
id: int
|
||||||
|
video_id: int
|
||||||
|
fetched_at: date
|
||||||
|
views_count: int
|
||||||
|
|
||||||
|
__timestamps__ = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video (
|
||||||
|
self,
|
||||||
|
) -> Video:
|
||||||
|
return self.belongs_to (Video)
|
||||||
|
|
||||||
|
def upsert (
|
||||||
|
self,
|
||||||
|
*args: str,
|
||||||
|
) -> None:
|
||||||
|
super ().upsert ('video_id', 'fetched_at')
|
||||||
|
|
||||||
|
|
||||||
|
class VideoTag (Model):
|
||||||
|
id: int
|
||||||
|
video_id: int
|
||||||
|
tag_id: int
|
||||||
|
tagged_at: date
|
||||||
|
untagged_at: date | None
|
||||||
|
|
||||||
|
__timestamps__ = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video (
|
||||||
|
self,
|
||||||
|
) -> Video:
|
||||||
|
return self.belongs_to (Video)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tag (
|
||||||
|
self,
|
||||||
|
) -> Tag:
|
||||||
|
return self.belongs_to (Tag)
|
||||||
|
|
||||||
|
def upsert (
|
||||||
|
self,
|
||||||
|
*args: str,
|
||||||
|
) -> None:
|
||||||
|
super ().upsert ('video_id', 'tag_id')
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# pylint: disable = missing-class-docstring
|
||||||
|
# pylint: disable = missing-function-docstring
|
||||||
|
|
||||||
|
"""
|
||||||
|
みてるぞ式魔改造(言ふほどか?)版 Eloquent
|
||||||
|
"""
|
||||||
|
|
||||||
|
import eloquent
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseManager (eloquent.DatabaseManager):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Model (eloquent.Model):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
def upsert (
|
||||||
|
self,
|
||||||
|
*args: str,
|
||||||
|
) -> None:
|
||||||
|
q = self.query ()
|
||||||
|
for arg in args:
|
||||||
|
q = q.where (arg, getattr (self, arg))
|
||||||
|
row = q.first ()
|
||||||
|
if row is not None:
|
||||||
|
self.id = row.id
|
||||||
|
# pylint: disable = invalid-name
|
||||||
|
# pylint: disable = attribute-defined-outside-init
|
||||||
|
self._Model__exists = True
|
||||||
|
self.save ()
|
||||||
-113
@@ -1,113 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any, Generic, Type, TypeVar, overload
|
|
||||||
|
|
||||||
from eloquent.orm.relations.dynamic_property import DynamicProperty
|
|
||||||
|
|
||||||
_TModel = TypeVar ('_TModel', bound = 'Model')
|
|
||||||
|
|
||||||
|
|
||||||
class Connection:
|
|
||||||
def select (self, query: str, bindings: dict[str, Any] | None = None) -> Any: ...
|
|
||||||
|
|
||||||
def insert (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
|
||||||
|
|
||||||
def update (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
|
||||||
|
|
||||||
def delete (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
|
||||||
|
|
||||||
def transaction (self, callback: Any) -> Any: ...
|
|
||||||
|
|
||||||
def begin_transaction (self) -> None: ...
|
|
||||||
|
|
||||||
def commit (self) -> None: ...
|
|
||||||
|
|
||||||
def rollback (self) -> None: ...
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionResolver:
|
|
||||||
def connection (self, name: str | None = None) -> Any: ...
|
|
||||||
|
|
||||||
def get_default_connection (self) -> str: ...
|
|
||||||
|
|
||||||
def set_default_connection (self, name: str) -> None: ...
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseManager:
|
|
||||||
connections: dict[str, Connection]
|
|
||||||
|
|
||||||
def __init__ (self, config: dict[str, Any]) -> None: ...
|
|
||||||
|
|
||||||
def connection (self, name: str | None = None) -> Connection: ...
|
|
||||||
|
|
||||||
def disconnect (self, name: str | None = None) -> None: ...
|
|
||||||
|
|
||||||
def reconnect (self, name: str | None = None) -> Connection: ...
|
|
||||||
|
|
||||||
def get_connections (self) -> dict[str, Connection]: ...
|
|
||||||
|
|
||||||
|
|
||||||
class Model:
|
|
||||||
id: int
|
|
||||||
|
|
||||||
def has_one (
|
|
||||||
self,
|
|
||||||
related_model: Type[Model],
|
|
||||||
foreign_key: str | None = None,
|
|
||||||
) -> DynamicProperty: ...
|
|
||||||
|
|
||||||
def has_many (
|
|
||||||
self,
|
|
||||||
related_model: Type[Model],
|
|
||||||
foreign_key: str | None = None,
|
|
||||||
) -> DynamicProperty: ...
|
|
||||||
|
|
||||||
def belongs_to (
|
|
||||||
self,
|
|
||||||
related_model: Type[Model],
|
|
||||||
foreign_key: str | None = None,
|
|
||||||
) -> DynamicProperty: ...
|
|
||||||
|
|
||||||
def belongs_to_many (
|
|
||||||
self,
|
|
||||||
related_model: Type[Model],
|
|
||||||
foreign_key: str | None = None,
|
|
||||||
) -> DynamicProperty: ...
|
|
||||||
|
|
||||||
def save (self) -> None: ...
|
|
||||||
|
|
||||||
def delete (self) -> None: ...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find (cls, id: int) -> Model | None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
@classmethod
|
|
||||||
def where (cls, field: str, operator: str, value: Any) -> QueryBuilder: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
@classmethod
|
|
||||||
def where (cls, field: str, value: Any) -> QueryBuilder: ...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def where_not_in (cls, column: str, values: list[Any] | tuple) -> QueryBuilder: ...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def where_not_null (cls, field: str) -> QueryBuilder: ...
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_connection_resolver (cls, resolver: DatabaseManager) -> None: ...
|
|
||||||
|
|
||||||
|
|
||||||
class QueryBuilder (Generic[_TModel]):
|
|
||||||
def first (self) -> _TModel | None: ...
|
|
||||||
|
|
||||||
def get (self) -> list[_TModel]: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def where (self, field: str, operator: str, value: Any) -> QueryBuilder: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def where (self, field: str, value: Any) -> QueryBuilder: ...
|
|
||||||
|
|
||||||
def where_null (self, field: str) -> QueryBuilder: ...
|
|
||||||
シンボリックリンク
+1
@@ -0,0 +1 @@
|
|||||||
|
db/eloquent.pyi
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
from eloquent import Model
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicProperty (Model): ...
|
|
||||||
@@ -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])
|
||||||
@@ -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 ())
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
# pylint: disable = missing-class-docstring
|
||||||
|
# pylint: disable = missing-function-docstring
|
||||||
|
|
||||||
|
"""
|
||||||
|
全動画の情報を取得し,JSON 形式で出力する.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from eloquent import DatabaseManager, Model
|
||||||
|
|
||||||
|
from db.config import DB
|
||||||
|
from db.models import Video
|
||||||
|
|
||||||
|
DB
|
||||||
|
|
||||||
|
|
||||||
|
def main (
|
||||||
|
) -> None:
|
||||||
|
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': [],
|
||||||
|
'uploaded_at': row.uploaded_at,
|
||||||
|
'deleted_at': deleted_at }
|
||||||
|
for video_tag in row.video_tags:
|
||||||
|
if video_tag.untagged_at is None:
|
||||||
|
video['tags'].append (video_tag.tag.name)
|
||||||
|
videos.append(video)
|
||||||
|
|
||||||
|
print (json.dumps (videos, default = str))
|
||||||
|
|
||||||
|
|
||||||
|
class DbConfig (TypedDict):
|
||||||
|
driver: str
|
||||||
|
host: str
|
||||||
|
database: str
|
||||||
|
user: str
|
||||||
|
password: str
|
||||||
|
prefix: str
|
||||||
|
|
||||||
|
|
||||||
|
class VideoDict (TypedDict):
|
||||||
|
id: int
|
||||||
|
code: str
|
||||||
|
user: str | None
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
tags: list[str]
|
||||||
|
uploaded_at: datetime
|
||||||
|
deleted_at: date | None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main ()
|
||||||
@@ -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`);
|
||||||
@@ -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;
|
||||||
+71
-187
@@ -1,3 +1,6 @@
|
|||||||
|
# pylint: disable = missing-class-docstring
|
||||||
|
# pylint: disable = missing-function-docstring
|
||||||
|
|
||||||
"""
|
"""
|
||||||
日次で実行し,ぼざクリ DB を最新に更新する.
|
日次で実行し,ぼざクリ DB を最新に更新する.
|
||||||
"""
|
"""
|
||||||
@@ -10,30 +13,38 @@ import random
|
|||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from dataclasses import dataclass
|
from datetime import datetime, timedelta
|
||||||
from datetime import date, datetime, timedelta
|
from typing import Any, TypedDict, cast
|
||||||
from typing import Any, Type, TypedDict, cast
|
|
||||||
|
|
||||||
|
import jaconv
|
||||||
import requests
|
import requests
|
||||||
from eloquent import DatabaseManager, Model
|
from eloquent import DatabaseManager, Model
|
||||||
from eloquent.orm.relations.dynamic_property import DynamicProperty
|
|
||||||
|
|
||||||
config: dict[str, DbConfig] = { 'mysql': { 'driver': 'mysql',
|
from db.config import DB
|
||||||
'host': 'localhost',
|
from db.models import (Comment,
|
||||||
'database': 'nizika_nico',
|
Tag,
|
||||||
'user': os.environ['MYSQL_USER'],
|
TrackedVideo,
|
||||||
'password': os.environ['MYSQL_PASS'],
|
User,
|
||||||
'prefix': '' } }
|
Video,
|
||||||
db = DatabaseManager (config)
|
VideoHistory,
|
||||||
Model.set_connection_resolver (db)
|
VideoTag)
|
||||||
|
|
||||||
|
|
||||||
def main (
|
def main (
|
||||||
) -> None:
|
) -> None:
|
||||||
now = datetime.now ()
|
now = datetime.now ()
|
||||||
|
|
||||||
api_data = search_nico_by_tags (['伊地知ニジカ', 'ぼざろクリーチャーシリーズ'])
|
api_data = search_nico_by_tags (['伊地知ニジカ',
|
||||||
|
'ぼざろクリーチャーシリーズ',
|
||||||
|
'ぼざろクリーチャーシリーズ外伝'])
|
||||||
|
|
||||||
|
DB.begin_transaction ()
|
||||||
|
try:
|
||||||
update_tables (api_data, now)
|
update_tables (api_data, now)
|
||||||
|
DB.commit ()
|
||||||
|
except Exception:
|
||||||
|
DB.rollback ()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def update_tables (
|
def update_tables (
|
||||||
@@ -44,8 +55,16 @@ def update_tables (
|
|||||||
|
|
||||||
for datum in api_data:
|
for datum in api_data:
|
||||||
tag_names: list[str] = datum['tags'].split ()
|
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 = Video ()
|
||||||
video.code = datum['contentId']
|
video.code = datum['contentId']
|
||||||
|
video.user_id = user.id if user else None
|
||||||
video.title = datum['title']
|
video.title = datum['title']
|
||||||
video.description = datum['description'] or ''
|
video.description = datum['description'] or ''
|
||||||
video.uploaded_at = datetime.fromisoformat (datum['startTime'])
|
video.uploaded_at = datetime.fromisoformat (datum['startTime'])
|
||||||
@@ -56,8 +75,11 @@ def update_tables (
|
|||||||
video_history.video_id = video.id
|
video_history.video_id = video.id
|
||||||
video_history.fetched_at = now
|
video_history.fetched_at = now
|
||||||
video_history.views_count = datum['viewCounter']
|
video_history.views_count = datum['viewCounter']
|
||||||
video_history.save ()
|
video_history.upsert ()
|
||||||
video_tags = video.video_tags.where_not_null ('untagged_at').get ()
|
video_tags = [video_tag for video_tag in video.video_tags
|
||||||
|
if video_tag.untagged_at is None]
|
||||||
|
tag: Tag | None
|
||||||
|
video_tag: VideoTag | None
|
||||||
for video_tag in video_tags:
|
for video_tag in video_tags:
|
||||||
tag = video_tag.tag
|
tag = video_tag.tag
|
||||||
if (tag is not None
|
if (tag is not None
|
||||||
@@ -70,7 +92,7 @@ def update_tables (
|
|||||||
tag = Tag ()
|
tag = Tag ()
|
||||||
tag.name = tag_name
|
tag.name = tag_name
|
||||||
tag.save ()
|
tag.save ()
|
||||||
video_tag = (Video.where ('video_id', video.id)
|
video_tag = (VideoTag.where ('video_id', video.id)
|
||||||
.where ('tag_id', tag.id)
|
.where ('tag_id', tag.id)
|
||||||
.where_null ('untagged_at')
|
.where_null ('untagged_at')
|
||||||
.first ())
|
.first ())
|
||||||
@@ -107,9 +129,9 @@ def update_tables (
|
|||||||
video.save ()
|
video.save ()
|
||||||
|
|
||||||
|
|
||||||
def fetch_comments (
|
def fetch_video_data (
|
||||||
video_code: str,
|
video_code: str,
|
||||||
) -> list[CommentResult]:
|
) -> dict[str, Any]:
|
||||||
time.sleep (1.2)
|
time.sleep (1.2)
|
||||||
|
|
||||||
headers = { 'X-Frontend-Id': '6',
|
headers = { 'X-Frontend-Id': '6',
|
||||||
@@ -124,10 +146,14 @@ def fetch_comments (
|
|||||||
url = (f"https://www.nicovideo.jp/api/watch/v3_guest/{ video_code }"
|
url = (f"https://www.nicovideo.jp/api/watch/v3_guest/{ video_code }"
|
||||||
+ f"?actionTrackId={ action_track_id }")
|
+ 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:
|
try:
|
||||||
nv_comment = res['data']['comment']['nvComment']
|
nv_comment = fetch_video_data (video_code)['data']['comment']['nvComment']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return []
|
return []
|
||||||
if nv_comment is None:
|
if nv_comment is None:
|
||||||
@@ -154,12 +180,6 @@ def fetch_comments (
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def search_nico_by_tag (
|
|
||||||
tag: str,
|
|
||||||
) -> list[VideoResult]:
|
|
||||||
return search_nico_by_tags ([tag])
|
|
||||||
|
|
||||||
|
|
||||||
def search_nico_by_tags (
|
def search_nico_by_tags (
|
||||||
tags: list[str],
|
tags: list[str],
|
||||||
) -> list[VideoResult]:
|
) -> list[VideoResult]:
|
||||||
@@ -173,17 +193,21 @@ def search_nico_by_tags (
|
|||||||
while to <= today:
|
while to <= today:
|
||||||
time.sleep (1.2)
|
time.sleep (1.2)
|
||||||
until = to + timedelta (days = 14)
|
until = to + timedelta (days = 14)
|
||||||
|
# pylint: disable = consider-using-f-string
|
||||||
query_filter = json.dumps ({ 'type': 'or',
|
query_filter = json.dumps ({ 'type': 'or',
|
||||||
'filters': [
|
'filters': [
|
||||||
{ 'type': 'range',
|
{ 'type': 'range',
|
||||||
'field': 'startTime',
|
'field': 'startTime',
|
||||||
'from': '%04d-%02d-%02dT00:00:00+09:00' % (to.year, to.month, to.day),
|
'from': ('%04d-%02d-%02dT00:00:00+09:00'
|
||||||
'to': '%04d-%02d-%02dT23:59:59+09:00' % (until.year, until.month, until.day),
|
% (to.year, to.month, to.day)),
|
||||||
|
'to': ('%04d-%02d-%02dT23:59:59+09:00'
|
||||||
|
% (until.year, until.month, until.day)),
|
||||||
'include_lower': True }] })
|
'include_lower': True }] })
|
||||||
params: VideoSearchParam = { 'q': ' OR '.join (tags),
|
params: VideoSearchParam = { 'q': ' OR '.join (tags),
|
||||||
'targets': 'tagsExact',
|
'targets': 'tagsExact',
|
||||||
'_sort': '-viewCounter',
|
'_sort': '-viewCounter',
|
||||||
'fields': ('contentId,'
|
'fields': ('contentId,'
|
||||||
|
'userId,'
|
||||||
'title,'
|
'title,'
|
||||||
'tags,'
|
'tags,'
|
||||||
'description,'
|
'description,'
|
||||||
@@ -198,167 +222,26 @@ def search_nico_by_tags (
|
|||||||
pass
|
pass
|
||||||
to = until + timedelta (days = 1)
|
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
|
return result_data
|
||||||
|
|
||||||
|
|
||||||
class Comment (Model):
|
|
||||||
__timestamps__ = False
|
|
||||||
|
|
||||||
video_id: int
|
|
||||||
comment_no: int
|
|
||||||
user_id: int
|
|
||||||
content: str
|
|
||||||
posted_at: datetime
|
|
||||||
nico_count: int
|
|
||||||
vpos_ms: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.belongs_to (Video)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def user (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.belongs_to (User)
|
|
||||||
|
|
||||||
def upsert (
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
row = (Comment.where ('video_id', self.video_id)
|
|
||||||
.where ('comment_no', self.comment_no)
|
|
||||||
.first ())
|
|
||||||
if row is not None:
|
|
||||||
self.id = row.id
|
|
||||||
self.save ()
|
|
||||||
|
|
||||||
|
|
||||||
class Tag (Model):
|
|
||||||
__timestamps__ = False
|
|
||||||
|
|
||||||
name: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video_tags (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.has_many (VideoTag)
|
|
||||||
|
|
||||||
|
|
||||||
class User (Model):
|
|
||||||
__timestamps__ = False
|
|
||||||
|
|
||||||
code: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def comments (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.has_many (Comment)
|
|
||||||
|
|
||||||
|
|
||||||
class Video (Model):
|
|
||||||
__timestamps__ = False
|
|
||||||
|
|
||||||
code: str
|
|
||||||
title: str
|
|
||||||
description: str
|
|
||||||
uploaded_at: datetime
|
|
||||||
deleted_at: datetime | None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video_histories (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.has_many (VideoHistory)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video_tags (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.has_many (VideoTag)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def comments (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.has_many (Comment)
|
|
||||||
|
|
||||||
def upsert (
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
row = Video.where ('code', self.code).first ()
|
|
||||||
if row is not None:
|
|
||||||
self.id = row.id
|
|
||||||
self.save ()
|
|
||||||
|
|
||||||
|
|
||||||
class VideoHistory (Model):
|
|
||||||
__timestamps__ = False
|
|
||||||
|
|
||||||
video_id: int
|
|
||||||
fetched_at: date
|
|
||||||
views_count: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.belongs_to (Video)
|
|
||||||
|
|
||||||
def upsert (
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
row = (VideoHistory.where ('video_id', self.video_id)
|
|
||||||
.where ('fetched_at', self.fetched_at)
|
|
||||||
.first ())
|
|
||||||
if row is not None:
|
|
||||||
self.id = row.id
|
|
||||||
self.save ()
|
|
||||||
|
|
||||||
|
|
||||||
class VideoTag (Model):
|
|
||||||
__timestamps__ = False
|
|
||||||
|
|
||||||
video_id: int
|
|
||||||
tag_id: int
|
|
||||||
tagged_at: date
|
|
||||||
untagged_at: date | None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.belongs_to (Video)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tag (
|
|
||||||
self,
|
|
||||||
) -> DynamicProperty:
|
|
||||||
return self.belongs_to (Tag)
|
|
||||||
|
|
||||||
def upsert (
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
row = (VideoTag.where ('video_id', self.video_id)
|
|
||||||
.where ('tag_id', self.tag_id)
|
|
||||||
.first ())
|
|
||||||
if row is not None:
|
|
||||||
self.id = row.id
|
|
||||||
self.save ()
|
|
||||||
|
|
||||||
|
|
||||||
class DbConfig (TypedDict):
|
|
||||||
driver: str
|
|
||||||
host: str
|
|
||||||
database: str
|
|
||||||
user: str
|
|
||||||
password: str
|
|
||||||
prefix: str
|
|
||||||
|
|
||||||
|
|
||||||
class VideoSearchParam (TypedDict):
|
class VideoSearchParam (TypedDict):
|
||||||
q: str
|
q: str
|
||||||
targets: str
|
targets: str
|
||||||
@@ -370,6 +253,7 @@ class VideoSearchParam (TypedDict):
|
|||||||
|
|
||||||
class VideoResult (TypedDict):
|
class VideoResult (TypedDict):
|
||||||
contentId: str
|
contentId: str
|
||||||
|
userId: int | None
|
||||||
title: str
|
title: str
|
||||||
tags: str
|
tags: str
|
||||||
description: str | None
|
description: str | None
|
||||||
@@ -396,7 +280,7 @@ class CommentResult (TypedDict):
|
|||||||
def normalise (
|
def normalise (
|
||||||
s: str,
|
s: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
return unicodedata.normalize ('NFKC', s).lower ()
|
return jaconv.hira2kata (unicodedata.normalize ('NFKC', s)).lower ()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする