From ded7cb608d7bfb19db7764863f583cca22051b5d Mon Sep 17 00:00:00 2001 From: j2qk3b Date: Mon, 25 Mar 2024 19:53:29 +0800 Subject: [PATCH] first commit --- .gitignore | 3 + LICENSE | 21 ++ README.md | 83 +++++++ anybt.py | 574 +++++++++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 160 ++++++++++++++ pyproject.toml | 18 ++ 6 files changed, 859 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 anybt.py create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d29dff9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +dist/ +__pycache__/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64db931 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 j2qk3b + +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/README.md b/README.md new file mode 100644 index 0000000..94fb067 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# AnyBT: An Open Sourced Decentralized BitTorrent Search Engine + +Introducing AnyBT, a tool for searching magnet link of all kinds of BitTorrent contents. It's based on a decentralized data protocol - [Glitter Protocol](https://twitter.com/GlitterProtocol). + +You could use AnyBT to search for magnet links base on file names and displays search results with a simple interface. + +There is also [a web version of AnyBT](https://anybt.eth.limo) works on [ENS](https://ens.domains/) and [IPFS](https://ipfs.tech/) available if you do not have Python environment. + +## Getting Started + +To get started, please follow these steps: + +### Prerequisites + +- Python (v3.7 or higher) + +### Installation + +Installing the tool, and you should make sure `your/download/path` in the `$PATH`, then you can use the command line tool. + +```shell +pip install anybt +``` + +### Options + +- `terms`:Specifies search terms to be queried. Required:yes. + +- `-p ` or `--page `:Specifies the page of results to display. Default: 0. + +- `-l ` or `--limit `:Specifies the number of per page to display. Default: 10. + +- `-s ` or `--sort `:Specifies the sorting sequence of results to display. Default: none. + - `hot` :sort by the file heat + - `size` :sort by the size of file + - `date` :sort by the original publication time of the file + +- `-t ` or `--type `:Specifies the category of result to display. Default: all. + - `video` :video categories. + - `document`:document categories. + - `image` :image categories. + - `music` :music categories. + - `software` :software categories. + - `package` :package categories. + +### Examples + +1. Search for keyword "Chaplin": + +```shell +anybt Chaplin +``` + +2. Search for keywords "Charlie Chaplin" and page 1, limit 5: + +```shell +anybt "Charlie Chaplin" -p 1 -l 5 +``` + +3. Search for keywords "Charlie Chaplin" and order by file size: + +```shell +anybt "Charlie Chaplin" -s size +``` + +4. Search for keywords "Charlie Chaplin" and only keep the video resource: + +```shell +anybt "Charlie Chaplin" -t video +``` + +## Built With + +- [glitter-sdk-py](https://github.com/glitternetwork/glitter-sdk-py) A Python SDK for interacting with the Glitter Protocol. + +## Contributing + +If you would like to contribute to this project, feel free to fork the repository and submit a pull request with your changes. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + diff --git a/anybt.py b/anybt.py new file mode 100644 index 0000000..157997b --- /dev/null +++ b/anybt.py @@ -0,0 +1,574 @@ +import re +import time +import requests +import datetime +from typing import List +from decimal import Decimal + + +def prepare_sql(sql_tpl: str, args: (list, tuple)): + return sql_tpl % escape_args(args) + + +def escape_args(args): + if isinstance(args, (tuple, list)): + return tuple(literal(arg) for arg in args) + elif isinstance(args, dict): + return {key: literal(val) for (key, val) in args.items()} + else: + # If it's not a dictionary let's try escaping it anyways. + # Worst case it will throw a Value error + return escape(args) + + +def literal(obj): + """Alias for escape() + + Non-standard, for internal use; do not use this in your applications. + """ + return escape(obj, encoders) + + +def escape(obj, mapping=None): + """Escape whatever value you pass to it. + + Non-standard, for internal use; do not use this in your applications. + """ + if isinstance(obj, str): + return "'" + escape_string(obj) + "'" + if isinstance(obj, (bytes, bytearray)): + ret = escape_bytes(obj) + return ret + return escape_item(obj, "utf8mb4", mapping=mapping) + + +def escape_item(val, charset, mapping=None): + if mapping is None: + mapping = encoders + encoder = mapping.get(type(val)) + + # Fallback to default when no encoder found + if not encoder: + try: + encoder = mapping[str] + except KeyError: + raise TypeError("no default type converter defined") + + if encoder in (escape_dict, escape_sequence): + val = encoder(val, charset, mapping) + else: + val = encoder(val, mapping) + return val + + +def escape_dict(val, charset, mapping=None): + n = {} + for k, v in val.items(): + quoted = escape_item(v, charset, mapping) + n[k] = quoted + return n + + +def escape_sequence(val, charset, mapping=None): + n = [] + for item in val: + quoted = escape_item(item, charset, mapping) + n.append(quoted) + return "(" + ",".join(n) + ")" + + +def escape_set(val, charset, mapping=None): + return ",".join([escape_item(x, charset, mapping) for x in val]) + + +def escape_bool(value, mapping=None): + return str(int(value)) + + +def escape_int(value, mapping=None): + return str(value) + + +def escape_float(value, mapping=None): + s = repr(value) + if s in ("inf", "nan"): + return s + if "e" not in s: + s += "e0" + return s + + +_escape_table = [chr(x) for x in range(128)] +_escape_table[0] = "\\0" +_escape_table[ord("\\")] = "\\\\" +_escape_table[ord("\n")] = "\\n" +_escape_table[ord("\r")] = "\\r" +_escape_table[ord("\032")] = "\\Z" +_escape_table[ord('"')] = '\\"' +_escape_table[ord("'")] = "\\'" + + +def escape_string(value, mapping=None): + """escapes *value* without adding quote. + + Value should be unicode + """ + return value.translate(_escape_table) + + +def escape_bytes_prefixed(value, mapping=None): + return "_binary'%s'" % value.decode("ascii", "surrogateescape").translate( + _escape_table + ) + + +def escape_bytes(value, mapping=None): + return "'%s'" % value.decode("ascii", "surrogateescape").translate(_escape_table) + + +def escape_str(value, mapping=None): + return "'%s'" % escape_string(str(value), mapping) + + +def escape_None(value, mapping=None): + return "NULL" + + +def escape_timedelta(obj, mapping=None): + seconds = int(obj.seconds) % 60 + minutes = int(obj.seconds // 60) % 60 + hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24 + if obj.microseconds: + fmt = "'{0:02d}:{1:02d}:{2:02d}.{3:06d}'" + else: + fmt = "'{0:02d}:{1:02d}:{2:02d}'" + return fmt.format(hours, minutes, seconds, obj.microseconds) + + +def escape_time(obj, mapping=None): + if obj.microsecond: + fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" + else: + fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}'" + return fmt.format(obj) + + +def escape_datetime(obj, mapping=None): + if obj.microsecond: + fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'" + else: + fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}'" + return fmt.format(obj) + + +def escape_date(obj, mapping=None): + fmt = "'{0.year:04}-{0.month:02}-{0.day:02}'" + return fmt.format(obj) + + +def escape_struct_time(obj, mapping=None): + return escape_datetime(datetime.datetime(*obj[:6])) + + +def Decimal2Literal(o, d): + return format(o, "f") + + +def _convert_second_fraction(s): + if not s: + return 0 + # Pad zeros to ensure the fraction length in microseconds + s = s.ljust(6, "0") + return int(s[:6]) + + +DATETIME_RE = re.compile( + r"(\d{1,4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?" +) + + +def convert_datetime(obj): + """Returns a DATETIME or TIMESTAMP column value as a datetime object: + + >>> datetime_or_None('2007-02-25 23:06:20') + datetime.datetime(2007, 2, 25, 23, 6, 20) + >>> datetime_or_None('2007-02-25T23:06:20') + datetime.datetime(2007, 2, 25, 23, 6, 20) + + Illegal values are returned as None: + + >>> datetime_or_None('2007-02-31T23:06:20') is None + True + >>> datetime_or_None('0000-00-00 00:00:00') is None + True + + """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + + m = DATETIME_RE.match(obj) + if not m: + return convert_date(obj) + + try: + groups = list(m.groups()) + groups[-1] = _convert_second_fraction(groups[-1]) + return datetime.datetime(*[int(x) for x in groups]) + except ValueError: + return convert_date(obj) + + +TIMEDELTA_RE = re.compile(r"(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") + + +def convert_timedelta(obj): + """Returns a TIME column as a timedelta object: + + >>> timedelta_or_None('25:06:17') + datetime.timedelta(1, 3977) + >>> timedelta_or_None('-25:06:17') + datetime.timedelta(-2, 83177) + + Illegal values are returned as None: + + >>> timedelta_or_None('random crap') is None + True + + Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but + can accept values as (+|-)DD HH:MM:SS. The latter format will not + be parsed correctly by this function. + """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + + m = TIMEDELTA_RE.match(obj) + if not m: + return obj + + try: + groups = list(m.groups()) + groups[-1] = _convert_second_fraction(groups[-1]) + negate = -1 if groups[0] else 1 + hours, minutes, seconds, microseconds = groups[1:] + + tdelta = ( + datetime.timedelta( + hours=int(hours), + minutes=int(minutes), + seconds=int(seconds), + microseconds=int(microseconds), + ) + * negate + ) + return tdelta + except ValueError: + return obj + + +TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?") + + +def convert_time(obj): + """Returns a TIME column as a time object: + + >>> time_or_None('15:06:17') + datetime.time(15, 6, 17) + + Illegal values are returned as None: + + >>> time_or_None('-25:06:17') is None + True + >>> time_or_None('random crap') is None + True + + Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but + can accept values as (+|-)DD HH:MM:SS. The latter format will not + be parsed correctly by this function. + + Also note that MySQL's TIME column corresponds more closely to + Python's timedelta and not time. However if you want TIME columns + to be treated as time-of-day and not a time offset, then you can + use set this function as the converter for FIELD_TYPE.TIME. + """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + + m = TIME_RE.match(obj) + if not m: + return obj + + try: + groups = list(m.groups()) + groups[-1] = _convert_second_fraction(groups[-1]) + hours, minutes, seconds, microseconds = groups + return datetime.time( + hour=int(hours), + minute=int(minutes), + second=int(seconds), + microsecond=int(microseconds), + ) + except ValueError: + return obj + + +def convert_date(obj): + """Returns a DATE column as a date object: + + >>> date_or_None('2007-02-26') + datetime.date(2007, 2, 26) + + Illegal values are returned as None: + + >>> date_or_None('2007-02-31') is None + True + >>> date_or_None('0000-00-00') is None + True + + """ + if isinstance(obj, (bytes, bytearray)): + obj = obj.decode("ascii") + try: + return datetime.date(*[int(x) for x in obj.split("-", 2)]) + except ValueError: + return obj + + +def through(x): + return x + + +convert_bit = through + +encoders = { + bool: escape_bool, + int: escape_int, + float: escape_float, + str: escape_str, + bytes: escape_bytes, + tuple: escape_sequence, + list: escape_sequence, + set: escape_sequence, + frozenset: escape_sequence, + dict: escape_dict, + type(None): escape_None, + datetime.date: escape_date, + datetime.datetime: escape_datetime, + datetime.timedelta: escape_timedelta, + datetime.time: escape_time, + time.struct_time: escape_struct_time, + Decimal: Decimal2Literal, +} +DECIMAL = 0 +TINY = 1 +SHORT = 2 +LONG = 3 +FLOAT = 4 +DOUBLE = 5 +NULL = 6 +TIMESTAMP = 7 +LONGLONG = 8 +INT24 = 9 +DATE = 10 +TIME = 11 +DATETIME = 12 +YEAR = 13 +NEWDATE = 14 +VARCHAR = 15 +BIT = 16 +JSON = 245 +NEWDECIMAL = 246 +ENUM = 247 +SET = 248 +TINY_BLOB = 249 +MEDIUM_BLOB = 250 +LONG_BLOB = 251 +BLOB = 252 +VAR_STRING = 253 +STRING = 254 +GEOMETRY = 255 + +CHAR = TINY +INTERVAL = ENUM + +decoders = { + BIT: convert_bit, + TINY: int, + SHORT: int, + LONG: int, + FLOAT: float, + DOUBLE: float, + LONGLONG: int, + INT24: int, + YEAR: int, + TIMESTAMP: convert_datetime, + DATETIME: convert_datetime, + TIME: convert_timedelta, + DATE: convert_date, + BLOB: through, + TINY_BLOB: through, + MEDIUM_BLOB: through, + LONG_BLOB: through, + STRING: through, + VAR_STRING: through, + VARCHAR: through, + DECIMAL: Decimal, + NEWDECIMAL: Decimal, +} + +# for MySQLdb compatibility +conversions = encoders.copy() +conversions.update(decoders) +Thing2Literal = escape_str + +encoders = {k: v for (k, v) in conversions.items() if type(k) is not int} + + +def format_file_size(size_bytes): + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes // 1024} KB" + elif size_bytes < 1024 * 1024 * 1024: + return f"{size_bytes // (1024 * 1024)} MB" + else: + return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB" + + +def get_icon(c): + icons = { + 'video': ':movie_camera:', + 'document': ':green_book:', + 'music': ':musical_note:', + 'software': ':laptop_computer:', + 'image': ':art:' + } + return icons.get(c, ':rocket:') + + +def query_string_prepare(queries: list): + query_string = [] + for query in queries: + query_string.append(str(query)) + + rst = " ".join(query_string) + return rst + + +def highlight_prepare(fields: List[str]): + place_hold = ",".join(["\"{}\""] * len(fields)) + option = """{"highlight":{ "style":"html","fields":[""" + place_hold.format(*fields) + """]}}""" + option = option.translate({ord('"'): "\""}) + return "/*+ SET_VAR(full_text_option='%s')*/" % option + + +def get_sql(args): + query = args.terms + queries = ["{}:{}^{}".format("file_name", "\"" + query + "\"", 1.0)] + query_str = query_string_prepare(queries) + highlight = highlight_prepare(["file_name"]) + + order_type = get_order_type(args.sort) + filter_type = get_filter_type(args.type) + + if filter_type == 'all': + sql = prepare_sql( + "select {} _id ,category ,file_name ,firstadd_utc_timestamp ,filesize ,total_count from library.dht where query_string_recency(%s) ".format( + highlight), [query_str]) + else: + query_str = [query_str, filter_type] + sql = prepare_sql( + "select {} _id ,category ,file_name ,firstadd_utc_timestamp ,filesize ,total_count from library.dht where query_string_recency(%s) and category=%s ".format( + highlight), query_str) + + page = args.page * args.limit + if order_type == 'none': + sql += "limit {},{}".format(page, args.limit) + else: + sql += "order by {} desc limit {},{}".format(order_type, page, args.limit) + + return sql + + +def get_filter_type(filter_type): + ftypes = { + 'video': 'video', + 'document': 'document', + 'music': 'music', + 'image': 'image', + 'software': 'software', + } + + return ftypes.get(filter_type, 'all') + + +def get_order_type(order_type): + otypes = { + 'hot': 'total_count', + 'size': 'filesize', + 'date': 'firstadd_utc_timestamp' + } + return otypes.get(order_type, 'none') + + +def query(args): + sql = get_sql(args) + endpoint = "https://gateway.magnode.ru/blockved/glitterchain/index/sql/simple_query" + req = {"sql": sql, "arguments": []} + r = requests.post(endpoint, json=req, timeout=30) + if r.status_code != 200: + return + rst = r.json() + return rst + + +def main(): + import time + import argparse + from rich.console import Console + from rich.table import Table + from rich.align import Align + from rich.tree import Tree + from rich.text import Text + + parser = argparse.ArgumentParser() + parser.add_argument('terms', type=str, + help='the desired terms for searching.') + parser.add_argument('-p', '--page', type=int, default=0, + help='The page of results you would like to display.') + parser.add_argument('-l', '--limit', type=int, default=10, + help='The limit of per page you would like to display.') + parser.add_argument('-s', '--sort', type=str, default='none', + help='The sort of results you would like to display.') + parser.add_argument('-t', '--type', type=str, default='all', + help='The type of results you would like to display.') + + args = parser.parse_args() + + rst = query(args) + + console = Console() + table = Table(show_header=True, header_style="bold magenta", expand=True) + table_centered = Align.center(table) + link = "You could also try a web version running on ENS & IPFS:\n https://anybt.eth.limo" + table.add_column(link) + table.add_column("ext", style="dim", no_wrap=True) + + for row in rst['result']: + row = row['row'] + category = get_icon(row["category"]["value"]) + " " + row["category"]["value"] + ext = Tree(category) + ext.add(format_file_size(float(row["filesize"]["value"]))) + ext.add("{} Hot".format(int(float(row["total_count"]["value"])))) + ext.add(time.strftime("%Y-%m-%d", time.localtime(float(row["firstadd_utc_timestamp"]["value"])))) + + content = Tree(row["_highlight_file_name"]["value"].replace("", "[red]").replace("", "[/red]")) + magnet_link = "magnet:?xt=urn:btih:{}".format(row["_id"]["value"]) + content.add(Text(magnet_link, overflow="fold")) + + table.add_row(content, ext) + + console.print(table) + + +if __name__ == '__main__': + main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0a4ed16 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,160 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.5.0" +files = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "markdown-it-py" +version = "2.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "requests" +version = "2.29.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.29.0-py3-none-any.whl", hash = "sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b"}, + {file = "requests-2.29.0.tar.gz", hash = "sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "urllib3" +version = "1.26.18" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, +] + +[package.extras] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "335e9a92084c5c9e64d579449afccff6ac8914b0a535911f690364cd072bfa1d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8121976 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "anybt" +version = "1.1.0" +description = "An Open Sourced Decentralized BitTorrent Search Engine" +authors = ["j2qk3b"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.7" +requests = "2.29.0" +rich = "^13.7.1" + +[tool.poetry.scripts] +anybt = "anybt:main" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api"