diff --git a/.gitignore b/.gitignore index 7c343a8..c5487d2 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ coverage.xml # Translations *.mo +*.po~ # Django stuff: *.log diff --git a/MANIFEST.in b/MANIFEST.in index 7a4e0a7..d99523a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ include LICENSE include README_zh.md +recursive-include src *LICENSE* +recursive-include src *README* diff --git a/README.md b/README.md index fc983d2..d0dbb5c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ work, please recommend it to your friends, Thanks. - Does not work in old versions of μTorrent which did not provided API `getpeers`. - **Please use this script in local network**, μTorrent Web API does not support HTTPS connections, it is not safe. +- If you can not accept read/write the ipfilter.dat file frequently, it can be + soft/symbolic link to a RAM disk. - I took preventive measures, if you stiil found a normal peer has been banned, please tell us via [issues board](https://github.com/SeaHOH/ban-peers/issues). @@ -88,19 +90,21 @@ Network File: ``` $ ban_peers -h -Welcome using Ban-Peers 0.9.1 +Welcome using Ban-Peers 0.9.2 -Usage: ban_peers [-H IP|DOMAIN] [-p PORT] [-a USERNAME:PASSWORD] [-e HOURS] - [-t MINUTES] [-f FORMAT] [-C] [-X] [-P] [-L] [-N] [-R] [-U] - [-A] [-O] [-h] [-v] - [IPFILTER-PATH] +usage: ban_peers.pyz [-H IP|DOMAIN] [-p PORT] [-a USERNAME:PASSWORD] [-e HOURS] + [-t MINUTES] [-f FORMAT] [-C] [-X] [-P] [-L] [-N] [-R] + [-U] [-A] [-O] [-s [CONFIG-FILE] | -l [CONFIG-FILE]] [-h] + [-v] + [IPFILTER-PATH] Checking & banning BitTorrent leech peers via Web API, remove ads, working for uTorrent. Positional Arguments: - IPFILTER-PATH Path of ipfilter dir/file, wait input if empty. IMPORTANT - NOTICE: must be the uTorrent settings path! + IPFILTER-PATH Path of ipfilter dir/file, will try load from config file + or wait input if empty. IMPORTANT NOTICE: must be the + uTorrent settings path! Optional Arguments: -H IP|DOMAIN, --host IP|DOMAIN @@ -110,11 +114,11 @@ Optional Arguments: -a USERNAME:PASSWORD, --authorization USERNAME:PASSWORD WebUI authorization, wait input if required -e HOURS, --expire HOURS - Ban expire time for peers, default 12 HOURS + Ban expire time for peers, default 12 hours -t MINUTES, --time-allowed-refuse MINUTES How much time to keep connecting before temporary banned - refused upload peers, at least 5 MINUTES, default 10 - MINUTES + refused upload peers, at least 5 minutes, default 10 + minutes -f FORMAT, --log-header FORMAT Format of log header, see time.strftime, default %H:%M:%S -C, --resolve-country @@ -136,28 +140,25 @@ Optional Arguments: Remove ads via set Advanced Settings, only working for localhost, and to fail in older uTorrent -O, --no-close-pairing - Don't turn off Web Pairing setting after remove ads + Don't turn off Web Pairing setting after + -s [CONFIG-FILE], --save-config [CONFIG-FILE] + Save current arguments to a config file except "--remove- + ads", "--help" and "--version". Save to default location + "/BanPeers/ban_peers.conf" if empty input + -l [CONFIG-FILE], --load-config [CONFIG-FILE] + Load arguments from a config file, will not overlaid the + inputted arguments. Load from current directory (use + conf/ini/cfg as extension name) or default location if + empty input -h, --help Show this help message and exit -v, --version Show version and exit ``` -```markdown -$ ban_peers -p 12345 -a username:password /var/lib/utserver -Welcome using Ban-Peers 0.9.1 -19:44:33 Set uTorrent setting 'webui.allow_pairing' to True -19:44:35 Set uTorrent setting 'gui.show_plus_upsell_nodes' to False **_Remove upsell tip in the sidebar_** -19:44:35 Set uTorrent setting 'webui.allow_pairing' to False **_disallow pairing_** -19:44:35 Set uTorrent setting 'bt.use_rangeblock' to False **_Won't restore after quit_** -19:44:35 Set uTorrent setting 'ipfilter.enable' to True -19:44:35 uTorrent auto-banning script start running -Choose your operation: (Q)uit, (S)top, (R)estart, (P)ause/Proceed -``` - -or - ```markdown $ ban-peers -Welcome using Ban-Peers 0.9.1 +Welcome using Ban-Peers 0.9.2 +No ipfilter has be inputted, try load from config file +Load ipfilter from config file fail, found nothing Please input uTorrent settings folder path or ipfilter file path: /var/lib/utserver Please input WebUI username: username @@ -167,8 +168,29 @@ Please input WebUI password: password **_No cover_** 19:44:35 Set uTorrent setting 'webui.allow_pairing' to False **_disallow pairing_** 19:44:35 Set uTorrent setting 'bt.use_rangeblock' to False **_Won't restore after quit_** 19:44:35 Set uTorrent setting 'ipfilter.enable' to True -19:44:35 uTorrent auto-banning script start running +19:44:35 Auto-banning script start running Choose your operation: (Q)uit, (S)top, (R)estart, (P)ause/Proceed +19:44:36 Auto-banning script quit running +... + +... +$ ban_peers -p 12345 -a username:password /var/lib/utserver --save-config +Welcome using Ban-Peers 0.9.2 +Start saving config file "/BanPeers/ban_peers.conf" +Save argument "ipfilter = /var/lib/utserver" +Save argument "port = 12345" +Save argument "authorization = username:password" +... + +... +$ ban-peers -p 54321 +Welcome using Ban-Peers 0.9.2 +No ipfilter has be inputted, try load from config file +Start loading config file "/BanPeers/ban_peers.conf" +Load argument "ipfilter = /var/lib/utserver" +**_Doesn't load inputted argument port_** +Load argument "authorization = username:password" +... ``` - Quit: exit the script. diff --git a/README_zh.md b/README_zh.md index 09ca7f1..f640768 100644 --- a/README_zh.md +++ b/README_zh.md @@ -20,6 +20,7 @@ # 注意事项 - 无法在未提供 `getpeers` API 的旧版本 μTorrent 中正常工作。 - **请在本地网络内使用此脚本**,μTorrent 网页 API 不支持 HTTPS 连接,它并不安全。 +- 如果无法接受频繁读写 ipfilter.dat 文件,可以将它软链接到内存盘。 - 虽然已采取一些预防措施,如果你仍然发现有正常的对端被错误屏蔽, 请反馈到 [issues 板块](https://github.com/SeaHOH/ban-peers/issues)。 @@ -78,18 +79,18 @@ Android: ``` ban_peers -h -欢迎使用 Ban-Peers 0.9.1 +欢迎使用 Ban-Peers 0.9.2 -用 法: ban_peers [-H IP|域名] [-p 端口] [-a 用户名:密码] [-e 小时] [-t 分钟] - [-f 格式] [-C] [-X] [-P] [-L] [-N] [-R] [-U] [-A] [-O] [-h] - [-v] - [IP屏蔽配置路径] +用 法: ban_peers.pyz [-H IP|域名] [-p 端口] [-a 用户名:密码] [-e 小时] + [-t 分钟] [-f 格式] [-C] [-X] [-P] [-L] [-N] [-R] [-U] + [-A] [-O] [-s [配置文件] | -l [配置文件]] [-h] [-v] + [IP屏蔽配置路径] 通过网页 API 检查并屏蔽 BitTorrent 吸血对端,移除广告,工作于 uTorrent。 位置参数: - IP屏蔽配置路径 ipfilter 目录或文件路径,留空将等待输入。重要提示: 必须是 - uTorrent 配置使用的路径! + IP屏蔽配置路径 ipfilter 目录或文件路径,留空将尝试从配置文件加载,或等待输 + 入。重要提示: 必须是 uTorrent 配置使用的路径! 可选参数: -H IP|域名, --host IP|域名 @@ -124,27 +125,23 @@ ban_peers -h 本的 uTorrent -O, --no-close-pairing 移除广告后,不关闭网络配对配置项 + -s [配置文件], --save-config [配置文件] + 保存当前参数到一个配置文件,不包括 "--remove-ads"、"--help" + 和 "--version",如果输入留空则保存到默认位置 "<你的配置目录> + \BanPeers\ban_peers.conf" + -l [配置文件], --load-config [配置文件] + 从一个配置文件加载参数,不会覆盖已输入的参数,如果输入留空 + 则尝试从当前目录 (使用 conf/ini/cfg 作为扩展名) 或默认位置 + 加载 -h, --help 显示此帮助信息并退出 -v, --version 显示版本信息并退出 ``` -```markdown -C:\Users\username>ban_peers -p 12345 -a username:password X:\uTorrent -欢迎使用 Ban-Peers 0.9.1 -19:44:33 设定 uTorrent 配置 'webui.allow_pairing' 到 True **_允许配对_** -19:44:35 设定 uTorrent 配置 'gui.show_plus_upsell_nodes' 到 False **_移除侧栏付费版升级提示_** -19:44:35 设定 uTorrent 配置 'webui.allow_pairing' 到 False **_禁止配对_** -19:44:35 设定 uTorrent 配置 'bt.use_rangeblock' 到 False **_脚本退出后不会自动恢复_** -19:44:35 设定 uTorrent 配置 'ipfilter.enable' 到 True -19:44:35 uTorrent 自动屏蔽脚本开始运行 -请选择你要执行的操作: (Q)退出,(S)停止,(R)重新开始,(P)暂停/恢复 -``` - -或者 - ```markdown C:\Users\username>ban_peers -欢迎使用 Ban-Peers 0.9.1 +欢迎使用 Ban-Peers 0.9.2 +没有输入 ipfilter,尝试从配置文件加载 +从配置文件加载 ipfilter 失败,什么都没有找到 请输入 uTorrent 配置文件夹路径,或者 ipfilter 文件路径: X:\uTorrent 请输入 WebUI 用户名: username @@ -156,6 +153,27 @@ X:\uTorrent 19:44:35 设定 uTorrent 配置 'ipfilter.enable' 到 True 19:44:35 uTorrent 自动屏蔽脚本开始运行 请选择你要执行的操作: (Q)退出,(S)停止,(R)重新开始,(P)暂停/恢复 +19:44:36 自动屏蔽脚本退出运行 +... + +... +C:\Users\username>ban_peers -p 12345 -a username:password X:\uTorrent --save-config +欢迎使用 Ban-Peers 0.9.2 +开始保存配置文件 "<你的配置目录>\BanPeers\ban_peers.conf" +保存参数 "ipfilter = X:\uTorrent" +保存参数 "port = 12345" +保存参数 "authorization = username:password" +... + +... +C:\Users\username>ban_peers -p 54321 +欢迎使用 Ban-Peers 0.9.2 +没有输入 ipfilter,尝试从配置文件加载 +开始保存配置文件 "\BanPeers\ban_peers.conf" +加载参数 "ipfilter = X:\uTorrent" +**_没有加载已输入的 port 参数_** +加载参数 "authorization = username:password" +... ``` - 退出:退出此脚本。 diff --git a/src/ban_peers/__init__.py b/src/ban_peers/__init__.py index a54da9a..260b798 100644 --- a/src/ban_peers/__init__.py +++ b/src/ban_peers/__init__.py @@ -8,7 +8,7 @@ uTorrent. """) __app_name__ = 'Ban-Peers' -__version__ = '0.9.1' +__version__ = '0.9.2' __author__ = 'SeaHOH' __email__ = 'seahoh@gmail.com' __license__ = 'MIT' @@ -397,12 +397,71 @@ class List2Attr: 'RELEVANCE': 21 } } - _32bit1 = ~(-1 << 32) - _32bit_signed_mini = -1 << 31 + + hash:str + status:int + name:str + size:int + progress:int + downloaded:int + uploaded:int + ratio:int + upspeed:int + downspeed:int + eta:int + label:str + peers_connected:int + peers_swarm:int + seeds_connected:int + seeds_swarm:int + availability:float + queue_position:int + remaining:int + download_url:str + rss_feed_url:str + status_message:str + stream_id:str + date_added:int + date_completed:int + app_update_url:str + save_path:str + + priority:int + first_piece:int + num_pieces:int + streamable:int + encoded_rate:int + duration:int + width:int + height:int + stream_eta:int + streamability:int + + country:str + ip:str + revdns:str + utp:int + port:int + client:str + flags:str + reqs_out:str + reqs_in:str + waited:int + hasherr:int + peerdl:int + maxup:int + maxdown:int + queued:int + inactive:int + relevance:int + _cache:dict _list:list _type:dict + _32bit1 = ~(-1 << 32) + _32bit_signed_mini = -1 << 31 + def __init__(self, list:List[Union[int, str]], type:str) -> None: object.__setattr__(self, '_cache', {}) object.__setattr__(self, '_list', list) @@ -426,7 +485,7 @@ def __getattr__(self, name:str) -> Union[int, float, str]: self._cache[name] = value return value - def __setattr__(self, name:str, value:Union[int, str]) -> None: + def __setattr__(self, name:str, value:Union[int, float, str]) -> None: name = name.upper() self._list[self._type[name]] if name == 'IP' and isinstance(value, str) and value[:1] == '[': @@ -456,8 +515,7 @@ def __init__(self, try: socket().connect((host, port)) except: - raise ValueError(_('Unable to connect %(host)s:%(port)d') - % {'host': host, 'port': port}) + raise ValueError(_('Unable to connect %(host)s:%(port)d') % vars()) self.file_ipfilter = ipfilter self._url_root = f'http://{host}:{port}/gui/' self._req = Request(self._url_root) @@ -637,7 +695,7 @@ def set_setting(self, s:str, v:Union[int, str, bool], log:bool=True) -> None: self.log(_('Set uTorrent setting %(name)r to %(value)s') % {'name': s, 'value': v}) - def get_props(self, hash:str) -> Dict: + def get_props(self, hash:str) -> dict: result = json.load(self.request(params={ 'action': 'getprops', 'hash': hash @@ -675,9 +733,6 @@ def ban_peers(self) -> None: def ban_push(self, hash:str, peer:List2Attr, reason:str='') -> None: ct = int(time.time()) ip = peer.ip - assert isinstance(ip, str) - assert isinstance(peer.downloaded, int) - assert isinstance(peer.uploaded, int) try: d = self._statistics.pop(ip) except KeyError: @@ -714,14 +769,6 @@ def log(msg): reasons = [] ct = time.monotonic() for torrent in self.get_torrents(): - assert isinstance(torrent.hash, str) - assert isinstance(torrent.name, str) - assert isinstance(torrent.size, int) - assert isinstance(torrent.progress, int) - assert isinstance(torrent.downloaded, int) - assert isinstance(torrent.eta, int) - assert isinstance(torrent.availability, float) - assert isinstance(torrent.remaining, int) size_millesimal = torrent.size // 1000 seeding = torrent.progress >= 1000 # uTorrent bug? hash = torrent.hash @@ -732,10 +779,8 @@ def log(msg): if self.check_fake_progress or self.check_serious_leech: files = list(self.get_files(hash)) size_todl = sum(file.size for file in files if file.priority) - assert isinstance(size_todl, int) size_todl_tenth = size_todl // 10 size_downloaded = sum(file.downloaded for file in files) - assert isinstance(size_downloaded, int) size_last_downloaded = torrent.downloaded - size_downloaded time_fp = 60 ratio_sl = 10 @@ -760,19 +805,6 @@ def log(msg): _1m // 2 if size_todl > _10g else _1m) allow_banned_refused_upload = torrent.availability > 10 for peer in self.get_peers(hash): - assert isinstance(peer.country, str) - assert isinstance(peer.ip, str) - assert isinstance(peer.port, int) - assert isinstance(peer.client, str) - assert isinstance(peer.flags, str) - assert isinstance(peer.progress, int) - assert isinstance(peer.downspeed, int) - assert isinstance(peer.upspeed, int) - assert isinstance(peer.waited, int) - assert isinstance(peer.uploaded, int) - assert isinstance(peer.downloaded, int) - assert isinstance(peer.inactive, int) - assert isinstance(peer.relevance, int) if peer.ip in self.ipfilter or peer.upspeed < 256 and \ peer.progress == peer.relevance == peer.downspeed == 0: continue @@ -816,7 +848,7 @@ def log(msg): assert isinstance(_sp, tuple) last_progress, last_uploaded, t = _sp fo = None - except (KeyError, TypeError): + except (KeyError, AssertionError): fo = True if fo or peer.progress < last_progress: last_progress = peer.progress @@ -997,7 +1029,7 @@ def log(self, *args, **kwargs) -> None: def run(self, pair=None, show_operations:bool=True) -> None: def log(state): - self.log(_('Auto-banning script %(state)s running') % {'state': state}) + self.log(_('Auto-banning script %(state)s running') % vars()) if self.running: return @@ -1134,9 +1166,8 @@ def get_session(self) -> None: self._session = result['session'] def set_setting(self, s:str, v:Union[int, str, bool]) -> None: - if self._session is None: + while self._session is None: self.get_session() - assert isinstance(self._session, str) while True: result = json.load(self.request({ 'session': self._session, @@ -1179,9 +1210,10 @@ def __exit__(self, etype, value, tb) -> Literal[True]: return True # Make no Exception would be raised -def main(args=None): +def main(argv=None): import inspect import argparse + from . import config try: tr = translation('argparse') @@ -1205,8 +1237,9 @@ def formatter_class(prog:str, *args, **kwargs) -> argparse.HelpFormatter: parser.add_argument('ipfilter', nargs='?', metavar=_('IPFILTER-PATH'), help=_( - 'Path of ipfilter dir/file, wait input if empty. ' - 'IMPORTANT NOTICE: must be the uTorrent settings path!')) + 'Path of ipfilter dir/file, will try load from config ' + 'file or wait input if empty. IMPORTANT NOTICE: must be ' + 'the uTorrent settings path!')) parser.add_argument('-H', '--host', type=str, metavar=_('IP|DOMAIN'), help=_( @@ -1235,8 +1268,8 @@ def formatter_class(prog:str, *args, **kwargs) -> argparse.HelpFormatter: metavar=_('FORMAT'), help=_( 'Format of log header, see time.strftime, ' - 'default %(time)s') - % {'time': kwargs['log_header_fmt'].replace("%", "%%")}) + 'default %(header)s') + % {'header': kwargs['log_header_fmt'].replace("%", "%%")}) parser.add_argument('-C', '--resolve-country', action='store_true', help=_( @@ -1276,6 +1309,26 @@ def formatter_class(prog:str, *args, **kwargs) -> argparse.HelpFormatter: action='store_true', help=_( 'Don\'t turn off Web Pairing setting after')) + cgroup = parser.add_mutually_exclusive_group() + cgroup.add_argument('-s', '--save-config', nargs='?', dest='config', + type=config.FileType('w'), + const=config._default_config_file, + metavar=_('CONFIG-FILE'), + help=_( + 'Save current arguments to a config file except ' + '"--remove-ads", "--help" and "--version". Save to ' + 'default location "%(config)s" if empty input') + % {'config': config._default_config_file}) + cgroup.add_argument('-l', '--load-config', nargs='?', dest='config', + type=config.FileType('r'), + const=config.current_dir_config_file() or + config._default_config_file, + metavar=_('CONFIG-FILE'), + help=_( + 'Load arguments from a config file, will not overlaid ' + 'the inputted arguments. Load from current directory ' + '(use conf/ini/cfg as extension name) or default ' + 'location if empty input')) parser.add_argument('-h', '--help', action='store_true', help=_( @@ -1284,7 +1337,7 @@ def formatter_class(prog:str, *args, **kwargs) -> argparse.HelpFormatter: action='store_true', help=_( 'Show version and exit')) - args = parser.parse_args(args) + args = parser.parse_args(argv) if args.version: print(__app_name__, __version__) @@ -1297,9 +1350,37 @@ def formatter_class(prog:str, *args, **kwargs) -> argparse.HelpFormatter: except ImportError: pass print() - print(parser.format_help()) + parser.print_help() sys.exit() + def get_logger(action): + def log(name, value): + action + print(_('%(action)s argument "%(name)s = %(value)s"') % vars()) + return log + + auto_loaded = False + if args.ipfilter is None and args.config is None: + print(_('No ipfilter has be inputted, try load from config file')) + # An automatic try, ignore errors + parser.error = lambda m: None + try: + parser.parse_args(['-l'], args) + except TypeError: + pass + finally: + auto_loaded = True + if args.config: + if args.config.writable(): + print(_('Start saving config file "%(name)s"') % {'name': args.config.name}) + config.save(vars(args), args.config, get_logger(_('Save'))) + else: + print(_('Start loading config file "%(name)s"') % {'name': args.config.name}) + config.load(vars(args), args.config, get_logger(_('Load'))) + args.config.close() + if args.ipfilter is None and auto_loaded: + print(_('Load ipfilter from config file fail, found nothing')) + if args.host: kwargs['host'] = args.host if args.port: diff --git a/src/ban_peers/appdirs.py b/src/ban_peers/appdirs.py new file mode 100644 index 0000000..5d274c0 --- /dev/null +++ b/src/ban_peers/appdirs.py @@ -0,0 +1,603 @@ +# -*- coding: utf-8 -*- +# type: ignore +# Copyright (c) 2005-2010 ActiveState Software Inc. +# Copyright (c) 2013 Eddy Petrișor + +"""Utilities for determining application-specific dirs. + +See for details and usage. +""" +# Dev Notes: +# - MSDN on where to store app data files: +# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120 +# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html +# - XDG spec for Un*x: https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +__version__ = "1.4.4" +__version_info__ = tuple(int(segment) for segment in __version__.split(".")) + +import sys +import os + +PY3 = sys.version_info[0] == 3 + +if PY3: + unicode = str + +if sys.platform.startswith('java'): + import platform + os_name = platform.java_ver()[3][0] + if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc. + system = 'win32' + elif os_name.startswith('Mac'): # "Mac OS X", etc. + system = 'darwin' + else: # "Linux", "SunOS", "FreeBSD", etc. + # Setting this to "linux2" is not ideal, but only Windows or Mac + # are actually checked for and the rest of the module expects + # *sys.platform* style strings. + system = 'linux2' +else: + system = sys.platform + + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by default "~/.local/share/". + """ + if system == "win32": + if appauthor is None: + appauthor = appname + const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == 'darwin': + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of data dirs should be + returned. By default, the first item from XDG_DATA_DIRS is + returned, or '/usr/local/share/', + if XDG_DATA_DIRS is not set + + Typical site data directories are: + Mac OS X: /Library/Application Support/ + Unix: /usr/local/share/ or /usr/share/ + Win XP: C:\Documents and Settings\All Users\Application Data\\ + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. + + For Unix, this is using the $XDG_DATA_DIRS[0] default. + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + elif system == 'darwin': + path = os.path.expanduser('/Library/Application Support') + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_DATA_DIRS + # only first, if multipath is False + path = os.getenv('XDG_DATA_DIRS', + os.pathsep.join(['/usr/local/share', '/usr/share'])) + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + if appname and version: + path = os.path.join(path, version) + return path + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user config directories are: + Mac OS X: ~/Library/Preferences/ + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. + That means, by default "~/.config/". + """ + if system == "win32": + path = user_data_dir(appname, appauthor, None, roaming) + elif system == 'darwin': + path = os.path.expanduser('~/Library/Preferences/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of config dirs should be + returned. By default, the first item from XDG_CONFIG_DIRS is + returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set + + Typical site config directories are: + Mac OS X: same as site_data_dir + Unix: /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in + $XDG_CONFIG_DIRS + Win *: same as site_data_dir + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + + For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if system == 'win32': + path = site_data_dir(appname, appauthor) + if appname and version: + path = os.path.join(path, version) + elif system == 'darwin': + path = os.path.expanduser('/Library/Preferences') + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [os.sep.join([x, appname]) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + +def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific cache dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Cache" to the base app data dir for Windows. See + discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Caches/ + Unix: ~/.cache/ (XDG default) + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache + Vista: C:\Users\\AppData\Local\\\Cache + + On Windows the only suggestion in the MSDN docs is that local settings go in + the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming + app data dir (the default returned by `user_data_dir` above). Apps typically + put cache data somewhere *under* the given dir here. Some examples: + ...\Mozilla\Firefox\Profiles\\Cache + ...\Acme\SuperApp\Cache\1.0 + OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. + This can be disabled with the `opinion=False` option. + """ + if system == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) + else: + path = os.path.join(path, appname) + if opinion: + path = os.path.join(path, "Cache") + elif system == 'darwin': + path = os.path.expanduser('~/Library/Caches') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_state_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific state dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user state directories are: + Mac OS X: same as user_data_dir + Unix: ~/.local/state/ # or in $XDG_STATE_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow this Debian proposal + to extend the XDG spec and support $XDG_STATE_HOME. + + That means, by default "~/.local/state/". + """ + if system in ["win32", "darwin"]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def user_log_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific log dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. You may + pass False to disable it. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Logs" to the base app data dir for Windows, and "log" to the + base cache dir for Unix. See discussion below. + + Typical user log directories are: + Mac OS X: ~/Library/Logs/ + Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs + Vista: C:\Users\\AppData\Local\\\Logs + + On Windows the only suggestion in the MSDN docs is that local settings + go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in + examples of what some windows apps use for a logs dir.) + + OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` + value for Windows and appends "log" to the user cache dir for Unix. + This can be disabled with the `opinion=False` option. + """ + if system == "darwin": + path = os.path.join( + os.path.expanduser('~/Library/Logs'), + appname) + elif system == "win32": + path = user_data_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "Logs") + else: + path = user_cache_dir(appname, appauthor, version) + version = False + if opinion: + path = os.path.join(path, "log") + if appname and version: + path = os.path.join(path, version) + return path + + +class AppDirs(object): + """Convenience wrapper for getting application dirs.""" + def __init__(self, appname=None, appauthor=None, version=None, + roaming=False, multipath=False): + self.appname = appname + self.appauthor = appauthor + self.version = version + self.roaming = roaming + self.multipath = multipath + + @property + def user_data_dir(self): + return user_data_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + + @property + def site_data_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + + @property + def user_config_dir(self): + return user_config_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + + @property + def site_config_dir(self): + return site_config_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + + @property + def user_cache_dir(self): + return user_cache_dir(self.appname, self.appauthor, + version=self.version) + + @property + def user_state_dir(self): + return user_state_dir(self.appname, self.appauthor, + version=self.version) + + @property + def user_log_dir(self): + return user_log_dir(self.appname, self.appauthor, + version=self.version) + + +#---- internal support stuff + +def _get_win_folder_from_registry(csidl_name): + """This is a fallback technique at best. I'm not sure if using the + registry for this guarantees us the correct answer for all CSIDL_* + names. + """ + if PY3: + import winreg as _winreg + else: + import _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey( + _winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" + ) + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + return dir + + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + +def _get_win_folder_with_jna(csidl_name): + import array + from com.sun import jna + from com.sun.jna.platform import win32 + + buf_size = win32.WinDef.MAX_PATH * 2 + buf = array.zeros('c', buf_size) + shell = win32.Shell32.INSTANCE + shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf) + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf = array.zeros('c', buf_size) + kernel = win32.Kernel32.INSTANCE + if kernel.GetShortPathName(dir, buf, buf_size): + dir = jna.Native.toString(buf.tostring()).rstrip("\0") + + return dir + +def _get_win_folder_from_environ(csidl_name): + env_var_name = { + "CSIDL_APPDATA": "APPDATA", + "CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE", + "CSIDL_LOCAL_APPDATA": "LOCALAPPDATA", + }[csidl_name] + + return os.environ[env_var_name] + +if system == "win32": + try: + from ctypes import windll + except ImportError: + try: + import com.sun.jna + except ImportError: + try: + if PY3: + import winreg as _winreg + else: + import _winreg + except ImportError: + _get_win_folder = _get_win_folder_from_environ + else: + _get_win_folder = _get_win_folder_from_registry + else: + _get_win_folder = _get_win_folder_with_jna + else: + _get_win_folder = _get_win_folder_with_ctypes + + +#---- self test code + +if __name__ == "__main__": + appname = "MyApp" + appauthor = "MyCompany" + + props = ("user_data_dir", + "user_config_dir", + "user_cache_dir", + "user_state_dir", + "user_log_dir", + "site_data_dir", + "site_config_dir") + + print("-- app dirs %s --" % __version__) + + print("-- app dirs (with optional 'version')") + dirs = AppDirs(appname, appauthor, version="1.0") + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'version')") + dirs = AppDirs(appname, appauthor) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'appauthor')") + dirs = AppDirs(appname) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (with disabled 'appauthor')") + dirs = AppDirs(appname, appauthor=False) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) diff --git a/src/ban_peers/appdirs_LICENSE.txt b/src/ban_peers/appdirs_LICENSE.txt new file mode 100644 index 0000000..107c614 --- /dev/null +++ b/src/ban_peers/appdirs_LICENSE.txt @@ -0,0 +1,23 @@ +# This is the MIT license + +Copyright (c) 2010 ActiveState Software Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/ban_peers/config.py b/src/ban_peers/config.py new file mode 100644 index 0000000..a5d5246 --- /dev/null +++ b/src/ban_peers/config.py @@ -0,0 +1,63 @@ +"""utils for read/write config file""" + +import os +import re +import argparse +from .appdirs import user_config_dir # type: ignore + + +filename = 'ban_peers' +_default_config_dir = user_config_dir('BanPeers') +_default_config_file = os.path.join(_default_config_dir, f'{filename}.conf') + +_skip_settings = {'remove_ads', 'config', 'help', 'version'} +_bool_settings = {'resolve_country', 'no_xunlei_reprieve', + 'no_fake_progress_check', 'no_serious_leech_check', + 'no_refused_upload_check', 'private_check', + 'log_unknown', 'no_close_pairing'} +_s2b = { + '0': False, 'false': False, 'no': False, + '1': True, 'true': True, 'yes': True +} + + +class FileType(argparse.FileType): + def __call__(self, string): + if 'w' in self._mode and string != '-': + dir = os.path.dirname(os.path.abspath(string)) + if not os.path.exists(dir): + os.makedirs(dir, exist_ok=True) + return super().__call__(string) + + +def current_dir_config_file(): + for ext in ['conf', 'ini', 'cfg']: + if os.path.exists(f'{filename}.{ext}'): + return f'{filename}.{ext}' + + +def save(config, file, log=None): + for name, value in config.items(): + if value and name not in _skip_settings: + file.write(f'{name} = {value}\n') + if log: + log(name, value) + + +def load(config, file, log=None): + for l in file.readlines(): + try: + name, value = re.split('\s*=\s*', l.strip(), 1) + except ValueError: + continue + if name in _skip_settings or name not in config or config[name]: + continue + if name in _bool_settings: + value = _s2b[value.lower()] + elif value.isdecimal(): + value = int(value) + if not value: + continue + config[name] = value + if log: + log(name, value) diff --git a/src/ban_peers/i18n/locale/README.md b/src/ban_peers/i18n/locale/README.md index edce9b1..a2d169f 100644 --- a/src/ban_peers/i18n/locale/README.md +++ b/src/ban_peers/i18n/locale/README.md @@ -36,8 +36,7 @@ There are few simple guides: - Update a existing language po file. Commonly translators need only do the last one step. -1. Generates a pot file, does the same (like new languages), then modify the - charset to UTF-8. +1. Generates a pot file, does the same (like new languages). 1. Merge pot file into po file use `msgmerge`. diff --git a/src/ban_peers/i18n/locale/banpeers.pot b/src/ban_peers/i18n/locale/banpeers.pot index 1381f13..66bce0d 100644 --- a/src/ban_peers/i18n/locale/banpeers.pot +++ b/src/ban_peers/i18n/locale/banpeers.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: Ban-Peers 0.9.1\n" +"Project-Id-Version: Ban-Peers 0.9.2\n" "Report-Msgid-Bugs-To: seahoh@gmail.com\n" -"POT-Creation-Date: 2020-10-08 23:06+0800\n" +"POT-Creation-Date: 2020-10-11 20:37+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -24,277 +24,326 @@ msgid "" "uTorrent.\n" msgstr "" -#: ban_peers/__init__.py:452 +#: ban_peers/__init__.py:511 msgid "Please input uTorrent settings folder path or ipfilter file path:\n" msgstr "" -#: ban_peers/__init__.py:459 +#: ban_peers/__init__.py:518 #, python-format msgid "Unable to connect %(host)s:%(port)d" msgstr "" -#: ban_peers/__init__.py:570 +#: ban_peers/__init__.py:628 msgid "Please input WebUI username: " msgstr "" -#: ban_peers/__init__.py:571 +#: ban_peers/__init__.py:629 msgid "Please input WebUI password: " msgstr "" -#: ban_peers/__init__.py:637 ban_peers/__init__.py:1160 +#: ban_peers/__init__.py:695 ban_peers/__init__.py:1188 #, python-format msgid "Set uTorrent setting %(name)r to %(value)s" msgstr "" -#: ban_peers/__init__.py:654 +#: ban_peers/__init__.py:712 #, python-format msgid "[%(hash)s] set property %(name)r to %(value)s" msgstr "" -#: ban_peers/__init__.py:693 +#: ban_peers/__init__.py:748 #, python-format msgid "" "Banned %(ip)s:%(port)d@%(country)s: %(reason)s, downloaded: %(dlsize)s, " "uploaded: %(ulsize)s" msgstr "" -#: ban_peers/__init__.py:706 +#: ban_peers/__init__.py:761 #, python-format msgid "[%(hash)s][%(torrent)s] found %(message)s: [%(client)s]@%(ip&port)s" msgstr "" -#: ban_peers/__init__.py:752 +#: ban_peers/__init__.py:797 #, python-format msgid "[%(hash)s][%(torrent)s] increase additional check threshold" msgstr "" -#: ban_peers/__init__.py:843 +#: ban_peers/__init__.py:873 #, python-format msgid "report fack progress [%(progress).1f%%]" msgstr "" -#: ban_peers/__init__.py:854 +#: ban_peers/__init__.py:884 msgid "offline download server" msgstr "" -#: ban_peers/__init__.py:859 +#: ban_peers/__init__.py:889 msgid "XunLei" msgstr "" -#: ban_peers/__init__.py:874 +#: ban_peers/__init__.py:904 msgid "player" msgstr "" -#: ban_peers/__init__.py:887 +#: ban_peers/__init__.py:917 msgid "fack client" msgstr "" -#: ban_peers/__init__.py:898 +#: ban_peers/__init__.py:928 msgid "leecher client" msgstr "" -#: ban_peers/__init__.py:942 +#: ban_peers/__init__.py:971 msgid "highly suspected of leecher" msgstr "" -#: ban_peers/__init__.py:960 +#: ban_peers/__init__.py:989 #, python-format msgid "refused upload [%(availability).3f]" msgstr "" -#: ban_peers/__init__.py:967 +#: ban_peers/__init__.py:996 msgid "unknown client" msgstr "" -#: ban_peers/__init__.py:981 +#: ban_peers/__init__.py:1010 #, python-format msgid "" "Statis: %(ipsc)d IPs, %(torrentsuc)d/%(torrentsc)d Torrents, D: %(dlsize)s, " "U: %(ulsize)s" msgstr "" -#: ban_peers/__init__.py:1003 +#: ban_peers/__init__.py:1032 #, python-format msgid "Auto-banning script %(state)s running" msgstr "" -#: ban_peers/__init__.py:1013 +#: ban_peers/__init__.py:1042 msgid "start" msgstr "" -#: ban_peers/__init__.py:1025 +#: ban_peers/__init__.py:1054 msgid "uTorrent has disconnected" msgstr "" -#: ban_peers/__init__.py:1033 +#: ban_peers/__init__.py:1062 #, python-format msgid "Unable to connect WebUI: %(url)s" msgstr "" -#: ban_peers/__init__.py:1038 +#: ban_peers/__init__.py:1067 #, python-format msgid "Error occurred: %(error)s, %(url)s" msgstr "" -#: ban_peers/__init__.py:1041 ban_peers/__init__.py:1180 +#: ban_peers/__init__.py:1070 ban_peers/__init__.py:1208 #, python-format msgid "%(logheader)s Error occurred: %(error)s" msgstr "" -#: ban_peers/__init__.py:1045 +#: ban_peers/__init__.py:1074 msgid "uTorrent has reconnected" msgstr "" -#: ban_peers/__init__.py:1054 +#: ban_peers/__init__.py:1083 msgid "Choose your operation: (Q)uit, (S)top, (R)estart, (P)ause/Proceed" msgstr "" -#: ban_peers/__init__.py:1057 +#: ban_peers/__init__.py:1086 msgid "DISCONNECTED" msgstr "" -#: ban_peers/__init__.py:1064 ban_peers/__init__.py:1069 +#: ban_peers/__init__.py:1093 ban_peers/__init__.py:1098 msgid "quit" msgstr "" -#: ban_peers/__init__.py:1071 +#: ban_peers/__init__.py:1100 msgid "stop" msgstr "" -#: ban_peers/__init__.py:1077 +#: ban_peers/__init__.py:1106 msgid "restart" msgstr "" -#: ban_peers/__init__.py:1082 +#: ban_peers/__init__.py:1111 msgid "pause" msgstr "" -#: ban_peers/__init__.py:1084 +#: ban_peers/__init__.py:1113 msgid "proceed" msgstr "" -#: ban_peers/__init__.py:1124 +#: ban_peers/__init__.py:1153 msgid "Pairing request has been rejected!" msgstr "" -#: ban_peers/__init__.py:1157 +#: ban_peers/__init__.py:1185 #, python-format msgid "set_setting(%(name)r, %(value)s) fail: %(error)s" msgstr "" -#: ban_peers/__init__.py:1209 +#: ban_peers/__init__.py:1238 msgid "IPFILTER-PATH" msgstr "" -#: ban_peers/__init__.py:1211 +#: ban_peers/__init__.py:1240 msgid "" -"Path of ipfilter dir/file, wait input if empty. IMPORTANT NOTICE: must be " -"the uTorrent settings path!" +"Path of ipfilter dir/file, will try load from config file or wait input if " +"empty. IMPORTANT NOTICE: must be the uTorrent settings path!" msgstr "" -#: ban_peers/__init__.py:1214 +#: ban_peers/__init__.py:1244 msgid "IP|DOMAIN" msgstr "" -#: ban_peers/__init__.py:1216 +#: ban_peers/__init__.py:1246 #, python-format msgid "WebUI host, default %(host)s" msgstr "" -#: ban_peers/__init__.py:1218 +#: ban_peers/__init__.py:1248 msgid "PORT" msgstr "" -#: ban_peers/__init__.py:1220 +#: ban_peers/__init__.py:1250 #, python-format msgid "WebUI port, default %(port)s" msgstr "" -#: ban_peers/__init__.py:1222 +#: ban_peers/__init__.py:1252 msgid "USERNAME:PASSWORD" msgstr "" -#: ban_peers/__init__.py:1224 +#: ban_peers/__init__.py:1254 msgid "WebUI authorization, wait input if required" msgstr "" -#: ban_peers/__init__.py:1226 +#: ban_peers/__init__.py:1256 msgid "HOURS" msgstr "" -#: ban_peers/__init__.py:1228 +#: ban_peers/__init__.py:1258 #, python-format msgid "Ban expire time for peers, default %(time)s hours" msgstr "" -#: ban_peers/__init__.py:1231 +#: ban_peers/__init__.py:1261 msgid "MINUTES" msgstr "" -#: ban_peers/__init__.py:1233 +#: ban_peers/__init__.py:1263 #, python-format msgid "" "How much time to keep connecting before temporary banned refused upload " "peers, at least 5 minutes, default %(time)s minutes" msgstr "" -#: ban_peers/__init__.py:1238 +#: ban_peers/__init__.py:1268 msgid "FORMAT" msgstr "" -#: ban_peers/__init__.py:1240 +#: ban_peers/__init__.py:1270 #, python-format -msgid "Format of log header, see time.strftime, default %(time)s" +msgid "Format of log header, see time.strftime, default %(header)s" msgstr "" -#: ban_peers/__init__.py:1246 +#: ban_peers/__init__.py:1276 msgid "Set uTorrent to resolved peer's country code at start-up" msgstr "" -#: ban_peers/__init__.py:1251 +#: ban_peers/__init__.py:1281 msgid "Banned XunLei directly, no more checking" msgstr "" -#: ban_peers/__init__.py:1255 +#: ban_peers/__init__.py:1285 msgid "Don't checking fake progress" msgstr "" -#: ban_peers/__init__.py:1259 +#: ban_peers/__init__.py:1289 msgid "Don't checking serious leech, except anonymous peers" msgstr "" -#: ban_peers/__init__.py:1263 +#: ban_peers/__init__.py:1293 msgid "" "Don't checking refused upload, this checking is useful to connect potential " "active peers" msgstr "" -#: ban_peers/__init__.py:1268 +#: ban_peers/__init__.py:1298 msgid "Enable checking for private torrents" msgstr "" -#: ban_peers/__init__.py:1272 +#: ban_peers/__init__.py:1302 msgid "Logging unknown clients" msgstr "" -#: ban_peers/__init__.py:1276 +#: ban_peers/__init__.py:1306 msgid "" "Remove ads via set Advanced Settings, only working for localhost, and to " "fail in older uTorrent" msgstr "" -#: ban_peers/__init__.py:1281 +#: ban_peers/__init__.py:1311 msgid "Don't turn off Web Pairing setting after" msgstr "" -#: ban_peers/__init__.py:1285 +#: ban_peers/__init__.py:1316 ban_peers/__init__.py:1326 +msgid "CONFIG-FILE" +msgstr "" + +#: ban_peers/__init__.py:1318 +#, python-format +msgid "" +"Save current arguments to a config file except \"--remove-ads\", \"--help\" " +"and \"--version\". Save to default location \"%(config)s\" if empty input" +msgstr "" + +#: ban_peers/__init__.py:1328 +msgid "" +"Load arguments from a config file, will not overlaid the inputted arguments. " +"Load from current directory (use conf/ini/cfg as extension name) or default " +"location if empty input" +msgstr "" + +#: ban_peers/__init__.py:1335 msgid "Show this help message and exit" msgstr "" -#: ban_peers/__init__.py:1289 +#: ban_peers/__init__.py:1339 msgid "Show version and exit" msgstr "" -#: ban_peers/__init__.py:1295 +#: ban_peers/__init__.py:1345 msgid "Welcome using" msgstr "" + +#: ban_peers/__init__.py:1359 +#, python-format +msgid "%(action)s argument \"%(name)s = %(value)s\"" +msgstr "" + +#: ban_peers/__init__.py:1364 +msgid "No ipfilter has be inputted, try load from config file" +msgstr "" + +#: ban_peers/__init__.py:1375 +#, python-format +msgid "Start saving config file \"%(name)s\"" +msgstr "" + +#: ban_peers/__init__.py:1376 +msgid "Save" +msgstr "" + +#: ban_peers/__init__.py:1378 +#, python-format +msgid "Start loading config file \"%(name)s\"" +msgstr "" + +#: ban_peers/__init__.py:1379 +msgid "Load" +msgstr "" + +#: ban_peers/__init__.py:1382 +msgid "Load ipfilter from config file fail, found nothing" +msgstr "" diff --git a/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/argparse.po b/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/argparse.po index 994254e..89a2df1 100644 --- a/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/argparse.po +++ b/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/argparse.po @@ -29,7 +29,7 @@ msgstr ".__call__() 未定义" #: argparse.py:1197 #, python-format msgid "unknown parser %(parser_name)r (choices: %(choices)s)" -msgstr "未知的分析器 %(parser_name)r (可选: %(choices)s)" +msgstr "未知的解析器 %(parser_name)r (可选: %(choices)s)" #: argparse.py:1257 #, python-format @@ -98,7 +98,7 @@ msgstr "显示此帮助信息并退出" #: argparse.py:1768 msgid "cannot have multiple subparser arguments" -msgstr "不能包含多个子分析器参数" +msgstr "不能包含多个子解析器参数" #: argparse.py:1820 argparse.py:2331 #, python-format diff --git a/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/banpeers.po b/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/banpeers.po index 90183ad..74ad253 100644 --- a/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/banpeers.po +++ b/src/ban_peers/i18n/locale/zh_CN/LC_MESSAGES/banpeers.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Ban-Peers 0.9.1\n" "Report-Msgid-Bugs-To: seahoh@gmail.com\n" -"POT-Creation-Date: 2020-10-08 23:06+0800\n" -"PO-Revision-Date: 2020-10-06 20:03+0800\n" +"POT-Creation-Date: 2020-10-11 20:37+0800\n" +"PO-Revision-Date: 2020-10-11 20:38+0800\n" "Last-Translator: SeaHOH \n" "Language-Team: Chinese (simplified)\n" "Language: zh_CN\n" @@ -25,34 +25,34 @@ msgid "" msgstr "" "通过网页 API 检查并屏蔽 BitTorrent 吸血对端,移除广告,工作于 uTorrent。" -#: ban_peers/__init__.py:452 +#: ban_peers/__init__.py:511 msgid "Please input uTorrent settings folder path or ipfilter file path:\n" msgstr "请输入 uTorrent 配置文件夹路径,或者 ipfilter 文件路径:\n" -#: ban_peers/__init__.py:459 +#: ban_peers/__init__.py:518 #, python-format msgid "Unable to connect %(host)s:%(port)d" msgstr "无法连接 %(host)s:%(port)d" -#: ban_peers/__init__.py:570 +#: ban_peers/__init__.py:628 msgid "Please input WebUI username: " msgstr "请输入 WebUI 用户名: " -#: ban_peers/__init__.py:571 +#: ban_peers/__init__.py:629 msgid "Please input WebUI password: " msgstr "请输入 WebUI 密码: " -#: ban_peers/__init__.py:637 ban_peers/__init__.py:1160 +#: ban_peers/__init__.py:695 ban_peers/__init__.py:1188 #, python-format msgid "Set uTorrent setting %(name)r to %(value)s" msgstr "设定 uTorrent 配置 %(name)r 到 %(value)s" -#: ban_peers/__init__.py:654 +#: ban_peers/__init__.py:712 #, python-format msgid "[%(hash)s] set property %(name)r to %(value)s" msgstr "[%(hash)s] 设定属性 %(name)r 到 %(value)s" -#: ban_peers/__init__.py:693 +#: ban_peers/__init__.py:748 #, python-format msgid "" "Banned %(ip)s:%(port)d@%(country)s: %(reason)s, downloaded: %(dlsize)s, " @@ -61,55 +61,55 @@ msgstr "" "已屏蔽 %(ip)s:%(port)d@%(country)s: %(reason)s, 已下载: %(dlsize)s, 已上传: " "%(ulsize)s" -#: ban_peers/__init__.py:706 +#: ban_peers/__init__.py:761 #, python-format msgid "[%(hash)s][%(torrent)s] found %(message)s: [%(client)s]@%(ip&port)s" msgstr "[%(hash)s][%(torrent)s] 发现%(message)s: [%(client)s]@%(ip&port)s" -#: ban_peers/__init__.py:752 +#: ban_peers/__init__.py:797 #, python-format msgid "[%(hash)s][%(torrent)s] increase additional check threshold" msgstr "[%(hash)s][%(torrent)s] 提高附加检测阀值" -#: ban_peers/__init__.py:843 +#: ban_peers/__init__.py:873 #, python-format msgid "report fack progress [%(progress).1f%%]" msgstr "汇报虚假进度 [%(progress).1f%%]" -#: ban_peers/__init__.py:854 +#: ban_peers/__init__.py:884 msgid "offline download server" msgstr "离线下载服务器" -#: ban_peers/__init__.py:859 +#: ban_peers/__init__.py:889 msgid "XunLei" msgstr "迅雷" -#: ban_peers/__init__.py:874 +#: ban_peers/__init__.py:904 msgid "player" msgstr "播放器" -#: ban_peers/__init__.py:887 +#: ban_peers/__init__.py:917 msgid "fack client" msgstr "假冒客户端" -#: ban_peers/__init__.py:898 +#: ban_peers/__init__.py:928 msgid "leecher client" msgstr "吸血客户端" -#: ban_peers/__init__.py:942 +#: ban_peers/__init__.py:971 msgid "highly suspected of leecher" msgstr "高度疑似吸血" -#: ban_peers/__init__.py:960 +#: ban_peers/__init__.py:989 #, python-format msgid "refused upload [%(availability).3f]" msgstr "拒绝上传 [%(availability).3f]" -#: ban_peers/__init__.py:967 +#: ban_peers/__init__.py:996 msgid "unknown client" msgstr "未知客户端" -#: ban_peers/__init__.py:981 +#: ban_peers/__init__.py:1010 #, python-format msgid "" "Statis: %(ipsc)d IPs, %(torrentsuc)d/%(torrentsc)d Torrents, D: %(dlsize)s, " @@ -118,127 +118,127 @@ msgstr "" "统计: %(ipsc)d IPs, %(torrentsuc)d/%(torrentsc)d Torrents, 已下载: " "%(dlsize)s, 已上传: %(ulsize)s" -#: ban_peers/__init__.py:1003 +#: ban_peers/__init__.py:1032 #, python-format msgid "Auto-banning script %(state)s running" msgstr "自动屏蔽脚本%(state)s运行" -#: ban_peers/__init__.py:1013 +#: ban_peers/__init__.py:1042 msgid "start" msgstr "开始" -#: ban_peers/__init__.py:1025 +#: ban_peers/__init__.py:1054 msgid "uTorrent has disconnected" msgstr "uTorrent 已断开连接" -#: ban_peers/__init__.py:1033 +#: ban_peers/__init__.py:1062 #, python-format msgid "Unable to connect WebUI: %(url)s" msgstr "无法连接 WebUI: %(url)s" -#: ban_peers/__init__.py:1038 +#: ban_peers/__init__.py:1067 #, python-format msgid "Error occurred: %(error)s, %(url)s" msgstr "发生错误: %(error)s, %(url)s" -#: ban_peers/__init__.py:1041 ban_peers/__init__.py:1180 +#: ban_peers/__init__.py:1070 ban_peers/__init__.py:1208 #, python-format msgid "%(logheader)s Error occurred: %(error)s" msgstr "%(logheader)s 发生错误: %(error)s" -#: ban_peers/__init__.py:1045 +#: ban_peers/__init__.py:1074 msgid "uTorrent has reconnected" msgstr "uTorrent 已重新连接" -#: ban_peers/__init__.py:1054 +#: ban_peers/__init__.py:1083 msgid "Choose your operation: (Q)uit, (S)top, (R)estart, (P)ause/Proceed" msgstr "请选择你要执行的操作: (Q)退出,(S)停止,(R)重新开始,(P)暂停/恢复" -#: ban_peers/__init__.py:1057 +#: ban_peers/__init__.py:1086 msgid "DISCONNECTED" msgstr "已断开连接" -#: ban_peers/__init__.py:1064 ban_peers/__init__.py:1069 +#: ban_peers/__init__.py:1093 ban_peers/__init__.py:1098 msgid "quit" msgstr "退出" -#: ban_peers/__init__.py:1071 +#: ban_peers/__init__.py:1100 msgid "stop" msgstr "停止" -#: ban_peers/__init__.py:1077 +#: ban_peers/__init__.py:1106 msgid "restart" msgstr "重新开始" -#: ban_peers/__init__.py:1082 +#: ban_peers/__init__.py:1111 msgid "pause" msgstr "暂停" -#: ban_peers/__init__.py:1084 +#: ban_peers/__init__.py:1113 msgid "proceed" msgstr "恢复" -#: ban_peers/__init__.py:1124 +#: ban_peers/__init__.py:1153 msgid "Pairing request has been rejected!" msgstr "配对请求已被拒绝!" -#: ban_peers/__init__.py:1157 +#: ban_peers/__init__.py:1185 #, python-format msgid "set_setting(%(name)r, %(value)s) fail: %(error)s" msgstr "set_setting(%(name)r, %(value)s) 失败: %(error)s" -#: ban_peers/__init__.py:1209 +#: ban_peers/__init__.py:1238 msgid "IPFILTER-PATH" msgstr "IP屏蔽配置路径" -#: ban_peers/__init__.py:1211 +#: ban_peers/__init__.py:1240 msgid "" -"Path of ipfilter dir/file, wait input if empty. IMPORTANT NOTICE: must be " -"the uTorrent settings path!" +"Path of ipfilter dir/file, will try load from config file or wait input if " +"empty. IMPORTANT NOTICE: must be the uTorrent settings path!" msgstr "" -"ipfilter 目录或文件路径,留空将等待输入。重要提示: 必须是 uTorrent 配置使用的" -"路径!" +"ipfilter 目录或文件路径,留空将尝试从配置文件加载,或等待输入。重要提示: 必须" +"是 uTorrent 配置使用的路径!" -#: ban_peers/__init__.py:1214 +#: ban_peers/__init__.py:1244 msgid "IP|DOMAIN" msgstr "IP|域名" -#: ban_peers/__init__.py:1216 +#: ban_peers/__init__.py:1246 #, python-format msgid "WebUI host, default %(host)s" msgstr "网页界面的主机,默认 %(host)s" -#: ban_peers/__init__.py:1218 +#: ban_peers/__init__.py:1248 msgid "PORT" msgstr "端口" -#: ban_peers/__init__.py:1220 +#: ban_peers/__init__.py:1250 #, python-format msgid "WebUI port, default %(port)s" msgstr "网页界面的端口,默认 %(port)s" -#: ban_peers/__init__.py:1222 +#: ban_peers/__init__.py:1252 msgid "USERNAME:PASSWORD" msgstr "用户名:密码" -#: ban_peers/__init__.py:1224 +#: ban_peers/__init__.py:1254 msgid "WebUI authorization, wait input if required" msgstr "网页界面的授权,如果需要将等待输入" -#: ban_peers/__init__.py:1226 +#: ban_peers/__init__.py:1256 msgid "HOURS" msgstr "小时" -#: ban_peers/__init__.py:1228 +#: ban_peers/__init__.py:1258 #, python-format msgid "Ban expire time for peers, default %(time)s hours" msgstr "屏蔽对端的过期时间,默认 %(time)s 小时" -#: ban_peers/__init__.py:1231 +#: ban_peers/__init__.py:1261 msgid "MINUTES" msgstr "分钟" -#: ban_peers/__init__.py:1233 +#: ban_peers/__init__.py:1263 #, python-format msgid "" "How much time to keep connecting before temporary banned refused upload " @@ -246,64 +246,117 @@ msgid "" msgstr "" "临时屏蔽拒绝上传的对端前保持连接的时间,最少 5 分钟,默认 %(time)s 分钟" -#: ban_peers/__init__.py:1238 +#: ban_peers/__init__.py:1268 msgid "FORMAT" msgstr "格式" -#: ban_peers/__init__.py:1240 +#: ban_peers/__init__.py:1270 #, python-format -msgid "Format of log header, see time.strftime, default %(time)s" -msgstr "日志头格式,参见 time.strftime,默认 %(time)s" +msgid "Format of log header, see time.strftime, default %(header)s" +msgstr "日志头格式,参见 time.strftime,默认 %(header)s" -#: ban_peers/__init__.py:1246 +#: ban_peers/__init__.py:1276 msgid "Set uTorrent to resolved peer's country code at start-up" msgstr "启动时,设置 uTorrent 解析对端国家代码" -#: ban_peers/__init__.py:1251 +#: ban_peers/__init__.py:1281 msgid "Banned XunLei directly, no more checking" msgstr "直接屏蔽迅雷,不进行更多的检查" -#: ban_peers/__init__.py:1255 +#: ban_peers/__init__.py:1285 msgid "Don't checking fake progress" msgstr "不进行虚假进度检查" -#: ban_peers/__init__.py:1259 +#: ban_peers/__init__.py:1289 msgid "Don't checking serious leech, except anonymous peers" msgstr "不进行严重吸血检查,匿名对端除外" -#: ban_peers/__init__.py:1263 +#: ban_peers/__init__.py:1293 msgid "" "Don't checking refused upload, this checking is useful to connect potential " "active peers" msgstr "不进行拒绝上传检查,此检查有助于连接潜在的活跃对端" -#: ban_peers/__init__.py:1268 +#: ban_peers/__init__.py:1298 msgid "Enable checking for private torrents" msgstr "启用对私有种子的检查" -#: ban_peers/__init__.py:1272 +#: ban_peers/__init__.py:1302 msgid "Logging unknown clients" msgstr "将未知客户端记入日志" -#: ban_peers/__init__.py:1276 +#: ban_peers/__init__.py:1306 msgid "" "Remove ads via set Advanced Settings, only working for localhost, and to " "fail in older uTorrent" msgstr "" "通过高级设置移除广告,仅工作于本地主机,也无法工作于较旧版本的 uTorrent" -#: ban_peers/__init__.py:1281 +#: ban_peers/__init__.py:1311 msgid "Don't turn off Web Pairing setting after" msgstr "移除广告后,不关闭网络配对配置项" -#: ban_peers/__init__.py:1285 +#: ban_peers/__init__.py:1316 ban_peers/__init__.py:1326 +msgid "CONFIG-FILE" +msgstr "配置文件" + +#: ban_peers/__init__.py:1318 +#, python-format +msgid "" +"Save current arguments to a config file except \"--remove-ads\", \"--help\" " +"and \"--version\". Save to default location \"%(config)s\" if empty input" +msgstr "" +"保存当前参数到一个配置文件,不包括 \"--remove-ads\"、\"--help\" 和 \"--" +"version\",如果输入留空则保存到默认位置 \"%(config)s\"" + +#: ban_peers/__init__.py:1328 +msgid "" +"Load arguments from a config file, will not overlaid the inputted arguments. " +"Load from current directory (use conf/ini/cfg as extension name) or default " +"location if empty input" +msgstr "" +"从一个配置文件加载参数,不会覆盖已输入的参数,如果输入留空则尝试从当前目录 " +"(使用 conf/ini/cfg 作为扩展名) 或默认位置加载" + +#: ban_peers/__init__.py:1335 msgid "Show this help message and exit" msgstr "显示此帮助信息并退出" -#: ban_peers/__init__.py:1289 +#: ban_peers/__init__.py:1339 msgid "Show version and exit" msgstr "显示版本信息并退出" -#: ban_peers/__init__.py:1295 +#: ban_peers/__init__.py:1345 msgid "Welcome using" msgstr "欢迎使用" + +#: ban_peers/__init__.py:1359 +#, python-format +msgid "%(action)s argument \"%(name)s = %(value)s\"" +msgstr "%(action)s参数 \"%(name)s = %(value)s\"" + +#: ban_peers/__init__.py:1364 +msgid "No ipfilter has be inputted, try load from config file" +msgstr "没有输入 ipfilter,尝试从配置文件加载" + +#: ban_peers/__init__.py:1375 +#, python-format +msgid "Start saving config file \"%(name)s\"" +msgstr "开始保存配置文件 \"%(name)s\"" + +#: ban_peers/__init__.py:1376 +msgid "Save" +msgstr "保存" + +#: ban_peers/__init__.py:1378 +#, python-format +msgid "Start loading config file \"%(name)s\"" +msgstr "开始加载配置文件 \"%(name)s\"" + +#: ban_peers/__init__.py:1379 +msgid "Load" +msgstr "加载" + +#: ban_peers/__init__.py:1382 +msgid "Load ipfilter from config file fail, found nothing" +msgstr "从配置文件加载 ipfilter 失败,什么都没有找到" diff --git a/src/ban_peers/i18n/msgfmt.py b/src/ban_peers/i18n/msgfmt.py index a27458b..ce4f318 100644 --- a/src/ban_peers/i18n/msgfmt.py +++ b/src/ban_peers/i18n/msgfmt.py @@ -130,8 +130,8 @@ def __init__(self, msg, fname, lno, content=''): self.content = content def __str__(self): - return ('PO file syntax error: {}\\n' - ' File "{}", line {}\\n' + return ('PO file syntax error: {}\n' + ' File "{}", line {}\n' ' {}').format(self.msg, self.fname, self.lno, self.content) diff --git a/src/ban_peers/vendor.txt b/src/ban_peers/vendor.txt index 33c0dc0..ca42ca7 100644 --- a/src/ban_peers/vendor.txt +++ b/src/ban_peers/vendor.txt @@ -4,3 +4,8 @@ Files copy from the cpython distribution It is not always installed, is used to build mo files when install package. i18n/gettext.py (part & modified) It enhance origin function for find/read mo files from zip archive. + +Files copy from others + + appdirs + Use a same version of it, ensure consistency.