Skip to content

Commit

Permalink
バージョン (帰ってきた) に対応
Browse files Browse the repository at this point in the history
  • Loading branch information
okaits committed Aug 5, 2024
1 parent 104c0fe commit 4710f72
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 134 deletions.
2 changes: 1 addition & 1 deletion examples/get_user_uploaded.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def main() -> None:
parser.add_argument("userid", help="対象ユーザのID (数字)")
args = parser.parse_args()

for video in nicovideo.user.APIResponse(user_id=int(args.userid)).videolist:
for video in nicovideo.user.get_metadata(user_id=int(args.userid)).videolist:
print(str(object=video))
time.sleep(5)

Expand Down
12 changes: 7 additions & 5 deletions nicovideo/apirawdicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ class UserDetailsUserDetailsUser(typing.TypedDict):
strippedDescription: str
isPremium: bool
registeredVersion: str
foloweeCount: int
folowerCount: int
followeeCount: int
followerCount: int
userLevel: UserAPIRawDicts.UserDetailsUserDetailsUserUserLevel
sns: list[UserAPIRawDicts.UserDetailsUserDetailsUserSNS]
coverImage: UserAPIRawDicts.UserDetailsUserDetailsUserCoverImage
coverImage: typing.Optional[UserAPIRawDicts.UserDetailsUserDetailsUserCoverImage]
icons: UserAPIRawDicts.UserDetailsUserDetailsUserIcons
class UserDetailsUserDetails(typing.TypedDict):
user: UserAPIRawDicts.UserDetailsUserDetailsUser
Expand Down Expand Up @@ -125,8 +125,10 @@ class NVAPIBodyDataItems(typing.TypedDict):
class NVAPIBodyData(typing.TypedDict):
items: list[UserAPIRawDicts.NVAPIBodyDataItems]
class NVAPIBody(typing.TypedDict):
data: list[UserAPIRawDicts.NVAPIBodyData]
data: UserAPIRawDicts.NVAPIBodyData
class NVAPI(typing.TypedDict):
body: UserAPIRawDicts.NVAPIBody
class RawDict(typing.TypedDict):
"""RawDict TypedDict: User._rawdictに格納されるdictの型ヒント"""
state: UserAPIRawDicts.State
nvapi: UserAPIRawDicts.NVAPIBody
nvapi: list[UserAPIRawDicts.NVAPI]
131 changes: 72 additions & 59 deletions nicovideo/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@
このモジュールは、ニコニコのユーザを扱います。
"""

#pylint: disable=W0212
from __future__ import annotations

import typing
import urllib.error
import urllib.request
import collections.abc

import bs4
import json5
import bs4

from . import errors
from . import apirawdicts
from . import video

NICOVIDEO_USERPAGE_URL = "https://www.nicovideo.jp/user/{}/video"
NICOVIDEO_USERPAGE_URL = "https://www.nicovideo.jp/user/{}/video?responseType=json"

class APIResponse():
class _APIResponse():
"""
ユーザの詳細 (e.g. ニックネーム, 投稿動画, etc.) を格納するクラスです。
Expand All @@ -39,6 +41,7 @@ class APIResponse():
__slots__ = ("user_id", "nickname", "description", "subscription", "version", "followee",
"follower", "level", "exp", "sns", "cover", "icon", "_rawdict")
user_id: int
_rawdict: apirawdicts.UserAPIRawDicts.RawDict
nickname: str
description: tuple[typing.Annotated[str, "HTML"], typing.Annotated[str, "Plain"]]
subscription: typing.Literal["premium", "general"]
Expand All @@ -63,60 +66,6 @@ class APIResponse():
]
icon: tuple[typing.Annotated[str, "小アイコン画像のURL"], typing.Annotated[str, "大アイコン画像のURL"]]

def __init__(self, user_id: int) -> None:
"""
ニコニコのAPIサーバからユーザ情報を取得します。
Args:
user_id (int): 対象となるユーザの、ニコニコ動画でのID (e.g. 9003560)
Raises:
errors.ContentNotFoundError: 指定された動画が存在しなかった場合に送出。
errors.APIRequestError: ニコニコのAPIサーバへのリクエストに失敗した場合に送出。
Example:
>>> APIResponse(9003560)
"""
try:
with urllib.request.urlopen(url=NICOVIDEO_USERPAGE_URL.format(user_id)) as res:
response_text = res.read()
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise errors.ContentNotFoundError from exc
raise errors.APIRequestError from exc
except urllib.error.URLError as exc:
raise errors.APIRequestError from exc

