maneki 0.10.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.
Files changed (136) hide show
  1. maneki-0.10.0/LICENSE +21 -0
  2. maneki-0.10.0/PKG-INFO +157 -0
  3. maneki-0.10.0/README.md +112 -0
  4. maneki-0.10.0/pyproject.toml +175 -0
  5. maneki-0.10.0/src/maneki/__init__.py +15 -0
  6. maneki-0.10.0/src/maneki/__main__.py +6 -0
  7. maneki-0.10.0/src/maneki/_ui_static/README.md +71 -0
  8. maneki-0.10.0/src/maneki/_ui_static/desktop-overrides.css +705 -0
  9. maneki-0.10.0/src/maneki/_ui_static/favicon.svg +4 -0
  10. maneki-0.10.0/src/maneki/_ui_static/index.html +158 -0
  11. maneki-0.10.0/src/maneki/_ui_static/main.jsx +4 -0
  12. maneki-0.10.0/src/maneki/_ui_static/maneki.css +1209 -0
  13. maneki-0.10.0/src/maneki/_ui_static/src/_api.js +195 -0
  14. maneki-0.10.0/src/maneki/_ui_static/src/_audio.js +193 -0
  15. maneki-0.10.0/src/maneki/_ui_static/src/_desktop.js +72 -0
  16. maneki-0.10.0/src/maneki/_ui_static/src/_md5.js +175 -0
  17. maneki-0.10.0/src/maneki/_ui_static/src/_video.js +189 -0
  18. maneki-0.10.0/src/maneki/_ui_static/src/_wiring.jsx +385 -0
  19. maneki-0.10.0/src/maneki/_ui_static/src/app.jsx +813 -0
  20. maneki-0.10.0/src/maneki/_ui_static/src/chrome.jsx +840 -0
  21. maneki-0.10.0/src/maneki/_ui_static/src/covers.jsx +104 -0
  22. maneki-0.10.0/src/maneki/_ui_static/src/data.jsx +23 -0
  23. maneki-0.10.0/src/maneki/_ui_static/src/overlays.jsx +286 -0
  24. maneki-0.10.0/src/maneki/_ui_static/src/tweaks-panel.jsx +568 -0
  25. maneki-0.10.0/src/maneki/_ui_static/src/video-views.jsx +884 -0
  26. maneki-0.10.0/src/maneki/_ui_static/src/views.jsx +192 -0
  27. maneki-0.10.0/src/maneki/_ui_static/src/visualizer.jsx +335 -0
  28. maneki-0.10.0/src/maneki/_ui_static/vendor/babel.min.js +2 -0
  29. maneki-0.10.0/src/maneki/_ui_static/vendor/react-dom.production.min.js +267 -0
  30. maneki-0.10.0/src/maneki/_ui_static/vendor/react.production.min.js +31 -0
  31. maneki-0.10.0/src/maneki/access_log.py +77 -0
  32. maneki-0.10.0/src/maneki/audio/__init__.py +19 -0
  33. maneki-0.10.0/src/maneki/audio/__main__.py +6 -0
  34. maneki-0.10.0/src/maneki/audio/_toml_dump.py +119 -0
  35. maneki-0.10.0/src/maneki/audio/cli/__init__.py +104 -0
  36. maneki-0.10.0/src/maneki/audio/cli/_scan.py +80 -0
  37. maneki-0.10.0/src/maneki/audio/cli/config_cmd.py +76 -0
  38. maneki-0.10.0/src/maneki/audio/cli/convert.py +196 -0
  39. maneki-0.10.0/src/maneki/audio/cli/cover.py +87 -0
  40. maneki-0.10.0/src/maneki/audio/cli/cover_pick.py +215 -0
  41. maneki-0.10.0/src/maneki/audio/cli/inspect.py +186 -0
  42. maneki-0.10.0/src/maneki/audio/cli/library.py +576 -0
  43. maneki-0.10.0/src/maneki/audio/cli/playlist.py +196 -0
  44. maneki-0.10.0/src/maneki/audio/cli/retag.py +120 -0
  45. maneki-0.10.0/src/maneki/audio/cli/ui.py +119 -0
  46. maneki-0.10.0/src/maneki/audio/config.py +273 -0
  47. maneki-0.10.0/src/maneki/audio/convert.py +244 -0
  48. maneki-0.10.0/src/maneki/audio/cover.py +294 -0
  49. maneki-0.10.0/src/maneki/audio/discover.py +205 -0
  50. maneki-0.10.0/src/maneki/audio/enrich/__init__.py +57 -0
  51. maneki-0.10.0/src/maneki/audio/enrich/_http.py +67 -0
  52. maneki-0.10.0/src/maneki/audio/enrich/acoustid.py +173 -0
  53. maneki-0.10.0/src/maneki/audio/enrich/coverart.py +97 -0
  54. maneki-0.10.0/src/maneki/audio/enrich/musicbrainz.py +229 -0
  55. maneki-0.10.0/src/maneki/audio/enrich/musichoarders.py +40 -0
  56. maneki-0.10.0/src/maneki/audio/library/__init__.py +67 -0
  57. maneki-0.10.0/src/maneki/audio/library/audit.py +137 -0
  58. maneki-0.10.0/src/maneki/audio/library/db.py +248 -0
  59. maneki-0.10.0/src/maneki/audio/library/fix.py +188 -0
  60. maneki-0.10.0/src/maneki/audio/library/load.py +155 -0
  61. maneki-0.10.0/src/maneki/audio/library/models.py +77 -0
  62. maneki-0.10.0/src/maneki/audio/library/rename.py +105 -0
  63. maneki-0.10.0/src/maneki/audio/library/scan.py +510 -0
  64. maneki-0.10.0/src/maneki/audio/lyrics/__init__.py +90 -0
  65. maneki-0.10.0/src/maneki/audio/lyrics/lrclib.py +98 -0
  66. maneki-0.10.0/src/maneki/audio/lyrics/sidecar.py +45 -0
  67. maneki-0.10.0/src/maneki/audio/metadata/__init__.py +35 -0
  68. maneki-0.10.0/src/maneki/audio/metadata/album.py +199 -0
  69. maneki-0.10.0/src/maneki/audio/metadata/models.py +104 -0
  70. maneki-0.10.0/src/maneki/audio/metadata/overrides.py +171 -0
  71. maneki-0.10.0/src/maneki/audio/metadata/read.py +378 -0
  72. maneki-0.10.0/src/maneki/audio/metadata/write.py +319 -0
  73. maneki-0.10.0/src/maneki/audio/naming.py +333 -0
  74. maneki-0.10.0/src/maneki/audio/pipeline/__init__.py +24 -0
  75. maneki-0.10.0/src/maneki/audio/pipeline/acoustid.py +65 -0
  76. maneki-0.10.0/src/maneki/audio/pipeline/album.py +459 -0
  77. maneki-0.10.0/src/maneki/audio/pipeline/dedupe.py +46 -0
  78. maneki-0.10.0/src/maneki/audio/pipeline/disc.py +102 -0
  79. maneki-0.10.0/src/maneki/audio/pipeline/filenames.py +68 -0
  80. maneki-0.10.0/src/maneki/audio/pipeline/footprint.py +41 -0
  81. maneki-0.10.0/src/maneki/audio/pipeline/progress.py +17 -0
  82. maneki-0.10.0/src/maneki/audio/pipeline/report.py +106 -0
  83. maneki-0.10.0/src/maneki/audio/pipeline/run.py +156 -0
  84. maneki-0.10.0/src/maneki/audio/pipeline/track.py +133 -0
  85. maneki-0.10.0/src/maneki/audio/playlist/__init__.py +19 -0
  86. maneki-0.10.0/src/maneki/audio/playlist/build.py +142 -0
  87. maneki-0.10.0/src/maneki/audio/playlist/io.py +72 -0
  88. maneki-0.10.0/src/maneki/audio/playlist/similarity.py +112 -0
  89. maneki-0.10.0/src/maneki/audio/radio.py +132 -0
  90. maneki-0.10.0/src/maneki/audio/serve/__init__.py +13 -0
  91. maneki-0.10.0/src/maneki/audio/serve/app.py +397 -0
  92. maneki-0.10.0/src/maneki/audio/serve/auth.py +57 -0
  93. maneki-0.10.0/src/maneki/audio/serve/config.py +80 -0
  94. maneki-0.10.0/src/maneki/audio/serve/cover_cache.py +94 -0
  95. maneki-0.10.0/src/maneki/audio/serve/covers.py +81 -0
  96. maneki-0.10.0/src/maneki/audio/serve/discovery.py +88 -0
  97. maneki-0.10.0/src/maneki/audio/serve/endpoints/__init__.py +1 -0
  98. maneki-0.10.0/src/maneki/audio/serve/endpoints/browsing.py +213 -0
  99. maneki-0.10.0/src/maneki/audio/serve/endpoints/extras.py +444 -0
  100. maneki-0.10.0/src/maneki/audio/serve/endpoints/lyrics.py +108 -0
  101. maneki-0.10.0/src/maneki/audio/serve/endpoints/media.py +256 -0
  102. maneki-0.10.0/src/maneki/audio/serve/endpoints/radio.py +85 -0
  103. maneki-0.10.0/src/maneki/audio/serve/endpoints/scan.py +40 -0
  104. maneki-0.10.0/src/maneki/audio/serve/endpoints/search.py +147 -0
  105. maneki-0.10.0/src/maneki/audio/serve/endpoints/stubs.py +322 -0
  106. maneki-0.10.0/src/maneki/audio/serve/endpoints/system.py +44 -0
  107. maneki-0.10.0/src/maneki/audio/serve/ids.py +28 -0
  108. maneki-0.10.0/src/maneki/audio/serve/index.py +159 -0
  109. maneki-0.10.0/src/maneki/audio/serve/logging.py +123 -0
  110. maneki-0.10.0/src/maneki/audio/serve/payloads.py +135 -0
  111. maneki-0.10.0/src/maneki/audio/serve/radio_proxy.py +123 -0
  112. maneki-0.10.0/src/maneki/audio/serve/scrobble.py +171 -0
  113. maneki-0.10.0/src/maneki/audio/serve/search_index.py +155 -0
  114. maneki-0.10.0/src/maneki/audio/serve/stars.py +155 -0
  115. maneki-0.10.0/src/maneki/audio/serve/watcher.py +107 -0
  116. maneki-0.10.0/src/maneki/audio/serve/xml.py +58 -0
  117. maneki-0.10.0/src/maneki/auth.py +72 -0
  118. maneki-0.10.0/src/maneki/cli/__init__.py +342 -0
  119. maneki-0.10.0/src/maneki/config.py +46 -0
  120. maneki-0.10.0/src/maneki/library.py +157 -0
  121. maneki-0.10.0/src/maneki/serve_app.py +478 -0
  122. maneki-0.10.0/src/maneki/video/__init__.py +5 -0
  123. maneki-0.10.0/src/maneki/video/cli/__init__.py +32 -0
  124. maneki-0.10.0/src/maneki/video/inspect.py +374 -0
  125. maneki-0.10.0/src/maneki/video/serve/__init__.py +13 -0
  126. maneki-0.10.0/src/maneki/video/serve/app.py +858 -0
  127. maneki-0.10.0/src/maneki/video/serve/demo.py +184 -0
  128. maneki-0.10.0/src/maneki/video/serve/hls.py +508 -0
  129. maneki-0.10.0/src/maneki/video/serve/poster.py +619 -0
  130. maneki-0.10.0/src/maneki/video/serve/scan.py +562 -0
  131. maneki-0.10.0/src/maneki/video/serve/scan_cache.py +281 -0
  132. maneki-0.10.0/src/maneki/video/serve/scan_state.py +100 -0
  133. maneki-0.10.0/src/maneki/video/serve/subtitles.py +474 -0
  134. maneki-0.10.0/src/maneki/video/serve/transcode.py +145 -0
  135. maneki-0.10.0/src/maneki/video/serve/transcode_budget.py +234 -0
  136. maneki-0.10.0/src/maneki/video/serve/watcher.py +141 -0
