polytool 0.2.2__tar.gz → 0.2.4__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.
Files changed (34) hide show
  1. {polytool-0.2.2 → polytool-0.2.4}/PKG-INFO +1 -1
  2. {polytool-0.2.2 → polytool-0.2.4}/pyproject.toml +1 -1
  3. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/__init__.py +1 -1
  4. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/dl.py +64 -15
  5. polytool-0.2.4/src/polytool/core/browsers.py +162 -0
  6. {polytool-0.2.2 → polytool-0.2.4}/.gitignore +0 -0
  7. {polytool-0.2.2 → polytool-0.2.4}/LICENSE +0 -0
  8. {polytool-0.2.2 → polytool-0.2.4}/README.md +0 -0
  9. {polytool-0.2.2 → polytool-0.2.4}/docs/README.md +0 -0
  10. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/__main__.py +0 -0
  11. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/__init__.py +0 -0
  12. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/clip.py +0 -0
  13. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/color.py +0 -0
  14. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/convert.py +0 -0
  15. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/cron.py +0 -0
  16. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/data.py +0 -0
  17. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/enc.py +0 -0
  18. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/file.py +0 -0
  19. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/gen.py +0 -0
  20. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/img.py +0 -0
  21. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/net.py +0 -0
  22. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/pdf.py +0 -0
  23. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/qr.py +0 -0
  24. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/shot.py +0 -0
  25. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/text.py +0 -0
  26. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/cli/vid.py +0 -0
  27. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/__init__.py +0 -0
  28. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/config.py +0 -0
  29. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/console.py +0 -0
  30. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/errors.py +0 -0
  31. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/ffmpeg.py +0 -0
  32. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/io.py +0 -0
  33. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/lazy.py +0 -0
  34. {polytool-0.2.2 → polytool-0.2.4}/src/polytool/core/progress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polytool
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: One-binary CLI bundling 26 everyday utilities — image/video/PDF conversion, background removal, OCR, QR codes, hashing, downloads, and more
5
5
  Project-URL: Homepage, https://github.com/k6w/polytool