soup = bs4.BeautifulSoup(markup=response_text, features="html.parser")
self._rawdict: apirawdicts.UserAPIRawDicts.RawDict
super().__setattr__("_rawdict", json5.loads(
str(object=soup.select("#js-initial-userpage-data")[0]["data-initial-data"])
))
if self._rawdict is None:
raise errors.APIRequestError("Invalid response from server.")
rawdict_userdata = self._rawdict["state"]["userDetails"]["userDetails"]["user"]
self.nickname: str
super().__setattr__("nickname", rawdict_userdata["nickname"])
super().__setattr__("description", (rawdict_userdata["decoratedDescriptionHtml"],
rawdict_userdata["strippedDescription"]))
super().__setattr__("subscription",
"premium" if rawdict_userdata["isPremium"] else "general")
super().__setattr__("version", rawdict_userdata["registeredVersion"])
super().__setattr__("followee", rawdict_userdata["foloweeCount"])
super().__setattr__("follower", rawdict_userdata["folowerCount"])
super().__setattr__("level", rawdict_userdata["userLevel"]["currentLevel"])
super().__setattr__("exp", rawdict_userdata["userLevel"]["currentLevelExperience"])
super().__setattr__("sns", frozenset(
[(sns["type"], sns["label"], sns["iconUrl"]) for sns in rawdict_userdata["sns"]]
))
super().__setattr__("cover", (
rawdict_userdata["coverImage"]["pcUrl"],
rawdict_userdata["coverImage"]["ogpUrl"],
rawdict_userdata["coverImage"]["smartphoneUrl"]
))
super().__setattr__("icon", (
rawdict_userdata["icons"]["small"],
rawdict_userdata["icons"]["large"]
))

@property
def videolist(self) -> collections.abc.Generator[video.APIResponse, None, None]:
"""
Expand All @@ -126,9 +75,9 @@ def videolist(self) -> collections.abc.Generator[video.APIResponse, None, None]:
Yields:
video.APIResponse: ユーザの投稿動画
"""
rawdict_videolist = self._rawdict["nvapi"]["data"][0]["items"]
rawdict_videolist = self._rawdict["nvapi"][0]["body"]["data"]["items"]
for rawdict_video in rawdict_videolist:
yield video.APIResponse(rawdict_video["essential"]["id"])
yield video.get_metadata(rawdict_video["essential"]["id"])

def __setattr__(self, _, name) -> typing.NoReturn:
raise errors.FrozenInstanceError(f"cannot assign to field '{name}'")
Expand All @@ -140,3 +89,67 @@ def __str__(self) -> str:
return self.nickname
def __hash__(self) -> int:
return self.user_id

APIResponse = typing.NewType("APIResponse", _APIResponse)

def get_metadata(user_id: int) -> APIResponse:
"""
ニコニコのAPIサーバからユーザ情報を取得します。
Args:
user_id (int): 対象となるユーザの、ニコニコ動画でのID (e.g. 9003560)
Raises:
errors.ContentNotFoundError: 指定された動画が存在しなかった場合に送出。
errors.APIRequestError: ニコニコのAPIサーバへのリクエストに失敗した場合に送出。
Example:
>>> get_metadata(9003560)
"""
gotapiresponse = _APIResponse()
try:
with urllib.request.urlopen(url=NICOVIDEO_USERPAGE_URL.format(user_id)) as res:
response_text = res.read()
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise errors.ContentNotFoundError from exc
raise errors.APIRequestError from exc
except urllib.error.URLError as exc:
raise errors.APIRequestError from exc

soup = bs4.BeautifulSoup(markup=response_text, features="html.parser")
object.__setattr__(gotapiresponse, "_rawdict", json5.loads(
str(object=soup.select("#js-initial-userpage-data")[0]["data-initial-data"])
))

