Merge pull request #6 from OverflowCat/i18n

feat(i18n): add `en` locale
This commit is contained in:
yzqzss 2023-08-17 04:42:03 +08:00 committed by GitHub
commit 67c4ef28db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1451 additions and 350 deletions

View File

@ -26,11 +26,15 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt install gettext
python -m pip install --upgrade pip
python -m pip install poetry ruff pytest
python -m pip install poetry pytest
# if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
poetry install
pip install -e .
- name: build
run: |
python build.py
- name: Lint with ruff
uses: chartboost/ruff-action@v1
with:

2
.gitignore vendored
View File

@ -4,6 +4,6 @@ config.json
.venv/
__pycache__/
videos/
.vscode/
.vscode/settings.json
bilibili_archive_dir/
dist/

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"charliermarsh.ruff",
"tamasfe.even-better-toml",
"esbenp.prettier-vscode"
]
}

View File

@ -10,4 +10,79 @@ pip install biliarchiver
## Usage
待补充。
```bash
biliarchiver --help
```
Follow these steps to start archiving:
1. Initialize a new workspace in current working directory:
```bash
biliarchiver init
```
2. Provide cookies and tokens following instructions:
```bash
biliarchiver auth
```
3. Download videos from BiliBili:
```bash
biliarchiver down --bvids BVXXXXXXXXX
```
- This command also accepts a list of BVIDs or path to a file. Details can be found in `biliarchiver down --help`.
4. Upload videos to Internet Archive:
```bash
biliarchiver up --bvids BVXXXXXXXXX
```
- This command also accepts a list of BVIDs or path to a file. Details can be found in `biliarchiver up --help`.
## Develop
### Install
Please use poetry to install dependencies:
```sh
poetry install
```
Build English locale if necessary. Refer to the last section for details.
### Run
```sh
poetry run biliarchiver --help
```
### Lint
```sh
poetry run ruff check .
```
### i18n
To generate and build locales, you need a GNU gettext compatible toolchain. You can install `mingw` and use `sh` to enter a bash shell on Windows.
Generate `biliarchiver.pot`:
```sh
find biliarchiver/ -name '*.py' | xargs xgettext -d base -o biliarchiver/locales/biliarchiver.pot
```
Add a new language:
```sh
msginit -i biliarchiver/locales/biliarchiver.pot -o en.po -l en
```
Update a language:
```sh
pnpx gpt-po sync --po biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po --pot biliarchiver/locales/biliarchiver.pot
```
**(Important)** Build a language:
```sh
msgfmt biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po -o biliarchiver/locales/en/LC_MESSAGES/biliarchiver.mo
```

View File

@ -7,156 +7,189 @@ from urllib.parse import urlparse
from internetarchive import get_item
from requests import Response
from rich import print
from biliarchiver.exception import VideosBasePathNotFoundError, VideosNotFinishedDownloadError
from biliarchiver.exception import (
VideosBasePathNotFoundError,
VideosNotFinishedDownloadError,
)
from biliarchiver.utils.identifier import human_readable_upper_part_map
from biliarchiver.config import BILIBILI_IDENTIFIER_PERFIX, config
from biliarchiver.utils.dirLock import UploadLock, AlreadyRunningError
from biliarchiver.utils.xml_chars import xml_chars_legalize
from biliarchiver.version import BILI_ARCHIVER_VERSION
from biliarchiver.i18n import _
def upload_bvid(bvid: str, *, update_existing: bool = False, collection: str):
try:
lock_dir = config.storage_home_dir / '.locks' / bvid
lock_dir = config.storage_home_dir / ".locks" / bvid
os.makedirs(lock_dir, exist_ok=True)
with UploadLock(lock_dir): # type: ignore
with UploadLock(lock_dir): # type: ignore
_upload_bvid(bvid, update_existing=update_existing, collection=collection)
except AlreadyRunningError:
print(f'已经有一个上传 {bvid} 的进程在运行,跳过')
print(_("已经有一个上传 {} 的进程在运行,跳过".format(bvid)))
except VideosBasePathNotFoundError:
print(f'没有找到 {bvid} 对应的文件夹。可能是因已存在 IA item 而跳过了下载,或者你传入了错误的 bvid')
print(_("没有找到 {} 对应的文件夹。可能是因已存在 IA item 而跳过了下载,或者你传入了错误的 bvid".format(bvid)))
except VideosNotFinishedDownloadError:
print(f'{bvid} 的视频还没有下载完成,跳过')
print(_("{} 的视频还没有下载完成,跳过".format(bvid)))
except Exception as e:
print(f'上传 {bvid} 时出错:')
print(_("上传 {} 时出错:".format(bvid)))
raise e
def _upload_bvid(bvid: str, *, update_existing: bool = False, collection: str):
access_key, secret_key = read_ia_keys(config.ia_key_file)
# identifier format: BiliBili-{bvid}_p{pid}-{upper_part}
# identifier format: BiliBili-{bvid}_p{pid}-{upper_part}
upper_part = human_readable_upper_part_map(string=bvid, backward=True)
OLD_videos_basepath: Path = config.storage_home_dir / 'videos' / bvid
videos_basepath: Path = config.storage_home_dir / 'videos' / f'{bvid}-{upper_part}'
OLD_videos_basepath: Path = config.storage_home_dir / "videos" / bvid
videos_basepath: Path = config.storage_home_dir / "videos" / f"{bvid}-{upper_part}"
if os.path.exists(OLD_videos_basepath):
print(f'检测到旧的视频主目录 {OLD_videos_basepath},将其重命名为 {videos_basepath}...')
print(f"检测到旧的视频主目录 {OLD_videos_basepath},将其重命名为 {videos_basepath}...")
os.rename(OLD_videos_basepath, videos_basepath)
if not os.path.exists(videos_basepath):
raise VideosBasePathNotFoundError(f'{videos_basepath}')
raise VideosBasePathNotFoundError(f"{videos_basepath}")
if not (videos_basepath / "_all_downloaded.mark").exists():
raise VideosNotFinishedDownloadError(f'{videos_basepath}')
raise VideosNotFinishedDownloadError(f"{videos_basepath}")
for local_identifier in os.listdir(videos_basepath):
remote_identifier = f'{local_identifier}-{upper_part}'
if os.path.exists(f'{videos_basepath}/{local_identifier}/_uploaded.mark') and not update_existing:
print(f'{local_identifier} => {remote_identifier} 已经上传过了(_uploaded.mark)')
remote_identifier = f"{local_identifier}-{upper_part}"
if (
os.path.exists(f"{videos_basepath}/{local_identifier}/_uploaded.mark")
and not update_existing
):
print(
_("{} => {} 已经上传过了(_uploaded.mark)").format(
local_identifier, remote_identifier
)
)
continue
if local_identifier.startswith('_') :
print(f'跳过带 _ 前缀的 local_identifier: {local_identifier}')
if local_identifier.startswith("_"):
print(_("跳过带 _ 前缀的 local_identifier: {}").format(local_identifier))
continue
if not local_identifier.startswith(BILIBILI_IDENTIFIER_PERFIX):
print(f'{local_identifier} 不是以 {BILIBILI_IDENTIFIER_PERFIX} 开头的正确 local_identifier')
print(
_("{} 不是以 {} 开头的正确 local_identifier").format(
local_identifier, BILIBILI_IDENTIFIER_PERFIX
)
)
continue
if not os.path.exists(f'{videos_basepath}/{local_identifier}/_downloaded.mark'):
print(f'{local_identifier} 没有下载完成')
if not os.path.exists(f"{videos_basepath}/{local_identifier}/_downloaded.mark"):
print(f"{local_identifier} " + _("没有下载完成"))
continue
pid = local_identifier.split('_')[-1][1:]
file_basename = local_identifier[len(BILIBILI_IDENTIFIER_PERFIX)+1:]
pid = local_identifier.split("_")[-1][1:]
file_basename = local_identifier[len(BILIBILI_IDENTIFIER_PERFIX) + 1 :]
print(f'=== 开始上传 {local_identifier} => {remote_identifier} ===')
print(f"=== {_('开始上传')} {local_identifier} => {remote_identifier} ===")
item = get_item(remote_identifier)
if item.exists and not update_existing:
print(f'item {remote_identifier} 已存在(item.exists)')
print(f"item {remote_identifier} {_('已存在')} (item.exists)")
if item.metadata.get("upload-state") == "uploaded":
print(f'{remote_identifier} 已经上传过了,跳过(item.metadata.uploaded)')
with open(f'{videos_basepath}/{local_identifier}/_uploaded.mark', 'w', encoding='utf-8') as f:
f.write('')
print(f"{remote_identifier} {_('已经上传过了,跳过')} (item.metadata.uploaded)")
with open(
f"{videos_basepath}/{local_identifier}/_uploaded.mark",
"w",
encoding="utf-8",
) as f:
f.write("")
continue
with open(f'{videos_basepath}/{local_identifier}/extra/{file_basename}.info.json', 'r', encoding='utf-8') as f:
with open(
f"{videos_basepath}/{local_identifier}/extra/{file_basename}.info.json",
"r",
encoding="utf-8",
) as f:
bv_info = json.load(f)
cover_url: str = bv_info['data']['View']['pic']
cover_url: str = bv_info["data"]["View"]["pic"]
cover_suffix = PurePath(urlparse(cover_url).path).suffix
filedict = {} # "remote filename": "local filename"
for filename in os.listdir(f'{videos_basepath}/{local_identifier}/extra'):
file = f'{videos_basepath}/{local_identifier}/extra/{filename}'
filedict = {} # "remote filename": "local filename"
for filename in os.listdir(f"{videos_basepath}/{local_identifier}/extra"):
file = f"{videos_basepath}/{local_identifier}/extra/{filename}"
if os.path.isfile(file):
if file.startswith('_'):
if file.startswith("_"):
continue
filedict[filename] = file
# 复制一份 cover 作为 itemimage
if filename == f'{bvid}_p{pid}{cover_suffix}':
filedict[f'{bvid}_p{pid}_itemimage{cover_suffix}'] = file
for filename in os.listdir(f'{videos_basepath}/{local_identifier}'):
file = f'{videos_basepath}/{local_identifier}/{filename}'
# 复制一份 cover 作为 itemimage
if filename == f"{bvid}_p{pid}{cover_suffix}":
filedict[f"{bvid}_p{pid}_itemimage{cover_suffix}"] = file
for filename in os.listdir(f"{videos_basepath}/{local_identifier}"):
file = f"{videos_basepath}/{local_identifier}/{filename}"
if os.path.isfile(file):
if os.path.basename(file).startswith('_'):
if os.path.basename(file).startswith("_"):
continue
if not os.path.isfile(file):
continue
continue
filedict[filename] = file
assert (f'{file_basename}.mp4' in filedict) or (f'{file_basename}.flv' in filedict)
assert (f"{file_basename}.mp4" in filedict) or (
f"{file_basename}.flv" in filedict
)
# IA 去重
for file_in_item in item.files:
if file_in_item["name"] in filedict:
filedict.pop(file_in_item["name"])
print(f"File {file_in_item['name']} already exists in {remote_identifier}.")
print(
f"File {file_in_item['name']} already exists in {remote_identifier}."
)
# with open(f'{videos_basepath}/_videos_info.json', 'r', encoding='utf-8') as f:
# videos_info = json.load(f)
tags = ['BiliBili', 'video']
for tag in bv_info['data']['Tags']:
tags.append(tag['tag_name'])
pubdate = bv_info['data']['View']['pubdate']
tags = ["BiliBili", "video"]
for tag in bv_info["data"]["Tags"]:
tags.append(tag["tag_name"])
pubdate = bv_info["data"]["View"]["pubdate"]
cid = None
p_part = None
for page in bv_info['data']['View']['pages']:
if page['page'] == int(pid):
cid = page['cid']
p_part = page['part']
for page in bv_info["data"]["View"]["pages"]:
if page["page"] == int(pid):
cid = page["cid"]
p_part = page["part"]
break
assert cid is not None
assert p_part is not None
aid = bv_info['data']['View']['aid']
owner_mid = bv_info['data']['View']['owner']['mid']
owner_creator: str = bv_info['data']['View']['owner']['name'] # UP 主
aid = bv_info["data"]["View"]["aid"]
owner_mid = bv_info["data"]["View"]["owner"]["mid"]
owner_creator: str = bv_info["data"]["View"]["owner"]["name"] # UP 主
mids: List[int] = [owner_mid]
creators: List[str] = [owner_creator]
if bv_info['data']['View'].get('staff') is not None:
mids = [] # owner_mid 在 staff 也有
if bv_info["data"]["View"].get("staff") is not None:
mids = [] # owner_mid 在 staff 也有
creators = []
for staff in bv_info['data']['View']['staff']:
mids.append(staff['mid']) if staff['mid'] not in mids else None
creators.append(staff['name']) if staff['name'] not in creators else None
external_identifier = [f"urn:bilibili:video:aid:{aid}",
f"urn:bilibili:video:bvid:{bvid}",
f"urn:bilibili:video:cid:{cid}"]
for staff in bv_info["data"]["View"]["staff"]:
mids.append(staff["mid"]) if staff["mid"] not in mids else None
creators.append(staff["name"]) if staff[
"name"
] not in creators else None
external_identifier = [
f"urn:bilibili:video:aid:{aid}",
f"urn:bilibili:video:bvid:{bvid}",
f"urn:bilibili:video:cid:{cid}",
]
for mid in mids:
external_identifier.append(f"urn:bilibili:video:mid:{mid}")
md = {
"mediatype": "movies",
"collection": collection,
"title": bv_info['data']['View']['title'] + f' P{pid} ' + p_part ,
"description": remote_identifier + ' uploading...',
'creator': creators if len(creators) > 1 else owner_creator, # type: list[str] | str
"title": bv_info["data"]["View"]["title"] + f" P{pid} " + p_part,
"description": remote_identifier + " uploading...",
"creator": creators
if len(creators) > 1
else owner_creator, # type: list[str] | str
# UTC time
'date': time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(pubdate)),
'year': time.strftime("%Y", time.gmtime(pubdate)),
"date": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(pubdate)),
"year": time.strftime("%Y", time.gmtime(pubdate)),
# 'aid': aid,
# 'bvid': bvid,
# 'cid': cid,
@ -166,8 +199,8 @@ def _upload_bvid(bvid: str, *, update_existing: bool = False, collection: str):
tags
), # Keywords should be separated by ; but it doesn't matter much; the alternative is to set one per field with subject[0], subject[1], ...
"upload-state": "uploading",
'originalurl': f'https://www.bilibili.com/video/{bvid}/?p={pid}',
'scanner': f'biliarchiver v{BILI_ARCHIVER_VERSION} (dev)',
"originalurl": f"https://www.bilibili.com/video/{bvid}/?p={pid}",
"scanner": f"biliarchiver v{BILI_ARCHIVER_VERSION} (dev)",
}
print(filedict)
@ -193,9 +226,9 @@ def _upload_bvid(bvid: str, *, update_existing: bool = False, collection: str):
)
tries = 100
item = get_item(remote_identifier) # refresh item
item = get_item(remote_identifier) # refresh item
while not item.exists and tries > 0:
print(f"Waiting for item to be created ({tries}) ...", end='\r')
print(f"Waiting for item to be created ({tries}) ...", end="\r")
time.sleep(30)
item = get_item(remote_identifier)
tries -= 1
@ -203,14 +236,14 @@ def _upload_bvid(bvid: str, *, update_existing: bool = False, collection: str):
new_md = {}
if item.metadata.get("upload-state") != "uploaded":
new_md["upload-state"] = "uploaded"
if item.metadata.get("creator") != md['creator']:
new_md["creator"] = md['creator']
if item.metadata.get("description", "") != bv_info['data']['View']['desc']:
new_md["description"] = bv_info['data']['View']['desc']
if item.metadata.get("scanner") != md['scanner']:
new_md["scanner"] = md['scanner']
if item.metadata.get("external-identifier") != md['external-identifier']:
new_md["external-identifier"] = md['external-identifier']
if item.metadata.get("creator") != md["creator"]:
new_md["creator"] = md["creator"]
if item.metadata.get("description", "") != bv_info["data"]["View"]["desc"]:
new_md["description"] = bv_info["data"]["View"]["desc"]
if item.metadata.get("scanner") != md["scanner"]:
new_md["scanner"] = md["scanner"]
if item.metadata.get("external-identifier") != md["external-identifier"]:
new_md["external-identifier"] = md["external-identifier"]
if new_md:
print("Updating metadata:")
print(new_md)
@ -230,16 +263,21 @@ def _upload_bvid(bvid: str, *, update_existing: bool = False, collection: str):
)
assert isinstance(r, Response)
r.raise_for_status()
with open(f'{videos_basepath}/{local_identifier}/_uploaded.mark', 'w', encoding='utf-8') as f:
f.write('')
print(f'==== {remote_identifier} 上传完成 ====')
with open(
f"{videos_basepath}/{local_identifier}/_uploaded.mark",
"w",
encoding="utf-8",
) as f:
f.write("")
print(f"==== {remote_identifier} {_('上传完成')} ====")
def read_ia_keys(keysfile):
''' Return: tuple(`access_key`, `secret_key`) '''
with open(keysfile, 'r', encoding='utf-8') as f:
"""Return: tuple(`access_key`, `secret_key`)"""
with open(keysfile, "r", encoding="utf-8") as f:
key_lines = f.readlines()
access_key = key_lines[0].strip()
secret_key = key_lines[1].strip()
return access_key, secret_key
return access_key, secret_key