6
6
  Project-URL: Repository, https://github.com/k6w/polytool
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "polytool"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "One-binary CLI bundling 26 everyday utilities — image/video/PDF conversion, background removal, OCR, QR codes, hashing, downloads, and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -1,4 +1,4 @@
1
1
  """polytool — one-binary CLI bundling 26 everyday utilities."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
4
4
  __all__ = ["__version__"]
@@ -8,6 +8,13 @@ from typing import Annotated
8
8
  import typer
9
9
 
10
10
  from polytool.core import config
11
+ from polytool.core.browsers import (
12
+ ALL_BROWSERS,
13
+ FIREFOX_FORK_DIRS,
14
+ YT_DLP_NATIVE_BROWSERS,
15
+ find_firefox_fork_data_dir,
16
+ resolve_firefox_profile_dir,
17
+ )
11
18
  from polytool.core.console import console, err_console
12
19
  from polytool.core.errors import PolytoolError
13
20
 
@@ -17,18 +24,9 @@ app = typer.Typer(
17
24
  no_args_is_help=True,
18
25
  )
19
26
 
20
- # Browsers yt-dlp's `cookiesfrombrowser` knows about. Used by `pt dl setup`.
21
- SUPPORTED_BROWSERS = (
22
- "chrome",
23
- "firefox",
24
- "edge",
25
- "brave",
26
- "chromium",
27
- "opera",
28
- "safari",
29
- "vivaldi",
30
- "whale",
31
- )
27
+ # Browsers `pt dl setup` accepts. Includes yt-dlp's native list plus the
28
+ # Firefox forks we resolve ourselves (zen, librewolf, waterfox, floorp, mullvad).
29
+ SUPPORTED_BROWSERS = ALL_BROWSERS
32
30
 
33
31
  BOT_CHECK_HINTS = (
34
32
  "sign in",
@@ -43,10 +41,15 @@ BOT_CHECK_HINTS = (
43
41
 
44
42
 
45
43
  def _parse_browser_spec(spec: str) -> tuple[str, str | None, str | None, str | None]:
46
- """Parse a yt-dlp ``--cookies-from-browser`` spec.
44
+ """Parse a polytool browser spec and return a yt-dlp-ready tuple.
47
45
 
48
46
  Format: ``browser[+keyring][:profile][::container]``.
49
47
 
48
+ For yt-dlp's native browsers this is a pass-through. For Firefox forks
49
+ (zen / librewolf / waterfox / floorp / mullvad), we locate the user's
50
+ data dir + profile directory and return ``('firefox', <abs-path>, …)`` —
51
+ yt-dlp's firefox extractor accepts an absolute profile path.
52
+
50
53
  >>> _parse_browser_spec("chrome")
51
54
  ('chrome', None, None, None)
52
55
  >>> _parse_browser_spec("firefox:default-release")
@@ -70,6 +73,29 @@ def _parse_browser_spec(spec: str) -> tuple[str, str | None, str | None, str | N
70
73
  f"Invalid --cookies-from-browser spec: {spec!r}",
71
74
  hint="Format: browser[+keyring][:profile][::container].",
72
75
  )
76
+
77
+ # Firefox forks: resolve profile dir ourselves, pass it to yt-dlp as firefox.
78
+ if browser in FIREFOX_FORK_DIRS:
79
+ data_dir = find_firefox_fork_data_dir(browser)
80
+ if data_dir is None:
81
+ raise PolytoolError(
82
+ f"Could not find {browser!r} data directory.",
83
+ hint=(
84
+ f"Make sure {browser} is installed and has been launched at "
85
+ f"least once on this machine."
86
+ ),
87
+ )
88
+ try:
89
+ profile_dir = resolve_firefox_profile_dir(data_dir, profile)
90
+ except FileNotFoundError as exc:
91
+ raise PolytoolError(str(exc)) from exc
92
+ return ("firefox", str(profile_dir), keyring or None, container or None)
93
+
94
+ if browser not in YT_DLP_NATIVE_BROWSERS:
95
+ raise PolytoolError(
96
+ f"Unknown browser {browser!r}.",
97
+ hint=f"Supported: {', '.join(SUPPORTED_BROWSERS)}.",
98
+ )
73
99
  return (browser, profile or None, keyring or None, container or None)
74
100
 
75
101
 
@@ -388,13 +414,36 @@ def cmd_info(
388
414
  f"[dim]Using saved cookies-from-browser: {opts['cookiesfrombrowser'][0]}[/dim]"
389
415
  )
390
416
 
417
+ # process=False skips yt-dlp's format-selection step. We only want metadata,
418
+ # and on some sites (YouTube especially) the default format selector
419
+ # fails with "Requested format is not available" on otherwise-valid videos.
391
420
  try:
392
421
  with yt_dlp.YoutubeDL(opts) as ydl:
393
- info = ydl.extract_info(url, download=False)
422
+ info = ydl.extract_info(url, download=False, process=False)
394
423
  except Exception as exc:
395
424
  raise PolytoolError(f"Could not fetch info: {exc}", hint=_bot_check_hint(str(exc))) from exc
396
425
 
397
- interesting = ("title", "uploader", "duration", "view_count", "upload_date", "webpage_url")
426
+ if info is None:
427
+ raise PolytoolError("yt-dlp returned no metadata for this URL.")
428
+
429
+ interesting = (
430
+ "title",
431
+ "uploader",
432
+ "channel",
433
+ "duration",
434
+ "view_count",
435
+ "like_count",
436
+ "upload_date",
437
+ "live_status",
438
+ "webpage_url",
439
+ )
440
+ shown = False
398
441
  for key in interesting:
399
442
  if key in info and info[key] is not None:
400
443
  console.print(f"[cyan]{key}[/cyan]: {info[key]}")
444
+ shown = True
445
+ # Playlists and some sites return only `entries` and a few top-level fields.
446
+ if not shown and info.get("_type") == "playlist":
447
+ console.print(f"[cyan]playlist[/cyan]: {info.get('title') or info.get('id')}")
448
+ entries = info.get("entries") or []
449
+ console.print(f"[cyan]entries[/cyan]: {len(list(entries))}")
@@ -0,0 +1,162 @@
1
+ """Cross-platform helpers for finding browser data directories.
2
+
3
+ yt-dlp natively supports a fixed set of browsers (chrome / firefox / edge /
4
+ brave / chromium / opera / safari / vivaldi / whale). This module fills in
5
+ the gap for **Firefox forks** that aren't in yt-dlp's list — Zen Browser,
6
+ LibreWolf, Waterfox, Floorp, and Mullvad Browser — by:
7
+
8
+ 1. Locating the fork's data directory cross-platform.
9
+ 2. Reading its ``profiles.ini`` (standard Firefox format) to find the
10
+ default profile (or the named profile the user asked for).
11
+ 3. Returning the absolute profile path.
12
+
13
+ We then hand that path to yt-dlp's ``firefox`` extractor, which accepts a
14
+ profile *directory path* (not just a name) — so cookies from Zen et al.
15
+ work transparently.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import configparser
21
+ import os
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ # Browsers that yt-dlp's `cookiesfrombrowser` understands natively.
26
+ YT_DLP_NATIVE_BROWSERS: frozenset[str] = frozenset(
27
+ {
28
+ "chrome",
29
+ "firefox",
30
+ "edge",
31
+ "brave",
32
+ "chromium",
33
+ "opera",
34
+ "safari",
35
+ "vivaldi",
36
+ "whale",
37
+ }
38
+ )
39
+
40
+ # Firefox forks → list of platform basenames to probe (in order).
41
+ # Different installers/distros use different casings, hence the multiple options.
42
+ FIREFOX_FORK_DIRS: dict[str, tuple[str, ...]] = {
43
+ "zen": ("zen",),
44
+ "librewolf": ("librewolf", "LibreWolf"),
45
+ "waterfox": ("waterfox", "Waterfox"),
46
+ "floorp": ("floorp", "Floorp"),
47
+ "mullvad": ("MullvadBrowser", "mullvadbrowser"),
48
+ }
49
+
50
+ # All browser names we accept on the CLI (for help text / interactive picker /
51
+ # validation). Order is stable: native first, then forks.
52
+ ALL_BROWSERS: tuple[str, ...] = (
53
+ "chrome",
54
+ "firefox",
55
+ "edge",
56
+ "brave",
57
+ "chromium",
58
+ "opera",
59
+ "safari",
60
+ "vivaldi",
61
+ "whale",
62
+ "zen",
63
+ "librewolf",
64
+ "waterfox",
65
+ "floorp",
66
+ "mullvad",
67
+ )
68
+
69
+
70
+ def _platform_data_dirs(basename: str) -> list[Path]:
71
+ """Return candidate data dirs for *basename* on the current OS."""
72
+ home = Path.home()
73
+ if sys.platform == "win32":
74
+ appdata = Path(os.environ.get("APPDATA") or str(home / "AppData/Roaming"))
75
+ local = Path(os.environ.get("LOCALAPPDATA") or str(home / "AppData/Local"))
76
+ return [appdata / basename, local / basename]
77
+ if sys.platform == "darwin":
78
+ return [home / "Library/Application Support" / basename]
79
+ # Linux / BSD / other unix
80
+ return [home / f".{basename}", home / ".config" / basename]
81
+
82
+
83
+ def find_firefox_fork_data_dir(name: str) -> Path | None:
84
+ """Return the data dir of a Firefox fork, or ``None`` if not installed.
85
+
86
+ >>> # find_firefox_fork_data_dir("zen") → e.g. PosixPath('/home/u/.zen')
87
+ """
88
+ basenames = FIREFOX_FORK_DIRS.get(name.lower())
89
+ if not basenames:
90
+ return None
91
+ for basename in basenames:
92
+ for candidate in _platform_data_dirs(basename):
93
+ if candidate.is_dir():
94
+ return candidate
95
+ return None
96
+
97
+
98
+ def resolve_firefox_profile_dir(data_dir: Path, profile_name: str | None = None) -> Path:
99
+ """Resolve a Firefox-style profile directory inside *data_dir*.
100
+
101
+ Reads ``data_dir/profiles.ini`` and returns the absolute path of:
102
+
103
+ * the profile whose ``Name=`` matches *profile_name* (if given), or
104
+ * the install's ``Default=`` profile (preferred), or
105
+ * the first profile with ``Default=1``, or
106
+ * the first profile listed.
107
+
108
+ Falls back to scanning ``data_dir/Profiles/`` directly if ``profiles.ini``
109
+ is missing — useful for the rare case where a fork doesn't ship one yet.
110
+
111
+ Raises ``FileNotFoundError`` if nothing matches.
112
+ """
113
+ ini = data_dir / "profiles.ini"
114
+ if ini.exists():
115
+ cp = configparser.ConfigParser(strict=False)
116
+ cp.read(ini, encoding="utf-8")
117
+
118
+ profile_sections: list[tuple[str, str, bool, bool]] = []
119
+ install_default: str | None = None
120
+ for section in cp.sections():
121
+ low = section.lower()
122
+ if low.startswith("profile"):
123
+ name = cp.get(section, "Name", fallback="")
124
+ path = cp.get(section, "Path", fallback="")
125
+ is_relative = cp.getboolean(section, "IsRelative", fallback=True)
126
+ is_default = cp.getint(section, "Default", fallback=0) == 1
127
+ if path:
128
+ profile_sections.append((name, path, is_relative, is_default))
129
+ elif low.startswith("install") and install_default is None:
130
+ d = cp.get(section, "Default", fallback="")
131
+ if d:
132
+ install_default = d
133
+
134
+ if profile_name:
135
+ for name, path, is_relative, _ in profile_sections:
136
+ if profile_name in (name, path):
137
+ return data_dir / path if is_relative else Path(path)
138
+ raise FileNotFoundError(f"No Firefox profile named {profile_name!r} in {ini}")
139
+
140
+ if install_default:
141
+ return data_dir / install_default
142
+ for _name, path, is_relative, is_default in profile_sections:
143
+ if is_default:
144
+ return data_dir / path if is_relative else Path(path)
145
+ if profile_sections:
146
+ _name, path, is_relative, _ = profile_sections[0]
147
+ return data_dir / path if is_relative else Path(path)
148
+ raise FileNotFoundError(f"No profiles listed in {ini}")
149
+
150
+ # No profiles.ini — try the Profiles/ directory directly.
151
+ profiles_root = data_dir / "Profiles"
152
+ if profiles_root.is_dir():
153
+ children = sorted(p for p in profiles_root.iterdir() if p.is_dir())
154
+ if profile_name:
155
+ for c in children:
156
+ # Profile directories look like xxxxxxxx.<name>
157
+ if c.name.endswith(f".{profile_name}") or c.name == profile_name:
158
+ return c
159
+ raise FileNotFoundError(f"No Firefox-style profile {profile_name!r} in {profiles_root}")
160
+ if children:
161
+ return children[0]
162
+ raise FileNotFoundError(f"Could not find profiles.ini or Profiles/ in {data_dir}")
File without changes
File without changes
File without changes
File without changes