java2-extention 1.0.0__py3-none-any.whl

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 @@
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