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.
- kikusan-0.1.0/.github/workflows/publish.yml +48 -0
- kikusan-0.1.0/.gitignore +13 -0
- kikusan-0.1.0/.python-version +1 -0
- kikusan-0.1.0/CLAUDE.md +3 -0
- kikusan-0.1.0/Dockerfile +29 -0
- kikusan-0.1.0/PKG-INFO +90 -0
- kikusan-0.1.0/README.md +76 -0
- kikusan-0.1.0/docker-compose.yml +12 -0
- kikusan-0.1.0/kikusan/__init__.py +3 -0
- kikusan-0.1.0/kikusan/cli.py +249 -0
- kikusan-0.1.0/kikusan/config.py +42 -0
- kikusan-0.1.0/kikusan/download.py +271 -0
- kikusan-0.1.0/kikusan/lyrics.py +75 -0
- kikusan-0.1.0/kikusan/search.py +87 -0
- kikusan-0.1.0/kikusan/spotify.py +195 -0
- kikusan-0.1.0/kikusan/web/__init__.py +1 -0
- kikusan-0.1.0/kikusan/web/app.py +110 -0
- kikusan-0.1.0/kikusan/web/static/style.css +168 -0
- kikusan-0.1.0/kikusan/web/templates/index.html +143 -0
- kikusan-0.1.0/pyproject.toml +22 -0
- kikusan-0.1.0/uv.lock +1078 -0
|
@@ -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
|
kikusan-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
kikusan-0.1.0/CLAUDE.md
ADDED
|
@@ -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/).
|
kikusan-0.1.0/Dockerfile
ADDED
|
@@ -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)
|
kikusan-0.1.0/README.md
ADDED
|
@@ -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,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()
|