diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml index 655b070..b2bc52e 100644 --- a/.github/workflows/python-package.yaml +++ b/.github/workflows/python-package.yaml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | 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 . diff --git a/.gitignore b/.gitignore index d3e920a..bed6e62 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ config.json .venv/ __pycache__/ videos/ -.vscode/ +.vscode/settings.json bilibili_archive_dir/ dist/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ff320c1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "tamasfe.even-better-toml", + "esbenp.prettier-vscode" + ] +} diff --git a/README.md b/README.md index 1acea63..7e143bb 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,75 @@ 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 +``` + +### Run + +```sh +poetry run biliarchiver --help +``` + +### Lint + +```sh +poetry run ruff check . +``` + +### i18n + +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 +``` + +Build a language: + +```sh +msgfmt biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po -o biliarchiver/locales/en/LC_MESSAGES/biliarchiver.mo +``` diff --git a/biliarchiver/_biliarchiver_upload_bvid.py b/biliarchiver/_biliarchiver_upload_bvid.py index 45c55eb..6731f72 100644 --- a/biliarchiver/_biliarchiver_upload_bvid.py +++ b/biliarchiver/_biliarchiver_upload_bvid.py @@ -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 \ No newline at end of file + return access_key, secret_key diff --git a/biliarchiver/archive_bvid.py b/biliarchiver/archive_bvid.py index bc01390..05d4db6 100644 --- a/biliarchiver/archive_bvid.py +++ b/biliarchiver/archive_bvid.py @@ -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)) diff --git a/biliarchiver/cli_tools/bili_archive_bvids.py b/biliarchiver/cli_tools/bili_archive_bvids.py index 205bd9f..460ac67 100644 --- a/biliarchiver/cli_tools/bili_archive_bvids.py +++ b/biliarchiver/cli_tools/bili_archive_bvids.py @@ -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 命令")) diff --git a/biliarchiver/cli_tools/biliarchiver.py b/biliarchiver/cli_tools/biliarchiver.py index 3d864e8..a5b574b 100644 --- a/biliarchiver/cli_tools/biliarchiver.py +++ b/biliarchiver/cli_tools/biliarchiver.py @@ -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() diff --git a/biliarchiver/cli_tools/down_command.py b/biliarchiver/cli_tools/down_command.py index fa80ac3..281698f 100644 --- a/biliarchiver/cli_tools/down_command.py +++ b/biliarchiver/cli_tools/down_command.py @@ -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 diff --git a/biliarchiver/cli_tools/get_command.py b/biliarchiver/cli_tools/get_command.py index fb0ddb3..54acbb9 100644 --- a/biliarchiver/cli_tools/get_command.py +++ b/biliarchiver/cli_tools/get_command.py @@ -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)) diff --git a/biliarchiver/cli_tools/up_command.py b/biliarchiver/cli_tools/up_command.py index 5485c77..1065f5f 100644 --- a/biliarchiver/cli_tools/up_command.py +++ b/biliarchiver/cli_tools/up_command.py @@ -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, diff --git a/biliarchiver/cli_tools/utils.py b/biliarchiver/cli_tools/utils.py index 86e6ced..1c47ad1 100644 --- a/biliarchiver/cli_tools/utils.py +++ b/biliarchiver/cli_tools/utils.py @@ -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 diff --git a/biliarchiver/config.py b/biliarchiver/config.py index 1cf39be..c65535f 100644 --- a/biliarchiver/config.py +++ b/biliarchiver/config.py @@ -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() diff --git a/biliarchiver/exception.py b/biliarchiver/exception.py index af3ffa9..14f533d 100644 --- a/biliarchiver/exception.py +++ b/biliarchiver/exception.py @@ -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 \ No newline at end of file + return _("请更新 biliarchiver。当前版本已过期:") + str(self.version) + + +class DirNotInitializedError(Exception): + def __init__(self): + pass + + def __str__(self): + return _("先在当前工作目录运行 `biliarchiver init` 以初始化配置") diff --git a/biliarchiver/i18n.py b/biliarchiver/i18n.py new file mode 100644 index 0000000..3201b05 --- /dev/null +++ b/biliarchiver/i18n.py @@ -0,0 +1,10 @@ +import gettext + +appname = "biliarchiver" + +i18n = gettext.translation( + appname, localedir="biliarchiver/locales", fallback=True, languages=["en"] +) + +_ = i18n.gettext +ngettext = i18n.ngettext diff --git a/biliarchiver/locales/.gitignore b/biliarchiver/locales/.gitignore new file mode 100644 index 0000000..6bb0d4c --- /dev/null +++ b/biliarchiver/locales/.gitignore @@ -0,0 +1 @@ +*.mo diff --git a/biliarchiver/locales/biliarchiver.pot b/biliarchiver/locales/biliarchiver.pot new file mode 100644 index 0000000..a2edffc --- /dev/null +++ b/biliarchiver/locales/biliarchiver.pot @@ -0,0 +1,423 @@ +# 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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-08-17 01:52+0800\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \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: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 "" diff --git a/biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po b/biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po new file mode 100644 index 0000000..ccde1e6 --- /dev/null +++ b/biliarchiver/locales/en/LC_MESSAGES/biliarchiver.po @@ -0,0 +1,418 @@ +# 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 , 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 \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 multiple group flags are used " +"simultaneously, 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: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" diff --git a/biliarchiver/version.py b/biliarchiver/version.py index 590cfac..25cf95b 100644 --- a/biliarchiver/version.py +++ b/biliarchiver/version.py @@ -1 +1 @@ -BILI_ARCHIVER_VERSION = '0.1.0' \ No newline at end of file +BILI_ARCHIVER_VERSION = "0.1.1" diff --git a/pyproject.toml b/pyproject.toml index 2840a16..c411535 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,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 = [] @@ -57,7 +56,7 @@ per-file-ignores = {} # Same as Black. line-length = 127 -# Assume Python 3.8 +# Assume Python 3.9 target-version = "py39" [tool.ruff.mccabe]