transmute-cli 0.2.3a0__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,335 @@
1
+ Metadata-Version: 2.5
2
+ Name: transmute-cli
3
+ Version: 0.2.3a
4
+ Summary: Convert YouTube or SoundCloud links into rich MP3s.
5
+ Project-URL: Homepage, https://github.com/vznh/transmute
6
+ Project-URL: Repository, https://github.com/vznh/transmute
7
+ Project-URL: Issues, https://github.com/vznh/transmute/issues
8
+ License-Expression: GPL-3.0-or-later
9
+ License-File: LICENSE
10
+ Keywords: id3,mp3,soundcloud,tui,youtube,yt-dlp
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console :: Curses
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: anthropic>=0.60
22
+ Requires-Dist: mutagen>=1.47
23
+ Requires-Dist: openai>=2.0
24
+ Requires-Dist: prompt-toolkit>=3.0.43
25
+ Requires-Dist: pymobiledevice3<12,>=11.45
26
+ Requires-Dist: yt-dlp>=2024.12.6
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Transmute
30
+
31
+ Convert YouTube or SoundCloud links into rich MP3s, from your terminal. We support single or batch operations.
32
+
33
+ ## How it works
34
+ 1. Authenticate
35
+
36
+ You can do any of the two options:
37
+ - Authenticate with your Claude or Codex account by signing in
38
+ - Bring your own API key (only Codex, Claude supported for now)
39
+ 2. Provide a correct URL
40
+
41
+ We support `youtube.com`, `youtu.be`, and `soundcloud.com` links, including
42
+ subdomains like `music.youtube.com`. Anything else is turned away as you paste it,
43
+ rather than failing later mid-download.
44
+
45
+ 3. Ensure you output to the right directory using `/out`
46
+
47
+
48
+ 4. We use Claude / Codex to utilize Web Search to ensure proper metadata is fulfilled
49
+
50
+ - This works especially for underground rap artists. Often times, uploaders are not the name of the author and we do a double-check and sanity check to ensure that the right artist is associated with the song.
51
+ - The image is not altered. From the link you provide, we attach that image to it.
52
+
53
+
54
+ ## Install
55
+
56
+ ### Homebrew
57
+
58
+ ```sh
59
+ brew tap vznh/transmute https://github.com/vznh/transmute
60
+ brew trust vznh/transmute
61
+ brew install transmute
62
+ ```
63
+
64
+ ffmpeg is installed as a dependency, so there is nothing else to set up. Then
65
+ run `transmute`.
66
+
67
+ Three commands rather than one, for two reasons worth knowing once:
68
+
69
+ - **The tap names a URL** because this repository is its own Homebrew tap.
70
+ Homebrew otherwise derives a tap's location from its name and would look for a
71
+ separate `vznh/homebrew-transmute` repository, which does not exist.
72
+ - **The trust step is required**, not optional. Homebrew refuses to load a
73
+ formula from an untrusted third-party tap. Trust is recorded per remote URL,
74
+ so you grant it once.
75
+
76
+ Both are one-time. Later releases need only `brew upgrade transmute`.
77
+
78
+ The build compiles two Rust extensions from source, so the first install takes a
79
+ few minutes.
80
+
81
+ ### From a checkout
82
+
83
+ ```sh
84
+ uv sync
85
+ uv run transmute
86
+ ```
87
+
88
+ Run both commands from the repository root. After the first `uv sync`, starting
89
+ Transmute only requires `uv run transmute`. ffmpeg is not installed for you here
90
+ — see Requirements below.
91
+
92
+ ### From PyPI
93
+
94
+ Not yet. `transmute-cli` is unpublished, so `uv tool install` and `pipx install`
95
+ do not work. Once it is released, the version must be pinned, because Transmute
96
+ is an alpha and both resolvers skip prereleases unless asked by name:
97
+
98
+ ```sh
99
+ uv tool install "transmute-cli==0.2.2a0" # or: pipx install "transmute-cli==0.2.2a0"
100
+ brew install ffmpeg # not included on this path
101
+ ```
102
+
103
+ ### Checking what you have
104
+
105
+ ```sh
106
+ transmute --version # transmute 0.2.2a
107
+ ```
108
+
109
+ `brew list --versions transmute` reports `0.2.2` for the same build. Homebrew
110
+ parses the version out of the release tag and drops the alpha suffix; PyPI will
111
+ normalise it the other way, to `0.2.2a0`. All three name one release. The
112
+ checkout version is now `0.2.3a`; it is not available through PyPI or Homebrew
113
+ until that release is published.
114
+
115
+ ## Requirements
116
+
117
+ - ffmpeg — installed for you by Homebrew, otherwise `brew install ffmpeg`
118
+ - Claude Code signed in with Claude, or Codex CLI signed in with ChatGPT, for
119
+ subscription-backed metadata enrichment
120
+ - [uv](https://docs.astral.sh/uv/) — only to run from a checkout or to develop
121
+
122
+ ## Use
123
+
124
+ Paste one or more links (even concatenated back-to-back) and hit Enter. Links are
125
+ queued and processed in the background (up to 4 at a time) — the input line stays
126
+ live at the bottom, so keep pasting while earlier tracks convert. A status bar below
127
+ the prompt shows the output dir, bitrate, and active/queued work. MP3s are written
128
+ at 320kbps with embedded metadata and cover art, to `~/Downloads` by default.
129
+
130
+ Unsupported links are rejected as you paste them; if a paste mixes supported and
131
+ unsupported links, the supported ones are queued and the rest are reported as
132
+ ignored. When a download does fail, the reason is summarized in a line you can act
133
+ on — `ffmpeg missing — brew install ffmpeg`, `requires login — needs browser
134
+ cookies`, `not available in your region`. Failures that retrying cannot fix, like a
135
+ private or removed video, are marked non-retryable and left out of `/retry`.
136
+
137
+ ## History and persistence
138
+
139
+ Prompt recall and structured download activity persist locally across restarts.
140
+ Recent completed and failed tracks are restored when Transmute opens; restored
141
+ failures can be retried and restored low-confidence entries can receive hints just
142
+ like entries from the current session. `/list` includes recent persisted tracks.
143
+
144
+ If Transmute is killed mid-download, the next launch notices the interrupted
145
+ session and turns whatever was still in flight into a retryable failure
146
+ (`interrupted — retry when ready`) instead of losing it. Several instances can run
147
+ at once against the same storage: a retry or a hint is claimed by exactly one of
148
+ them, and starting one does not disturb work already running in another.
149
+
150
+ Your output directory and bitrate also persist. `/out` and `/quality` are saved to
151
+ `~/.transmute/settings.json` and restored on the next launch. A runtime command in
152
+ the current session overrides the saved file, which overrides the built-in defaults
153
+ (`~/Downloads`, 320kbps); a missing or unreadable settings file falls back to those
154
+ defaults without overwriting it.
155
+
156
+ Prompt recall is stored in `~/.transmute/history`, and download activity is stored
157
+ in `~/.transmute/activity.sqlite3`. URLs and metadata remain local and unencrypted,
158
+ with owner-only permissions on the storage directory and files. `/clear` removes
159
+ persisted completed and failed activity as well as clearing the current view.
160
+ In-flight jobs are not canceled, so they can appear again when they finish.
161
+
162
+ ## Metadata enrichment
163
+
164
+ After each download, Transmute runs a provider-backed web search to find the track's
165
+ canonical metadata — artist, title, album, release year, genre. Tags are written into
166
+ the MP3 (cover art is preserved) and the file is renamed to
167
+ `Artist - Title.mp3`. If a file with that name already exists, Transmute keeps it
168
+ and adds a numeric suffix (`Artist - Title (1).mp3`) rather than overwriting it.
169
+ Both CLI and API enrichment pin OpenAI/Codex to `gpt-6-luna` and Anthropic/Claude
170
+ to `claude-haiku-5-5`.
171
+
172
+ Auth (in order of preference):
173
+
174
+ 1. **Entered API key** — run `/key` to open a masked prompt. It accepts one
175
+ OpenAI `sk-…` or Anthropic `sk-ant-…` key, auto-detects the provider, and
176
+ replaces any previously entered key. The key stays in memory only and is
177
+ never written to Transmute's command history. Use `/key clear` to remove it.
178
+ 2. **Environment API key** — `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` is selected
179
+ automatically. If both are set, OpenAI takes priority. API usage is billed
180
+ separately by the detected provider.
181
+ 3. **Claude subscription** (recommended) — when the local `claude` CLI is signed
182
+ in, Transmute uses its Claude subscription auth. Run `/login` or `/login claude` to
183
+ sign in.
184
+ 4. **ChatGPT subscription** — when Claude is not signed in, Transmute runs an
185
+ ephemeral, read-only `codex exec` lookup with live web search and local shell
186
+ access disabled. Run `/login codex` once to sign in with ChatGPT.
187
+
188
+ Among subscription CLIs, Transmute picks the first signed-in CLI (Claude, then
189
+ Codex). While neither is available, it checks sign-in status every 15 seconds and
190
+ enables enrichment when one becomes available.
191
+
192
+ Choose a provider with `/enrich codex|claude|api`, or toggle the selected provider
193
+ with `/enrich on|off`. With no usable credentials, enrichment is skipped
194
+ automatically.
195
+
196
+ ## Spotify iPhone sync
197
+
198
+ Connect and unlock one iPhone over USB, trust the computer if prompted, and
199
+ install Spotify on the phone. Run `/sync` (or `/sync spotify`) to scan every MP3
200
+ in the current output folder and Spotify's app documents, then review the
201
+ read-only preview. Exact matches are marked **Already synced**; possible
202
+ duplicates and filename conflicts are dimmed and marked with `?` for review.
203
+ Nothing transfers until you select tracks and move to **Continue**.
204
+
205
+ In the preview, use Up/Down to browse, Enter to select a row, `a` to select
206
+ high-confidence items, and `A` to select every transferable item. Enter on
207
+ Continue starts the selected copies. Tab or Shift-Tab switches between the
208
+ report and URL input without clearing its draft. Esc closes a preview; while
209
+ copying, Esc asks Transmute to stop between files. Completed copies stay done,
210
+ and `/sync retry` reopens the failed or cancelled rows. Sync only adds files; it
211
+ does not delete phone or computer files or replace existing files.
212
+
213
+ `/sync music` explains the manual Mac workflow: import the output folder into
214
+ Music, then use Finder's iPhone Music sync. Copying files to a folder does not
215
+ register them in Apple Music, and exporting tracks back from the iPhone Music
216
+ library is unsupported.
217
+
218
+ ## Commands
219
+
220
+ | Command | Description |
221
+ | --- | --- |
222
+ | `/out [dir]` | show or set the output directory; a bare name or folder-editor entry is under `~/`; with no arg, opens a separate `~/` editor — Tab (or Shift-Tab) switches focus, Enter applies, Esc cancels |
223
+ | `/quality [128\|192\|256\|320]` | show or set MP3 bitrate |
224
+ | `/enrich [codex\|claude\|api\|on\|off]` | choose or toggle web-search metadata enrichment |
225
+ | `/key [clear]` | securely enter one OpenAI or Anthropic API key, or clear it |
226
+ | `↑` / `↓` | select failed or low-confidence History entries — Enter retries a failure; low-confidence entries open an inline hint input |
227
+ | `/list` | show recent converted and failed tracks across sessions |
228
+ | `/retry` | requeue all retryable failed downloads |
229
+ | `/sync [spotify\|music\|retry]` | preview additive Spotify sync, show the Mac Music/Finder handoff, or reopen failed/cancelled rows |
230
+ | `/login [codex\|claude]` | log in to a subscription provider (opens browser) |
231
+ | `/logout [codex\|claude]` | log out of a subscription provider |
232
+ | `/clear` | clear completed and failed activity from the screen and persistent history |
233
+ | `/help` | show the full command reference while keeping URL and command input live; Esc closes help |
234
+ | `/quit` | exit (also `/exit`, `/q`, Ctrl-D, or double Ctrl-C) |
235
+
236
+ The main input remains available while `/help`, `/sync`, or `/out` is open.
237
+ With the sync report open, press Tab or Shift-Tab to switch focus; its selection
238
+ shortcuts only apply while the report has focus. While the folder editor is
239
+ open, press Tab (when no completion menu is active) or Shift-Tab to switch
240
+ between it and URL input. Directory completion
241
+ continues to use Tab in the folder editor. Folder edits take effect only after
242
+ Enter confirms them; downloads submitted before then use the currently applied
243
+ settings. With help open, scroll over the command reference with the mouse wheel
244
+ or trackpad. If `/help` and `/out` are both open, Esc closes the folder editor
245
+ first, then closes help on the next press.
246
+
247
+ ## Development
248
+
249
+ ### Browser prototype
250
+
251
+ The optional [web prototype](web/README.md) provides a basic YouTube/SoundCloud
252
+ link-to-MP3 interface without enrichment. A local helper retrieves source audio;
253
+ the browser converts it to MP3. It is a checkout-only prototype, not a hosted
254
+ service, and does not change the terminal application.
255
+
256
+ ### Checks
257
+
258
+ ```sh
259
+ uv run pytest
260
+ uv run ruff check .
261
+ ```
262
+
263
+ If [`just`](https://just.systems/) is installed, the same workflows are available
264
+ as `just run`, `just test`, `just lint`, and `just check`.
265
+
266
+ Releasing to PyPI and the Homebrew tap is described in
267
+ [`packaging/README.md`](packaging/README.md). After a dependency or version
268
+ change, regenerate the formula with `just formula`.
269
+
270
+ ## Project map
271
+
272
+ `App` owns session state and orchestration. The interface modules read that
273
+ state and translate gestures into `App` calls; the services below run without
274
+ prompt_toolkit and return data rather than rendering it.
275
+
276
+ ```text
277
+ main
278
+ └── App (state + orchestration)
279
+ ├── prompt_toolkit adapters (layout, keys, commands, widgets, style)
280
+ ├── downloader (yt-dlp/ffmpeg)
281
+ ├── enrich (provider calls + normalized tags + ID3 writes)
282
+ ├── sync (preview, selection results, safe local/phone transfer)
283
+ │ └── iphone (USB Spotify Documents adapter)
284
+ └── settings, history (durable local storage)
285
+ ```
286
+
287
+ Entry point
288
+
289
+ - `transmute/main.py` — process startup, `--version`/`--help`, and wiring
290
+ - `transmute/__main__.py` — supports `python -m transmute`
291
+
292
+ Core
293
+
294
+ - `transmute/app.py` — session state, job lifecycle, and worker coordination
295
+ - `transmute/config.py` — settings model and shared operational constants
296
+
297
+ Interface
298
+
299
+ - `transmute/layout.py` — prompt_toolkit window arrangement
300
+ - `transmute/keys.py` — keyboard behavior
301
+ - `transmute/commands.py` — slash-command dispatch and implementations
302
+ - `transmute/widgets.py` — reusable prompt components
303
+ - `transmute/style.py` — theme and user-facing UI constants
304
+
305
+ Services
306
+
307
+ - `transmute/downloader.py` — local yt-dlp/ffmpeg pipeline
308
+ - `transmute/enrich.py` — provider selection, web research, and ID3 tagging
309
+ - `transmute/sync.py` — read-only MP3 comparison plans and explicit transfers
310
+ - `transmute/iphone.py` — USB-only adapter for Spotify's vended Documents folder
311
+ - `transmute/settings.py` — persistent output directory and bitrate storage
312
+ - `transmute/history.py` — persistent download activity storage
313
+
314
+ Tests
315
+
316
+ - `tests/test_main.py` — entry-point argument handling
317
+ - `tests/test_app.py` — selection, retry, modal, queue, and notice state
318
+ - `tests/test_downloader.py` — URL parsing, yt-dlp options, and error classification
319
+ - `tests/test_enrich.py` — credentials, provider calls, tagging, and renaming
320
+ - `tests/test_settings.py` — settings round-trip, rejection, and fallback
321
+ - `tests/test_history.py` — activity storage, retry/hint claiming, session recovery
322
+ - `tests/test_packaging.py` — Homebrew resource closure derived from `uv.lock`
323
+
324
+ ## License
325
+
326
+ Transmute is licensed under the [GNU General Public License v3.0 or later](LICENSE).
327
+
328
+ Copyright (C) 2026 vznh.
329
+
330
+ The licence is copyleft because dependencies require it, not by preference:
331
+ `transmute/enrich.py` imports [mutagen](https://github.com/quodlibet/mutagen)
332
+ to write ID3 frames (GPL-2.0-or-later), and the USB sync adapter uses
333
+ [pymobiledevice3](https://github.com/doronz88/pymobiledevice3) (GPL-3.0-or-later).
334
+ The project is GPL-3.0-or-later to remain compatible with both. ffmpeg is a
335
+ separate program invoked as a subprocess and does not affect this.
@@ -0,0 +1,21 @@
1
+ transmute/__init__.py,sha256=-DvfFvTLndVI0MlLl3sYe5bKMmNSx_te_GAx-3xU44Y,23
2
+ transmute/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
+ transmute/app.py,sha256=tcwpV13vxqbhmaPQIRNoTdJqKADAt4UEXoeSqgkteQY,56680
4
+ transmute/commands.py,sha256=QX7R03OZUpEptmvOlsnk74Hr7Q-yV1xrZfYri4hfIbo,11854
5
+ transmute/config.py,sha256=xcBQqZqvn_pJ4lzqle-VuSHwuK_P-oh_QsNp-X2xcbQ,558
6
+ transmute/downloader.py,sha256=lvNC9WrwodTGpy9elJ1ZCo3_SVQEdTKuE_D13l8QT6U,7583
7
+ transmute/enrich.py,sha256=Wh_P-qllhP1QjedPEAxtsxh43a1cZgS9q3DXEgYJvss,31101
8
+ transmute/history.py,sha256=uGc2DDgu5P0Bjv9nFFawUXPyDsn1RTUAlcylmuj9B38,30643
9
+ transmute/iphone.py,sha256=gvTtTPxyW58H8PW2pjX-FWQZcaLPJFAGS6pOczXRgD8,10466
10
+ transmute/keys.py,sha256=pLUway9gkJ0AStde2jiLapo4U1thy1G_dbBjtw5gQcg,4252
11
+ transmute/layout.py,sha256=R7N69y7V4gO607WTdI8EJoituBi7BYOma7xRRjRSaEU,4304
12
+ transmute/main.py,sha256=L7J-wgDl5yYzT-mVdRNSiAkRO07CyHiRiIGXF-XL-Bw,1056
13
+ transmute/settings.py,sha256=CeQ0MkopT39RihhS0ietkDRsYJ8t_CQ7MS8KMogqPPQ,4104
14
+ transmute/style.py,sha256=e1MqLbEuxOnKKre5UVZYlVU1gQEQ6er2bKRilSG_gs0,904
15
+ transmute/sync.py,sha256=fXVnDu5WMti95SHlmk-jGiqQig2eA_boywjp2dHH72w,22389
16
+ transmute/widgets.py,sha256=iQJfh0PnorBQBIfDPZobZQ2goZp7bRmthwBsHcB6pPI,3025
17
+ transmute_cli-0.2.3a0.dist-info/METADATA,sha256=ow9B5CyluA-BNtqo6Qrvapl02lrQg20AerqgM7Gu0ok,15587
18
+ transmute_cli-0.2.3a0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
19
+ transmute_cli-0.2.3a0.dist-info/entry_points.txt,sha256=h9oU1SLPkZdHVz2AanvilyGZCCxXrRPtIGfHVTrREvg,50
20
+ transmute_cli-0.2.3a0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
21
+ transmute_cli-0.2.3a0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ transmute = transmute.main:main