reel-sync 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,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: reel-sync
3
+ Version: 1.0.0
4
+ Summary: Fully-local sync tool for the Sony ICD-UX570 voice recorder
5
+ Author: Nicolas
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/reel-sync/
8
+ Keywords: sony,icd-ux570,voice-recorder,sync,dictaphone
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: rich>=13.0
12
+ Requires-Dist: mutagen>=1.47
13
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
14
+
15
+ # REEL
16
+
17
+ **A fully-local, automatic sync tool for the Sony ICD-UX570.** Plug in the recorder, and Reel pulls every new recording onto your PC, names it by the moment you hit record, and files it into the right category — Interviews, Songs, Memes, Podcasts, Voice Notes. Re-plug the same stick and nothing copies twice. A clean terminal, a progress bar, done.
18
+
19
+ > Everything stays on your machine. The recorder is never wiped. The library is just folders — open it in Explorer any time.
20
+
21
+ ---
22
+
23
+ ## What it does
24
+
25
+ - **Detects the recorder** the moment it mounts (by volume label *or* by spotting its `REC_FILE` folder — works even if the label changes).
26
+ - **Copies only what's new.** Each file is fingerprinted; already-synced recordings are skipped.
27
+ - **Names everything cleanly** — `2026-06-08_1432_reel-7F3A.mp3` (date-first, sorts forever).
28
+ - **Auto-categorises** using simple, tunable rules:
29
+ - device `MUSIC` folder → **Songs**, `PODCASTS` → **Podcasts**
30
+ - a filename keyword (`interview`, `meme`, …) wins if present
31
+ - otherwise by length: very short → **Memes**, very long → **Interviews**, else **Voice Notes**
32
+ - **Mirrors** to any extra drives you list (USB / cloud), so sync doubles as backup.
33
+ - **Runs automatically** in watch mode — leave it running and every plug-in just syncs.
34
+
35
+ Files land here:
36
+ ```
37
+ Reel/
38
+ ├── Interviews/2026/2026-06-08_1432_reel-7F3A.mp3
39
+ ├── Songs/2026/2026-05-30_2011_reel-A1C9.mp3
40
+ ├── Memes/2026/…
41
+ └── .reel/manifest.json ← dedup index (so nothing copies twice)
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Install
47
+
48
+ Once it's published, anyone can install it from PyPI:
49
+ ```
50
+ pip install reel-sync
51
+ ```
52
+ (The package is `reel-sync`; the command you run is just `reel`.)
53
+
54
+ ### From this folder (for development)
55
+
56
+ 1. **Install Python 3.11+** (python.org → tick *Add Python to PATH*).
57
+ 2. **Install Reel as a command** — open a terminal in this folder once:
58
+ ```
59
+ pip install -e .
60
+ ```
61
+ That puts a `reel` command on your PATH, so you can run it from **any** folder
62
+ (including `C:\Users\YOU>`), not just this one.
63
+ 3. **Set your path** in `config.toml`:
64
+ ```toml
65
+ [library]
66
+ sync_root = "C:/Users/YOURNAME/Reel"
67
+ ```
68
+ Point `sync_root` at a cloud-synced folder (pCloud / OneDrive / Dropbox) and your off-site backup is automatic.
69
+
70
+ Reel finds this `config.toml` automatically wherever you run it from (it also
71
+ checks `./config.toml` and `~/.reel/config.toml` first). No config at all? It
72
+ falls back to sensible defaults (`~/Reel`).
73
+
74
+ ---
75
+
76
+ ## Use it
77
+
78
+ You really only need two commands. Run them from anywhere — your home prompt is fine.
79
+
80
+ **First time** — run this, then plug in the recorder when it asks:
81
+ ```
82
+ reel setup
83
+ ```
84
+ A short welcome, then it waits for your ICD-UX570, does the big initial copy, and
85
+ **stays running as auto-sync** from there. (Run `reel setup` again later and it
86
+ just says *"Already set up :)"*.)
87
+
88
+ **Every day after** — start auto-sync and leave it running:
89
+ ```
90
+ reel start
91
+ ```
92
+ It does the initial sync, then keeps syncing automatically whenever you **drop a
93
+ file onto the recorder from your PC** — and it **stops by itself the moment you
94
+ unplug** the recorder. (Press **Ctrl + C** to stop sooner.)
95
+
96
+ That's the whole flow. The rest is optional:
97
+
98
+ ```
99
+ reel sync # a single one-off sync, then exit (rarely needed)
100
+ reel sort <path> # auto-filter a folder/files you drop in (or use the .bat)
101
+ reel status # what's in your library, per category
102
+ reel devices # is the recorder detected right now?
103
+ reel open # open the Reel library in Explorer
104
+ reel reset # clear first-time setup (to see the welcome again)
105
+ reel --theme dark start# dark terminal theme
106
+ ```
107
+
108
+ (`python -m reel start` and `python run.py start` work identically.)
109
+
110
+ ### Make it truly hands-off (optional)
111
+ Put a shortcut to `reel start` in your Startup folder (`Win+R` → `shell:startup`).
112
+ Reel will be running and watching from the moment you log in.
113
+
114
+ ---
115
+
116
+ ## Branding
117
+
118
+ Light by default — colours tuned for a white terminal. Want dark? `theme = "dark"` in `config.toml`, or `--theme dark` on any command. Identity lives in `reel/branding.py`.
119
+
120
+ ## Notes
121
+
122
+ - **Privacy:** nothing leaves your machine except the copies you put in your own mirror/cloud folders.
123
+ - **Safety:** `delete_after_sync = false` by default — Reel copies off the recorder and never deletes from it.
124
+ - **Coming later:** built-in playback, a podcast queue, smarter song-vs-voice detection. See `ROADMAP.md`.
125
+
126
+ ---
127
+
128
+ *REEL v1.0.0 — plug in, walk away.*
@@ -0,0 +1,114 @@
1
+ # REEL
2
+
3
+ **A fully-local, automatic sync tool for the Sony ICD-UX570.** Plug in the recorder, and Reel pulls every new recording onto your PC, names it by the moment you hit record, and files it into the right category — Interviews, Songs, Memes, Podcasts, Voice Notes. Re-plug the same stick and nothing copies twice. A clean terminal, a progress bar, done.
4
+
5
+ > Everything stays on your machine. The recorder is never wiped. The library is just folders — open it in Explorer any time.
6
+
7
+ ---
8
+
9
+ ## What it does
10
+
11
+ - **Detects the recorder** the moment it mounts (by volume label *or* by spotting its `REC_FILE` folder — works even if the label changes).
12
+ - **Copies only what's new.** Each file is fingerprinted; already-synced recordings are skipped.
13
+ - **Names everything cleanly** — `2026-06-08_1432_reel-7F3A.mp3` (date-first, sorts forever).
14
+ - **Auto-categorises** using simple, tunable rules:
15
+ - device `MUSIC` folder → **Songs**, `PODCASTS` → **Podcasts**
16
+ - a filename keyword (`interview`, `meme`, …) wins if present
17
+ - otherwise by length: very short → **Memes**, very long → **Interviews**, else **Voice Notes**
18
+ - **Mirrors** to any extra drives you list (USB / cloud), so sync doubles as backup.
19
+ - **Runs automatically** in watch mode — leave it running and every plug-in just syncs.
20
+
21
+ Files land here:
22
+ ```
23
+ Reel/
24
+ ├── Interviews/2026/2026-06-08_1432_reel-7F3A.mp3
25
+ ├── Songs/2026/2026-05-30_2011_reel-A1C9.mp3
26
+ ├── Memes/2026/…
27
+ └── .reel/manifest.json ← dedup index (so nothing copies twice)
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Install
33
+
34
+ Once it's published, anyone can install it from PyPI:
35
+ ```
36
+ pip install reel-sync
37
+ ```
38
+ (The package is `reel-sync`; the command you run is just `reel`.)
39
+
40
+ ### From this folder (for development)
41
+
42
+ 1. **Install Python 3.11+** (python.org → tick *Add Python to PATH*).
43
+ 2. **Install Reel as a command** — open a terminal in this folder once:
44
+ ```
45
+ pip install -e .
46
+ ```
47
+ That puts a `reel` command on your PATH, so you can run it from **any** folder
48
+ (including `C:\Users\YOU>`), not just this one.
49
+ 3. **Set your path** in `config.toml`:
50
+ ```toml
51
+ [library]
52
+ sync_root = "C:/Users/YOURNAME/Reel"
53
+ ```
54
+ Point `sync_root` at a cloud-synced folder (pCloud / OneDrive / Dropbox) and your off-site backup is automatic.
55
+
56
+ Reel finds this `config.toml` automatically wherever you run it from (it also
57
+ checks `./config.toml` and `~/.reel/config.toml` first). No config at all? It
58
+ falls back to sensible defaults (`~/Reel`).
59
+
60
+ ---
61
+
62
+ ## Use it
63
+
64
+ You really only need two commands. Run them from anywhere — your home prompt is fine.
65
+
66
+ **First time** — run this, then plug in the recorder when it asks:
67
+ ```
68
+ reel setup
69
+ ```
70
+ A short welcome, then it waits for your ICD-UX570, does the big initial copy, and
71
+ **stays running as auto-sync** from there. (Run `reel setup` again later and it
72
+ just says *"Already set up :)"*.)
73
+
74
+ **Every day after** — start auto-sync and leave it running:
75
+ ```
76
+ reel start
77
+ ```
78
+ It does the initial sync, then keeps syncing automatically whenever you **drop a
79
+ file onto the recorder from your PC** — and it **stops by itself the moment you
80
+ unplug** the recorder. (Press **Ctrl + C** to stop sooner.)
81
+
82
+ That's the whole flow. The rest is optional:
83
+
84
+ ```
85
+ reel sync # a single one-off sync, then exit (rarely needed)
86
+ reel sort <path> # auto-filter a folder/files you drop in (or use the .bat)
87
+ reel status # what's in your library, per category
88
+ reel devices # is the recorder detected right now?
89
+ reel open # open the Reel library in Explorer
90
+ reel reset # clear first-time setup (to see the welcome again)
91
+ reel --theme dark start# dark terminal theme
92
+ ```
93
+
94
+ (`python -m reel start` and `python run.py start` work identically.)
95
+
96
+ ### Make it truly hands-off (optional)
97
+ Put a shortcut to `reel start` in your Startup folder (`Win+R` → `shell:startup`).
98
+ Reel will be running and watching from the moment you log in.
99
+
100
+ ---
101
+
102
+ ## Branding
103
+
104
+ Light by default — colours tuned for a white terminal. Want dark? `theme = "dark"` in `config.toml`, or `--theme dark` on any command. Identity lives in `reel/branding.py`.
105
+
106
+ ## Notes
107
+
108
+ - **Privacy:** nothing leaves your machine except the copies you put in your own mirror/cloud folders.
109
+ - **Safety:** `delete_after_sync = false` by default — Reel copies off the recorder and never deletes from it.
110
+ - **Coming later:** built-in playback, a podcast queue, smarter song-vs-voice detection. See `ROADMAP.md`.
111
+
112
+ ---
113
+
114
+ *REEL v1.0.0 — plug in, walk away.*
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ # Published on PyPI as "reel-sync" (plain "reel" was taken). Friends install with
6
+ # `pip install reel-sync`; the import package and the command both stay `reel`.
7
+ [project]
8
+ name = "reel-sync"
9
+ dynamic = ["version"] # single-sourced from reel/__init__.py
10
+ description = "Fully-local sync tool for the Sony ICD-UX570 voice recorder"
11
+ readme = "README.md"
12
+ requires-python = ">=3.9"
13
+ license = "MIT"
14
+ authors = [{ name = "Nicolas" }]
15
+ keywords = ["sony", "icd-ux570", "voice-recorder", "sync", "dictaphone"]
16
+ dependencies = [
17
+ "rich>=13.0",
18
+ "mutagen>=1.47",
19
+ "tomli>=2.0; python_version < '3.11'",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://pypi.org/project/reel-sync/"
24
+
25
+ [project.scripts]
26
+ reel = "reel.__main__:main"
27
+
28
+ [tool.setuptools]
29
+ packages = ["reel"]
30
+ py-modules = []
31
+
32
+ [tool.setuptools.dynamic]
33
+ version = { attr = "reel.__version__" }
@@ -0,0 +1,2 @@
1
+ """Reel — a fully-local, automatic sync tool for the Sony ICD-UX570."""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,157 @@
1
+ """
2
+ __main__.py — the Reel command line.
3
+
4
+ reel setup first-time setup, then flows into auto-sync
5
+ reel start auto-sync: runs passively, syncs on any change
6
+ reel sync one-off sync right now (rarely needed)
7
+ reel sort <path>... run the same auto-filter on a folder/files (drag-drop)
8
+ reel status library summary (counts per category, size)
9
+ reel open open your Reel library in the file manager
10
+ reel devices list any recorders currently detected
11
+ reel reset clear first-time setup (to see the welcome again)
12
+
13
+ Global: --config path/to/config.toml --theme light|dark
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from .config import load, resolve_path
22
+
23
+
24
+ def build_parser():
25
+ # shared options accepted both before AND after the subcommand
26
+ common = argparse.ArgumentParser(add_help=False)
27
+ common.add_argument("--config", default=argparse.SUPPRESS)
28
+ common.add_argument("--theme", choices=["light", "dark"], default=argparse.SUPPRESS)
29
+
30
+ p = argparse.ArgumentParser(prog="reel", parents=[common],
31
+ description="Reel — Sony ICD-UX570 sync")
32
+ sub = p.add_subparsers(dest="cmd") # optional: bare `reel` -> splash
33
+ sub.add_parser("setup", parents=[common])
34
+ sub.add_parser("start", parents=[common])
35
+ sub.add_parser("sync", parents=[common])
36
+ ps = sub.add_parser("sort", parents=[common]); ps.add_argument("paths", nargs="+")
37
+ sub.add_parser("status", parents=[common])
38
+ sub.add_parser("open", parents=[common])
39
+ sub.add_parser("devices", parents=[common])
40
+ sub.add_parser("reset", parents=[common])
41
+ return p
42
+
43
+
44
+ def main(argv=None):
45
+ argv = list(sys.argv[1:] if argv is None else argv)
46
+
47
+ # Friendly front doors, handled before argparse:
48
+ # reel -> welcome splash
49
+ # reel help / -h -> clean command reference
50
+ if not argv or argv[0] in ("help", "-h", "--help"):
51
+ from .console import Reel
52
+ con = Reel(theme=load(resolve_path(None)).theme)
53
+ con.splash() if not argv else con.help_screen()
54
+ return 0
55
+
56
+ args = build_parser().parse_args(argv)
57
+ cfg = load(resolve_path(getattr(args, "config", None)))
58
+ theme = getattr(args, "theme", None)
59
+ if theme:
60
+ cfg.theme = theme
61
+
62
+ from .console import Reel
63
+ con = Reel(theme=cfg.theme)
64
+
65
+ if not args.cmd:
66
+ con.splash()
67
+ return 0
68
+
69
+ if args.cmd == "setup":
70
+ from .runner import first_setup
71
+ first_setup(cfg, con)
72
+
73
+ elif args.cmd == "start":
74
+ from .watch import auto_sync
75
+ from .runner import load_profile
76
+ con.logo()
77
+ con.greeting(load_profile(cfg).get("name"))
78
+ auto_sync(cfg, con)
79
+
80
+ elif args.cmd == "sync":
81
+ from .runner import sync_once
82
+ con.logo()
83
+ sync_once(cfg, con, sources=None)
84
+
85
+ elif args.cmd == "reset":
86
+ from .runner import reset_setup
87
+ if reset_setup(cfg):
88
+ con.ok("setup reset — run 'reel setup' to see the welcome again.")
89
+ else:
90
+ con.dim("nothing to reset — setup hasn't run yet.")
91
+
92
+ elif args.cmd == "sort":
93
+ from .runner import sync_once
94
+ con.logo()
95
+ paths = [Path(p) for p in args.paths]
96
+ missing = [str(p) for p in paths if not p.exists()]
97
+ if missing:
98
+ con.err("not found: " + ", ".join(missing))
99
+ return 1
100
+ sync_once(cfg, con, sources=paths)
101
+
102
+ elif args.cmd == "status":
103
+ _status(cfg, con)
104
+
105
+ elif args.cmd == "devices":
106
+ from . import device
107
+ devs = device.find_devices(cfg)
108
+ if not devs:
109
+ con.warn("no recorder detected.")
110
+ for d in devs:
111
+ con.ok(f"detected: {d}")
112
+
113
+ elif args.cmd == "open":
114
+ import os, subprocess
115
+ cfg.sync_root.mkdir(parents=True, exist_ok=True)
116
+ if sys.platform == "win32":
117
+ os.startfile(cfg.sync_root) # noqa: S606
118
+ elif sys.platform == "darwin":
119
+ subprocess.run(["open", str(cfg.sync_root)])
120
+ else:
121
+ subprocess.run(["xdg-open", str(cfg.sync_root)])
122
+
123
+ return 0
124
+
125
+
126
+ def _status(cfg, con):
127
+ from . import sync, branding
128
+ manifest = sync.load_manifest(cfg)
129
+ by_cat, total_bytes = {}, 0
130
+ for entry in manifest.values():
131
+ by_cat[entry["category"]] = by_cat.get(entry["category"], 0) + 1
132
+ # size from disk (authoritative)
133
+ if cfg.sync_root.exists():
134
+ for p in cfg.sync_root.rglob("*"):
135
+ if p.is_file() and ".reel" not in p.parts:
136
+ try:
137
+ total_bytes += p.stat().st_size
138
+ except OSError:
139
+ pass
140
+ lines = []
141
+ for cat in branding.CATEGORIES:
142
+ n = by_cat.get(cat, 0)
143
+ if n:
144
+ g = branding.CATEGORY_GLYPH.get(cat, "·")
145
+ col = branding.CATEGORY_COLOR.get(cat, "white")
146
+ lines.append(f" [{col}]{g}[/{col}] {cat:<12} {n}")
147
+ targets = ", ".join(str(t) for t in cfg.mirror_targets) or "(none)"
148
+ body = (f" library {cfg.sync_root}\n"
149
+ f" recorded {len(manifest)} files · {total_bytes/(1024*1024):.1f} MB\n"
150
+ f" mirrors {targets}\n")
151
+ if lines:
152
+ body += "\n" + "\n".join(lines)
153
+ con.panel("REEL status", body)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,76 @@
1
+ """
2
+ branding.py — Reel's identity in one place.
3
+
4
+ Clean and modern: a calm wordmark, light by default, dark on request.
5
+ No CRT, no scanlines. Colours are chosen to read on a WHITE terminal first;
6
+ the dark theme just brightens them.
7
+ """
8
+
9
+ NAME = "REEL"
10
+ VERSION = "1.0.0"
11
+ TAGLINE = "voice-recorder sync"
12
+
13
+ # A small, tasteful wordmark. Printed once at the top of a run.
14
+ LOGO = r"""
15
+ ___ ___ ___ _
16
+ | _ \ __| __| | · {tag}
17
+ | / _|| _|| |__
18
+ |_|_\___|___|____| {ver}
19
+ """
20
+
21
+ # Big "Welcome" wordmark, shown on first-time setup.
22
+ WELCOME_ART = r"""
23
+ __ __ _
24
+ \ \ / /__| | ___ ___ _ __ ___ ___
25
+ \ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \
26
+ \ V V / __/ | (_| (_) | | | | | | __/
27
+ \_/\_/ \___|_|\___\___/|_| |_| |_|\___|
28
+ """
29
+
30
+ # Categories the sorter produces, with a stable display order + glyph.
31
+ CATEGORIES = ["Interviews", "Voice Notes", "Songs", "Podcasts", "Memes", "Other"]
32
+ CATEGORY_GLYPH = {
33
+ "Interviews": "▤",
34
+ "Voice Notes": "✎",
35
+ "Songs": "♪",
36
+ "Podcasts": "◉",
37
+ "Memes": "★",
38
+ "Other": "·",
39
+ }
40
+
41
+ # Semantic styles per theme. Keys are used by console.py to build a rich Theme.
42
+ # Light = default (assumes a white/bright terminal background).
43
+ THEMES = {
44
+ "light": {
45
+ "brand": "bold #0b5fa5", # deep blue wordmark
46
+ "accent": "#0b5fa5",
47
+ "ok": "bold #1a7f37", # green
48
+ "warn": "bold #9a6700", # amber-brown
49
+ "err": "bold #cf222e", # red
50
+ "muted": "#6e7781", # grey
51
+ "value": "default", # the terminal's own fg — readable on any bg
52
+ "bar": "#0b5fa5",
53
+ "bar_bg": "#d0d7de",
54
+ },
55
+ "dark": {
56
+ "brand": "bold #4493f8",
57
+ "accent": "#4493f8",
58
+ "ok": "bold #3fb950",
59
+ "warn": "bold #d29922",
60
+ "err": "bold #f85149",
61
+ "muted": "#8b949e",
62
+ "value": "default",
63
+ "bar": "#4493f8",
64
+ "bar_bg": "#30363d",
65
+ },
66
+ }
67
+
68
+ # Per-category accent (rich colour names / hex). Used in summaries.
69
+ CATEGORY_COLOR = {
70
+ "Interviews": "blue",
71
+ "Voice Notes": "cyan",
72
+ "Songs": "magenta",
73
+ "Podcasts": "green",
74
+ "Memes": "yellow",
75
+ "Other": "white",
76
+ }
@@ -0,0 +1,35 @@
1
+ """
2
+ classify.py — decide which category a recording belongs in. Rules, not AI.
3
+
4
+ Signal order (first hit wins):
5
+ 1. Source folder on the device — MUSIC -> Songs, PODCASTS -> Podcasts.
6
+ 2. Filename keyword — "interview", "song", "meme", "podcast", …
7
+ 3. Duration — very short -> Memes, very long -> Interviews,
8
+ otherwise -> Voice Notes.
9
+
10
+ All thresholds and keywords live in config, so it's tunable without code edits.
11
+ """
12
+ from __future__ import annotations
13
+
14
+
15
+ def classify(*, source: str, filename: str, duration_sec: float | None, cfg) -> str:
16
+ src = (source or "").lower()
17
+ if src == "music":
18
+ return "Songs"
19
+ if src == "podcast":
20
+ return "Podcasts"
21
+
22
+ name = (filename or "").lower()
23
+ for kw, category in cfg.category_keywords.items():
24
+ if kw.lower() in name:
25
+ return category
26
+
27
+ d = duration_sec or 0
28
+ if d and d <= cfg.meme_max_sec:
29
+ return "Memes"
30
+ if d and d >= cfg.interview_min_sec:
31
+ return "Interviews"
32
+ if d:
33
+ return "Voice Notes"
34
+ # no duration available and no other signal
35
+ return "Voice Notes" if src == "voice" else "Other"
@@ -0,0 +1,92 @@
1
+ """config.py — set it once. Every value has a sensible default."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ try:
9
+ import tomllib
10
+ except ModuleNotFoundError: # pragma: no cover
11
+ import tomli as tomllib
12
+
13
+
14
+ @dataclass
15
+ class Config:
16
+ # where synced recordings land (your "Reel" library)
17
+ sync_root: Path = Path.home() / "Reel"
18
+ # optional extra drives to also receive a copy (USB / cloud-synced folder)
19
+ mirror_targets: list[Path] = field(default_factory=list)
20
+
21
+ # how the ICD-UX570 shows up
22
+ device_labels: list[str] = field(
23
+ default_factory=lambda: ["IC RECORDER", "MEMORY CARD", "IC RECORDER MEMORY CARD"])
24
+ delete_from_device: bool = False # safe default: copy, never wipe the recorder
25
+
26
+ # categorisation thresholds (seconds)
27
+ meme_max_sec: int = 20 # <= this -> Memes
28
+ interview_min_sec: int = 480 # >= this -> Interviews
29
+ # filename keyword -> forced category (case-insensitive substring)
30
+ category_keywords: dict = field(default_factory=lambda: {
31
+ "interview": "Interviews",
32
+ "song": "Songs",
33
+ "meme": "Memes",
34
+ "podcast": "Podcasts",
35
+ })
36
+
37
+ # watch mode
38
+ watch_interval_sec: int = 4
39
+ close_on_unplug: bool = True # close the terminal window when the recorder is removed
40
+
41
+ # terminal theme: "light" (default) or "dark"
42
+ theme: str = "light"
43
+
44
+ @property
45
+ def manifest_path(self) -> Path:
46
+ return self.sync_root / ".reel" / "manifest.json"
47
+
48
+ @property
49
+ def profile_path(self) -> Path:
50
+ return self.sync_root / ".reel" / "profile.json"
51
+
52
+
53
+ def resolve_path(explicit: str | os.PathLike | None = None) -> Path | None:
54
+ """Find config.toml wherever you run `reel` from. First match wins:
55
+ 1. an explicit --config path
56
+ 2. ./config.toml (the folder you're standing in)
57
+ 3. ~/.reel/config.toml (a stable per-user spot)
58
+ 4. config.toml next to the installed package (the project folder)
59
+ Returns None if none exist -> Reel runs on built-in defaults."""
60
+ candidates = []
61
+ if explicit:
62
+ candidates.append(Path(explicit).expanduser())
63
+ candidates.append(Path.cwd() / "config.toml")
64
+ candidates.append(Path.home() / ".reel" / "config.toml")
65
+ candidates.append(Path(__file__).resolve().parent.parent / "config.toml")
66
+ for c in candidates:
67
+ if c.exists():
68
+ return c
69
+ return None
70
+
71
+
72
+ def load(path: str | os.PathLike | None) -> Config:
73
+ cfg = Config()
74
+ if path is None or not Path(path).exists():
75
+ return cfg
76
+ with open(path, "rb") as f:
77
+ data = tomllib.load(f)
78
+
79
+ def g(section, key, default):
80
+ return data.get(section, {}).get(key, default)
81
+
82
+ cfg.sync_root = Path(g("library", "sync_root", str(cfg.sync_root))).expanduser()
83
+ cfg.mirror_targets = [Path(x).expanduser() for x in g("library", "mirror_targets", [])]
84
+ cfg.device_labels = g("device", "labels", cfg.device_labels)
85
+ cfg.delete_from_device = bool(g("device", "delete_after_sync", False))
86
+ cfg.meme_max_sec = int(g("categories", "meme_max_sec", cfg.meme_max_sec))
87
+ cfg.interview_min_sec = int(g("categories", "interview_min_sec", cfg.interview_min_sec))
88
+ cfg.category_keywords = g("categories", "keywords", cfg.category_keywords)
89
+ cfg.watch_interval_sec = int(g("watch", "interval_sec", cfg.watch_interval_sec))
90
+ cfg.close_on_unplug = bool(g("watch", "close_on_unplug", cfg.close_on_unplug))
91
+ cfg.theme = g("ui", "theme", cfg.theme)
92
+ return cfg