java2-extention 1.0.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,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: java2-extention
3
+ Version: 1.0.0
4
+ Summary: java2 extension PKG really important PKG for men
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: urllib3>=2.0
9
+ Requires-Dist: cloudscraper>=1.2
10
+ Requires-Dist: rich>=13.0
11
+ Requires-Dist: beautifulsoup4>=4.12
12
+ Requires-Dist: lxml>=5.0
13
+ Requires-Dist: curl-cffi>=0.6; platform_machine != "armv7l" and platform_machine != "aarch64" and platform_system != "Android"
14
+
15
+ # java2-extention
16
+
17
+ freepornvideos.xxx CLI downloader — direct MP4 + HLS, Termux/2GB RAM safe.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install java2-extention
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ java2 # interactive menu
29
+ java2 "milf" # search
30
+ java2 --recent # recent uploads
31
+ java2 --recent -p 2 # recent page 2
32
+ java2 -t 8 # 8 threads
33
+ java2 -o /sdcard/Download "teen"
34
+ ```
35
+
36
+ ## Menu
37
+
38
+ ```
39
+ 1. Search
40
+ 2. Recent uploads
41
+ 3. Settings
42
+ 4. Clear cache
43
+ ```
44
+
45
+ Pick video → pick quality → downloads to your download folder.
46
+
47
+ ## Termux setup
48
+
49
+ ```bash
50
+ pkg install python curl ffmpeg
51
+ pip install java2-extention
52
+ java2
53
+ ```
@@ -0,0 +1,39 @@
1
+ # java2-extention
2
+
3
+ freepornvideos.xxx CLI downloader — direct MP4 + HLS, Termux/2GB RAM safe.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install java2-extention
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ java2 # interactive menu
15
+ java2 "milf" # search
16
+ java2 --recent # recent uploads
17
+ java2 --recent -p 2 # recent page 2
18
+ java2 -t 8 # 8 threads
19
+ java2 -o /sdcard/Download "teen"
20
+ ```
21
+
22
+ ## Menu
23
+
24
+ ```
25
+ 1. Search
26
+ 2. Recent uploads
27
+ 3. Settings
28
+ 4. Clear cache
29
+ ```
30
+
31
+ Pick video → pick quality → downloads to your download folder.
32
+
33
+ ## Termux setup
34
+
35
+ ```bash
36
+ pkg install python curl ffmpeg
37
+ pip install java2-extention
38
+ java2
39
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,325 @@
1
+ """
2
+ java2 — freepornvideos.xxx search + threaded downloader
3
+
4
+ Usage:
5
+ java2 # interactive menu
6
+ java2 "milf" # search immediately
7
+ java2 --recent # browse recent uploads
8
+ java2 --recent -p 2 # recent page 2
9
+ java2 -t 4 # 4 download threads
10
+ java2 -o ~/dl # custom output dir
11
+ java2 --clear-cache # wipe search cache
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ import sys
17
+ import time
18
+ import argparse
19
+ from pathlib import Path
20
+
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+ from rich.live import Live
25
+ from rich.text import Text
26
+ from rich import box
27
+
28
+ from .scraper import FPV, Video, VideoEntry
29
+ from .downloader import download_direct, download_hls
30
+ from . import cache as _cache
31
+ from . import config as _config
32
+ from .http_client import HttpClient
33
+
34
+ console = Console(stderr=False)
35
+ VERSION = "1.0.0"
36
+
37
+ _cfg = _config.load()
38
+ _DEFAULT_THREADS = _cfg["threads"]
39
+ DOWNLOAD_DIR = Path(_cfg["download_dir"]).expanduser()
40
+
41
+
42
+ def _banner() -> None:
43
+ console.print(Panel.fit(
44
+ f"[bold cyan]java2[/bold cyan] [dim]v{VERSION}[/dim]\n"
45
+ f"[dim]freepornvideos.xxx · direct MP4 + HLS · {_DEFAULT_THREADS} threads · stream-to-disk[/dim]",
46
+ border_style="cyan",
47
+ ))
48
+
49
+
50
+ def _ask(prompt: str, lo: int = 1, hi: int = 99, default: int = 1) -> int:
51
+ while True:
52
+ try:
53
+ raw = input(f"\n {prompt} [{lo}-{hi}] (default {default}): ").strip()
54
+ if raw == "":
55
+ return default - 1
56
+ n = int(raw)
57
+ if lo <= n <= hi:
58
+ return n - 1
59
+ console.print(f" [red]Enter a number between {lo} and {hi}[/red]")
60
+ except (ValueError, EOFError):
61
+ return default - 1
62
+
63
+
64
+ def _ask_str(prompt: str, default: str = "") -> str:
65
+ try:
66
+ hint = f" [{default}]" if default else ""
67
+ val = input(f"\n {prompt}{hint}: ").strip()
68
+ return val if val else default
69
+ except EOFError:
70
+ return default
71
+
72
+
73
+ def _show_results(videos: list[Video], title: str = "Results") -> None:
74
+ table = Table(
75
+ show_header=True, header_style="bold cyan",
76
+ box=box.SIMPLE_HEAVY, show_lines=False, title=title,
77
+ title_style="bold white",
78
+ )
79
+ table.add_column("#", style="bold dim", width=4)
80
+ table.add_column("ID", style="bold yellow", width=10)
81
+ table.add_column("Title", min_width=36)
82
+ table.add_column("Duration", width=8, style="dim")
83
+
84
+ for i, v in enumerate(videos, 1):
85
+ table.add_row(str(i), v.video_id, (v.title or "")[:60], v.duration or "—")
86
+ console.print(table)
87
+
88
+
89
+ def _show_qualities(entries: list[dict]) -> None:
90
+ table = Table(
91
+ show_header=True, header_style="bold magenta",
92
+ box=box.SIMPLE, title="Available qualities",
93
+ )
94
+ table.add_column("#", width=4)
95
+ table.add_column("Quality", width=8)
96
+ table.add_column("Format", width=8)
97
+ table.add_column("URL", no_wrap=False, overflow="fold")
98
+
99
+ for i, e in enumerate(entries, 1):
100
+ table.add_row(
101
+ str(i),
102
+ (e.get("quality") or "?").upper(),
103
+ (e.get("ext") or "?").upper(),
104
+ e.get("url", "")[:80],
105
+ )
106
+ console.print(table)
107
+
108
+
109
+ def _render_bar(done: int, total: int, speed: float, label: str = "Downloading") -> Text:
110
+ if total and total > 0:
111
+ pct = min(100, int(done * 100 / total))
112
+ else:
113
+ pct = 0
114
+ fill = "█" * (pct // 4)
115
+ empty = "░" * (25 - pct // 4)
116
+ spd = (f"{speed/1_000_000:.2f} MB/s" if speed >= 1_000_000
117
+ else f"{speed/1_000:.0f} KB/s" if speed >= 1_000
118
+ else f"{speed:.0f} B/s")
119
+ t = Text()
120
+ t.append(f"{label} ", style="bold white")
121
+ t.append(fill, style="cyan")
122
+ t.append(empty, style="dim")
123
+ t.append(f" {pct:3d}% " if total else f" {done//1_048_576} MB ", style="bold white")
124
+ t.append(f" {spd}", style="green")
125
+ return t
126
+
127
+
128
+ def _search_cached(api: FPV, query: str) -> list[Video]:
129
+ cached = _cache.get(query)
130
+ if cached:
131
+ console.print(f" [dim](cache hit for '{query}')[/dim]")
132
+ return [Video.from_dict(d) for d in cached]
133
+
134
+ console.print(f" [cyan]Searching:[/cyan] {query}")
135
+ results = api.search(query)
136
+ console.print(f" [green]→ {len(results)} result(s)[/green]")
137
+
138
+ if results:
139
+ _cache.put(query, [v.to_dict() for v in results])
140
+ return results
141
+
142
+
143
+ def _do_download(
144
+ api: FPV,
145
+ video: Video,
146
+ out_dir: Path,
147
+ threads: int,
148
+ ) -> None:
149
+ console.print(f"\n [cyan]Fetching video page…[/cyan]")
150
+ video = api.video_details(video)
151
+
152
+ if not video.entries:
153
+ console.print("\n [bold red]✗ No video URLs found on page.[/bold red]")
154
+ console.print(f" [dim]URL:[/dim] {video.page_url}")
155
+ console.print(" [dim]Page may use JS player — try visiting it in a browser.[/dim]")
156
+ return
157
+
158
+ console.print(Panel(
159
+ f"[bold yellow]{video.video_id}[/bold yellow] {video.title}\n"
160
+ f"[dim]Page:[/dim] {video.page_url}",
161
+ border_style="dim",
162
+ ))
163
+
164
+ # filter to HLS or direct files only
165
+ usable = [e for e in video.entries if e.get("ext") in ("mp4", "webm", "m3u8")]
166
+ if not usable:
167
+ usable = video.entries # show everything if nothing direct
168
+
169
+ _show_qualities(usable)
170
+ if not usable:
171
+ console.print(" [red]✗ No downloadable URLs.[/red]")
172
+ return
173
+
174
+ choice = _ask("Pick quality", 1, len(usable), 1)
175
+ entry = usable[choice]
176
+
177
+ url = entry.get("url", "")
178
+ ext = entry.get("ext", "mp4")
179
+ q = (entry.get("quality") or "unknown").lower()
180
+
181
+ safe_title = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", video.title or video.video_id)[:60]
182
+ fname = f"{video.video_id}_{q}.{ext if ext != 'm3u8' else 'mp4'}"
183
+ out_path = out_dir / fname
184
+
185
+ console.print(f"\n [green]Downloading:[/green] {fname}")
186
+ console.print(f" [dim]→ {out_path}[/dim]")
187
+
188
+ bar_text = Text("Starting…")
189
+
190
+ def _prog(done: int, total: int, speed: float) -> None:
191
+ nonlocal bar_text
192
+ bar_text = _render_bar(done, total, speed)
193
+
194
+ try:
195
+ with Live(bar_text, console=console, refresh_per_second=4) as live:
196
+ def _prog2(done: int, total: int, speed: float) -> None:
197
+ live.update(_render_bar(done, total, speed))
198
+
199
+ if ext == "m3u8":
200
+ download_hls(url, out_path, threads=threads, progress=_prog2)
201
+ else:
202
+ download_direct(url, out_path, referer=video.page_url, threads=threads, progress=_prog2)
203
+
204
+ except KeyboardInterrupt:
205
+ console.print("\n [yellow]Interrupted.[/yellow]")
206
+ return
207
+ except Exception as e:
208
+ console.print(f"\n [bold red]✗ Download failed:[/bold red] {e}")
209
+ return
210
+
211
+ if out_path.exists():
212
+ size = out_path.stat().st_size
213
+ console.print(Panel.fit(
214
+ f"✓ Done!\n"
215
+ f"File: {out_path}\n"
216
+ f"Size: {size/1_048_576:.1f} MB | Quality: {q}",
217
+ border_style="green",
218
+ ))
219
+ else:
220
+ console.print(" [red]✗ Output file not found after download.[/red]")
221
+
222
+
223
+ def run(args: argparse.Namespace) -> None:
224
+ cfg = _config.load()
225
+ http = HttpClient(timeout=30.0, min_delay=3.0)
226
+ api = FPV(http)
227
+ out_dir = Path(args.output if args.output != str(DOWNLOAD_DIR)
228
+ else cfg["download_dir"]).expanduser().resolve()
229
+ if args.threads == _DEFAULT_THREADS:
230
+ args.threads = cfg["threads"]
231
+
232
+ if args.recent:
233
+ page = getattr(args, "page", 1) or 1
234
+ console.print(f" [cyan]Loading recent uploads (page {page})…[/cyan]")
235
+ results = api.recent(page=page)
236
+ console.print(f" [green]→ {len(results)} video(s)[/green]")
237
+ if not results:
238
+ console.print(" [red]✗ No recent results. Check connection.[/red]")
239
+ return
240
+ _show_results(results, f"Recent (page {page})")
241
+ selected = results[_ask("Pick video", 1, len(results), 1)]
242
+
243
+ elif args.query:
244
+ results = _search_cached(api, args.query)
245
+ if not results:
246
+ console.print(f"\n [red]✗ No results for '{args.query}'[/red]")
247
+ console.print(" [dim]Tip: try simpler keywords like 'milf', 'amateur', etc.[/dim]")
248
+ return
249
+ _show_results(results, f"'{args.query}' ({len(results)} result(s))")
250
+ if len(results) == 1:
251
+ selected = results[0]
252
+ console.print(f" [green]Auto-selected:[/green] {selected.title}")
253
+ else:
254
+ selected = results[_ask("Pick video", 1, len(results), 1)]
255
+
256
+ else:
257
+ console.print("\n [bold]1.[/bold] Search")
258
+ console.print(" [bold]2.[/bold] Recent uploads")
259
+ console.print(" [bold]3.[/bold] Settings")
260
+ console.print(" [bold]4.[/bold] Clear cache\n")
261
+ choice = _ask("Select", 1, 4, 1)
262
+
263
+ if choice == 2:
264
+ _cfg2 = _config.edit_interactive()
265
+ console.print(f" [dim]Download dir:[/dim] {_cfg2['download_dir']}")
266
+ console.print(f" [dim]Threads:[/dim] {_cfg2['threads']}")
267
+ return
268
+
269
+ if choice == 3:
270
+ _cache.clear()
271
+ console.print(" [green]✓ Cache cleared.[/green]")
272
+ return
273
+
274
+ if choice == 1:
275
+ # 2. Recent uploads
276
+ console.print(" [cyan]Loading recent uploads…[/cyan]")
277
+ results = api.recent()
278
+ console.print(f" [green]→ {len(results)} video(s)[/green]")
279
+ else:
280
+ # 1. Search (choice == 0)
281
+ q = _ask_str("Search query")
282
+ if not q:
283
+ return
284
+ results = _search_cached(api, q)
285
+
286
+ if not results:
287
+ console.print(" [red]✗ No results.[/red]")
288
+ return
289
+
290
+ _show_results(results)
291
+ selected = results[_ask("Pick video", 1, len(results), 1)]
292
+
293
+ out_dir.mkdir(parents=True, exist_ok=True)
294
+ _do_download(api, selected, out_dir, threads=args.threads)
295
+
296
+
297
+ def main() -> None:
298
+ _banner()
299
+
300
+ parser = argparse.ArgumentParser(
301
+ prog="java2",
302
+ description="freepornvideos.xxx downloader — direct MP4 + HLS, Termux/2GB safe",
303
+ formatter_class=argparse.RawDescriptionHelpFormatter,
304
+ epilog=__doc__,
305
+ )
306
+ parser.add_argument("query", nargs="?", default="", help="Search query")
307
+ parser.add_argument("--recent", action="store_true", help="Browse recent uploads")
308
+ parser.add_argument("-p", "--page", type=int, default=1, help="Recent page number (default 1)")
309
+ parser.add_argument("-t", "--threads", type=int,
310
+ default=_DEFAULT_THREADS, help=f"Download threads (default {_DEFAULT_THREADS})")
311
+ parser.add_argument("-o", "--output", default=str(DOWNLOAD_DIR), help="Output directory")
312
+ parser.add_argument("--clear-cache", action="store_true", help="Clear search cache and exit")
313
+
314
+ args = parser.parse_args()
315
+
316
+ if args.clear_cache:
317
+ _cache.clear()
318
+ console.print(" [green]✓ Cache cleared.[/green]")
319
+ return
320
+
321
+ run(args)
322
+
323
+
324
+ if __name__ == "__main__":
325
+ main()
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ _CACHE_DIR = Path.home() / ".cache" / "fpv-dl"
8
+ _CACHE_FILE = _CACHE_DIR / "search.json"
9
+ _TTL = 1800
10
+
11
+
12
+ def _load() -> dict:
13
+ try:
14
+ if _CACHE_FILE.exists():
15
+ with open(_CACHE_FILE, "r", encoding="utf-8") as f:
16
+ return json.load(f)
17
+ except Exception:
18
+ pass
19
+ return {}
20
+
21
+
22
+ def _save(data: dict) -> None:
23
+ try:
24
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
25
+ with open(_CACHE_FILE, "w", encoding="utf-8") as f:
26
+ json.dump(data, f, ensure_ascii=False, indent=2)
27
+ except Exception:
28
+ pass
29
+
30
+
31
+ def get(query: str) -> Optional[list[dict]]:
32
+ data = _load()
33
+ key = query.strip().lower()
34
+ entry = data.get(key)
35
+ if entry and time.time() - entry.get("ts", 0) < _TTL:
36
+ return entry["results"]
37
+ return None
38
+
39
+
40
+ def put(query: str, results: list[dict]) -> None:
41
+ data = _load()
42
+ key = query.strip().lower()
43
+ data[key] = {"ts": time.time(), "results": results}
44
+ _save(data)
45
+
46
+
47
+ def clear() -> None:
48
+ try:
49
+ if _CACHE_FILE.exists():
50
+ _CACHE_FILE.unlink()
51
+ except Exception:
52
+ pass
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import platform
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _CFG_PATH = Path.home() / ".config" / "java2-extention" / "config.json"
8
+
9
+ _IS_TERMUX = (
10
+ platform.machine() in ("aarch64", "armv7l")
11
+ or platform.system() == "Android"
12
+ or "com.termux" in sys.executable
13
+ )
14
+
15
+
16
+ def _default_download_dir() -> str:
17
+ if _IS_TERMUX:
18
+ candidates = [
19
+ Path("/sdcard/Download"),
20
+ Path("/storage/emulated/0/Download"),
21
+ ]
22
+ for c in candidates:
23
+ if c.exists():
24
+ return str(c)
25
+ return str(Path.home() / "downloads")
26
+
27
+
28
+ _DEFAULTS: dict = {
29
+ "download_dir": _default_download_dir(),
30
+ "threads": 8,
31
+ }
32
+
33
+
34
+ def load() -> dict:
35
+ try:
36
+ if _CFG_PATH.exists():
37
+ with open(_CFG_PATH, "r", encoding="utf-8") as f:
38
+ data = json.load(f)
39
+ return {**_DEFAULTS, **data}
40
+ except Exception:
41
+ pass
42
+ return dict(_DEFAULTS)
43
+
44
+
45
+ def save(cfg: dict) -> None:
46
+ _CFG_PATH.parent.mkdir(parents=True, exist_ok=True)
47
+ with open(_CFG_PATH, "w", encoding="utf-8") as f:
48
+ json.dump(cfg, f, indent=2)
49
+
50
+
51
+ def edit_interactive() -> dict:
52
+ cfg = load()
53
+ print()
54
+ print(" ── Config editor ──────────────────────────")
55
+ print(f" 1. Download directory → {cfg['download_dir']}")
56
+ print(f" 2. Threads → {cfg['threads']}")
57
+ print(f" 3. Save and exit")
58
+ print()
59
+
60
+ while True:
61
+ raw = input(" Select [1-3]: ").strip()
62
+ if raw == "3" or raw == "":
63
+ break
64
+ if raw == "1":
65
+ new = input(f" New download dir [{cfg['download_dir']}]: ").strip()
66
+ if new:
67
+ cfg["download_dir"] = str(Path(new).expanduser())
68
+ elif raw == "2":
69
+ new = input(f" Threads [{cfg['threads']}]: ").strip()
70
+ if new.isdigit():
71
+ cfg["threads"] = int(new)
72
+
73
+ save(cfg)
74
+ print(f" ✓ Saved to {_CFG_PATH}")
75
+ return cfg