kikusan 0.1.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,48 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ push:
8
+ tags:
9
+ - "v*"
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+
22
+ - name: Install build dependencies
23
+ run: pip install build
24
+
25
+ - name: Build package
26
+ run: python -m build
27
+
28
+ - name: Upload build artifacts
29
+ uses: actions/upload-artifact@v4
30
+ with:
31
+ name: dist
32
+ path: dist/
33
+
34
+ publish:
35
+ needs: build
36
+ runs-on: ubuntu-latest
37
+ environment: pypi
38
+ permissions:
39
+ id-token: write
40
+ steps:
41
+ - name: Download build artifacts
42
+ uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+
47
+ - name: Publish to PyPI
48
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Downloads
13
+ downloads/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,3 @@
1
+ # Project description
2
+
3
+ Kikusan is a tool to search and download music from youtube music. It must use yt-dlp in the background. It must be usable through CLI and also have a web app (subcommand "web"). The web app should be really simple, but must support search functionality. It should be deployable with docker and have an example docker-compose file. It must add lyrics via lrc files to the downloaded files (via https://lrclib.net/).
@@ -0,0 +1,29 @@
1
+ FROM python:3.12-slim
2
+
3
+ # Install ffmpeg for audio processing
4
+ RUN apt-get update && \
5
+ apt-get install -y --no-install-recommends ffmpeg && \
6
+ rm -rf /var/lib/apt/lists/*
7
+
8
+ # Install uv for fast package management
9
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
10
+
11
+ WORKDIR /app
12
+
13
+ # Copy project files
14
+ COPY pyproject.toml uv.lock ./
15
+ COPY kikusan/ ./kikusan/
16
+
17
+ # Install dependencies
18
+ RUN uv sync --frozen
19
+
20
+ # Create downloads directory
21
+ RUN mkdir -p /downloads
22
+
23
+ ENV KIKUSAN_DOWNLOAD_DIR=/downloads
24
+ ENV KIKUSAN_WEB_PORT=8000
25
+
26
+ EXPOSE 8000
27
+
28
+ # Run the web server
29
+ CMD ["uv", "run", "kikusan", "web", "--host", "0.0.0.0"]
kikusan-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: kikusan
3
+ Version: 0.1.0
4
+ Summary: Search and download music from YouTube Music with lyrics
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: click>=8.0.0
7
+ Requires-Dist: fastapi[standard]>=0.115.0
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: mutagen>=1.47.0
10
+ Requires-Dist: spotipy>=2.24.0
11
+ Requires-Dist: yt-dlp>=2025.12.8
12
+ Requires-Dist: ytmusicapi>=1.8.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Kikusan
16
+
17
+ Search and download music from YouTube Music with lyrics.
18
+
19
+ ## Features
20
+
21
+ - Search YouTube Music
22
+ - Download audio in OPUS/MP3/FLAC format
23
+ - Playlist support (download entire playlists)
24
+ - Quick download (search and download first match)
25
+ - Automatic lyrics fetching from lrclib.net (LRC format)
26
+ - CLI and web interface
27
+ - Docker support
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ uv sync
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### CLI
38
+
39
+ ```bash
40
+ # Search for music
41
+ kikusan search "Bohemian Rhapsody"
42
+
43
+ # Download by video ID
44
+ kikusan download bSnlKl_PoQU
45
+
46
+ # Download by URL
47
+ kikusan download --url "https://music.youtube.com/watch?v=bSnlKl_PoQU"
48
+
49
+ # Search and download first match
50
+ kikusan download --query "Bohemian Rhapsody Queen"
51
+
52
+ # Download entire playlist
53
+ kikusan download --url "https://music.youtube.com/playlist?list=..."
54
+
55
+ # Custom filename format
56
+ kikusan download bSnlKl_PoQU --filename "%(title)s"
57
+
58
+ # Options
59
+ kikusan download bSnlKl_PoQU --output ~/Music --format mp3
60
+ ```
61
+
62
+ ### Web Interface
63
+
64
+ ```bash
65
+ kikusan web
66
+ # Open http://localhost:8000
67
+ ```
68
+
69
+ ### Docker
70
+
71
+ ```bash
72
+ docker compose up -d
73
+ # Open http://localhost:8000
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ Environment variables:
79
+
80
+ | Variable | Default | Description |
81
+ |----------|---------|-------------|
82
+ | `KIKUSAN_DOWNLOAD_DIR` | `./downloads` | Download directory |
83
+ | `KIKUSAN_AUDIO_FORMAT` | `opus` | Audio format (opus, mp3, flac) |
84
+ | `KIKUSAN_FILENAME_TEMPLATE` | `%(artist,uploader)s - %(title)s` | Filename template (yt-dlp format) |
85
+ | `KIKUSAN_WEB_PORT` | `8000` | Web server port |
86
+
87
+ ## Requirements
88
+
89
+ - Python 3.12+
90
+ - ffmpeg (for audio processing)
@@ -0,0 +1,76 @@
1
+ # Kikusan
2
+
3
+ Search and download music from YouTube Music with lyrics.
4
+
5
+ ## Features
6
+
7
+ - Search YouTube Music
8
+ - Download audio in OPUS/MP3/FLAC format
9
+ - Playlist support (download entire playlists)
10
+ - Quick download (search and download first match)
11
+ - Automatic lyrics fetching from lrclib.net (LRC format)
12
+ - CLI and web interface
13
+ - Docker support
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ uv sync
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### CLI
24
+
25
+ ```bash
26
+ # Search for music
27
+ kikusan search "Bohemian Rhapsody"
28
+
29
+ # Download by video ID
30
+ kikusan download bSnlKl_PoQU
31
+
32
+ # Download by URL
33
+ kikusan download --url "https://music.youtube.com/watch?v=bSnlKl_PoQU"
34
+
35
+ # Search and download first match
36
+ kikusan download --query "Bohemian Rhapsody Queen"
37
+
38
+ # Download entire playlist
39
+ kikusan download --url "https://music.youtube.com/playlist?list=..."
40
+
41
+ # Custom filename format
42
+ kikusan download bSnlKl_PoQU --filename "%(title)s"
43
+
44
+ # Options
45
+ kikusan download bSnlKl_PoQU --output ~/Music --format mp3
46
+ ```
47
+
48
+ ### Web Interface
49
+
50
+ ```bash
51
+ kikusan web
52
+ # Open http://localhost:8000
53
+ ```
54
+
55
+ ### Docker
56
+
57
+ ```bash
58
+ docker compose up -d
59
+ # Open http://localhost:8000
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ Environment variables:
65
+
66
+ | Variable | Default | Description |
67
+ |----------|---------|-------------|
68
+ | `KIKUSAN_DOWNLOAD_DIR` | `./downloads` | Download directory |
69
+ | `KIKUSAN_AUDIO_FORMAT` | `opus` | Audio format (opus, mp3, flac) |
70
+ | `KIKUSAN_FILENAME_TEMPLATE` | `%(artist,uploader)s - %(title)s` | Filename template (yt-dlp format) |
71
+ | `KIKUSAN_WEB_PORT` | `8000` | Web server port |
72
+
73
+ ## Requirements
74
+
75
+ - Python 3.12+
76
+ - ffmpeg (for audio processing)
@@ -0,0 +1,12 @@
1
+ services:
2
+ kikusan:
3
+ build: .
4
+ ports:
5
+ - "8000:8000"
6
+ volumes:
7
+ - ./downloads:/downloads
8
+ environment:
9
+ - KIKUSAN_DOWNLOAD_DIR=/downloads
10
+ - KIKUSAN_AUDIO_FORMAT=opus
11
+ - KIKUSAN_WEB_PORT=8000
12
+ restart: unless-stopped
@@ -0,0 +1,3 @@
1
+ """Kikusan - Search and download music from YouTube Music with lyrics."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,249 @@
1
+ """Command-line interface for Kikusan."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from kikusan.config import get_config
9
+ from kikusan.download import download, download_url
10
+ from kikusan.search import search
11
+
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format="%(message)s",
15
+ )
16
+
17
+
18
+ @click.group()
19
+ @click.version_option()
20
+ def main():
21
+ """Kikusan - Search and download music from YouTube Music."""
22
+ pass
23
+
24
+
25
+ @main.command()
26
+ @click.argument("query")
27
+ @click.option("-l", "--limit", default=10, help="Maximum number of results")
28
+ def search_cmd(query: str, limit: int):
29
+ """Search for music on YouTube Music."""
30
+ results = search(query, limit=limit)
31
+
32
+ if not results:
33
+ click.echo("No results found.")
34
+ return
35
+
36
+ click.echo(f"\nFound {len(results)} results:\n")
37
+
38
+ for i, track in enumerate(results, 1):
39
+ album_info = f" [{track.album}]" if track.album else ""
40
+ click.echo(f"{i:2}. {track.title} - {track.artist}{album_info}")
41
+ click.echo(f" ID: {track.video_id} Duration: {track.duration_display}")
42
+ click.echo()
43
+
44
+
45
+ # Register search command with alias
46
+ main.add_command(search_cmd, name="search")
47
+
48
+
49
+ @main.command()
50
+ @click.argument("video_id", required=False)
51
+ @click.option("--url", "-u", help="YouTube, YouTube Music, or Spotify URL")
52
+ @click.option("--query", "-q", help="Search query (downloads first match)")
53
+ @click.option("--output", "-o", type=click.Path(), help="Output directory")
54
+ @click.option(
55
+ "--format",
56
+ "-f",
57
+ "audio_format",
58
+ default=None,
59
+ type=click.Choice(["opus", "mp3", "flac"]),
60
+ help="Audio format (default: opus)",
61
+ )
62
+ @click.option(
63
+ "--filename",
64
+ "-n",
65
+ "filename_template",
66
+ default=None,
67
+ help="Filename template (default: '%(artist,uploader)s - %(title)s')",
68
+ )
69
+ @click.option("--no-lyrics", is_flag=True, help="Skip fetching lyrics")
70
+ def download_cmd(
71
+ video_id: str | None,
72
+ url: str | None,
73
+ query: str | None,
74
+ output: str | None,
75
+ audio_format: str | None,
76
+ filename_template: str | None,
77
+ no_lyrics: bool,
78
+ ):
79
+ """Download a track by video ID, URL, or search query.
80
+
81
+ Examples:
82
+
83
+ kikusan download VIDEO_ID
84
+
85
+ kikusan download --url "https://music.youtube.com/watch?v=..."
86
+
87
+ kikusan download --url "https://music.youtube.com/playlist?list=..."
88
+
89
+ kikusan download --url "https://open.spotify.com/playlist/..."
90
+
91
+ kikusan download --query "Bohemian Rhapsody Queen"
92
+ """
93
+ if not video_id and not url and not query:
94
+ raise click.UsageError("One of VIDEO_ID, --url, or --query is required")
95
+
96
+ config = get_config()
97
+ output_dir = Path(output) if output else config.download_dir
98
+ fmt = audio_format or config.audio_format
99
+ template = filename_template or config.filename_template
100
+
101
+ try:
102
+ # Search and download first match
103
+ if query:
104
+ results = search(query, limit=1)
105
+ if not results:
106
+ raise click.ClickException(f"No results found for: {query}")
107
+
108
+ track = results[0]
109
+ click.echo(f"Found: {track.title} - {track.artist}")
110
+
111
+ audio_path = download(
112
+ video_id=track.video_id,
113
+ output_dir=output_dir,
114
+ audio_format=fmt,
115
+ filename_template=template,
116
+ fetch_lyrics=not no_lyrics,
117
+ )
118
+ if audio_path:
119
+ click.echo(f"Downloaded: {audio_path}")
120
+ return
121
+
122
+ # Handle URL (YouTube, YouTube Music, or Spotify)
123
+ if url:
124
+ from kikusan.spotify import is_spotify_url
125
+
126
+ if is_spotify_url(url):
127
+ _download_spotify_url(
128
+ url=url,
129
+ output_dir=output_dir,
130
+ audio_format=fmt,
131
+ filename_template=template,
132
+ fetch_lyrics=not no_lyrics,
133
+ )
134
+ else:
135
+ result = download_url(
136
+ url=url,
137
+ output_dir=output_dir,
138
+ audio_format=fmt,
139
+ filename_template=template,
140
+ fetch_lyrics=not no_lyrics,
141
+ )
142
+
143
+ if isinstance(result, list):
144
+ click.echo(f"Downloaded {len(result)} tracks to {output_dir}")
145
+ elif result:
146
+ click.echo(f"Downloaded: {result}")
147
+ else:
148
+ click.echo("Download completed but could not locate file.")
149
+ return
150
+
151
+ # Download by video ID
152
+ audio_path = download(
153
+ video_id=video_id,
154
+ output_dir=output_dir,
155
+ audio_format=fmt,
156
+ filename_template=template,
157
+ fetch_lyrics=not no_lyrics,
158
+ )
159
+
160
+ if audio_path:
161
+ click.echo(f"Downloaded: {audio_path}")
162
+ else:
163
+ click.echo("Download completed but could not locate file.")
164
+
165
+ except Exception as e:
166
+ raise click.ClickException(str(e))
167
+
168
+
169
+ def _download_spotify_url(
170
+ url: str,
171
+ output_dir: Path,
172
+ audio_format: str,
173
+ filename_template: str,
174
+ fetch_lyrics: bool,
175
+ ) -> None:
176
+ """Download tracks from a Spotify playlist/album by searching YouTube Music."""
177
+ from kikusan.spotify import get_tracks_from_url
178
+
179
+ spotify_tracks = get_tracks_from_url(url)
180
+
181
+ if not spotify_tracks:
182
+ click.echo("No tracks found in Spotify URL.")
183
+ return
184
+
185
+ click.echo(f"Found {len(spotify_tracks)} tracks in Spotify playlist/album")
186
+
187
+ downloaded = 0
188
+ skipped = 0
189
+ failed = 0
190
+
191
+ for i, sp_track in enumerate(spotify_tracks, 1):
192
+ click.echo(f"[{i}/{len(spotify_tracks)}] Searching: {sp_track.artist} - {sp_track.name}")
193
+
194
+ # Search YouTube Music for this track
195
+ results = search(sp_track.search_query, limit=1)
196
+
197
+ if not results:
198
+ click.echo(f" Not found on YouTube Music, skipping")
199
+ failed += 1
200
+ continue
201
+
202
+ yt_track = results[0]
203
+ click.echo(f" Found: {yt_track.title} - {yt_track.artist}")
204
+
205
+ try:
206
+ audio_path = download(
207
+ video_id=yt_track.video_id,
208
+ output_dir=output_dir,
209
+ audio_format=audio_format,
210
+ filename_template=filename_template,
211
+ fetch_lyrics=fetch_lyrics,
212
+ )
213
+
214
+ if audio_path and "Skipping" not in str(audio_path):
215
+ downloaded += 1
216
+ else:
217
+ skipped += 1
218
+
219
+ except Exception as e:
220
+ click.echo(f" Failed: {e}")
221
+ failed += 1
222
+
223
+ click.echo(f"\nCompleted: {downloaded} downloaded, {skipped} skipped, {failed} failed")
224
+
225
+
226
+ main.add_command(download_cmd, name="download")
227
+
228
+
229
+ @main.command()
230
+ @click.option("--host", default="0.0.0.0", help="Host to bind to")
231
+ @click.option("--port", "-p", default=None, type=int, help="Port to listen on")
232
+ def web(host: str, port: int | None):
233
+ """Start the web interface."""
234
+ import uvicorn
235
+
236
+ from kikusan.config import get_config
237
+
238
+ config = get_config()
239
+ server_port = port or config.web_port
240
+
241
+ click.echo(f"Starting web server at http://{host}:{server_port}")
242
+
243
+ from kikusan.web.app import app
244
+
245
+ uvicorn.run(app, host=host, port=server_port)
246
+
247
+
248
+ if __name__ == "__main__":
249
+ main()
@@ -0,0 +1,42 @@
1
+ """Configuration handling for Kikusan."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ # Default filename template: Artist - Title
8
+ DEFAULT_FILENAME_TEMPLATE = "%(artist,uploader)s - %(title)s"
9
+
10
+
11
+ @dataclass
12
+ class Config:
13
+ """Application configuration."""
14
+
15
+ download_dir: Path
16
+ audio_format: str
17
+ filename_template: str
18
+ web_port: int
19
+ spotify_client_id: str | None
20
+ spotify_client_secret: str | None
21
+
22
+ @classmethod
23
+ def from_env(cls) -> "Config":
24
+ """Create config from environment variables with defaults."""
25
+ return cls(
26
+ download_dir=Path(os.getenv("KIKUSAN_DOWNLOAD_DIR", "./downloads")),
27
+ audio_format=os.getenv("KIKUSAN_AUDIO_FORMAT", "opus"),
28
+ filename_template=os.getenv("KIKUSAN_FILENAME_TEMPLATE", DEFAULT_FILENAME_TEMPLATE),
29
+ web_port=int(os.getenv("KIKUSAN_WEB_PORT", "8000")),
30
+ spotify_client_id=os.getenv("SPOTIFY_CLIENT_ID"),
31
+ spotify_client_secret=os.getenv("SPOTIFY_CLIENT_SECRET"),
32
+ )
33
+
34
+ @property
35
+ def spotify_configured(self) -> bool:
36
+ """Check if Spotify credentials are configured."""
37
+ return bool(self.spotify_client_id and self.spotify_client_secret)
38
+
39
+
40
+ def get_config() -> Config:
41
+ """Get the current configuration."""
42
+ return Config.from_env()