maneki-0.10.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Morten Hansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
maneki-0.10.0/PKG-INFO ADDED
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: maneki
3
+ Version: 0.10.0
4
+ Summary: Self-hosted media toolkit: audio today (convert / audit / retag rips, Textual TUI, Subsonic-compatible HTTP server), video next (folder-first metadata, HLS streaming).
5
+ Keywords: audio,music,video,hls,media-server,ffmpeg,subsonic,tui,library-manager,musicbrainz,airplay,tailscale
6
+ Author: Morten Hansen
7
+ Author-email: Morten Hansen <morten@winterop.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Multimedia :: Sound/Audio
18
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
19
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Players
20
+ Classifier: Topic :: Multimedia :: Video
21
+ Classifier: Typing :: Typed
22
+ Requires-Dist: typer>=0.24.2
23
+ Requires-Dist: mutagen>=1.47.0
24
+ Requires-Dist: rich>=14.0.0
25
+ Requires-Dist: pillow>=11.0.0
26
+ Requires-Dist: httpx>=0.28.1
27
+ Requires-Dist: pydantic>=2.10.0
28
+ Requires-Dist: pydantic-settings>=2.6.0
29
+ Requires-Dist: fastapi>=0.136.1
30
+ Requires-Dist: uvicorn[standard]>=0.46.0
31
+ Requires-Dist: zeroconf>=0.148.0
32
+ Requires-Dist: watchdog>=6.0.0
33
+ Requires-Dist: paho-mqtt>=2.1.0
34
+ Requires-Dist: jinja2>=3.1.6
35
+ Requires-Dist: itsdangerous>=2.2.0
36
+ Requires-Dist: python-multipart>=0.0.27
37
+ Requires-Dist: structlog>=25.5.0
38
+ Requires-Python: >=3.13
39
+ Project-URL: Homepage, https://github.com/winterop-com/maneki
40
+ Project-URL: Repository, https://github.com/winterop-com/maneki
41
+ Project-URL: Documentation, https://winterop-com.github.io/maneki
42
+ Project-URL: Issues, https://github.com/winterop-com/maneki/issues
43
+ Project-URL: Changelog, https://github.com/winterop-com/maneki/releases
44
+ Description-Content-Type: text/markdown
45
+
46
+ # Maneki
47
+
48
+ [![CI](https://github.com/winterop-com/maneki/actions/workflows/ci.yml/badge.svg)](https://github.com/winterop-com/maneki/actions/workflows/ci.yml)
49
+ [![PyPI version](https://img.shields.io/pypi/v/maneki)](https://pypi.org/project/maneki/)
50
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
51
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
52
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](https://winterop-com.github.io/maneki/)
53
+
54
+ Python 3.13 CLI for a self-hosted media library — audio (convert rips + serve via Subsonic) and video (HLS streaming with on-demand segments, sidecar + embedded subtitles, contact-sheet posters, click-in folder browser). Point `maneki serve` at one directory; it scans recursively, auto-detects what's audio and what's video, and serves both kinds plus the web SPA on one port. The Subsonic mount appears only when audio is present, the video mount only when video is present.
55
+
56
+ ## The name
57
+
58
+ **Maneki** (招き, *mah-neh-kee*) is the Japanese word for *beckoning*. It's the verb-form of *maneki-neko* (招き猫) — the small ceramic cat with a raised paw you've seen on shop counters and restaurant windows across Japan. The cat sits there quietly all day; when you walk in, its paw is already up, inviting you in. Whoever placed it didn't have to do anything; the cat does the welcoming.
59
+
60
+ A self-hosted media server has the same job. It lives on a box in the corner. You don't see it, you don't manage it, you don't poke at it. When you open the app on your phone or laptop and want to watch a film or play music, your library should already be there, ready, waving you in. No URL to type, no port to remember, no "is the server up?" — just open and play.
61
+
62
+ That's what `maneki serve <library>` aims to be: the cat on the shelf.
63
+
64
+ ## Install
65
+
66
+ The lowest-friction way is [`uvx`](https://docs.astral.sh/uv/) — it downloads, caches, and runs the latest published `maneki` in one step. No install step required:
67
+
68
+ ```bash
69
+ uvx maneki --help
70
+ ```
71
+
72
+ For daily / persistent use (PATH-installed, no per-run network check):
73
+
74
+ ```bash
75
+ uv tool install maneki
76
+ maneki --help
77
+ ```
78
+
79
+ You'll also need `ffmpeg` and `ffprobe` on `$PATH` for the convert pipeline:
80
+
81
+ ```bash
82
+ brew install ffmpeg # macOS
83
+ sudo apt install ffmpeg # Debian / Ubuntu
84
+ ```
85
+
86
+ ## Quickstart
87
+
88
+ ```bash
89
+ # One process, one URL, both protocols. Point at any directory - maneki
90
+ # scans recursively and only mounts the kinds with content.
91
+ uvx maneki serve ~/Downloads/library # audio on /audio/rest/*, video on /video/*
92
+ uvx maneki serve ~/Downloads/library --ui # also serve the web SPA at /
93
+
94
+ # Shared across audio + video:
95
+ uvx maneki library info ~/Downloads/library # kind counts (audio + video)
96
+ uvx maneki library list ~/Downloads/library # full file inventory (or `ls`)
97
+ uvx maneki library inspect ~/Downloads/library/some.mkv # tags + cover (audio) or ffprobe streams (video)
98
+
99
+ # Audio tooling:
100
+ uvx maneki audio convert ./input ./output # convert rips
101
+ uvx maneki audio library audit ./output # audit
102
+ uvx maneki audio playlist gen ./output --seed <track> --minutes 60 # auto-generate a mix
103
+ ```
104
+
105
+ ## Screenshots
106
+
107
+ The browser SPA — bordered panels with floating titles, same palette across audio + video. Vertical AUDIO / VIDEO rail on the left switches modes; the rail self-hides when only one kind is mounted at the library root.
108
+
109
+ Audio — Artists → Albums → Tracks plus Now Playing top band with the FFT spectrum visualizer:
110
+
111
+ ![Browser UI — audio library, Daft Punk · Homework](docs/screenshots/web-audio.png)
112
+
113
+ Video — folder browser, instant-paint row thumbnails, sticky breadcrumb. Click into any season for the episode list:
114
+
115
+ ![Browser UI — video folder browse](docs/screenshots/web-video-browse.png)
116
+
117
+ Video player — video.js v8 on the HLS source, contact-sheet poster (16:9, generated server-side from 9 sample frames + header strip with codec / resolution / duration / size), `t` for theater mode, `f` for browser fullscreen, captions menu auto-prefers English / English-SDH:
118
+
119
+ ![Browser UI — video player with contact-sheet poster](docs/screenshots/web-video-player.png)
120
+
121
+ More in the [serve guide](https://winterop-com.github.io/maneki/guides/serve-unified/) and the [video guide](https://winterop-com.github.io/maneki/guides/video/).
122
+
123
+ ## Documentation
124
+
125
+ Full docs are at **[docs/](docs/index.md)** — built with MkDocs Material. Run them locally:
126
+
127
+ ```bash
128
+ make docs-serve # http://127.0.0.1:8000
129
+ ```
130
+
131
+ Or jump straight to:
132
+
133
+ - [Architecture](docs/architecture.md) — how all the pieces fit together (process model, data flow, audio subprocess, SQLite index, FFT visualizer)
134
+ - [Quickstart](docs/guides/quickstart.md) — end-to-end walkthrough including iPhone + Tailscale + Amperfy
135
+ - [maneki serve](docs/guides/serve-unified.md) — single-library mode, auto-detect, web SPA
136
+ - [maneki library](docs/guides/library.md) — cross-cutting info / list / inspect against any root
137
+ - [maneki audio convert](docs/guides/convert.md) — codec / bitrate / enrichment matrix
138
+ - [maneki audio library](docs/guides/audio-library.md) — audit rules + auto-fix + SQLite index
139
+ - [maneki video](docs/guides/video.md) — HLS, subtitles, posters, folder browser
140
+ - [maneki audio playlist](docs/guides/playlist.md) — auto-generated `.m3u8` mixes anchored to a seed track
141
+ - [Desktop apps](docs/guides/desktop.md) — Tauri + Electron generic Subsonic clients
142
+ - [Mobile (Subsonic)](docs/guides/mobile.md) — play:Sub / Amperfy / Symfonium / DSub / Tempo
143
+ - [Edge cases](docs/edge-cases.md) — every weirdness encountered on real rips
144
+ - [Roadmap](docs/roadmap.md) — what's next
145
+ - [Development](docs/guides/development.md) — directory layout + test patterns + commit style
146
+
147
+ ## Status
148
+
149
+ v0.9.0 · audio (Subsonic-compat) + video (HLS, sidecar + embedded subtitles, 16:9 contact-sheet posters, folder browser, watcher hot-reload) share one `maneki serve` and the web SPA at `/`. ruff + mypy + pyright clean, full pytest suite green (648 tests).
150
+
151
+ **Audio**: OpenSubsonic-compatible (`multipleGenres`, `transcodeOffset`, `songLyrics` extensions), backs heart / star buttons with a persistent `<root>/.maneki/stars.toml`, returns sub-ms FTS5-ranked `/search3` results, promotes LRC lyrics to `synced: true`, tested against Symfonium / Amperfy / play:Sub / Feishin on iOS / Android / desktop. Persistent SQLite library index at `<root>/.maneki/index.db` keeps cold starts under a second; the filesystem watcher does per-album incremental rescans.
152
+
153
+ **Video**: on-demand HLS with bounded foreground concurrency (rapid seeks don't wedge the player), single-ffmpeg multi-stream subtitle extraction (a 45-track .mkv opens in ~2s instead of stalling on 45 parallel ffmpegs), instant-paint row thumbnails + 16:9 contact-sheet posters with hash-derived cache filenames (deeply-nested rel paths don't blow NAME_MAX), `--no-cover-images` to skip contact sheets entirely on slow disks, `--prewarm-cache` to fill thumbs / posters / subtitles at startup, watcher hot-reload of the SQLite-backed scan index so adds / deletes / renames appear without a restart. Same `index.db` file as the audio side, separate `videos` table.
154
+
155
+ ## License
156
+
157
+ See LICENSE in the repo root.
@@ -0,0 +1,112 @@
1
+ # Maneki
2
+
3
+ [![CI](https://github.com/winterop-com/maneki/actions/workflows/ci.yml/badge.svg)](https://github.com/winterop-com/maneki/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/maneki)](https://pypi.org/project/maneki/)
5
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](https://winterop-com.github.io/maneki/)
8
+
9
+ Python 3.13 CLI for a self-hosted media library — audio (convert rips + serve via Subsonic) and video (HLS streaming with on-demand segments, sidecar + embedded subtitles, contact-sheet posters, click-in folder browser). Point `maneki serve` at one directory; it scans recursively, auto-detects what's audio and what's video, and serves both kinds plus the web SPA on one port. The Subsonic mount appears only when audio is present, the video mount only when video is present.
10
+
11
+ ## The name
12
+
13
+ **Maneki** (招き, *mah-neh-kee*) is the Japanese word for *beckoning*. It's the verb-form of *maneki-neko* (招き猫) — the small ceramic cat with a raised paw you've seen on shop counters and restaurant windows across Japan. The cat sits there quietly all day; when you walk in, its paw is already up, inviting you in. Whoever placed it didn't have to do anything; the cat does the welcoming.
14
+
15
+ A self-hosted media server has the same job. It lives on a box in the corner. You don't see it, you don't manage it, you don't poke at it. When you open the app on your phone or laptop and want to watch a film or play music, your library should already be there, ready, waving you in. No URL to type, no port to remember, no "is the server up?" — just open and play.
16
+
17
+ That's what `maneki serve <library>` aims to be: the cat on the shelf.
18
+
19
+ ## Install
20
+
21
+ The lowest-friction way is [`uvx`](https://docs.astral.sh/uv/) — it downloads, caches, and runs the latest published `maneki` in one step. No install step required:
22
+
23
+ ```bash
24
+ uvx maneki --help
25
+ ```
26
+
27
+ For daily / persistent use (PATH-installed, no per-run network check):
28
+
29
+ ```bash
30
+ uv tool install maneki
31
+ maneki --help
32
+ ```
33
+
34
+ You'll also need `ffmpeg` and `ffprobe` on `$PATH` for the convert pipeline:
35
+
36
+ ```bash
37
+ brew install ffmpeg # macOS
38
+ sudo apt install ffmpeg # Debian / Ubuntu
39
+ ```
40
+
41
+ ## Quickstart
42
+
43
+ ```bash
44
+ # One process, one URL, both protocols. Point at any directory - maneki
45
+ # scans recursively and only mounts the kinds with content.
46
+ uvx maneki serve ~/Downloads/library # audio on /audio/rest/*, video on /video/*
47
+ uvx maneki serve ~/Downloads/library --ui # also serve the web SPA at /
48
+
49
+ # Shared across audio + video:
50
+ uvx maneki library info ~/Downloads/library # kind counts (audio + video)
51
+ uvx maneki library list ~/Downloads/library # full file inventory (or `ls`)
52
+ uvx maneki library inspect ~/Downloads/library/some.mkv # tags + cover (audio) or ffprobe streams (video)
53
+
54
+ # Audio tooling:
55
+ uvx maneki audio convert ./input ./output # convert rips
56
+ uvx maneki audio library audit ./output # audit
57
+ uvx maneki audio playlist gen ./output --seed <track> --minutes 60 # auto-generate a mix
58
+ ```
59
+
60
+ ## Screenshots
61
+
62
+ The browser SPA — bordered panels with floating titles, same palette across audio + video. Vertical AUDIO / VIDEO rail on the left switches modes; the rail self-hides when only one kind is mounted at the library root.
63
+
64
+ Audio — Artists → Albums → Tracks plus Now Playing top band with the FFT spectrum visualizer:
65
+
66
+ ![Browser UI — audio library, Daft Punk · Homework](docs/screenshots/web-audio.png)
67
+
68
+ Video — folder browser, instant-paint row thumbnails, sticky breadcrumb. Click into any season for the episode list:
69
+
70
+ ![Browser UI — video folder browse](docs/screenshots/web-video-browse.png)
71
+
72
+ Video player — video.js v8 on the HLS source, contact-sheet poster (16:9, generated server-side from 9 sample frames + header strip with codec / resolution / duration / size), `t` for theater mode, `f` for browser fullscreen, captions menu auto-prefers English / English-SDH:
73
+
74
+ ![Browser UI — video player with contact-sheet poster](docs/screenshots/web-video-player.png)
75
+
76
+ More in the [serve guide](https://winterop-com.github.io/maneki/guides/serve-unified/) and the [video guide](https://winterop-com.github.io/maneki/guides/video/).
77
+
78
+ ## Documentation
79
+
80
+ Full docs are at **[docs/](docs/index.md)** — built with MkDocs Material. Run them locally:
81
+
82
+ ```bash
83
+ make docs-serve # http://127.0.0.1:8000
84
+ ```
85
+
86
+ Or jump straight to:
87
+
88
+ - [Architecture](docs/architecture.md) — how all the pieces fit together (process model, data flow, audio subprocess, SQLite index, FFT visualizer)
89
+ - [Quickstart](docs/guides/quickstart.md) — end-to-end walkthrough including iPhone + Tailscale + Amperfy
90
+ - [maneki serve](docs/guides/serve-unified.md) — single-library mode, auto-detect, web SPA
91
+ - [maneki library](docs/guides/library.md) — cross-cutting info / list / inspect against any root
92
+ - [maneki audio convert](docs/guides/convert.md) — codec / bitrate / enrichment matrix
93
+ - [maneki audio library](docs/guides/audio-library.md) — audit rules + auto-fix + SQLite index
94
+ - [maneki video](docs/guides/video.md) — HLS, subtitles, posters, folder browser
95
+ - [maneki audio playlist](docs/guides/playlist.md) — auto-generated `.m3u8` mixes anchored to a seed track
96
+ - [Desktop apps](docs/guides/desktop.md) — Tauri + Electron generic Subsonic clients
97
+ - [Mobile (Subsonic)](docs/guides/mobile.md) — play:Sub / Amperfy / Symfonium / DSub / Tempo
98
+ - [Edge cases](docs/edge-cases.md) — every weirdness encountered on real rips
99
+ - [Roadmap](docs/roadmap.md) — what's next
100
+ - [Development](docs/guides/development.md) — directory layout + test patterns + commit style
101
+
102
+ ## Status
103
+
104
+ v0.9.0 · audio (Subsonic-compat) + video (HLS, sidecar + embedded subtitles, 16:9 contact-sheet posters, folder browser, watcher hot-reload) share one `maneki serve` and the web SPA at `/`. ruff + mypy + pyright clean, full pytest suite green (648 tests).
105
+
106
+ **Audio**: OpenSubsonic-compatible (`multipleGenres`, `transcodeOffset`, `songLyrics` extensions), backs heart / star buttons with a persistent `<root>/.maneki/stars.toml`, returns sub-ms FTS5-ranked `/search3` results, promotes LRC lyrics to `synced: true`, tested against Symfonium / Amperfy / play:Sub / Feishin on iOS / Android / desktop. Persistent SQLite library index at `<root>/.maneki/index.db` keeps cold starts under a second; the filesystem watcher does per-album incremental rescans.
107
+
108
+ **Video**: on-demand HLS with bounded foreground concurrency (rapid seeks don't wedge the player), single-ffmpeg multi-stream subtitle extraction (a 45-track .mkv opens in ~2s instead of stalling on 45 parallel ffmpegs), instant-paint row thumbnails + 16:9 contact-sheet posters with hash-derived cache filenames (deeply-nested rel paths don't blow NAME_MAX), `--no-cover-images` to skip contact sheets entirely on slow disks, `--prewarm-cache` to fill thumbs / posters / subtitles at startup, watcher hot-reload of the SQLite-backed scan index so adds / deletes / renames appear without a restart. Same `index.db` file as the audio side, separate `videos` table.
109
+
110
+ ## License
111
+
112
+ See LICENSE in the repo root.
@@ -0,0 +1,175 @@
1
+ [project]
2
+ name = "maneki"
3
+ version = "0.10.0"
4
+ description = "Self-hosted media toolkit: audio today (convert / audit / retag rips, Textual TUI, Subsonic-compatible HTTP server), video next (folder-first metadata, HLS streaming)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "Morten Hansen", email = "morten@winterop.com" },
11
+ ]
12
+ keywords = [
13
+ "audio",
14
+ "music",
15
+ "video",
16
+ "hls",
17
+ "media-server",
18
+ "ffmpeg",
19
+ "subsonic",
20
+ "tui",
21
+ "library-manager",
22
+ "musicbrainz",
23
+ "airplay",
24
+ "tailscale",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 3 - Alpha",
28
+ "Environment :: Console",
29
+ "Intended Audience :: End Users/Desktop",
30
+ "Operating System :: MacOS",
31
+ "Operating System :: POSIX :: Linux",
32
+ "Programming Language :: Python :: 3",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Multimedia :: Sound/Audio",
35
+ "Topic :: Multimedia :: Sound/Audio :: Conversion",
36
+ "Topic :: Multimedia :: Sound/Audio :: Players",
37
+ "Topic :: Multimedia :: Video",
38
+ "Typing :: Typed",
39
+ ]
40
+ dependencies = [
41
+ "typer>=0.24.2",
42
+ "mutagen>=1.47.0",
43
+ "rich>=14.0.0",
44
+ "pillow>=11.0.0",
45
+ "httpx>=0.28.1",
46
+ "pydantic>=2.10.0",
47
+ "pydantic-settings>=2.6.0",
48
+ "fastapi>=0.136.1",
49
+ "uvicorn[standard]>=0.46.0",
50
+ "zeroconf>=0.148.0",
51
+ "watchdog>=6.0.0",
52
+ "paho-mqtt>=2.1.0",
53
+ "jinja2>=3.1.6",
54
+ "itsdangerous>=2.2.0",
55
+ "python-multipart>=0.0.27",
56
+ "structlog>=25.5.0",
57
+ ]
58
+
59
+ [project.urls]
60
+ Homepage = "https://github.com/winterop-com/maneki"
61
+ Repository = "https://github.com/winterop-com/maneki"
62
+ Documentation = "https://winterop-com.github.io/maneki"
63
+ Issues = "https://github.com/winterop-com/maneki/issues"
64
+ Changelog = "https://github.com/winterop-com/maneki/releases"
65
+
66
+ [project.scripts]
67
+ maneki = "maneki.cli:app"
68
+
69
+ [dependency-groups]
70
+ dev = [
71
+ "coverage[toml]>=7.13.5",
72
+ "mypy>=1.20.2",
73
+ "pytest>=9.0.3",
74
+ "pytest-cov>=7.1.0",
75
+ "pytest-asyncio>=1.3.0",
76
+ "ruff>=0.15.12",
77
+ "pyright>=1.1.409",
78
+ "mkdocs>=1.6.1",
79
+ "mkdocs-material>=9.7.6",
80
+ "mkdocstrings[python]>=1.0.4",
81
+ "mkdocs-claude-theme",
82
+ ]
83
+
84
+ [build-system]
85
+ requires = ["uv_build>=0.9.0,<0.12.0"]
86
+ build-backend = "uv_build"
87
+
88
+ [tool.uv.sources]
89
+ mkdocs-claude-theme = { git = "https://github.com/mortenoh/mkdocs-claude-theme.git" }
90
+
91
+ [tool.ruff]
92
+ target-version = "py313"
93
+ line-length = 120
94
+
95
+ [tool.ruff.lint]
96
+ fixable = ["ALL"]
97
+ select = ["E", "W", "F", "I", "D"]
98
+ ignore = ["D203", "D213"]
99
+
100
+ [tool.ruff.lint.pydocstyle]
101
+ convention = "google"
102
+
103
+ [tool.ruff.lint.per-file-ignores]
104
+ "tests/**/*.py" = ["D"]
105
+ "**/__init__.py" = ["D104"]
106
+ "src/**/*.py" = ["D102", "D105", "D107"]
107
+ "src/maneki/audio/_ui_static/**" = []
108
+
109
+ [tool.ruff.format]
110
+ quote-style = "double"
111
+ indent-style = "space"
112
+ skip-magic-trailing-comma = false
113
+ docstring-code-format = true
114
+ docstring-code-line-length = "dynamic"
115
+
116
+ [tool.pytest.ini_options]
117
+ asyncio_mode = "auto"
118
+ testpaths = ["tests"]
119
+ norecursedirs = [".git", ".venv", "__pycache__", "desktop", "src/maneki/audio/_ui_static"]
120
+ filterwarnings = [
121
+ "ignore:Pydantic serializer warnings:UserWarning",
122
+ ]
123
+ markers = [
124
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
125
+ ]
126
+
127
+ [tool.coverage.run]
128
+ branch = true
129
+ dynamic_context = "test_function"
130
+ relative_files = true
131
+ source = ["maneki"]
132
+ omit = ["*/cli/*"]
133
+
134
+ [tool.coverage.report]
135
+ exclude_also = ["if TYPE_CHECKING:"]
136
+ precision = 2
137
+ show_missing = true
138
+ skip_covered = true
139
+
140
+ [tool.mypy]
141
+ python_version = "3.13"
142
+ warn_return_any = true
143
+ warn_unused_configs = true
144
+ disallow_untyped_defs = true
145
+ check_untyped_defs = true
146
+ no_implicit_optional = true
147
+ warn_unused_ignores = true
148
+ strict_equality = true
149
+ mypy_path = ["src"]
150
+
151
+ [[tool.mypy.overrides]]
152
+ module = "tests.*"
153
+ disallow_untyped_defs = false
154
+
155
+ [[tool.mypy.overrides]]
156
+ module = ["mutagen.*"]
157
+ ignore_missing_imports = true
158
+
159
+ [tool.pyright]
160
+ include = ["src", "tests"]
161
+ exclude = ["src/maneki/audio/_ui_static", "**/.venv"]
162
+ pythonVersion = "3.13"
163
+ typeCheckingMode = "strict"
164
+ useLibraryCodeForTypes = true
165
+ reportPrivateUsage = false
166
+ reportUnusedFunction = false
167
+ reportUnknownMemberType = false
168
+ reportUnknownArgumentType = false
169
+ reportUnknownParameterType = false
170
+ reportUnknownVariableType = false
171
+ reportMissingTypeArgument = false
172
+ reportMissingTypeStubs = false
173
+ reportUnknownLambdaType = false
174
+ reportMissingImports = "warning"
175
+ reportMissingModuleSource = false
@@ -0,0 +1,15 @@
1
+ """Self-hosted media toolkit — audio (and video, planned).
2
+
3
+ The top-level package exposes only the version. Audio capabilities live
4
+ under `maneki.audio.*`; video capabilities will live under
5
+ `maneki.video.*` once they land.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ try:
11
+ from importlib.metadata import version as _get_version
12
+
13
+ __version__ = _get_version("maneki")
14
+ except Exception: # pragma: no cover - uninstalled / dev tree
15
+ __version__ = "unknown"
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m maneki`."""
2
+
3
+ from maneki.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,71 @@
1
+ # Maneki Design v2 (Claude Designer prototype)
2
+
3
+ The Maneki desktop UI. The source files in `src/` are the raw output
4
+ of Claude Designer — sibling JSX modules that share state via
5
+ `window.MK_*` globals. `index.html` loads them in dependency order via
6
+ Babel-standalone so the prototype runs with no build step (same
7
+ runtime model as the Claude Designer preview).
8
+
9
+ Wiring to the real Subsonic API lives in the `_`-prefixed files
10
+ (`_api.js`, `_audio.js`, `_md5.js`, `_wiring.jsx`) which survive
11
+ design-zip drops; designer-authored files (`app.jsx`, `chrome.jsx`,
12
+ etc.) get replaced wholesale on each iteration.
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ desktop/react/
18
+ ├── index.html ← entry — boots React UMD + Babel + script tags
19
+ ├── main.jsx ← renders <window.MK_App /> into #root
20
+ ├── maneki.css ← all styles, theme tokens, layout variants
21
+ ├── favicon.svg
22
+ └── src/
23
+ ├── data.jsx mock library data → window.MK_DATA
24
+ ├── covers.jsx procedural album-cover generator
25
+ ├── visualizer.jsx Canvas FFT (bars / mirror / radial / ambient)
26
+ ├── views.jsx LoginView, StarBtn, ConnectionBanner, ...
27
+ ├── overlays.jsx Shortcuts panel, command palette, search dropdown, lyrics overlay
28
+ ├── tweaks-panel.jsx Designer-time tweak controls (layout / accent / viz / density / ...)
29
+ ├── chrome.jsx TopBar, Sidebar, NowPlaying, MainArea, FullscreenViz, ...
30
+ └── app.jsx <App /> — wires state + actions, registers window.MK_App
31
+ ```
32
+
33
+ ## Run
34
+
35
+ ```bash
36
+ make desktop-react-tauri-dev # Tauri wrapper (separate app bundle)
37
+ make desktop-react-electron-dev # Electron wrapper (separate app bundle)
38
+
39
+ # Or as a plain webpage:
40
+ cd desktop/react && python3 -m http.server 1900
41
+ open http://127.0.0.1:1900/
42
+ ```
43
+
44
+ The prototype uses mock data (`src/data.jsx`) — no Subsonic server
45
+ required. Wiring to the real `/rest/*` API comes after the design
46
+ direction is locked.
47
+
48
+ ## Why no build step
49
+
50
+ The Claude Designer artifact uses sibling globals on `window` rather
51
+ than ES module imports. Loading via `<script type="text/babel">` in
52
+ the same order as the design preview means every new iteration can
53
+ be dropped in with `cp -r`. When the design stabilises we can swap
54
+ to Vite + TypeScript without touching the JSX source.
55
+
56
+ ## What's wired
57
+
58
+ - Real Subsonic auth (`/rest/ping` with salted token)
59
+ - Library load: `getArtists` -> per-artist `getArtist` -> per-album `getAlbum`
60
+ - Track playback via `/rest/stream` driven by an `<audio>` element
61
+ - Star toggle via `/rest/star` / `/rest/unstar`
62
+ - Cover art via `/rest/getCoverArt`
63
+ - Internet radio via `getInternetRadioStations`
64
+ - Session persistence in localStorage; auto-resume on next launch
65
+
66
+ ## Not (yet) wired
67
+
68
+ - Server-side search (`/rest/search3`) — currently filters the in-memory tree
69
+ - Playlists (`/rest/getPlaylists`)
70
+ - Lyrics fetching (`/rest/getLyrics`)
71
+ - ICY now-playing metadata for radio streams