diff --git a/README.md b/README.md index 0b78dff..778d2ba 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,19 @@ bilifm season $uid $sid [OPTIONS] - Options: - -o, --directory 选择音频保存地址 +### series 模式 +```bash +bilifm season $uid $sid [OPTIONS] +``` +- uid, sid 的获取: + 打开用户空间中的合集和列表, 找到列表点击更多, 然后从URL中获取 +https://space.bilibili.com/488978908/channel/seriesdetail?sid=888434&ctype=0 + +例如上面链接, uid为488978908, sid为888434. 使用下面命令 +```bash +bilifm series 488978908 888434 +``` ## Features diff --git a/src/bilifm/command.py b/src/bilifm/command.py index 76827cb..c1cbf6f 100644 --- a/src/bilifm/command.py +++ b/src/bilifm/command.py @@ -5,6 +5,7 @@ from .audio import Audio from .fav import Fav from .season import Season +from .series import Series from .user import User from .util import Directory, Path @@ -63,3 +64,22 @@ def season(uid: str, sid: str, directory: Directory = None): audio = Audio(id) audio.download() typer.echo("Download complete") + + + +@app.command() +def series(uid: str, sid: str, directory: Directory = None): + """ download bilibili video series + because the api of series lacks the series name, executing + this command will not create a folder for the series + """ + ser = Series(uid, sid) + ret = ser.get_videos() + if not ret: + typer.Exit(1) + return + + for id in ser.videos: + audio = Audio(id) + audio.download() + typer.echo("Download complete") diff --git a/src/bilifm/series.py b/src/bilifm/series.py new file mode 100644 index 0000000..bb488bb --- /dev/null +++ b/src/bilifm/series.py @@ -0,0 +1,77 @@ +"""download bilibili video series, 视频列表""" + +import typer +from .util import request + +headers: dict[str, str] = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + "Referer": "https://www.bilibili.com", +} + + +class Series: + series_url: str = "https://api.bilibili.com/x/series/archives" + retry: int = 3 + + def __init__(self, uid: str, series_id: str, page_size=30) -> None: + self.uid = uid + self.series_id = series_id + self.page_size = page_size + self.videos = [] + self.total = 0 + + def get_videos(self): + params = { + "mid": self.uid, + "series_id": self.series_id, + "pn": 1, + "ps": self.page_size, + "current_id": self.uid, + } + + def wrapped_request(): + """wrap request with retry""" + for _ in range(self.retry): + res = request( + method="get", url=self.series_url, params=params, headers=headers + ).json() + if self.__response_succeed(res): + return res + self.__handle_error_response(res) + return None + + res = wrapped_request() + if res is None: + return False + + self.total = res["data"]["page"]["total"] + + for i in range(1, self.total // self.page_size + 2): + params["pn"] = i + res = wrapped_request() + if res: + bvids = [ar["bvid"] for ar in res["data"]["archives"]] + self.videos.extend(bvids) + else: + typer.echo( + f"skip audios from {(i-1)* self.page_size} to {i * self.page_size}" + ) + + return True + + def __handle_error_response(self, response): + try: + archives = response["data"]["archives"] + except KeyError: + archives = 0 # something null not none + if archives is None: + typer.echo(f"Error: uid {self.uid} or sid {self.series_id} error.") + else: + typer.echo("Error: Unknown problem.") + typer.echo(f"resp: {response}") + + def __response_succeed(self, response) -> bool: + try: + return response["data"]["archives"] is not None + except KeyError: + return False