if gotapiresponse._rawdict is None:
raise errors.APIRequestError("Invalid response from server.")
rawdict_userdata = gotapiresponse._rawdict["state"]["userDetails"]["userDetails"]["user"]
object.__setattr__(gotapiresponse, "user_id", user_id)
object.__setattr__(gotapiresponse, "nickname", rawdict_userdata["nickname"])
object.__setattr__(gotapiresponse, "description",
(rawdict_userdata["decoratedDescriptionHtml"],
rawdict_userdata["strippedDescription"])
)
object.__setattr__(gotapiresponse, "subscription",
"premium" if rawdict_userdata["isPremium"] else "general")
object.__setattr__(gotapiresponse, "version", rawdict_userdata["registeredVersion"])
object.__setattr__(gotapiresponse, "followee", rawdict_userdata["followeeCount"])
object.__setattr__(gotapiresponse, "follower", rawdict_userdata["followerCount"])
object.__setattr__(gotapiresponse, "level", rawdict_userdata["userLevel"]["currentLevel"])
object.__setattr__(gotapiresponse, "exp",
rawdict_userdata["userLevel"]["currentLevelExperience"])
object.__setattr__(gotapiresponse, "sns", frozenset(
[(sns["type"], sns["label"], sns["iconUrl"]) for sns in rawdict_userdata["sns"]]
))
if rawdict_userdata["coverImage"]:
object.__setattr__(gotapiresponse, "cover", (
rawdict_userdata["coverImage"]["pcUrl"],
rawdict_userdata["coverImage"]["ogpUrl"],
rawdict_userdata["coverImage"]["smartphoneUrl"]
))
else:
object.__setattr__(gotapiresponse, "cover", None)
object.__setattr__(gotapiresponse, "icon", (
rawdict_userdata["icons"]["small"],
rawdict_userdata["icons"]["large"]
))
return APIResponse(gotapiresponse)
141 changes: 74 additions & 67 deletions nicovideo/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
このモジュールは、ニコニコの動画を取り扱います。
"""

#pylint: disable=W0212
from __future__ import annotations

import datetime
Expand All @@ -10,14 +12,13 @@
import urllib.error
import urllib.request

import bs4
import json5

from . import apirawdicts, errors, user

NICOVIDEO_VIDEOPAGE_URL = "https://www.nicovideo.jp/watch/{}"
NICOVIDEO_VIDEOPAGE_URL = "https://www.nicovideo.jp/watch/{}?responseType=json"

class APIResponse():
class _APIResponse():
"""
動画の詳細(e.g. タイトル, 概要, etc.)を格納するクラスです。
Expand All @@ -30,10 +31,10 @@ class APIResponse():
upload_date (datetime.datetime): 動画の投稿時間
thumbnail (dict[typing.Literal["large", "middle", "ogp", "player", "small"], str]): サムネイル
counters (dict[typing.Literal["comment", "like", "mylist", "view"], str]): 各種カウンタ
genre (dict[typing.Literal["label", "key"], str]): 動画ジャンル
genre (typing.Optional[dict[typing.Literal["label", "key"], str]]): 動画ジャンル
"""
__slots__ = ("nicovideo_id", "title", "update", "description",
"duration", "upload_date", "_rawdict")
__slots__ = ("nicovideo_id", "title", "update", "description", "genre",
"duration", "upload_date", "thumbnails", "_rawdict", "counters")
nicovideo_id: str
_rawdict: apirawdicts.VideoAPIRawDicts.RawDict
title: str
Expand All @@ -43,71 +44,16 @@ class APIResponse():
upload_date: datetime.datetime
thumbnails: dict[typing.Literal["large", "middle", "ogp", "player", "small"], str]
counters: dict[typing.Literal["comment", "like", "mylist", "view"], str]
genre: dict[typing.Literal["label", "key"], str]

def __init__(self, video_id: str):
"""
ニコニコのAPIサーバから動画情報を取得します。
Args:
video_id (str): 対象となる動画の、ニコニコ動画での動画ID (e.g. sm9)
Raises:
errors.ContentNotFoundError: 指定された動画が存在しなかった場合に送出。
errors.APIRequestError: ニコニコのAPIサーバへのリクエストに失敗した場合に送出。
Example:
>>> APIResponse("sm9")
"""
super().__setattr__("nicovideo_id", video_id)

try:
with urllib.request.urlopen(url=NICOVIDEO_VIDEOPAGE_URL.format(video_id)) as res:
response_text = res.read()
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise errors.ContentNotFoundError from exc
raise errors.APIRequestError from exc
except urllib.error.URLError as exc:
raise errors.APIRequestError from exc

soup = bs4.BeautifulSoup(markup=response_text, features="html.parser")
super().__setattr__("_rawdict", json5.loads(
str(object=soup.select(selector="#js-initial-watch-data")[0]["data-api-data"])
))
if self._rawdict is None:
raise errors.APIRequestError("Invalid response from server.")

super().__setattr__("title", self._rawdict["video"]["title"])
super().__setattr__("update", datetime.datetime.now())
super().__setattr__("description", self._rawdict["video"]["description"])
super().__setattr__("duration", self._rawdict["video"]["duration"])
super().__setattr__("upload_date", datetime.datetime.fromisoformat(
self._rawdict["video"]["registeredAt"]
))
super().__setattr__("thumbnail", {
"large": self._rawdict["video"]["thumbnail"]["largeUrl"],
"middle": self._rawdict["video"]["thumbnail"]["middleUrl"],
"ogp": self._rawdict["video"]["thumbnail"]["ogp"],
"player": self._rawdict["video"]["thumbnail"]["player"],
"small": self._rawdict["video"]["thumbnail"]["url"]
})
super().__setattr__("counters", {
"comment": self._rawdict["video"]["count"]["comment"],
"like": self._rawdict["video"]["count"]["like"],
"mylist": self._rawdict["video"]["count"]["mylist"],
"view": self._rawdict["video"]["count"]["view"]
})
super().__setattr__("genre", {
"label": self._rawdict["genre"]["label"],
"key": self._rawdict["genre"]["key"]
})
genre: typing.Optional[dict[typing.Literal["label", "key"], str]]

@property
def uploader(self) -> user.APIResponse:
def uploader(self) -> user._APIResponse:
"""動画の投稿者を取得する。"""
return user.APIResponse(user_id=int(self._rawdict["owner"]["id"]))
return user.get_metadata(user_id=int(self._rawdict["owner"]["id"]))

@functools.cached_property
def cached_uploader(self) -> user.APIResponse:
@property
@functools.cache
def cached_uploader(self) -> user._APIResponse:
"""動画の投稿者を取得する。(初回にキャッシュするので最新ではない可能性がある。)"""
return self.uploader

Expand All @@ -123,3 +69,64 @@ def __hash__(self) -> int:
return int("".join(
[str(object=ord(character)) for character in self.nicovideo_id]
))

APIResponse = typing.NewType("APIResponse", _APIResponse)

def get_metadata(video_id: str) -> APIResponse:
"""
ニコニコのAPIサーバから動画情報を取得します。
Args:
video_id (str): 対象となる動画の、ニコニコ動画での動画ID (e.g. sm9)
Raises:
errors.ContentNotFoundError: 指定された動画が存在しなかった場合に送出。
errors.APIRequestError: ニコニコのAPIサーバへのリクエストに失敗した場合に送出。
Example:
>>> get_metadata("sm9")
"""
gotapiresponse = _APIResponse()
object.__setattr__(gotapiresponse, "nicovideo_id", video_id)

try:
with urllib.request.urlopen(url=NICOVIDEO_VIDEOPAGE_URL.format(video_id)) as res:
response_text = res.read()
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise errors.ContentNotFoundError from exc
raise errors.APIRequestError from exc
except urllib.error.URLError as exc:
raise errors.APIRequestError from exc

object.__setattr__(gotapiresponse, "_rawdict", json5.loads(response_text)["data"]["response"])
if gotapiresponse._rawdict is None:
raise errors.APIRequestError("Invalid response from server.")

object.__setattr__(gotapiresponse, "title", gotapiresponse._rawdict["video"]["title"])
object.__setattr__(gotapiresponse, "update", datetime.datetime.now())
object.__setattr__(gotapiresponse, "description",
gotapiresponse._rawdict["video"]["description"])
object.__setattr__(gotapiresponse, "duration", gotapiresponse._rawdict["video"]["duration"])
object.__setattr__(gotapiresponse, "upload_date", datetime.datetime.fromisoformat(
gotapiresponse._rawdict["video"]["registeredAt"]
))
object.__setattr__(gotapiresponse, "thumbnails", {
"large": gotapiresponse._rawdict["video"]["thumbnail"]["largeUrl"],
"middle": gotapiresponse._rawdict["video"]["thumbnail"]["middleUrl"],
"ogp": gotapiresponse._rawdict["video"]["thumbnail"]["ogp"],
"player": gotapiresponse._rawdict["video"]["thumbnail"]["player"],
"small": gotapiresponse._rawdict["video"]["thumbnail"]["url"]
})
object.__setattr__(gotapiresponse, "counters", {
"comment": gotapiresponse._rawdict["video"]["count"]["comment"],
"like": gotapiresponse._rawdict["video"]["count"]["like"],
"mylist": gotapiresponse._rawdict["video"]["count"]["mylist"],
"view": gotapiresponse._rawdict["video"]["count"]["view"]
})
if gotapiresponse._rawdict["genre"]:
object.__setattr__(gotapiresponse, "genre", {
"label": gotapiresponse._rawdict["genre"]["label"],
"key": gotapiresponse._rawdict["genre"]["key"]
})
else:
object.__setattr__(gotapiresponse, "genre", None)
return APIResponse(gotapiresponse)
Loading

0 comments on commit 4710f72

Please sign in to comment.