bt1337xearch 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.3
2
+ Name: bt1337xearch
3
+ Version: 0.1.0
4
+ Summary: Better search for 1337x[.]to with basic filters
5
+ Author: SimoneFelici
6
+ Author-email: SimoneFelici <felicisimone9@gmail.com>
7
+ Requires-Dist: scrapling[fetchers]>=0.3.8
8
+ Requires-Dist: textual>=6.5.0
9
+ Requires-Python: >=3.14
10
+ Description-Content-Type: text/markdown
11
+
12
+ # bt1337xearch
13
+
14
+ Better search for 1337x[.]to with basic filters
15
+
16
+ ## What it does
17
+
18
+ bt1337xearch is a command-line tool that searches 1337x.to torrents with:
19
+ - Category filtering (Movies, TV, Games, etc.)
20
+ - Sorting options (by time, size, seeders, leechers)
21
+ - Keyword filtering (include/exclude keywords)
22
+
23
+ ## Installation
24
+ ```bash
25
+ uv tool install bt1337xearch
26
+ ```
27
+ or
28
+ ```bash
29
+ git clone https://github.com/SimoneFelici/bt1337xearch.git
30
+ cd bt1337xearch
31
+ uv tool install .
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Basic search (Probably broken)
37
+ ```bash
38
+ bt1337xearch -n "Dexter"
39
+ ```
40
+
41
+ ### Search with category (Recommended)
42
+ ```bash
43
+ bt1337xearch -n "Lupin" -c ANIME
44
+ ```
45
+
46
+ ### Search with sorting
47
+ ```bash
48
+ bt1337xearch -n "Alien" -c MOVIE -s TIME -o DESC
49
+ ```
50
+
51
+ ### Advanced filtering
52
+ Use `+` to include words and `~` to exclude words:
53
+ ```bash
54
+ # Include only results with "1080p" or "BluRay"
55
+ bt1337xearch -n "Dexter" -f +1080p +BluRay
56
+
57
+ # Exclude results with "CAM" or "TS"
58
+ bt1337xearch -n "Movie" -f ~CAM ~TS
59
+
60
+ # Combine include and exclude
61
+ bt1337xearch -n "Dexter" -c TV -f +1080p "~x265 HEVC" ~CAM
62
+ ```
63
+
64
+ ## Options
65
+
66
+ | Option | Description | Choices |
67
+ |--------|-------------|---------|
68
+ | `-n, --name` | Name of the media (required) | - |
69
+ | `-c, --category` | Filter by category | MOVIE, TV, GAME, MUSIC, APP, DOCU, ANIME, OTHER, XXX |
70
+ | `-s, --sort` | Sort results by | TIME, SIZE, SEED, LEECH |
71
+ | `-o, --order` | Sort order (default: DESC) | ASC, DESC |
72
+ | `-f, --filter` | Filter by keywords | Use `+word` to include, `~word` to exclude |
73
+
74
+ ## Examples
75
+ ```bash
76
+ # Search for Dexter TV series, sorted by seeders
77
+ bt1337xearch -n Dexter -c TV -s SEED
78
+
79
+ # Search for movies with "1080p", exclude "CAM" releases
80
+ bt1337xearch -n "Angel's Egg" -c MOVIE -s TIME -o DESC -f +1080p '~CAM'
81
+
82
+ # Search for games, sorted by upload time
83
+ bt1337xearch -n "Peak" -c GAME -s TIME -o ASC
84
+ ```
85
+
86
+ ## Output
87
+
88
+ For each result, the tool displays:
89
+ - Name
90
+ - Seeders
91
+ - Leechers
92
+ - Upload date
93
+ - File size
94
+ - Uploader
@@ -0,0 +1,83 @@
1
+ # bt1337xearch
2
+
3
+ Better search for 1337x[.]to with basic filters
4
+
5
+ ## What it does
6
+
7
+ bt1337xearch is a command-line tool that searches 1337x.to torrents with:
8
+ - Category filtering (Movies, TV, Games, etc.)
9
+ - Sorting options (by time, size, seeders, leechers)
10
+ - Keyword filtering (include/exclude keywords)
11
+
12
+ ## Installation
13
+ ```bash
14
+ uv tool install bt1337xearch
15
+ ```
16
+ or
17
+ ```bash
18
+ git clone https://github.com/SimoneFelici/bt1337xearch.git
19
+ cd bt1337xearch
20
+ uv tool install .
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Basic search (Probably broken)
26
+ ```bash
27
+ bt1337xearch -n "Dexter"
28
+ ```
29
+
30
+ ### Search with category (Recommended)
31
+ ```bash
32
+ bt1337xearch -n "Lupin" -c ANIME
33
+ ```
34
+
35
+ ### Search with sorting
36
+ ```bash
37
+ bt1337xearch -n "Alien" -c MOVIE -s TIME -o DESC
38
+ ```
39
+
40
+ ### Advanced filtering
41
+ Use `+` to include words and `~` to exclude words:
42
+ ```bash
43
+ # Include only results with "1080p" or "BluRay"
44
+ bt1337xearch -n "Dexter" -f +1080p +BluRay
45
+
46
+ # Exclude results with "CAM" or "TS"
47
+ bt1337xearch -n "Movie" -f ~CAM ~TS
48
+
49
+ # Combine include and exclude
50
+ bt1337xearch -n "Dexter" -c TV -f +1080p "~x265 HEVC" ~CAM
51
+ ```
52
+
53
+ ## Options
54
+
55
+ | Option | Description | Choices |
56
+ |--------|-------------|---------|
57
+ | `-n, --name` | Name of the media (required) | - |
58
+ | `-c, --category` | Filter by category | MOVIE, TV, GAME, MUSIC, APP, DOCU, ANIME, OTHER, XXX |
59
+ | `-s, --sort` | Sort results by | TIME, SIZE, SEED, LEECH |
60
+ | `-o, --order` | Sort order (default: DESC) | ASC, DESC |
61
+ | `-f, --filter` | Filter by keywords | Use `+word` to include, `~word` to exclude |
62
+
63
+ ## Examples
64
+ ```bash
65
+ # Search for Dexter TV series, sorted by seeders
66
+ bt1337xearch -n Dexter -c TV -s SEED
67
+
68
+ # Search for movies with "1080p", exclude "CAM" releases
69
+ bt1337xearch -n "Angel's Egg" -c MOVIE -s TIME -o DESC -f +1080p '~CAM'
70
+
71
+ # Search for games, sorted by upload time
72
+ bt1337xearch -n "Peak" -c GAME -s TIME -o ASC
73
+ ```
74
+
75
+ ## Output
76
+
77
+ For each result, the tool displays:
78
+ - Name
79
+ - Seeders
80
+ - Leechers
81
+ - Upload date
82
+ - File size
83
+ - Uploader
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "bt1337xearch"
3
+ version = "0.1.0"
4
+ description = "Better search for 1337x[.]to with basic filters"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "SimoneFelici", email = "felicisimone9@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "scrapling[fetchers]>=0.3.8",
12
+ "textual>=6.5.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ bt1337xearch = "bt1337xearch:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.7,<0.10.0"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,4 @@
1
+ from .parser import parser
2
+
3
+ def main() -> None:
4
+ parser();
@@ -0,0 +1,317 @@
1
+ from enum import Enum
2
+ from scrapling.fetchers import Fetcher
3
+ import argparse
4
+ import logging
5
+ from curl_cffi.requests.exceptions import SessionClosed
6
+ from textual.app import App, ComposeResult
7
+ from textual.widgets import Header, Footer, Static, Label, LoadingIndicator
8
+ from textual.containers import VerticalScroll, Vertical
9
+ from textual.binding import Binding
10
+ import threading
11
+
12
+ Fetcher.adaptive = True
13
+ logging.getLogger('scrapling').setLevel(logging.WARNING)
14
+ logging.getLogger('scrapling').setLevel(logging.CRITICAL)
15
+
16
+ roles = [
17
+ "user",
18
+ 'uploader',
19
+ "vip",
20
+ "trial-uploader"
21
+ ]
22
+
23
+ class ResultWidget(Static):
24
+ def __init__(self, result: dict, **kwargs):
25
+ super().__init__(**kwargs)
26
+ self.result = result
27
+
28
+ def compose(self) -> ComposeResult:
29
+ yield Label(f"[bold cyan]{self.result['name']}[/]")
30
+ yield Label(f"[yellow]Link:[/]")
31
+ yield Label(f"{self.result['link']}")
32
+ yield Label(f"[green]Seeds:[/] {self.result['seeds']} | [red]Leeches:[/] {self.result['leeches']}")
33
+ yield Label(f"[blue]Size:[/] {self.result['size']} | [magenta]Date:[/] {self.result['date']}")
34
+ yield Label(f"[dim]Uploader: {self.result['uploader']}[/]")
35
+ yield Label("[dim]─" * 50 + "[/]")
36
+
37
+ class MyApp(App):
38
+ TITLE = "bt1337xearch"
39
+
40
+ BINDINGS = [
41
+ Binding("left,h", "prev_page", "Previous 5", show=True),
42
+ Binding("right,l", "next_page", "Next 5", show=True),
43
+ Binding("q", "quit", "Esci", show=True),
44
+ ]
45
+
46
+ def __init__(self, kitchen, **kwargs):
47
+ super().__init__(**kwargs)
48
+ self.kitchen = kitchen
49
+ self.all_results = []
50
+ self.current_page = 0
51
+ self.results_per_page = 5
52
+ self.fetched_pages = set()
53
+ self.fetching_pages = set()
54
+ self.max_page_number = None
55
+ self.lock = threading.Lock()
56
+ self.is_fetching = False
57
+
58
+ def compose(self) -> ComposeResult:
59
+ yield Header()
60
+ with Vertical(id="results-container"):
61
+ yield LoadingIndicator(id="loading")
62
+ yield Static(id="status")
63
+ yield Footer()
64
+
65
+ def on_mount(self):
66
+ self.update_status("Loading...")
67
+ self.fetch_page_async(1)
68
+
69
+ def fetch_page_async(self, page_number: int):
70
+ if page_number in self.fetched_pages or page_number in self.fetching_pages:
71
+ return
72
+
73
+ if self.max_page_number and page_number > self.max_page_number:
74
+ return
75
+
76
+ self.fetching_pages.add(page_number)
77
+ thread = threading.Thread(
78
+ target=self.fetch_page,
79
+ args=(page_number,),
80
+ daemon=True,
81
+ name=f"fetch_page_{page_number}"
82
+ )
83
+ thread.start()
84
+
85
+ def fetch_page(self, page_number: int):
86
+ cook = self.kitchen.generate()
87
+ url = cook + str(page_number) + '/'
88
+
89
+ try:
90
+ page = Fetcher.get(url)
91
+
92
+ if page.status != 200:
93
+ with self.lock:
94
+ self.max_page_number = page_number - 1
95
+ self.fetching_pages.discard(page_number)
96
+ return
97
+
98
+ if page.find_by_text('No results were returned.'):
99
+ with self.lock:
100
+ self.max_page_number = page_number - 1
101
+ self.fetching_pages.discard(page_number)
102
+ return
103
+
104
+ rows = page.xpath('//tbody/tr')
105
+ page_results = []
106
+
107
+ for row in rows:
108
+ name = row.css('td.coll-1.name a::text').get()
109
+ if self.kitchen.remove and any(word.lower() in name.lower() for word in self.kitchen.remove):
110
+ continue
111
+ if self.kitchen.search and not any(word.lower() in name.lower() for word in self.kitchen.search):
112
+ continue
113
+
114
+ link = self.kitchen.base_url + row.css('td.coll-1.name a:not(.icon)::attr(href)').get()
115
+ seeds = row.css('td.coll-2.seeds::text').get()
116
+ leeches = row.css('td.coll-3.leeches::text').get()
117
+ date = row.css('td.coll-date::text').get()
118
+
119
+ size = None
120
+ uploader = None
121
+ for role in roles:
122
+ size = row.css(f'td.coll-4.size.mob-{role}::text').get()
123
+ uploader = row.css(f'td.coll-5.{role} a::text').get()
124
+ if size and uploader:
125
+ break
126
+
127
+ page_results.append({
128
+ 'name': name,
129
+ 'link': link,
130
+ 'seeds': seeds,
131
+ 'leeches': leeches,
132
+ 'date': date,
133
+ 'size': size,
134
+ 'uploader': uploader,
135
+ 'page': page_number
136
+ })
137
+
138
+ with self.lock:
139
+ self.all_results.extend(page_results)
140
+ self.fetched_pages.add(page_number)
141
+ self.fetching_pages.discard(page_number)
142
+
143
+ self.call_from_thread(self.show_current_page)
144
+
145
+ except SessionClosed:
146
+ with self.lock:
147
+ self.max_page_number = page_number - 1
148
+ self.fetching_pages.discard(page_number)
149
+ except Exception as e:
150
+ with self.lock:
151
+ self.fetching_pages.discard(page_number)
152
+ self.call_from_thread(self.update_status, f"Page error {page_number}: {str(e)}")
153
+
154
+ def get_results_for_display_page(self, display_page: int) -> list:
155
+ start_idx = display_page * self.results_per_page
156
+ end_idx = start_idx + self.results_per_page
157
+
158
+ with self.lock:
159
+ return self.all_results[start_idx:end_idx]
160
+
161
+ def show_current_page(self):
162
+ container = self.query_one("#results-container")
163
+ container.remove_children()
164
+
165
+ page_results = self.get_results_for_display_page(self.current_page)
166
+
167
+ if not page_results and self.current_page == 0:
168
+ container.mount(LoadingIndicator())
169
+ self.update_status("Loading...")
170
+ elif not page_results:
171
+ container.mount(Label("[yellow]No results[/]"))
172
+ else:
173
+ for result in page_results:
174
+ container.mount(ResultWidget(result))
175
+
176
+ with self.lock:
177
+ total_results = len(self.all_results)
178
+ fetched_count = len(self.fetched_pages)
179
+ fetching_count = len(self.fetching_pages)
180
+
181
+ total_pages = (total_results + self.results_per_page - 1) // self.results_per_page if total_results > 0 else 1
182
+ start_idx = self.current_page * self.results_per_page + 1
183
+ end_idx = min((self.current_page + 1) * self.results_per_page, total_results)
184
+
185
+ status_msg = f"Page {self.current_page + 1}/{total_pages} | Results {start_idx}-{end_idx} of {total_results}"
186
+ if fetching_count > 0:
187
+ status_msg += f" | Loading... (pages: {fetched_count})"
188
+ else:
189
+ status_msg += f" | Loaded pages: {fetched_count}"
190
+
191
+ self.update_status(status_msg)
192
+
193
+ self.prefetch_pages()
194
+
195
+ def prefetch_pages(self):
196
+ current_result_start = self.current_page * self.results_per_page
197
+ current_result_end = current_result_start + self.results_per_page
198
+
199
+ results_per_site_page = 20
200
+
201
+ pages_ahead = 3
202
+ future_result_end = current_result_end + (pages_ahead * self.results_per_page)
203
+
204
+ site_pages_needed = (future_result_end // results_per_site_page) + 2
205
+
206
+ for site_page in range(1, site_pages_needed + 1):
207
+ self.fetch_page_async(site_page)
208
+
209
+ def update_status(self, message: str):
210
+ try:
211
+ status = self.query_one("#status")
212
+ status.update(message)
213
+ except:
214
+ pass
215
+
216
+ def action_next_page(self):
217
+ with self.lock:
218
+ total_results = len(self.all_results)
219
+
220
+ max_page = (total_results - 1) // self.results_per_page
221
+
222
+ if total_results == 0 or self.current_page < max_page:
223
+ self.current_page += 1
224
+ self.show_current_page()
225
+
226
+ def action_prev_page(self):
227
+ if self.current_page > 0:
228
+ self.current_page -= 1
229
+ self.show_current_page()
230
+
231
+ class Category(Enum):
232
+ MOVIE = "Movies"
233
+ TV = "TV"
234
+ GAME = "Games"
235
+ MUSIC = "Music"
236
+ APP = "Apps"
237
+ DOCU = "Documentaries"
238
+ ANIME = "Anime"
239
+ OTHER = "Other"
240
+ XXX = "XXX"
241
+
242
+ class Sort(Enum):
243
+ TIME = "time"
244
+ SIZE = "size"
245
+ SEED = "seeders"
246
+ LEECH = "leechers"
247
+
248
+ class Ord(Enum):
249
+ ASC = "asc"
250
+ DESC = "desc"
251
+
252
+ class Url:
253
+ base_url = "https://1337x.to"
254
+
255
+ def __init__(self, name: str, category: Category = None, sort: Sort = None, ord: Ord = Ord.DESC, search: list[str] = None, remove: list[str] = None):
256
+ self.name = name
257
+ self.category = category
258
+ self.sort = sort
259
+ self.ord = ord
260
+ self.search = [] or search
261
+ self.remove = [] or remove
262
+
263
+ def generate(self) -> str:
264
+ search_name = self.name.replace(" ", "+")
265
+
266
+ if self.sort and self.category:
267
+ return f"{self.base_url}/sort-category-search/{search_name}/{self.category.value}/{self.sort.value}/{self.ord.value}/"
268
+ elif self.sort:
269
+ return f"{self.base_url}/sort-search/{search_name}/{self.sort.value}/{self.ord.value}/"
270
+ elif self.category:
271
+ return f"{self.base_url}/category-search/{search_name}/{self.category.value}/"
272
+ else:
273
+ return f"{self.base_url}/search/{search_name}/"
274
+
275
+ def argo() -> Url:
276
+ parser = argparse.ArgumentParser(
277
+ prog='bt1337xearch',
278
+ description='Better search for 1337x[.]to',
279
+ epilog='Example:\nbt1337xearch -n Dexter -c TV -s TIME -o ASC'
280
+ )
281
+ parser.add_argument("-n", "--name", help="Name of the Media", required=True)
282
+ parser.add_argument("-c", "--category", help="Category", choices=['MOVIE', 'TV', 'GAME', 'MUSIC', 'APP', 'DOCU', 'ANIME', 'OTHER', 'XXX'])
283
+ parser.add_argument("-s", "--sort", help="Sort by", choices=['TIME', 'SIZE', 'SEED', 'LEECH'])
284
+ parser.add_argument("-o", "--order", help="Order by", choices=['ASC', 'DESC'], default='DESC')
285
+ parser.add_argument("-f", "--filter", nargs='+', help="Filter by words\nYou can use '~' and '+' to filter with or without that word.")
286
+
287
+ args = parser.parse_args()
288
+
289
+ search = []
290
+ remove = []
291
+
292
+ if (args.filter):
293
+ for filter in args.filter:
294
+ if (filter[0] == '+'):
295
+ search.append(filter[1:].strip())
296
+ elif (filter[0] == '~'):
297
+ remove.append(filter[1:].strip())
298
+
299
+ kitchen = Url(
300
+ args.name,
301
+ category=Category[args.category] if args.category else None,
302
+ sort=Sort[args.sort] if args.sort else None,
303
+ ord=Ord[args.order],
304
+ search=search,
305
+ remove=remove
306
+ )
307
+ return(kitchen)
308
+
309
+ def parser() -> None:
310
+ try:
311
+ kitchen = argo()
312
+ app = MyApp(kitchen, ansi_color=True)
313
+ app.run()
314
+
315
+ except KeyboardInterrupt:
316
+ print("\n\nSearch interrupted")
317
+ exit(0)