View File

@ -18,28 +18,35 @@ from bilix.exception import APIResourceError
from biliarchiver.config import BILIBILI_IDENTIFIER_PERFIX
from biliarchiver.config import config
from biliarchiver.utils.identifier import human_readable_upper_part_map
from biliarchiver.i18n import _
@raise_api_error
async def new_get_subtitle_info(client: httpx.AsyncClient, bvid, cid):
params = {'bvid': bvid, 'cid': cid}
res = await req_retry(client, 'https://api.bilibili.com/x/player/v2', params=params)
params = {"bvid": bvid, "cid": cid}
res = await req_retry(client, "https://api.bilibili.com/x/player/v2", params=params)
info = json.loads(res.text)
if info['code'] == -400:
raise APIError('未找到字幕信息', params)
if info["code"] == -400:
raise APIError(_("未找到字幕信息"), params)
# 这里 monkey patch 一下把返回 lan_doc 改成返回 lan这样生成的字幕文件名就是 语言代码 而不是 中文名 了
# 例如
# lan_doc: 中文(中国)
# lang: zh-CN
# return [[f'http:{i["subtitle_url"]}', i['lan_doc']] for i in info['data']['subtitle']['subtitles']]
return [[f'http:{i["subtitle_url"]}', i['lan']] for i in info['data']['subtitle']['subtitles']]
# return [[f'http:{i["subtitle_url"]}', i['lan_doc']] for i in info['data']['subtitle']['subtitles']]
return [
[f'http:{i["subtitle_url"]}', i["lan"]]
for i in info["data"]["subtitle"]["subtitles"]
]
api.get_subtitle_info = new_get_subtitle_info
@raise_api_error
async def new_get_video_info(client: httpx.AsyncClient, url: str):
"""
"""
monkey patch 一下只使用 API 获取 video_info
理由
- API 目前只支持一般的 AV/BV 视频可以预防我们不小心下到了番剧/影视剧之类的版权内容
@ -48,104 +55,148 @@ async def new_get_video_info(client: httpx.AsyncClient, url: str):
"""
# print("using api")
return await api._get_video_info_from_api(client, url)
api.get_video_info = new_get_video_info
async def _new_attach_dash_and_durl_from_api(client: httpx.AsyncClient, video_info: api.VideoInfo):
params = {'cid': video_info.cid, 'bvid': video_info.bvid,
'qn': 120, # 如无 dash 资源少数老视频fallback 到 4K 超清 durl
'fnval': 4048, # 如 dash 资源可用,请求 dash 格式的全部可用流
'fourk': 1, # 请求 4k 资源
'fnver': 0, 'platform': 'pc', 'otype': 'json'}
dash_response = await req_retry(client, 'https://api.bilibili.com/x/player/playurl',
params=params, follow_redirects=True)
async def _new_attach_dash_and_durl_from_api(
client: httpx.AsyncClient, video_info: api.VideoInfo
):
params = {
"cid": video_info.cid,
"bvid": video_info.bvid,
"qn": 120, # 如无 dash 资源少数老视频fallback 到 4K 超清 durl
"fnval": 4048, # 如 dash 资源可用,请求 dash 格式的全部可用流
"fourk": 1, # 请求 4k 资源
"fnver": 0,
"platform": "pc",
"otype": "json",
}
dash_response = await req_retry(
client,
"https://api.bilibili.com/x/player/playurl",
params=params,
follow_redirects=True,
)
dash_json = json.loads(dash_response.text)
if dash_json['code'] != 0:
raise APIResourceError(dash_json['message'], video_info.bvid)
if dash_json["code"] != 0:
raise APIResourceError(dash_json["message"], video_info.bvid)
dash, other = None, []
if 'dash' in dash_json['data']:
if "dash" in dash_json["data"]:
dash = api.Dash.from_dict(dash_json)
if 'durl' in dash_json['data']:
for i in dash_json['data']['durl']:
suffix = re.search(r'\.([a-zA-Z0-9]+)\?', i['url']).group(1) # type: ignore
other.append(api.Media(base_url=i['url'], backup_url=i['backup_url'], size=i['size'], suffix=suffix))
if "durl" in dash_json["data"]:
for i in dash_json["data"]["durl"]:
suffix = re.search(r"\.([a-zA-Z0-9]+)\?", i["url"]).group(1) # type: ignore
other.append(
api.Media(
base_url=i["url"],
backup_url=i["backup_url"],
size=i["size"],
suffix=suffix,
)
)
video_info.dash, video_info.other = dash, other
# NOTE: 临时修复,等 bilix 发布了 https://github.com/HFrost0/bilix/pull/174 的版本后,删掉这个 patch
# NOTE: 临时修复,等 bilix 发布了 https://github.com/HFrost0/bilix/pull/174 的版本后,删掉这个 patch
api._attach_dash_and_durl_from_api = _new_attach_dash_and_durl_from_api
async def archive_bvid(d: DownloaderBilibili, bvid: str, *, logined: bool=False, semaphore: asyncio.Semaphore):
async def archive_bvid(
d: DownloaderBilibili,
bvid: str,
*,
logined: bool = False,
semaphore: asyncio.Semaphore,
):
async with semaphore:
assert d.hierarchy is True, 'hierarchy 必须为 True' # 为保持后续目录结构、文件命名的一致性
assert d.client.cookies.get('SESSDATA') is not None, 'sess_data 不能为空' # 开个大会员呗,能下 4k 呢。
assert logined is True, '请先检查 SESSDATA 是否过期,再将 logined 设置为 True' # 防误操作
assert d.hierarchy is True, _("hierarchy 必须为 True") # 为保持后续目录结构、文件命名的一致性
assert d.client.cookies.get("SESSDATA") is not None, _(
"sess_data 不能为空"
) # 开个大会员呗,能下 4k 呢。
assert logined is True, _("请先检查 SESSDATA 是否过期,再将 logined 设置为 True") # 防误操作
upper_part = human_readable_upper_part_map(string=bvid, backward=True)
OLD_videos_basepath: Path = config.storage_home_dir / 'videos' / bvid
videos_basepath: Path = config.storage_home_dir / 'videos' / f'{bvid}-{upper_part}'
OLD_videos_basepath: Path = config.storage_home_dir / "videos" / bvid
videos_basepath: Path = (
config.storage_home_dir / "videos" / f"{bvid}-{upper_part}"
)
if os.path.exists(OLD_videos_basepath):
print(f'检测到旧的视频目录 {OLD_videos_basepath},将其重命名为 {videos_basepath}...')
print(
_("检测到旧的视频目录 {},将其重命名为 {}...").format(
OLD_videos_basepath, videos_basepath
)
)
os.rename(OLD_videos_basepath, videos_basepath)
if os.path.exists(videos_basepath / '_all_downloaded.mark'):
print(f'{bvid} 所有分p都已下载过了')
if os.path.exists(videos_basepath / "_all_downloaded.mark"):
# print(f"{bvid} 所有分p都已下载过了")
print(_("{} 所有分p都已下载过了").format(bvid))
return
url = f'https://www.bilibili.com/video/{bvid}/'
url = f"https://www.bilibili.com/video/{bvid}/"
# 为了获取 pages先请求一次
try:
first_video_info = await api.get_video_info(d.client, url)
except APIResourceError as e:
print(f'{bvid} 获取 video_info 失败,原因:{e}')
print(_("{} 获取 video_info 失败,原因:{}").format(bvid, e))
return
os.makedirs(videos_basepath, exist_ok=True)
pid = 0
for page in first_video_info.pages:
pid += 1 # pid 从 1 开始
if not page.p_url.endswith(f'?p={pid}'):
raise NotImplementedError(f'{bvid} 的 P{pid} 不存在 (可能视频被 UP主/B站 删了),请报告此问题,我们需要这个样本!')
pid += 1 # pid 从 1 开始
if not page.p_url.endswith(f"?p={pid}"):
raise NotImplementedError(
_("{} 的 P{} 不存在 (可能视频被 UP 主 / B 站删了),请报告此问题,我们需要这个样本!").format(
bvid, pid
)
)
file_basename = f'{bvid}_p{pid}'
video_basepath = videos_basepath / f'{BILIBILI_IDENTIFIER_PERFIX}-{file_basename}'
video_extrapath = video_basepath / 'extra'
if os.path.exists(f'{video_basepath}/_downloaded.mark'):
print(f'{file_basename}: 已经下载过了')
file_basename = f"{bvid}_p{pid}"
video_basepath = (
videos_basepath / f"{BILIBILI_IDENTIFIER_PERFIX}-{file_basename}"
)
video_extrapath = video_basepath / "extra"
if os.path.exists(f"{video_basepath}/_downloaded.mark"):
print(_("{}: 已经下载过了").format(file_basename))
continue
def delete_cache(reason: str = ''):
def delete_cache(reason: str = ""):
if not os.path.exists(video_basepath):
return
_files_in_video_basepath = os.listdir(video_basepath)
for _file in _files_in_video_basepath:
if _file.startswith(file_basename):
print(f'{file_basename}: {reason},删除缓存: {_file}')
print(_("{}: {},删除缓存: {}").format(file_basename, reason, _file))
os.remove(video_basepath / _file)
delete_cache('为防出错,清空上次未完成的下载缓存')
delete_cache(_("为防出错,清空上次未完成的下载缓存"))
video_info = await api.get_video_info(d.client, page.p_url)
print(f'{file_basename}: {video_info.title}...')
print(f"{file_basename}: {video_info.title}...")
os.makedirs(video_basepath, exist_ok=True)
os.makedirs(video_extrapath, exist_ok=True)
old_p_name = video_info.pages[video_info.p].p_name
old_h1_title = video_info.h1_title
# 在 d.hierarchy is True 且 h1_title 超长的情况下, bilix 会将 p_name 作为文件名
video_info.pages[video_info.p].p_name = file_basename # 所以这里覆盖 p_name 为 file_basename
video_info.h1_title = 'iiiiii' * 50 # 然后假装超长标题
video_info.pages[
video_info.p
].p_name = file_basename # 所以这里覆盖 p_name 为 file_basename
video_info.h1_title = "iiiiii" * 50 # 然后假装超长标题
# 这样 bilix 保存的文件名就是我们想要的了(谁叫 bilix 不支持自定义文件名呢)
# NOTE: p_name 似乎也不宜过长,否则还是会被 bilix 截断。
# 但是我们以 {bvid}_p{pid} 作为文件名,这个长度是没问题的。
codec = None
quality = None
if video_info.dash:
# 选择编码 dvh->hev->avc
# 不选 av0 ,毕竟目前没几个设备能拖得动
codec_candidates = ['dvh', 'hev', 'avc']
codec_candidates = ["dvh", "hev", "avc"]
for codec_candidate in codec_candidates:
for media in video_info.dash.videos:
if media.codec.startswith(codec_candidate):
@ -155,93 +206,115 @@ async def archive_bvid(d: DownloaderBilibili, bvid: str, *, logined: bool=False,
break
if codec is not None:
break
assert codec is not None and quality is not None, f'{file_basename}: 没有 dvh、avc 或 hevc 编码的视频'
assert (
codec is not None and quality is not None
), f"{file_basename}: " + _("没有 dvh、avc 或 hevc 编码的视频")
elif video_info.other:
print(f'{file_basename}: 未解析到 dash 资源,交给 bilix 处理 ...')
codec = ''
# print(f"{file_basename}: 未解析到 dash 资源,交给 bilix 处理 ...")
print("{file_basename}: " + _("未解析到 dash 资源,交给 bilix 处理 ..."))
codec = ""
quality = 0
else:
raise APIError(f'{file_basename}: 未解析到视频资源', page.p_url)
raise APIError("{file_basename}: " + _("未解析到视频资源"), page.p_url)
assert codec is not None
assert isinstance(quality, (int, str))
cor1 = d.get_video(page.p_url ,video_info=video_info, path=video_basepath,
quality=quality, # 选择最高画质
codec=codec, # 编码
# 下载 ass 弹幕(bilix 会自动调用 danmukuC 将 pb 弹幕转为 ass)、封面、字幕
# 弹幕、封面、字幕都会被放进 extra 子目录里,所以需要 d.hierarchy is True
dm=True, image=True, subtitle=True
)
cor1 = d.get_video(
page.p_url,
video_info=video_info,
path=video_basepath,
quality=quality, # 选择最高画质
codec=codec, # 编码
# 下载 ass 弹幕(bilix 会自动调用 danmukuC 将 pb 弹幕转为 ass)、封面、字幕
# 弹幕、封面、字幕都会被放进 extra 子目录里,所以需要 d.hierarchy is True
dm=True,
image=True,
subtitle=True,
)
# 下载原始的 pb 弹幕
cor2 = d.get_dm(page.p_url, video_info=video_info, path=video_extrapath)
# 下载视频超详细信息BV 级别,不是分 P 级别)
cor3 = download_bilibili_video_detail(d.client, bvid, f'{video_extrapath}/{file_basename}.info.json')
cor3 = download_bilibili_video_detail(
d.client, bvid, f"{video_extrapath}/{file_basename}.info.json"
)
coroutines = [cor1, cor2, cor3]
tasks = [asyncio.create_task(cor) for cor in coroutines]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result, cor in zip(results, coroutines):
if isinstance(result, Exception):
print("出错,其他任务完成后将抛出异常...")
print(_("出错,其他任务完成后将抛出异常..."))
for task in tasks:
task.cancel()
raise result
if codec.startswith('hev') and not os.path.exists(video_basepath / f'{file_basename}.mp4'):
if codec.startswith("hev") and not os.path.exists(
video_basepath / f"{file_basename}.mp4"
):
# 如果有下载缓存文件(以 file_basename 开头的文件),说明这个 hevc 的 dash 资源存在,只是可能因为网络之类的原因下载中途失败了
delete_cache('下载出错')
delete_cache(_("下载出错"))
# 下载缓存文件都不存在,应该是对应的 dash 资源根本就没有,一些老视频会出现这种情况。
# 换 avc 编码
print(f'{file_basename}: 视频文件没有被下载?也许是 hevc 对应的 dash 资源不存在,尝试 avc ……')
print(
_("{}: 视频文件没有被下载?也许是 hevc 对应的 dash 资源不存在,尝试 avc ……").format(
file_basename
)
)
assert video_info.dash is not None
for media in video_info.dash.videos:
if media.codec.startswith('avc'):
if media.codec.startswith("avc"):
codec = media.codec
print(f'{file_basename}: "{codec}" "{media.quality}" ...')
break
cor4 = d.get_video(page.p_url ,video_info=video_info, path=video_basepath,
quality=0, # 选择最高画质
codec=codec, # 编码
# 下载 ass 弹幕(bilix 会自动调用 danmukuC 将 pb 弹幕转为 ass)、封面、字幕
# 弹幕、封面、字幕都会被放进 extra 子目录里,所以需要 d.hierarchy is True
dm=True, image=True, subtitle=True
)
cor4 = d.get_video(
page.p_url,
video_info=video_info,
path=video_basepath,
quality=0, # 选择最高画质
codec=codec, # 编码
# 下载 ass 弹幕(bilix 会自动调用 danmukuC 将 pb 弹幕转为 ass)、封面、字幕
# 弹幕、封面、字幕都会被放进 extra 子目录里,所以需要 d.hierarchy is True
dm=True,
image=True,
subtitle=True,
)
await asyncio.gather(cor4)
assert os.path.exists(video_basepath / f'{file_basename}.mp4') or os.path.exists(video_basepath / f'{file_basename}.flv')
assert os.path.exists(
video_basepath / f"{file_basename}.mp4"
) or os.path.exists(video_basepath / f"{file_basename}.flv")
# 还原为了自定义文件名而做的覆盖
video_info.pages[video_info.p].p_name = old_p_name
video_info.h1_title = old_h1_title
# 单 p 下好了
async with aiofiles.open(f'{video_basepath}/_downloaded.mark', 'w', encoding='utf-8') as f:
await f.write('')
async with aiofiles.open(
f"{video_basepath}/_downloaded.mark", "w", encoding="utf-8"
) as f:
await f.write("")
# bv 对应的全部 p 下好了
async with aiofiles.open(f'{videos_basepath}/_all_downloaded.mark', 'w', encoding='utf-8') as f:
await f.write('')
async with aiofiles.open(
f"{videos_basepath}/_all_downloaded.mark", "w", encoding="utf-8"
) as f:
await f.write("")
async def download_bilibili_video_detail(client, bvid, filename):
if os.path.exists(filename):
print(f'{bvid} 视频详情已存在')
print(_("{} 的视频详情已存在").format(bvid))
return
# url = 'https://api.bilibili.com/x/web-interface/view'
url = 'https://api.bilibili.com/x/web-interface/view/detail' # 超详细 APIBV 级别,不是分 P 级别)
params = {'bvid': bvid}
r = await req_retry(client, url, params=params ,follow_redirects=True)
url = "https://api.bilibili.com/x/web-interface/view/detail" # 超详细 APIBV 级别,不是分 P 级别)
params = {"bvid": bvid}
r = await req_retry(client, url, params=params, follow_redirects=True)
r.raise_for_status()
r_json = r.json()
assert r_json['code'] == 0, f'{bvid} 视频详情获取失败'
assert r_json["code"] == 0, _("{} 的视频详情获取失败").format(bvid)
async with aiofiles.open(filename, 'w', encoding='utf-8') as f:
async with aiofiles.open(filename, "w", encoding="utf-8") as f:
# f.write(json.dumps(r.json(), indent=4, ensure_ascii=False))
await f.write(r.text)
print(f'{bvid} 视频详情已保存')
print(_("{} 的视频详情已保存").format(bvid))

View File

@ -17,6 +17,7 @@ from biliarchiver.utils.identifier import human_readable_upper_part_map
from biliarchiver.utils.ffmpeg import check_ffmpeg
from biliarchiver.version import BILI_ARCHIVER_VERSION
from biliarchiver.cli_tools.utils import read_bvids
from biliarchiver.i18n import _
install()
@ -68,7 +69,7 @@ def _down(
min_free_space_gb: int,
skip_to: int,
):
assert check_ffmpeg() is True, "ffmpeg 未安装"
assert check_ffmpeg() is True, _("ffmpeg 未安装")
bvids_list = read_bvids(bvids)
@ -122,10 +123,11 @@ def _down(
# print(f'任务 {task} 已完成')
tasks.remove(task)
if not check_free_space():
print(f"剩余空间不足 {min_free_space_gb} GiB")
s = _("剩余空间不足 {} GiB").format(min_free_space_gb)
print(s)
for task in tasks:
task.cancel()
raise RuntimeError(f"剩余空间不足 {min_free_space_gb} GiB")
raise RuntimeError(s)
for index, bvid in enumerate(bvids_list):
if index < skip_to:
@ -136,7 +138,7 @@ def _down(
upper_part = human_readable_upper_part_map(string=bvid, backward=True)
remote_identifier = f"{BILIBILI_IDENTIFIER_PERFIX}-{bvid}_p1-{upper_part}"
if check_ia_item_exist(client, remote_identifier):
print(f"IA 上已存在 {remote_identifier} ,跳过")
print(_("IA 上已存在 {},跳过").format(remote_identifier))
continue
upper_part = human_readable_upper_part_map(string=bvid, backward=True)
@ -144,7 +146,7 @@ def _down(
config.storage_home_dir / "videos" / f"{bvid}-{upper_part}"
)
if os.path.exists(videos_basepath / "_all_downloaded.mark"):
print(f"{bvid} 所有分p都已下载过了")
print(_("{} 所有分p都已下载过了").format(bvid))
continue
if len(tasks) >= config.video_concurrency:
@ -177,7 +179,7 @@ def update_cookies_from_browser(client: AsyncClient, browser: str):
f = getattr(browser_cookie3, browser.lower())
cookies_to_update = f(domain_name="bilibili.com")
client.cookies.update(cookies_to_update)
print(f"{browser} 品尝了 {len(cookies_to_update)} 块 cookies")
print(_("{} 品尝了 {} 块 cookies").format(browser, len(cookies_to_update)))
except AttributeError:
raise AttributeError(f"Invalid Browser {browser}")
@ -190,7 +192,7 @@ def update_cookies_from_file(client: AsyncClient, cookies_path: Union[str, Path]
else:
raise TypeError(f"cookies_path: {type(cookies_path)}")
assert os.path.exists(cookies_path), f"cookies 文件不存在: {cookies_path}"
assert os.path.exists(cookies_path), _("cookies 文件不存在: {}").format(cookies_path)
from http.cookiejar import MozillaCookieJar
@ -205,7 +207,8 @@ def update_cookies_from_file(client: AsyncClient, cookies_path: Union[str, Path]
if "bilibili.com" not in cookie.domain:
continue
if cookie.name in loadded_keys:
print(f"跳过重复的 cookies: {cookie.name}")
print(_("跳过重复的 cookies"), end="")
print(f": {cookie.name}")
# httpx 不能处理不同域名的同名 cookies只好硬去重了
continue
assert cookie.value is not None
@ -214,9 +217,9 @@ def update_cookies_from_file(client: AsyncClient, cookies_path: Union[str, Path]
)
loadded_keys.append(cookie.name)
loadded_cookies += 1
print(f"{cookies_path} 品尝了 {loadded_cookies} 块 cookies")
print(_("{} 品尝了 {} 块 cookies").format(cookies_path, loadded_cookies))
if loadded_cookies > 100:
print("吃了过多的 cookies可能导致 httpx.Client 怠工,响应非常缓慢")
print(_("吃了过多的 cookies可能导致 httpx.Client 怠工,响应非常缓慢"))
assert client.cookies.get("SESSDATA") is not None, "SESSDATA 不存在"
# print(f'SESS_DATA: {client.cookies.get("SESSDATA")}')
@ -227,15 +230,13 @@ def is_login(cilent: Client) -> bool:
r.raise_for_status()
nav_json = r.json()
if nav_json["code"] == 0:
print("BiliBili 登录成功,饼干真香。")
print(
"NOTICE: 存档过程中请不要在 cookies 的源浏览器访问 B 站,避免 B 站刷新"
" cookies 导致我们半路下到的视频全是 480P 的优酷土豆级醇享画质。"
)
print(_("BiliBili 登录成功,饼干真香。"))
print(_("NOTICE: 存档过程中请不要在 cookies 的源浏览器访问 B 站,避免 B 站刷新"), end=" ")
print(_("cookies 导致我们半路下到的视频全是 480P 的优酷土豆级醇享画质。"))
return True
print("未登录/SESSDATA无效/过期,你这饼干它保真吗?")
print(_("未登录/SESSDATA无效/过期,你这饼干它保真吗?"))
return False
if __name__ == "__main__":
raise DeprecationWarning("已废弃直接运行此命令,请改用 biliarchiver 命令")
raise DeprecationWarning(_("已废弃直接运行此命令,请改用 biliarchiver 命令"))

View File

@ -1,4 +1,5 @@
import click
from biliarchiver.i18n import _
from biliarchiver.cli_tools.up_command import up
from biliarchiver.cli_tools.down_command import down
from biliarchiver.cli_tools.get_command import get
@ -44,7 +45,7 @@ def biliarchiver():
pass
@biliarchiver.command(help=click.style("初始化所需目录", fg="cyan"))
@biliarchiver.command(help=click.style(_("初始化所需目录"), fg="cyan"))
def init():
import pathlib
@ -59,5 +60,16 @@ biliarchiver.add_command(down)
biliarchiver.add_command(get)
@biliarchiver.command(help=click.style(_("配置账号信息"), fg="cyan"))
def auth():
click.echo(click.style("Bilibili", bg="yellow"))
click.echo(_("登录后将哔哩哔哩的 cookies 复制到 `config.json` 指定的文件(默认为 `~/.cookies.txt`)中。"))
click.echo("")
click.echo(click.style("Internet archive", bg="yellow"))
click.echo(_("前往 https://archive.org/account/s3.php 获取 Access Key 和 Secret Key。"))
click.echo(_("""将它们放在 `config.json` 指定的文件(默认为 `~/.bili_ia_keys.txt`)中,两者由换行符分隔。"""))
if __name__ == "__main__":
biliarchiver()

View File

@ -1,10 +1,11 @@
import click
from rich.console import Console
from biliarchiver.i18n import _
@click.command(help=click.style("从哔哩哔哩下载", fg="cyan"))
@click.command(help=click.style(_("从哔哩哔哩下载"), fg="cyan"))
@click.option(
"--bvids", type=click.STRING, required=True, help="空白字符分隔的 bvids 列表(记得加引号),或文件路径"
"--bvids", type=click.STRING, required=True, help=_("空白字符分隔的 bvids 列表(记得加引号),或文件路径")
)
@click.option(
"--skip-ia-check",
@ -12,24 +13,24 @@ from rich.console import Console
is_flag=True,
default=False,
show_default=True,
help="不检查 IA 上是否已存在对应 BVID 的 item ,直接开始下载",
help=_("不检查 IA 上是否已存在对应 BVID 的 item ,直接开始下载"),
)
@click.option(
"--from-browser",
"--fb",
type=str,
default=None,
help="从指定浏览器导入 cookies (否则导入 config.json 中的 cookies_file)",
help=_("从指定浏览器导入 cookies (否则导入 config.json 中的 cookies_file)"),
)
@click.option(
"--min-free-space-gb",
type=int,
default=10,
help="最小剩余空间 (GB),用超退出",
help=_("最小剩余空间 (GB),用超退出"),
show_default=True,
)
@click.option(
"--skip-to", type=int, default=0, show_default=True, help="跳过文件开头 bvid 的个数"
"--skip-to", type=int, default=0, show_default=True, help=_("跳过文件开头 bvid 的个数")
)
def down(**kwargs):
from biliarchiver.cli_tools.bili_archive_bvids import _down

View File

@ -1,4 +1,5 @@
import asyncio
from gettext import ngettext
import os
from pathlib import Path
import re
@ -9,62 +10,12 @@ import json
import click
from click_option_group import optgroup
from biliarchiver.i18n import _, ngettext
from bilix.sites.bilibili import api
from rich import print
""" def arg_parse():
parser = argparse.ArgumentParser()
# 为啥是 by-xxx 而不是 from-xxx ?因为命令行里好敲……
ranking_group = parser.add_argument_group()
ranking_group.title = 'by ranking'
ranking_group.description = '排行榜(全站榜,非个性推荐榜)'
ranking_group.add_argument(
'--by-ranking', action='store_true', help='从排行榜获取 bvids')
ranking_group.add_argument('--ranking-rid', type=int, default=0,
help='目标排行 rid0 为全站排行榜。rid 等于分区的 tid [default: 0]')
up_videos_group = parser.add_argument_group()
up_videos_group.title = 'by up videos'
up_videos_group.description = 'up 主用户页投稿'
up_videos_group.add_argument(
'--by-up_videos', action='store_true', help='从 up 主用户页获取全部的投稿的 bvids')
up_videos_group.add_argument(
'--up_videos-mid', type=str, help='目标 up 主的 mid (也可以是用户页的 URL)')
popular_precious_group = parser.add_argument_group()
popular_precious_group.title = 'popular precious'
popular_precious_group.description = '入站必刷,更新频率低'
popular_precious_group.add_argument(
'--by-popular_precious', action='store_true', help='从入站必刷获取 bvids', dest='by_popular_precious')
popular_series_group = parser.add_argument_group()
popular_series_group.title = 'popular series'
popular_series_group.description = '每周必看每周五晚18:00更新'
popular_series_group.add_argument(
'--by-popular_series', action='store_true', help='从每周必看获取 bvids', dest='by_popular_series')
popular_series_group.add_argument(
'--popular_series-number', type=int, default=1, help='获取第几期(周) [default: 1]')
popular_series_group.add_argument(
'--all-popular_series', action='store_true', help='自动获取全部的每周必看(增量)', dest='all_popular_series')
space_fav_season = parser.add_argument_group()
space_fav_season.title = 'space_fav_season'
space_fav_season.description = '获取合集或视频列表内视频'
space_fav_season.add_argument('--by-space_fav_season', type=str,
help='合集或视频列表 sid (或 URL)', dest='by_space_fav_season', default=None)
favour_group = parser.add_argument_group()
favour_group.title = 'favour'
favour_group.description = '收藏夹'
favour_group.add_argument(
'--by-fav', type=str, help='收藏夹 fid (或 URL)', dest='by_fav', default=None)
args = parser.parse_args()
return args
"""
async def by_series(url_or_sid: str) -> Path:
sid = sid = (
re.search(r"sid=(\d+)", url_or_sid).groups()[0]
@ -72,7 +23,7 @@ async def by_series(url_or_sid: str) -> Path:
else url_or_sid
) # type: ignore
client = AsyncClient(**api.dft_client_settings)
print(f"正在获取 {sid} 的视频列表……")
print(_("正在获取 {sid} 的视频列表……").format(sid=sid))
col_name, up_name, bvids = await api.get_collect_info(client, sid)
filepath = f"bvids/by-sapce_fav_season/sid-{sid}-{int(time.time())}.txt"
os.makedirs(os.path.dirname(filepath), exist_ok=True)
@ -80,8 +31,16 @@ async def by_series(url_or_sid: str) -> Path:
with open(abs_filepath, "w", encoding="utf-8") as f:
for bv_id in bvids:
f.write(f"{bv_id}" + "\n")
print(f"已获取 {col_name}{up_name})的 {len(bvids)} 个视频")
print(f"{abs_filepath}")
# print(f"已获取 {col_name}{up_name})的 {len(bvids)} 个视频")
count = len(bvids)
print(
ngettext(
"已获取 {}{})的一个视频",
"已获取 {}{})的 {count} 个视频",
count,
).format(col_name, up_name, count=count)
)
print(_("存储到 {}").format(abs_filepath))
return Path(abs_filepath)
@ -112,7 +71,11 @@ def by_ranking(rid: int) -> Path:
for bvid in bvids:
f.write(f"{bvid}" + "\n")
abs_filepath = os.path.abspath(bvids_filepath)
print(f"已保存 {len(bvids)} 个 bvid 到 {abs_filepath}")
print(
ngettext("已保存一个 bvid 到 {}", "已保存 {count} 个 bvid 到 {}", len(bvids)).format(
abs_filepath, count=len(bvids)
)
)
return Path(abs_filepath)
@ -127,7 +90,7 @@ async def by_up_videos(url_or_mid: str) -> Path:
mid = url_or_mid
assert isinstance(mid, str)
assert mid.isdigit(), "mid 应是数字字符串"
assert mid.isdigit(), _("mid 应是数字字符串")
client = AsyncClient(**api.dft_client_settings)
ps = 30 # 每页视频数,最小 1最大 50默认 30
@ -135,32 +98,41 @@ async def by_up_videos(url_or_mid: str) -> Path:
keyword = "" # 搜索关键词
bv_ids = []
pn = 1
print(f"获取第 {pn} 页...")
print(ngettext("获取第 {} 页...", "获取第 {} 页...", pn).format(pn))
up_name, total_size, bv_ids_page = await api.get_up_info(
client, mid, pn, ps, order, keyword
)
bv_ids += bv_ids_page
# print(f"{mid} {up_name} 共 {total_size} 个视频. (如果最新的视频为合作视频的非主作者UP 名可能会识别错误,但不影响获取 bvid 列表)")
print(
f"{mid} {up_name}{total_size} 个视频. (如果最新的视频为合作视频的非主作者UP 名可能会识别错误,但不影响获取 bvid 列表)"
ngettext("{} {}{} 个视频.", "{} {}{} 个视频.", total_size).format(
mid, up_name, total_size
)
)
print(_("如果最新的视频为合作视频的非主作者UP 名可能会识别错误,但不影响获取 bvid 列表)"))
while pn < total_size / ps:
pn += 1
print(f"获取第 {pn} 页 (10s...)")
# print(f"获取第 {pn} 页 (10s...)")
print(ngettext("获取第 {} 页 (10 秒...)", "获取第 {} 页 (10 秒...)", pn).format(pn))
await asyncio.sleep(10)
_, _, bv_ids_page = await api.get_up_info(client, mid, pn, ps, order, keyword)
_x, _y, bv_ids_page = await api.get_up_info(client, mid, pn, ps, order, keyword)
bv_ids += bv_ids_page
print(mid, up_name, total_size)
await client.aclose()
assert len(bv_ids) == len(set(bv_ids)), "有重复的 bv_id"
assert total_size == len(bv_ids), "视频总数不匹配"
assert len(bv_ids) == len(set(bv_ids)), _("有重复的 bv_id")
assert total_size == len(bv_ids), _("视频总数不匹配")
filepath = f"bvids/by-up_videos/mid-{mid}-{int(time.time())}.txt"
os.makedirs(os.path.dirname(filepath), exist_ok=True)
abs_filepath = os.path.abspath(filepath)
with open(abs_filepath, "w", encoding="utf-8") as f:
for bv_id in bv_ids:
f.write(f"{bv_id}" + "\n")
print(f"已保存 {len(bv_ids)} 个 bvid 到 {abs_filepath}")
print(
ngettext("已保存一个 bvid 到 {}", "已保存 {count} 个 bvid 到 {}", len(bv_ids)).format(
abs_filepath, count=len(bv_ids)
)
)
return Path(abs_filepath)
@ -179,7 +151,12 @@ def by_popular_precious():
abs_filepath = os.path.abspath(filepath)
with open(abs_filepath, "w", encoding="utf-8") as f:
f.write("\n".join(bvids))
print(f"已保存 {len(bvids)} 个 bvid 到 {abs_filepath}")
# print(f"已保存 {len(bvids)} 个 bvid 到 {abs_filepath}")
print(
ngettext("已保存一个 bvid 到 {}", "已保存 {count} 个 bvid 到 {}", len(bvids)).format(
abs_filepath, count=len(bvids)
)
)
def by_popular_series_one(number: int):
@ -198,7 +175,12 @@ def by_popular_series_one(number: int):
abs_filepath = os.path.abspath(filepath)
with open(abs_filepath, "w", encoding="utf-8") as f:
f.write("\n".join(bvids))
print(f"已保存 {len(bvids)} 个 bvid 到 {abs_filepath}")
# print(f"已保存 {len(bvids)} 个 bvid 到 {abs_filepath}")
print(
ngettext("已保存一个 bvid 到 {}", "已保存 {count} 个 bvid 到 {}", len(bvids)).format(
abs_filepath, count=len(bvids)
)
)
def not_got_popular_series() -> list[int]:
@ -242,20 +224,35 @@ async def by_favlist(url_or_fid: str):
if media_left is None:
print(f"fav_name: {fav_name}, up_name: {up_name}, total_size: {total_size}")
media_left = total_size - PAGE_SIZE * page_num
print(f"还剩 ~{media_left // PAGE_SIZE}", end="\r")
print(
ngettext("还剩 ~{}", "还剩 ~{}", media_left // PAGE_SIZE).format(
media_left // PAGE_SIZE
),
end="\r",
)
await asyncio.sleep(2)
page_num += 1
await client.aclose()
assert total_size is not None
assert len(bvids) == len(set(bvids)), "有重复的 bvid"
print(f"{len(bvids)} 个有效视频,{total_size-len(bvids)} 个失效视频")
assert len(bvids) == len(set(bvids)), _("有重复的 bvid")
print(
ngettext(
"{} 个有效视频,{} 个失效视频",
"{} 个有效视频,{} 个失效视频",
total_size,
).format(len(bvids), total_size - len(bvids))
)
filepath = f"bvids/by-favour/fid-{fid}-{int(time.time())}.txt"
os.makedirs(os.path.dirname(filepath), exist_ok=True)
abs_filepath = os.path.abspath(filepath)
with open(abs_filepath, "w", encoding="utf-8") as f:
f.write("\n".join(bvids))
f.write("\n")
print(f"已保存 {len(bvids)} 个 bvid 到 {abs_filepath}")
print(
ngettext("已保存一个 bvid 到 {}", "已保存 {count} 个 bvid 到 {}", len(bvids)).format(
abs_filepath, count=len(bvids)
)
)
async def main(
@ -306,21 +303,21 @@ class URLorIntParamType(click.ParamType):
@click.command(
short_help=click.style("批量获取 BV 号", fg="cyan"),
help="请通过 flag 指定至少一种批量获取 BV 号的方式。多个不同组的 flag 同时使用时,将会先后通过不同方式获取。",
short_help=click.style(_("批量获取 BV 号"), fg="cyan"),
help=_("请通过 flag 指定至少一种批量获取 BV 号的方式。多个不同组的 flag 同时使用时,将会先后通过不同方式获取。"),
)
@optgroup.group("合集")
@optgroup.group(_("合集"))
@optgroup.option(
"--series",
"-s",
help=click.style("合集或视频列表内视频", fg="red"),
help=click.style(_("合集或视频列表内视频"), fg="red"),
type=URLorIntParamType("sid"),
)
@optgroup.group("排行榜")
@optgroup.group(_("排行榜"))
@optgroup.option(
"--ranking",
"-r",
help=click.style("排行榜(全站榜,非个性推荐榜)", fg="yellow"),
help=click.style(_("排行榜(全站榜,非个性推荐榜)"), fg="yellow"),
is_flag=True,
)
@optgroup.option(
@ -328,25 +325,27 @@ class URLorIntParamType(click.ParamType):
"--ranking-id",
default=0,
show_default=True,
help=click.style("目标排行 rid0 为全站排行榜。rid 等于分区的 tid", fg="yellow"),
help=click.style(_("目标排行 rid0 为全站排行榜。rid 等于分区的 tid"), fg="yellow"),
type=int,
)
@optgroup.group("UP 主")
@optgroup.group(_("UP 主"))
@optgroup.option(
"--up-videos",
"-u",
help=click.style("UP 主用户页投稿", fg="cyan"),
help=click.style(_("UP 主用户页投稿"), fg="cyan"),
type=URLorIntParamType("mid"),
)
@optgroup.group("入站必刷")
@optgroup.group(_("入站必刷"))
@optgroup.option(
"--popular-precious", help=click.style("入站必刷,更新频率低", fg="bright_red"), is_flag=True
"--popular-precious",
help=click.style(_("入站必刷,更新频率低"), fg="bright_red"),
is_flag=True,
)
@optgroup.group("每周必看")
@optgroup.group(_("每周必看"))
@optgroup.option(
"--popular-series",
"-p",
help=click.style("每周必看,每周五晚 18:00 更新", fg="magenta"),
help=click.style(_("每周必看,每周五晚 18:00 更新"), fg="magenta"),
is_flag=True,
)
@optgroup.option(
@ -354,18 +353,18 @@ class URLorIntParamType(click.ParamType):
default=1,
type=int,
show_default=True,
help=click.style("获取第几期(周)", fg="magenta"),
help=click.style(_("获取第几期(周)"), fg="magenta"),
)
@optgroup.option(
"--all-popular-series",
help=click.style("自动获取全部的每周必看(增量)", fg="magenta"),
help=click.style(_("自动获取全部的每周必看(增量)"), fg="magenta"),
is_flag=True,
)
@optgroup.group("收藏夹")
@optgroup.group(_("收藏夹"))
@optgroup.option(
"--favlist",
"--fav",
help=click.style("收藏夹", fg="green"),
help=click.style(_("用户收藏夹"), fg="green"),
type=URLorIntParamType("fid"),
)
def get(**kwargs):
@ -377,7 +376,7 @@ def get(**kwargs):
and not kwargs["popular_precious"]
and not kwargs["popular_series"]
):
click.echo(click.style("ERROR: 请指定至少一种获取方式。", fg="red"))
click.echo(click.style(_("ERROR: 请指定至少一种获取方式。"), fg="red"))
click.echo(get.get_help(click.Context(get)))
return
asyncio.run(main(**kwargs))

View File

@ -3,6 +3,7 @@ import click
import os
from biliarchiver.cli_tools.utils import read_bvids
from biliarchiver.i18n import _
DEFAULT_COLLECTION = "opensource_movies"
@ -10,21 +11,29 @@ DEFAULT_COLLECTION = "opensource_movies"
开放 collection 任何人均可上传
通过 biliarchiver 上传的 item 会在24小时内被自动转移到 bilibili_videos collection
"""
"""
An open collection. Anyone can upload.
Items uploaded by biliarchiver will be automatically moved to bilibili_videos collection within 24 hours.
"""
BILIBILI_VIDEOS_COLLECTION = "bilibili_videos"
""" 由 arkiver 管理。bilibili_videos 属于 social-media-video 的子集 """
""" Managed by arkiver. bilibili_videos is a subset of social-media-video """
BILIBILI_VIDEOS_SUB_1_COLLECTION = "bilibili_videos_sub_1"
""" 由 yzqzss 管理。属于 bilibili_videos 的子集 """
""" Managed by yzqzss. A subset of bilibili_videos """
@click.command(help=click.style("上传至互联网档案馆", fg="cyan"))
@click.option("--bvids", type=click.STRING, default=None, help="bvids 列表的文件路径")
@click.command(help=click.style(_("上传至互联网档案馆"), fg="cyan"))
@click.option("--bvids", type=click.STRING, default=None, help=_("bvids 列表的文件路径"))
@click.option(
"--by-storage-home-dir",
is_flag=True,
default=False,
help="使用 `$storage_home_dir/videos` 目录下的所有视频",
help=_("使用 `$storage_home_dir/videos` 目录下的所有视频"),
)
@click.option("--update-existing", is_flag=True, default=False, help="更新已存在的 item")
@click.option("--update-existing", is_flag=True, default=False, help=_("更新已存在的 item"))
@click.option(
"--collection",
default=DEFAULT_COLLECTION,
@ -35,7 +44,8 @@ BILIBILI_VIDEOS_SUB_1_COLLECTION = "bilibili_videos_sub_1"
BILIBILI_VIDEOS_SUB_1_COLLECTION,
]
),
help=f"Collection to upload to. (非默认值仅限 collection 管理员使用) [default: {DEFAULT_COLLECTION}]",
help=_("欲上传至的 collection. (非默认值仅限 collection 管理员使用)")
+ f" [default: {DEFAULT_COLLECTION}]",
)
def up(
bvids: TextIOWrapper,

View File

@ -1,5 +1,6 @@
from pathlib import Path
from biliarchiver.utils.identifier import is_bvid
from biliarchiver.i18n import _
def read_bvids(bvids: str) -> list[str]:
@ -15,8 +16,8 @@ def read_bvids(bvids: str) -> list[str]:
del bvids
for bvid in bvids_list:
assert is_bvid(bvid), f"bvid {bvid} 不合法"
assert is_bvid(bvid), _("bvid {} 不合法").format(bvid)
assert bvids_list is not None and len(bvids_list) > 0, "bvids 为空"
assert bvids_list is not None and len(bvids_list) > 0, _("bvids 为空")
return bvids_list

View File

@ -2,13 +2,16 @@ from dataclasses import dataclass
import os
import json
from pathlib import Path
from biliarchiver.i18n import _
from biliarchiver.exception import DirNotInitializedError
CONFIG_FILE = 'config.json'
BILIBILI_IDENTIFIER_PERFIX = 'BiliBili' # IA identifier 前缀。
CONFIG_FILE = "config.json"
BILIBILI_IDENTIFIER_PERFIX = "BiliBili" # IA identifier 前缀。
class singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(singleton, cls).__call__(*args, **kwargs)
@ -20,41 +23,46 @@ class _Config(metaclass=singleton):
video_concurrency: int = 3
part_concurrency: int = 10
stream_retry: int = 20
storage_home_dir: Path = Path('bilibili_archive_dir/').expanduser()
ia_key_file: Path = Path('~/.bili_ia_keys.txt').expanduser()
cookies_file: Path = Path('~/.cookies.txt').expanduser()
storage_home_dir: Path = Path("bilibili_archive_dir/").expanduser()
ia_key_file: Path = Path("~/.bili_ia_keys.txt").expanduser()
cookies_file: Path = Path("~/.cookies.txt").expanduser()
def __init__(self):
self.is_right_pwd()
if not os.path.exists(CONFIG_FILE):
print(f'{CONFIG_FILE} 不存在,创建中...')
print(_("{} 不存在,创建中...").format(CONFIG_FILE))
self.save()
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
print(f'Loading {CONFIG_FILE} ...')
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
print(f"Loading {CONFIG_FILE} ...")
config_file = json.load(f)
self.video_concurrency: int = config_file['video_concurrency']
self.part_concurrency: int = config_file['part_concurrency']
self.stream_retry: int = config_file['stream_retry']
self.storage_home_dir: Path = Path(config_file['storage_home_dir']).expanduser()
self.ia_key_file: Path = Path(config_file['ia_key_file']).expanduser()
self.cookies_file: Path = Path(config_file['cookies_file']).expanduser()
self.video_concurrency: int = config_file["video_concurrency"]
self.part_concurrency: int = config_file["part_concurrency"]
self.stream_retry: int = config_file["stream_retry"]
self.storage_home_dir: Path = Path(config_file["storage_home_dir"]).expanduser()
self.ia_key_file: Path = Path(config_file["ia_key_file"]).expanduser()
self.cookies_file: Path = Path(config_file["cookies_file"]).expanduser()
def save(self):
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump({
'video_concurrency': self.video_concurrency,
'part_concurrency': self.part_concurrency,
'stream_retry': self.stream_retry,
'storage_home_dir': str(self.storage_home_dir),
'ia_key_file': str(self.ia_key_file),
'cookies_file': str(self.cookies_file),
}, f, ensure_ascii=False, indent=4)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(
{
"video_concurrency": self.video_concurrency,
"part_concurrency": self.part_concurrency,
"stream_retry": self.stream_retry,
"storage_home_dir": str(self.storage_home_dir),
"ia_key_file": str(self.ia_key_file),
"cookies_file": str(self.cookies_file),
},
f,
ensure_ascii=False,
indent=4,
)
def is_right_pwd(self):
if not os.path.exists('biliarchiver.home'):
raise Exception('先在当前工作目录运行 biliarchiver init 以初始化配置')
if not os.path.exists("biliarchiver.home"):
raise DirNotInitializedError()
config = _Config()

View File

@ -1,3 +1,6 @@
from biliarchiver.i18n import _
class VideosBasePathNotFoundError(FileNotFoundError):
def __init__(self, path: str):
self.path = path
@ -5,6 +8,7 @@ class VideosBasePathNotFoundError(FileNotFoundError):
def __str__(self):
return f"Videos base path {self.path} not found"
class VideosNotFinishedDownloadError(FileNotFoundError):
def __init__(self, path: str):
self.path = path
@ -12,9 +16,18 @@ class VideosNotFinishedDownloadError(FileNotFoundError):
def __str__(self):
return f"Videos not finished download: {self.path}"
class VersionOutdatedError(Exception):
def __init__(self, version):
self.version = version
def __str__(self):
return "Version outdated: %s" % self.version
return _("请更新 biliarchiver。当前版本已过期") + str(self.version)
class DirNotInitializedError(Exception):
def __init__(self):
pass
def __str__(self):
return _("先在当前工作目录运行 `biliarchiver init` 以初始化配置")

16
biliarchiver/i18n.py Normal file
View File

@ -0,0 +1,16 @@
import gettext
import locale
default_lang, default_enc = locale.getdefaultlocale()
default_lang = default_lang or "en"
languages = ["en"] if not default_lang.lower().startswith("zh") else ["zh_CN"]
appname = "biliarchiver"
i18n = gettext.translation(
appname, localedir="biliarchiver/locales", fallback=True, languages=languages
)
_ = i18n.gettext
ngettext = i18n.ngettext

1
biliarchiver/locales/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.mo

View File

@ -0,0 +1,427 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-17 03:36+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: biliarchiver/archive_bvid.py:30
msgid "未找到字幕信息"
msgstr ""
#: biliarchiver/archive_bvid.py:114
msgid "hierarchy 必须为 True"
msgstr ""
#: biliarchiver/archive_bvid.py:116
msgid "sess_data 不能为空"
msgstr ""
#: biliarchiver/archive_bvid.py:118
msgid "请先检查 SESSDATA 是否过期,再将 logined 设置为 True"
msgstr ""
#: biliarchiver/archive_bvid.py:127
msgid "检测到旧的视频目录 {},将其重命名为 {}..."
msgstr ""
#: biliarchiver/archive_bvid.py:135
msgid "{} 所有分p都已下载过了"
msgstr ""
#: biliarchiver/archive_bvid.py:143
msgid "{} 获取 video_info 失败,原因:{}"
msgstr ""
#: biliarchiver/archive_bvid.py:153
msgid ""
"{} 的 P{} 不存在 (可能视频被 UP 主 / B 站删了),请报告此问题,我们需要这个样"
"本!"
msgstr ""
#: biliarchiver/archive_bvid.py:164
msgid "{}: 已经下载过了"
msgstr ""
#: biliarchiver/archive_bvid.py:173
msgid "{}: {},删除缓存: {}"
msgstr ""
#: biliarchiver/archive_bvid.py:176
msgid "为防出错,清空上次未完成的下载缓存"
msgstr ""
#: biliarchiver/archive_bvid.py:211
msgid "没有 dvh、avc 或 hevc 编码的视频"
msgstr ""
#: biliarchiver/archive_bvid.py:214
msgid "未解析到 dash 资源,交给 bilix 处理 ..."
msgstr ""
#: biliarchiver/archive_bvid.py:218
msgid "未解析到视频资源"
msgstr ""
#: biliarchiver/archive_bvid.py:246
msgid "出错,其他任务完成后将抛出异常..."
msgstr ""
#: biliarchiver/archive_bvid.py:255
msgid "下载出错"
msgstr ""
#: biliarchiver/archive_bvid.py:260
msgid "{}: 视频文件没有被下载?也许是 hevc 对应的 dash 资源不存在,尝试 avc ……"
msgstr ""
#: biliarchiver/archive_bvid.py:307
msgid "{} 的视频详情已存在"
msgstr ""
#: biliarchiver/archive_bvid.py:315
msgid "{} 的视频详情获取失败"
msgstr ""
#: biliarchiver/archive_bvid.py:320
msgid "{} 的视频详情已保存"
msgstr ""
#: biliarchiver/cli_tools/biliarchiver.py:48
msgid "初始化所需目录"
msgstr ""
#: biliarchiver/cli_tools/biliarchiver.py:63
msgid "配置账号信息"
msgstr ""
#: biliarchiver/cli_tools/biliarchiver.py:66
msgid ""
"登录后将哔哩哔哩的 cookies 复制到 `config.json` 指定的文件(默认为 `~/."
"cookies.txt`)中。"
msgstr ""
#: biliarchiver/cli_tools/biliarchiver.py:69
msgid "前往 https://archive.org/account/s3.php 获取 Access Key 和 Secret Key。"
msgstr ""
#: biliarchiver/cli_tools/biliarchiver.py:70
msgid ""
"将它们放在 `config.json` 指定的文件(默认为 `~/.bili_ia_keys.txt`)中,两者由"
"换行符分隔。"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:72
msgid "ffmpeg 未安装"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:126
msgid "剩余空间不足 {} GiB"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:141
msgid "IA 上已存在 {},跳过"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:149
msgid "{} 的所有分p都已下载过了"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:182
#: biliarchiver/cli_tools/bili_archive_bvids.py:220
msgid "从 {} 品尝了 {} 块 cookies"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:195
msgid "cookies 文件不存在: {}"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:210
msgid "跳过重复的 cookies"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:222
msgid "吃了过多的 cookies可能导致 httpx.Client 怠工,响应非常缓慢"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:233
msgid "BiliBili 登录成功,饼干真香。"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:234
msgid "NOTICE: 存档过程中请不要在 cookies 的源浏览器访问 B 站,避免 B 站刷新"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:235
msgid "cookies 导致我们半路下到的视频全是 480P 的优酷土豆级醇享画质。"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:237
msgid "未登录/SESSDATA无效/过期,你这饼干它保真吗?"
msgstr ""
#: biliarchiver/cli_tools/bili_archive_bvids.py:242
msgid "已废弃直接运行此命令,请改用 biliarchiver 命令"
msgstr ""
#: biliarchiver/cli_tools/down_command.py:6
msgid "从哔哩哔哩下载"
msgstr ""
#: biliarchiver/cli_tools/down_command.py:8
msgid "空白字符分隔的 bvids 列表(记得加引号),或文件路径"
msgstr ""
#: biliarchiver/cli_tools/down_command.py:16
msgid "不检查 IA 上是否已存在对应 BVID 的 item ,直接开始下载"
msgstr ""
#: biliarchiver/cli_tools/down_command.py:23
msgid "从指定浏览器导入 cookies (否则导入 config.json 中的 cookies_file)"
msgstr ""
#: biliarchiver/cli_tools/down_command.py:29
msgid "最小剩余空间 (GB),用超退出"
msgstr ""
#: biliarchiver/cli_tools/down_command.py:33
msgid "跳过文件开头 bvid 的个数"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:26
#, python-brace-format
msgid "正在获取 {sid} 的视频列表……"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:38
msgid "已获取 {}{})的一个视频"
msgid_plural "已获取 {}{})的 {count} 个视频"
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:43
msgid "存储到 {}"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:75
#: biliarchiver/cli_tools/get_command.py:132
#: biliarchiver/cli_tools/get_command.py:156
#: biliarchiver/cli_tools/get_command.py:180
#: biliarchiver/cli_tools/get_command.py:252
msgid "已保存一个 bvid 到 {}"
msgid_plural "已保存 {count} 个 bvid 到 {}"
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:93
msgid "mid 应是数字字符串"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:101
msgid "获取第 {} 页..."
msgid_plural "获取第 {} 页..."
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:108
msgid "{} {} 共 {} 个视频."
msgid_plural "{} {} 共 {} 个视频."
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:112
msgid ""
"如果最新的视频为合作视频的非主作者UP 名可能会识别错误,但不影响获取 bvid "
"列表)"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:116
msgid "获取第 {} 页 (10 秒...)"
msgid_plural "获取第 {} 页 (10 秒...)"
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:123
msgid "有重复的 bv_id"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:124
msgid "视频总数不匹配"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:228
msgid "还剩 ~{} 页"
msgid_plural "还剩 ~{} 页"
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:237
msgid "有重复的 bvid"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:240
msgid "{} 个有效视频,{} 个失效视频"
msgid_plural "{} 个有效视频,{} 个失效视频"
msgstr[0] ""
msgstr[1] ""
#: biliarchiver/cli_tools/get_command.py:306
msgid "批量获取 BV 号"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:307
msgid ""
"请通过 flag 指定至少一种批量获取 BV 号的方式。多个不同组的 flag 同时使用时,"
"将会先后通过不同方式获取。"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:309
msgid "合集"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:313
msgid "合集或视频列表内视频"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:316
msgid "排行榜"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:320
msgid "排行榜(全站榜,非个性推荐榜)"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:328
msgid "目标排行 rid0 为全站排行榜。rid 等于分区的 tid"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:331
msgid "UP 主"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:335
msgid "UP 主用户页投稿"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:338
msgid "入站必刷"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:341
msgid "入站必刷,更新频率低"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:344
msgid "每周必看"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:348
msgid "每周必看,每周五晚 18:00 更新"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:356
msgid "获取第几期(周)"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:360
msgid "自动获取全部的每周必看(增量)"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:363
msgid "收藏夹"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:367
msgid "用户收藏夹"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:379
msgid "ERROR: 请指定至少一种获取方式。"
msgstr ""
#: biliarchiver/cli_tools/up_command.py:28
msgid "上传至互联网档案馆"
msgstr ""
#: biliarchiver/cli_tools/up_command.py:29
msgid "bvids 列表的文件路径"
msgstr ""
#: biliarchiver/cli_tools/up_command.py:34
msgid "使用 `$storage_home_dir/videos` 目录下的所有视频"
msgstr ""
#: biliarchiver/cli_tools/up_command.py:36
msgid "更新已存在的 item"
msgstr ""
#: biliarchiver/cli_tools/up_command.py:47
msgid "欲上传至的 collection. (非默认值仅限 collection 管理员使用)"
msgstr ""
#: biliarchiver/cli_tools/utils.py:19
msgid "bvid {} 不合法"
msgstr ""
#: biliarchiver/cli_tools/utils.py:21
msgid "bvids 为空"
msgstr ""
#: biliarchiver/config.py:33
msgid "{} 不存在,创建中..."
msgstr ""
#: biliarchiver/exception.py:25
msgid "请更新 biliarchiver。当前版本已过期"
msgstr ""
#: biliarchiver/exception.py:33
msgid "先在当前工作目录运行 `biliarchiver init` 以初始化配置"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:30
msgid "已经有一个上传 {} 的进程在运行,跳过"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:32
msgid ""
"没有找到 {} 对应的文件夹。可能是因已存在 IA item 而跳过了下载,或者你传入了错"
"误的 bvid"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:34
msgid "{} 的视频还没有下载完成,跳过"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:36
msgid "上传 {} 时出错:"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:64
msgid "{} => {} 已经上传过了(_uploaded.mark)"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:70
msgid "跳过带 _ 前缀的 local_identifier: {}"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:74
msgid "{} 不是以 {} 开头的正确 local_identifier"
msgstr ""
#: biliarchiver/_biliarchiver_upload_bvid.py:80
msgid "没有下载完成"
msgstr ""

View File

@ -0,0 +1,422 @@
# English translations for PACKAGE package.
# Copyright (C) 2023 THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Neko <overflowcat@gmail.com>, 2023.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-17 00:03+0800\n"
"PO-Revision-Date: 2023-08-17 00:05+0800\n"
"Last-Translator: Neko <overflowcat@gmail.com>\n"
"Language-Team: English\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: biliarchiver/archive_bvid.py:30
msgid "未找到字幕信息"
msgstr "Subtitle information not found"
#: biliarchiver/archive_bvid.py:114
msgid "hierarchy 必须为 True"
msgstr "hierarchy must be True"
#: biliarchiver/archive_bvid.py:116
msgid "sess_data 不能为空"
msgstr "sess_data cannot be empty"
#: biliarchiver/archive_bvid.py:118
msgid "请先检查 SESSDATA 是否过期,再将 logined 设置为 True"
msgstr "Please check if SESSDATA has expired first, then set logined to True"
#: biliarchiver/archive_bvid.py:127
msgid "检测到旧的视频目录 {},将其重命名为 {}..."
msgstr "Detected old video directory {}, renaming it to {}..."
#: biliarchiver/archive_bvid.py:135
msgid "{} 所有分p都已下载过了"
msgstr "All parts of {} have already been downloaded"
#: biliarchiver/archive_bvid.py:143
msgid "{} 获取 video_info 失败,原因:{}"
msgstr "Failed to get video_info for {}, reason: {}"
#: biliarchiver/archive_bvid.py:153
msgid "{} 的 P{} 不存在 (可能视频被 UP 主 / B 站删了),请报告此问题,我们需要这个样本!"
msgstr ""
"P{} of {} does not exist (video might have been deleted by UP master / B site), please report this issue, we need this "
"sample!"
#: biliarchiver/archive_bvid.py:164
msgid "{}: 已经下载过了"
msgstr "{}: Already downloaded"
#: biliarchiver/archive_bvid.py:173
msgid "{}: {},删除缓存: {}"
msgstr "{}: {}, deleting cache: {}"
#: biliarchiver/archive_bvid.py:176
msgid "为防出错,清空上次未完成的下载缓存"
msgstr "To prevent errors, clear the last unfinished download cache"
#: biliarchiver/archive_bvid.py:211
msgid "没有 dvh、avc 或 hevc 编码的视频"
msgstr "No video with dvh, avc, or hevc encoding"
#: biliarchiver/archive_bvid.py:214
msgid "未解析到 dash 资源,交给 bilix 处理 ..."
msgstr "Failed to parse dash resource, handing it over to bilix ..."
#: biliarchiver/archive_bvid.py:218
msgid "未解析到视频资源"
msgstr "Failed to parse video resource"
#: biliarchiver/archive_bvid.py:246
msgid "出错,其他任务完成后将抛出异常..."
msgstr "Error, exception will be thrown after other tasks are completed..."
#: biliarchiver/archive_bvid.py:255
msgid "下载出错"
msgstr "Download error"
#: biliarchiver/archive_bvid.py:260
msgid "{}: 视频文件没有被下载?也许是 hevc 对应的 dash 资源不存在,尝试 avc ……"
msgstr "{}: Video file not downloaded? Maybe the dash resource corresponding to hevc does not exist, trying avc ..."
#: biliarchiver/archive_bvid.py:307
msgid "{} 的视频详情已存在"
msgstr "Video details of {} already exist"
#: biliarchiver/archive_bvid.py:315
msgid "{} 的视频详情获取失败"
msgstr "Failed to obtain video details of {}"
#: biliarchiver/archive_bvid.py:320
msgid "{} 的视频详情已保存"
msgstr "Video details of {} have been saved"
#: biliarchiver/cli_tools/biliarchiver.py:48
msgid "初始化所需目录"
msgstr "Initialize required directories"
#: biliarchiver/cli_tools/biliarchiver.py:63
msgid "配置账号信息"
msgstr "Configure account information"
#: biliarchiver/cli_tools/biliarchiver.py:66
msgid "登录后将哔哩哔哩的 cookies 复制到 `config.json` 指定的文件(默认为 `~/.cookies.txt`)中。"
msgstr "After logging in, copy the Bilibili cookies to the file specified by `config.json` (default is `~/.cookies.txt`)."
#: biliarchiver/cli_tools/biliarchiver.py:69
msgid "前往 https://archive.org/account/s3.php 获取 Access Key 和 Secret Key。"
msgstr "Visit https://archive.org/account/s3.php to obtain the Access Key and Secret Key."
#: biliarchiver/cli_tools/biliarchiver.py:70
msgid "将它们放在 `config.json` 指定的文件(默认为 `~/.bili_ia_keys.txt`)中,两者由换行符分隔。"
msgstr "Place them in the file specified by `config.json` (default is `~/.bili_ia_keys.txt`), separated by a newline."
#: biliarchiver/cli_tools/bili_archive_bvids.py:72
msgid "ffmpeg 未安装"
msgstr "ffmpeg is not installed"
#: biliarchiver/cli_tools/bili_archive_bvids.py:126
msgid "剩余空间不足 {} GiB"
msgstr "Insufficient remaining space {} GiB"
#: biliarchiver/cli_tools/bili_archive_bvids.py:141
msgid "IA 上已存在 {},跳过"
msgstr "Already exists on IA {}, skipping"
#: biliarchiver/cli_tools/bili_archive_bvids.py:149
msgid "{} 的所有分p都已下载过了"
msgstr "All parts of {} have been downloaded"
#: biliarchiver/cli_tools/bili_archive_bvids.py:182
#: biliarchiver/cli_tools/bili_archive_bvids.py:220
msgid "从 {} 品尝了 {} 块 cookies"
msgstr "Tasted {} pieces of cookies from {}"
#: biliarchiver/cli_tools/bili_archive_bvids.py:195
msgid "cookies 文件不存在: {}"
msgstr "Cookies file does not exist: {}"
#: biliarchiver/cli_tools/bili_archive_bvids.py:210
msgid "跳过重复的 cookies"
msgstr "Skipping duplicate cookies"
#: biliarchiver/cli_tools/bili_archive_bvids.py:222
msgid "吃了过多的 cookies可能导致 httpx.Client 怠工,响应非常缓慢"
msgstr "Too many cookies can slow down httpx.Client"
#: biliarchiver/cli_tools/bili_archive_bvids.py:233
msgid "BiliBili 登录成功,饼干真香。"
msgstr "Logged in successfully."
#: biliarchiver/cli_tools/bili_archive_bvids.py:234
msgid "NOTICE: 存档过程中请不要在 cookies 的源浏览器访问 B 站,避免 B 站刷新"
msgstr "NOTICE: During the archiving process, please do not visit the website in the browser that you got the cookies from,"
#: biliarchiver/cli_tools/bili_archive_bvids.py:235
msgid "cookies 导致我们半路下到的视频全是 480P 的优酷土豆级醇享画质。"
msgstr "or we may downloaded blurry 480P videos halfway."
#: biliarchiver/cli_tools/bili_archive_bvids.py:237
msgid "未登录/SESSDATA无效/过期,你这饼干它保真吗?"
msgstr "Not logged in, or SESSDATA invalid/expired. Your cookies seems to have expired."
#: biliarchiver/cli_tools/bili_archive_bvids.py:242
msgid "已废弃直接运行此命令,请改用 biliarchiver 命令"
msgstr "This command is deprecated. Use `biliarchiver down` instead."
#: biliarchiver/cli_tools/down_command.py:6
msgid "从哔哩哔哩下载"
msgstr "Download from BiliBili"
#: biliarchiver/cli_tools/down_command.py:8
msgid "空白字符分隔的 bvids 列表(记得加引号),或文件路径"
msgstr "List of bvids separated by whitespace characters (remember to add quotes) or file path"
#: biliarchiver/cli_tools/down_command.py:16
msgid "不检查 IA 上是否已存在对应 BVID 的 item ,直接开始下载"
msgstr "Start downloading directly without checking if the corresponding BVID item already exists on IA"
#: biliarchiver/cli_tools/down_command.py:23
msgid "从指定浏览器导入 cookies (否则导入 config.json 中的 cookies_file)"
msgstr "Import cookies from the specified browser (otherwise import from cookies_file in config.json)"
#: biliarchiver/cli_tools/down_command.py:29
msgid "最小剩余空间 (GB),用超退出"
msgstr "Minimum free space (GB), exit if exceeded"
#: biliarchiver/cli_tools/down_command.py:33
msgid "跳过文件开头 bvid 的个数"
msgstr "Skip the number of bvids at the beginning of the file"
#: biliarchiver/cli_tools/get_command.py:26
#, python-brace-format
msgid "正在获取 {sid} 的视频列表……"
msgstr "Fetching the video list of {sid}…"
#: biliarchiver/cli_tools/get_command.py:39
msgid "已获取 {}{})的一个视频"
msgid_plural "已获取 {}{})的 {count} 个视频"
msgstr[0] "Fetched one video from {}({})"
msgstr[1] "Fetched {count} videos from {}({})"
#: biliarchiver/cli_tools/get_command.py:43
msgid "存储到 {}"
msgstr "Saved to {}"
#: biliarchiver/cli_tools/get_command.py:75
#: biliarchiver/cli_tools/get_command.py:132
#: biliarchiver/cli_tools/get_command.py:156
#: biliarchiver/cli_tools/get_command.py:180
#: biliarchiver/cli_tools/get_command.py:252
msgid "已保存一个 bvid 到 {}"
msgid_plural "已保存 {count} 个 bvid 到 {}"
msgstr[0] "Saved one bvid to {}"
msgstr[1] "Saved {count} bvids to {}"
#: biliarchiver/cli_tools/get_command.py:93
msgid "mid 应是数字字符串"
msgstr "mid should be a numeric string"
#: biliarchiver/cli_tools/get_command.py:101
msgid "获取第 {} 页..."
msgid_plural "获取第 {} 页..."
msgstr[0] "Fetching page {}..."
msgstr[1] "Fetching pages {}..."
#: biliarchiver/cli_tools/get_command.py:108
msgid "{} {} 共 {} 个视频."
msgid_plural "{} {} 共 {} 个视频."
msgstr[0] "{} {} has {} video."
msgstr[1] "{} {} has {} videos."
#: biliarchiver/cli_tools/get_command.py:112
msgid "如果最新的视频为合作视频的非主作者UP 名可能会识别错误,但不影响获取 bvid 列表)"
msgstr ""
"(If the latest video is by a non-primary author of a collaborative video, the UP name may be misidentified, but it "
"doesn't affect fetching the bvid list)"
#: biliarchiver/cli_tools/get_command.py:116
msgid "获取第 {} 页 (10 秒...)"
msgid_plural "获取第 {} 页 (10 秒...)"
msgstr[0] "Fetching page {} (10 seconds...)"
msgstr[1] "Fetching pages {} (10 seconds...)"
#: biliarchiver/cli_tools/get_command.py:123
msgid "有重复的 bv_id"
msgstr ""
#: biliarchiver/cli_tools/get_command.py:124
msgid "视频总数不匹配"
msgstr "Video count mismatch"
#: biliarchiver/cli_tools/get_command.py:228
msgid "还剩 ~{} 页"
msgid_plural "还剩 ~{} 页"
msgstr[0] "~{} page left"
msgstr[1] "~{} pages left"
#: biliarchiver/cli_tools/get_command.py:237
msgid "有重复的 bvid"
msgstr "Duplicate bvid exists"
#: biliarchiver/cli_tools/get_command.py:240
msgid "{} 个有效视频,{} 个失效视频"
msgid_plural "{} 个有效视频,{} 个失效视频"
msgstr[0] "{} valid video, {} invalid video"
msgstr[1] "{} valid videos, {} invalid videos"
#: biliarchiver/cli_tools/get_command.py:306
msgid "批量获取 BV 号"
msgstr "Batch fetching BV numbers"
#: biliarchiver/cli_tools/get_command.py:307
msgid "请通过 flag 指定至少一种批量获取 BV 号的方式。多个不同组的 flag 同时使用时,将会先后通过不同方式获取。"
msgstr ""
"Please specify at least one way to batch fetch BV numbers through the flag.When flags of multiple groups are used "
"together, they will be fetched in sequence."
#: biliarchiver/cli_tools/get_command.py:309
msgid "合集"
msgstr "Collection"
#: biliarchiver/cli_tools/get_command.py:313
msgid "合集或视频列表内视频"
msgstr "Videos in collection or video list"
#: biliarchiver/cli_tools/get_command.py:316
msgid "排行榜"
msgstr "Ranking"
#: biliarchiver/cli_tools/get_command.py:320
msgid "排行榜(全站榜,非个性推荐榜)"
msgstr "Ranking (site-wide, not personalized recommendations)"
#: biliarchiver/cli_tools/get_command.py:328
msgid "目标排行 rid0 为全站排行榜。rid 等于分区的 tid"
msgstr "Target ranking rid, 0 for the site-wide ranking. rid is equal to the section's tid"
#: biliarchiver/cli_tools/get_command.py:331
msgid "UP 主"
msgstr "Uploader"
#: biliarchiver/cli_tools/get_command.py:335
msgid "UP 主用户页投稿"
msgstr "Uploader's posts"
#: biliarchiver/cli_tools/get_command.py:338
msgid "入站必刷"
msgstr "Must-watch on entry"
#: biliarchiver/cli_tools/get_command.py:341
msgid "入站必刷,更新频率低"
msgstr "Must-watch on entry, low update frequency"
#: biliarchiver/cli_tools/get_command.py:344
msgid "每周必看"
msgstr "Must-watch weekly"
#: biliarchiver/cli_tools/get_command.py:348
msgid "每周必看,每周五晚 18:00 更新"
msgstr "Must-watch weekly, updated every Friday at 18:00 UTC+8"
#: biliarchiver/cli_tools/get_command.py:356
msgid "获取第几期(周)"
msgstr "Fetch which period (week)"
#: biliarchiver/cli_tools/get_command.py:360
msgid "自动获取全部的每周必看(增量)"
msgstr "Automatically fetch all weekly must-watches (incrementally)"
#: biliarchiver/cli_tools/get_command.py:363
msgid "收藏夹"
msgstr "Favorites"
#: biliarchiver/cli_tools/get_command.py:367
msgid "用户收藏夹"
msgstr "User's favlist"
#: biliarchiver/cli_tools/get_command.py:379
msgid "ERROR: 请指定至少一种获取方式。"
msgstr "ERROR: Please specify at least one retrieval method."
#: biliarchiver/cli_tools/up_command.py:28
msgid "上传至互联网档案馆"
msgstr "Upload to Internet Archive"
#: biliarchiver/cli_tools/up_command.py:29
msgid "bvids 列表的文件路径"
msgstr "File path of the bvids list"
#: biliarchiver/cli_tools/up_command.py:34
msgid "使用 `$storage_home_dir/videos` 目录下的所有视频"
msgstr "Use all videos under the `$storage_home_dir/videos` directory"
#: biliarchiver/cli_tools/up_command.py:36
msgid "更新已存在的 item"
msgstr "Update an existing item"
#: biliarchiver/cli_tools/up_command.py:47
msgid "欲上传至的 collection. (非默认值仅限 collection 管理员使用)"
msgstr "Collection to upload to. (Non-default values are for collection administrators only)"
#: biliarchiver/cli_tools/utils.py:19
msgid "bvid {} 不合法"
msgstr "bvid {} is invalid"
#: biliarchiver/cli_tools/utils.py:21
msgid "bvids 为空"
msgstr "bvids are empty"
#: biliarchiver/config.py:33
msgid "{} 不存在,创建中..."
msgstr "{} does not exist. Creating..."
#: biliarchiver/exception.py:25
msgid "请更新 biliarchiver。当前版本已过期"
msgstr "Please update biliarchiver. The current version is outdated:"
#: biliarchiver/exception.py:33
msgid "先在当前工作目录运行 `biliarchiver init` 以初始化配置"
msgstr "Run `biliarchiver init` first to initialize the configuration for the current working directory"
#: biliarchiver/_biliarchiver_upload_bvid.py:30
msgid "已经有一个上传 {} 的进程在运行,跳过"
msgstr "There's already a process uploading {} running. Skip"
#: biliarchiver/_biliarchiver_upload_bvid.py:32
msgid "没有找到 {} 对应的文件夹。可能是因已存在 IA item 而跳过了下载,或者你传入了错误的 bvid"
msgstr ""
"Did not find the folder corresponding to {}. Might be due to an existing IA item so the download was skipped, or you "
"entered a wrong bvid"
#: biliarchiver/_biliarchiver_upload_bvid.py:34
msgid "{} 的视频还没有下载完成,跳过"
msgstr "The video of {} has not been downloaded yet, skipping"
#: biliarchiver/_biliarchiver_upload_bvid.py:36
msgid "上传 {} 时出错:"
msgstr "Error occurred when uploading {}:"
#: biliarchiver/_biliarchiver_upload_bvid.py:64
msgid "{} => {} 已经上传过了(_uploaded.mark)"
msgstr "{} => {} has already been uploaded (_uploaded.mark)"
#: biliarchiver/_biliarchiver_upload_bvid.py:70
msgid "跳过带 _ 前缀的 local_identifier: {}"
msgstr "Skipping local_identifier with _ prefix: {}"
#: biliarchiver/_biliarchiver_upload_bvid.py:74
msgid "{} 不是以 {} 开头的正确 local_identifier"
msgstr "{} is not the correct local_identifier that starts with {}"
#: biliarchiver/_biliarchiver_upload_bvid.py:80
msgid "没有下载完成"
msgstr "Download not finished"

View File

@ -1 +1 @@
BILI_ARCHIVER_VERSION = '0.1.0'
BILI_ARCHIVER_VERSION = "0.1.1"

8
build.py Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env python3
import subprocess
if __name__ == "__main__":
print("Building i18n...")
subprocess.run(["msgfmt", "biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po", "-o", "biliarchiver/locales/en/LC_MESSAGES/biliarchiver.mo"])
print("Building with poetry...")
subprocess.run(["poetry", "build"])

View File

@ -5,6 +5,7 @@ description = ""
authors = ["yzqzss <yzqzss@yandex.com>"]
readme = "README.md"
packages = [{ include = "biliarchiver" }]
include = ["biliarchiver/locales/**/*.mo"]
[tool.poetry.dependencies]
python = "^3.9"
@ -27,7 +28,6 @@ build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
select = ["E9", "F63", "F7", "F82"]
ignore = []
@ -36,28 +36,13 @@ fixable = ["ALL"]
unfixable = []
# Exclude a variety of commonly ignored directories.
exclude = [
".direnv",
".git",
".git-rewrite",
".pants.d",
".pytype",
".ruff_cache",
".js",
".venv",
"__pypackages__",
"_build",
"build",
"dist",
"node_modules",
"venv",
]
exclude = []
per-file-ignores = {}
# Same as Black.
line-length = 127
# Assume Python 3.8
# Assume Python 3.9
target-version = "py39"
[tool.ruff.mccabe]