mirror of
https://github.com/saveweb/biliarchiver.git
synced 2024-09-19 11:05:28 -07:00
Merge pull request #6 from OverflowCat/i18n
feat(i18n): add `en` locale
This commit is contained in:
commit
67c4ef28db
6
.github/workflows/python-package.yaml
vendored
6
.github/workflows/python-package.yaml
vendored
@ -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
2
.gitignore
vendored
@ -4,6 +4,6 @@ config.json
|
||||
.venv/
|
||||
__pycache__/
|
||||
videos/
|
||||
.vscode/
|
||||
.vscode/settings.json
|
||||
bilibili_archive_dir/
|
||||
dist/
|
||||
|
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"charliermarsh.ruff",
|
||||
"tamasfe.even-better-toml",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
77
README.md
77
README.md
@ -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
|
||||
```
|
||||
|
@ -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
|
||||
|
@ -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' # 超详细 API(BV 级别,不是分 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" # 超详细 API(BV 级别,不是分 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))
|
||||
|
@ -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 命令"))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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='目标排行 rid,0 为全站排行榜。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("目标排行 rid,0 为全站排行榜。rid 等于分区的 tid", fg="yellow"),
|
||||
help=click.style(_("目标排行 rid,0 为全站排行榜。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))
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
16
biliarchiver/i18n.py
Normal 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
1
biliarchiver/locales/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.mo
|
427
biliarchiver/locales/biliarchiver.pot
Normal file
427
biliarchiver/locales/biliarchiver.pot
Normal 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 "目标排行 rid,0 为全站排行榜。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 ""
|
422
biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po
Normal file
422
biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po
Normal 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 "目标排行 rid,0 为全站排行榜。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"
|
@ -1 +1 @@
|
||||
BILI_ARCHIVER_VERSION = '0.1.0'
|
||||
BILI_ARCHIVER_VERSION = "0.1.1"
|
||||
|
8
build.py
Executable file
8
build.py
Executable 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"])
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user