mooze 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.
mooze-1.0.0/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 X0fx
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
+ <<<<<<< HEAD
22
+ SOFTWARE.
23
+ =======
24
+ SOFTWARE.
25
+ >>>>>>> 0978b1d7cfdb5680d7cb2ca9e75957e063ab97c0
mooze-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: mooze
3
+ Version: 1.0.0
4
+ Summary: A modern, TUI-based Spotify and YouTube audio downloader.
5
+ Author-email: Reyhan Janus <reyhanjanus@gmail.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Environment :: Console
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: textual>=0.38.1
14
+ Requires-Dist: yt-dlp>=2023.10.13
15
+ Requires-Dist: pillow>=10.0.0
16
+ Dynamic: license-file
17
+
18
+ # 🎵 Mooze
19
+
20
+
21
+ <!-- PROJECT BADGES -->
22
+
23
+ <div align="left">
24
+ <img src="https://img.shields.io/badge/Python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python Version" />
25
+ <img src="https://img.shields.io/badge/UI-Textual%20TUI-22C55E?style=flat-square&logo=gnuterminal&logoColor=white" alt="Textual TUI" />
26
+ <img src="https://img.shields.io/badge/Engine-yt--dlp-FF0000?style=flat-square&logo=youtube&logoColor=white" alt="yt-dlp Engine" />
27
+ <img src="https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-4B5563?style=flat-square" alt="Platform Support" />
28
+ <img src="https://img.shields.io/badge/License-MIT-F59E0B?style=flat-square&logo=opensourceinitiative&logoColor=white" alt="MIT License" />
29
+ </div>
30
+
31
+ ---
32
+
33
+ ![1784285736694](image/README/1784285736694.png)
34
+
35
+ ---
36
+
37
+ ## ✨ Features
38
+
39
+ * **Dual Download Modes:**
40
+ * **Single Mode:** Paste a Spotify track link or type a raw search query (e.g., "Coldplay Yellow").
41
+ * **Batch Mode:** Paste a list of multiple Spotify links (one per line) to download them all at once. Batch downloads are automatically zipped into a neat archive!
42
+ * **Smart Spotify Translation:** Bypasses web-scrapers using oEmbed and Googlebot masking to accurately extract Track + Artist data.
43
+ * **Audiophile Quality:** Choose your preferred output:
44
+ * MP3 - High Quality (320kbps)
45
+ * MP3 - Normal (128kbps)
46
+ * WAV - Best Quality (Lossless)
47
+ * **Modern Terminal UI:** Built with Textual, featuring mouse support, smooth toggle switches, and background asynchronous workers to keep the UI perfectly responsive while downloading.
48
+
49
+ ---
50
+
51
+ ## 🛠️ Prerequisites
52
+
53
+ Mooze uses `yt-dlp` under the hood, which requires **FFmpeg** to convert video files into clean MP3/WAV audio.
54
+
55
+ **Windows:**
56
+
57
+ ```PowerShell
58
+ winget install ffmpeg
59
+ ```
60
+
61
+ **MacOS:**
62
+
63
+ ```PowerShell
64
+ brew install ffmpeg
65
+ ```
66
+
67
+ **Linux:**
68
+
69
+ ```PowerShell
70
+ sudo apt install ffmpeg
71
+ ```
72
+
73
+ *(Note: Restart your terminal after installing FFmpeg so your system recognizes it.)*
74
+
75
+ ---
76
+
77
+ ## 🚀 Installation
78
+
79
+ Because Mooze is packaged professionally, you can install it globally on your system.
80
+
81
+ 1. Clone the repository:
82
+
83
+ ```Shell
84
+ git clone [https://github.com/X0fx/mooze.git](https://github.com/X0fx/mooze.git)
85
+ cd mooze
86
+ ```
87
+
88
+ 2. Install the application:
89
+
90
+ ```Shell
91
+ pip install -e .
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 📖 User Manual
97
+
98
+ Once installed, simply type `mooze` in any terminal to launch the dashboard.
99
+
100
+ ### 📥 Single Song Download
101
+
102
+ 1. Ensure the **Batch Mode** switch is toggled **OFF** .
103
+ 2. In the input box, paste a **Spotify Track Link** (e.g., `https://open.spotify.com/track/...`) OR type a search term (e.g., `Mozart Requiem`).
104
+ 3. Select your desired audio format from the dropdown menu.
105
+ 4. Type your save folder path (e.g., `C:/Users/YourName/Downloads` or just `./` for the current folder).
106
+ 5. Click **Search & Download** .
107
+
108
+ ### 📦 Batch Downloading
109
+
110
+ 1. Toggle the **Batch Mode** switch to **ON** . The UI will dynamically expand.
111
+ 2. Paste multiple Spotify links into the text area. **You must put exactly one link per line.**
112
+ 3. Select your audio format and your save location.
113
+ 4. Click **Download Batch** .
114
+
115
+ *Note: Mooze will create a temporary folder, process all your songs, compress them into a `Mooze_Batch_Archive.zip` file in your save directory, and clean up the temporary files automatically.*
116
+
117
+ ---
118
+
119
+ ## ⚠️ Disclaimer
120
+
121
+ This tool is for educational purposes and personal use only. It relies on publicly available APIs and YouTube search mechanisms. Please respect digital rights and support the artists you listen to.
mooze-1.0.0/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # 🎵 Mooze
2
+
3
+
4
+ <!-- PROJECT BADGES -->
5
+
6
+ <div align="left">
7
+ <img src="https://img.shields.io/badge/Python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python Version" />
8
+ <img src="https://img.shields.io/badge/UI-Textual%20TUI-22C55E?style=flat-square&logo=gnuterminal&logoColor=white" alt="Textual TUI" />
9
+ <img src="https://img.shields.io/badge/Engine-yt--dlp-FF0000?style=flat-square&logo=youtube&logoColor=white" alt="yt-dlp Engine" />
10
+ <img src="https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-4B5563?style=flat-square" alt="Platform Support" />
11
+ <img src="https://img.shields.io/badge/License-MIT-F59E0B?style=flat-square&logo=opensourceinitiative&logoColor=white" alt="MIT License" />
12
+ </div>
13
+
14
+ ---
15
+
16
+ ![1784285736694](image/README/1784285736694.png)
17
+
18
+ ---
19
+
20
+ ## ✨ Features
21
+
22
+ * **Dual Download Modes:**
23
+ * **Single Mode:** Paste a Spotify track link or type a raw search query (e.g., "Coldplay Yellow").
24
+ * **Batch Mode:** Paste a list of multiple Spotify links (one per line) to download them all at once. Batch downloads are automatically zipped into a neat archive!
25
+ * **Smart Spotify Translation:** Bypasses web-scrapers using oEmbed and Googlebot masking to accurately extract Track + Artist data.
26
+ * **Audiophile Quality:** Choose your preferred output:
27
+ * MP3 - High Quality (320kbps)
28
+ * MP3 - Normal (128kbps)
29
+ * WAV - Best Quality (Lossless)
30
+ * **Modern Terminal UI:** Built with Textual, featuring mouse support, smooth toggle switches, and background asynchronous workers to keep the UI perfectly responsive while downloading.
31
+
32
+ ---
33
+
34
+ ## 🛠️ Prerequisites
35
+
36
+ Mooze uses `yt-dlp` under the hood, which requires **FFmpeg** to convert video files into clean MP3/WAV audio.
37
+
38
+ **Windows:**
39
+
40
+ ```PowerShell
41
+ winget install ffmpeg
42
+ ```
43
+
44
+ **MacOS:**
45
+
46
+ ```PowerShell
47
+ brew install ffmpeg
48
+ ```
49
+
50
+ **Linux:**
51
+
52
+ ```PowerShell
53
+ sudo apt install ffmpeg
54
+ ```
55
+
56
+ *(Note: Restart your terminal after installing FFmpeg so your system recognizes it.)*
57
+
58
+ ---
59
+
60
+ ## 🚀 Installation
61
+
62
+ Because Mooze is packaged professionally, you can install it globally on your system.
63
+
64
+ 1. Clone the repository:
65
+
66
+ ```Shell
67
+ git clone [https://github.com/X0fx/mooze.git](https://github.com/X0fx/mooze.git)
68
+ cd mooze
69
+ ```
70
+
71
+ 2. Install the application:
72
+
73
+ ```Shell
74
+ pip install -e .
75
+ ```
76
+
77
+ ---
78
+
79
+ ## 📖 User Manual
80
+
81
+ Once installed, simply type `mooze` in any terminal to launch the dashboard.
82
+
83
+ ### 📥 Single Song Download
84
+
85
+ 1. Ensure the **Batch Mode** switch is toggled **OFF** .
86
+ 2. In the input box, paste a **Spotify Track Link** (e.g., `https://open.spotify.com/track/...`) OR type a search term (e.g., `Mozart Requiem`).
87
+ 3. Select your desired audio format from the dropdown menu.
88
+ 4. Type your save folder path (e.g., `C:/Users/YourName/Downloads` or just `./` for the current folder).
89
+ 5. Click **Search & Download** .
90
+
91
+ ### 📦 Batch Downloading
92
+
93
+ 1. Toggle the **Batch Mode** switch to **ON** . The UI will dynamically expand.
94
+ 2. Paste multiple Spotify links into the text area. **You must put exactly one link per line.**
95
+ 3. Select your audio format and your save location.
96
+ 4. Click **Download Batch** .
97
+
98
+ *Note: Mooze will create a temporary folder, process all your songs, compress them into a `Mooze_Batch_Archive.zip` file in your save directory, and clean up the temporary files automatically.*
99
+
100
+ ---
101
+
102
+ ## ⚠️ Disclaimer
103
+
104
+ This tool is for educational purposes and personal use only. It relies on publicly available APIs and YouTube search mechanisms. Please respect digital rights and support the artists you listen to.
File without changes
@@ -0,0 +1,105 @@
1
+ import yt_dlp
2
+ import urllib.request
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ def get_spotify_query(url: str) -> str:
8
+ """Uses a Googlebot disguise to extract both the song title AND the artist."""
9
+ clean_url = url.split('?')[0]
10
+
11
+ try:
12
+ headers = {
13
+ 'User-Agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
14
+ }
15
+ req = urllib.request.Request(clean_url, headers=headers)
16
+ html = urllib.request.urlopen(req).read().decode('utf-8')
17
+
18
+ title_match = re.search(r'<title>(.*?)</title>', html, re.IGNORECASE)
19
+ if title_match:
20
+ raw_title = title_match.group(1)
21
+ clean_title = raw_title.replace('| Spotify', '').replace('- song and lyrics by', ' ').replace('- song by', ' ').replace('- single by', ' ')
22
+ if clean_title.strip():
23
+ return clean_title.strip()
24
+ except Exception:
25
+ pass
26
+
27
+ try:
28
+ oembed_url = f"https://open.spotify.com/oembed?url={clean_url}"
29
+ req = urllib.request.Request(oembed_url, headers={'User-Agent': 'Mozilla/5.0'})
30
+ response = urllib.request.urlopen(req).read().decode('utf-8')
31
+ data = json.loads(response)
32
+
33
+ if "title" in data and "author_name" in data:
34
+ return f"{data['title']} {data['author_name']}"
35
+ elif "title" in data:
36
+ return data["title"]
37
+ except Exception:
38
+ pass
39
+
40
+ raise ValueError("Could not translate Spotify link. Try typing the song name instead!")
41
+
42
+ def download_song(search_query: str, save_location: str, format_choice: str, progress_callback=None):
43
+
44
+ if "spotify.com" in search_query:
45
+ search_query = get_spotify_query(search_query)
46
+
47
+ codec = "mp3"
48
+ quality = "192"
49
+ embed_art = True
50
+
51
+ # 1. Parse the custom syntax (e.g. ".flac, 192")
52
+ try:
53
+ parts = format_choice.split(",")
54
+ if len(parts) == 2:
55
+ codec = parts[0].strip()[1:].lower() # Removes the '.' and forces lowercase
56
+ # Extract just the digits in case they accidentally typed "192kbps"
57
+ quality_str = ''.join(filter(str.isdigit, parts[1]))
58
+ if quality_str:
59
+ quality = quality_str
60
+ except Exception:
61
+ pass # Fallback to mp3 defaults if parsing catastrophically fails
62
+
63
+ # 2. Prevent FFmpeg crashes by disabling image art for specific containers
64
+ if codec in ["wav", "flac", "opus", "ogg"]:
65
+ embed_art = False
66
+
67
+ def my_hook(d):
68
+ if d['status'] == 'downloading':
69
+ downloaded = d.get('downloaded_bytes', 0)
70
+ total = d.get('total_bytes') or d.get('total_bytes_estimate', 0)
71
+ if progress_callback and total > 0:
72
+ progress_callback(downloaded, total)
73
+
74
+ postprocessors: list[dict[str, Any]] = [{
75
+ 'key': 'FFmpegExtractAudio',
76
+ 'preferredcodec': codec,
77
+ 'preferredquality': quality,
78
+ }]
79
+
80
+ if embed_art:
81
+ postprocessors.append({
82
+ 'key': 'FFmpegThumbnailsConvertor',
83
+ 'format': 'jpg',
84
+ })
85
+ postprocessors.append({
86
+ 'key': 'EmbedThumbnail',
87
+ })
88
+
89
+ postprocessors.append({
90
+ 'key': 'FFmpegMetadata',
91
+ 'add_metadata': True,
92
+ })
93
+
94
+ options: Any = {
95
+ 'format': 'bestaudio/best',
96
+ 'outtmpl': f'{save_location}/%(title)s.%(ext)s',
97
+ 'default_search': 'ytsearch1:',
98
+ 'noplaylist': True,
99
+ 'writethumbnail': embed_art,
100
+ 'progress_hooks': [my_hook],
101
+ 'postprocessors': postprocessors,
102
+ }
103
+
104
+ with yt_dlp.YoutubeDL(options) as ydl:
105
+ ydl.download([search_query])
@@ -0,0 +1,316 @@
1
+ from textual.app import App, ComposeResult
2
+ from textual.widgets import Header, Footer, Input, Button, TextArea, Label, ProgressBar, TabbedContent, TabPane, Collapsible
3
+ from textual.containers import Horizontal, Vertical
4
+ from textual import on, work, events
5
+ from textual.binding import Binding
6
+ from .downloader import download_song
7
+ import os
8
+ import shutil
9
+ import json
10
+ import sys
11
+ import subprocess
12
+
13
+ CONFIG_FILE = os.path.expanduser("~/.mooze_settings.json")
14
+
15
+ class StaticHeader(Header):
16
+ def on_click(self, event: events.Click):
17
+ event.stop()
18
+ event.prevent_default()
19
+
20
+ def load_settings():
21
+ if os.path.exists(CONFIG_FILE):
22
+ try:
23
+ with open(CONFIG_FILE, "r") as f:
24
+ return json.load(f)
25
+ except Exception:
26
+ pass
27
+ return {}
28
+
29
+ def save_settings(format_choice, save_location, theme_choice):
30
+ try:
31
+ with open(CONFIG_FILE, "w") as f:
32
+ json.dump({
33
+ "format": format_choice,
34
+ "save_location": save_location,
35
+ "theme": theme_choice
36
+ }, f)
37
+ except Exception:
38
+ pass
39
+
40
+ class MoozeApp(App):
41
+ TITLE = "Mooze"
42
+ COMMAND_PALETTE_BINDING = ""
43
+
44
+ CSS = """
45
+ #main-area { height: 1fr; margin: 1; }
46
+
47
+ /* FIXED: Changed from 1fr to auto so it hugs the search inputs tightly */
48
+ #tabs { height: auto; }
49
+ #single-tab, #batch-tab { padding: 1 2; height: auto; }
50
+
51
+ #single-search-input { margin-bottom: 1; }
52
+
53
+ #batch-label { margin-bottom: 1; color: $text-muted; }
54
+ #batch-search-input { height: auto; min-height: 3; max-height: 12; }
55
+
56
+ #download-settings { margin-top: 1; height: auto; }
57
+ #options-row { height: auto; align: left middle; padding-top: 1; }
58
+ #options-row Input { width: 1fr; margin: 0 1; }
59
+
60
+ .download-btn { width: 100%; margin-top: 1; }
61
+ #my-progress-bar { height: auto; margin: 1 2; display: none; }
62
+
63
+ #options-row Input.error, #single-search-input.error, #batch-search-input.error { border: tall red; }
64
+ """
65
+
66
+ BINDINGS = [
67
+ Binding("d", "quit", "Quit Mooze"),
68
+ Binding("s", "save_png", "Screenshot (.png)"),
69
+ Binding("k", "toggle_footer", "Toggle Keys Help"),
70
+ Binding("escape", "remove_focus", "Deselect", show=False),
71
+ Binding("alt+p", "command_palette", "Palette", show=False),
72
+ Binding("ctrl+p", "command_palette", "Palette", show=False),
73
+ Binding("backslash", "command_palette", "Palette", show=False),
74
+ ]
75
+
76
+ def compose(self) -> ComposeResult:
77
+ yield StaticHeader(show_clock=False)
78
+
79
+ with Vertical(id="main-area"):
80
+ with TabbedContent(id="tabs"):
81
+ with TabPane("Single Download", id="single-tab"):
82
+ yield Input(placeholder="Paste a Spotify link or search for a song here...", id="single-search-input")
83
+
84
+ with TabPane("Batch Download", id="batch-tab"):
85
+ yield Label("Paste your list of Spotify links (One per line):", id="batch-label")
86
+ yield TextArea(id="batch-search-input")
87
+
88
+ with Collapsible(title="Download Settings (Format & Save Path)", id="download-settings"):
89
+ with Horizontal(id="options-row"):
90
+ yield Input(placeholder="Format (e.g. .flac, 192)", id="format-input")
91
+ yield Input(placeholder="Save Path (e.g. C:/Music)", id="save-location")
92
+
93
+ yield Button("Start Download", variant="success", id="download-btn", classes="download-btn")
94
+ yield ProgressBar(id="my-progress-bar")
95
+
96
+ yield Footer()
97
+
98
+ def on_mount(self):
99
+ settings = load_settings()
100
+
101
+ if "theme" in settings and settings["theme"]:
102
+ self.theme = str(settings["theme"])
103
+ else:
104
+ self.theme = "textual-dark"
105
+
106
+ if "format" in settings and settings["format"]:
107
+ self.query_one("#format-input", Input).value = settings["format"]
108
+
109
+ if "save_location" in settings and settings["save_location"]:
110
+ self.query_one("#save-location", Input).value = settings["save_location"]
111
+
112
+ @on(events.MouseDown)
113
+ def handle_background_click(self, event: events.MouseDown):
114
+ widget, _ = self.screen.get_widget_at(event.screen_x, event.screen_y)
115
+ if widget and widget.id in ("main-area", "options-row", "single-tab", "batch-tab", "download-settings"):
116
+ self.set_focus(None)
117
+
118
+ def action_remove_focus(self):
119
+ self.set_focus(None)
120
+
121
+ def action_toggle_footer(self):
122
+ footer = self.query_one(Footer)
123
+ footer.display = not footer.display
124
+
125
+ # =========================================================================
126
+ # COMMAND PALETTE
127
+ # =========================================================================
128
+ def _apply_and_save_theme(self, theme_name: str):
129
+ self.theme = theme_name
130
+ fmt = self.query_one("#format-input", Input).value
131
+ loc = self.query_one("#save-location", Input).value
132
+ save_settings(fmt, loc, theme_name)
133
+ self.notify(f"Theme updated to {theme_name}", severity="information")
134
+
135
+ def action_theme_dark(self): self._apply_and_save_theme("textual-dark")
136
+ def action_theme_light(self): self._apply_and_save_theme("textual-light")
137
+ def action_theme_solarized_dark(self): self._apply_and_save_theme("solarized-dark")
138
+ def action_theme_solarized_light(self): self._apply_and_save_theme("solarized-light")
139
+ def action_theme_dracula(self): self._apply_and_save_theme("dracula")
140
+ def action_theme_nord(self): self._apply_and_save_theme("nord")
141
+ def action_theme_monokai(self): self._apply_and_save_theme("monokai")
142
+
143
+ def action_clear_inputs(self):
144
+ self.query_one("#single-search-input", Input).value = ""
145
+ self.query_one("#batch-search-input", TextArea).text = ""
146
+ self.notify("All inputs cleared.", severity="information")
147
+
148
+ def action_open_download_folder(self):
149
+ path = self.query_one("#save-location", Input).value
150
+ if not path or not os.path.exists(path):
151
+ self.notify("The save folder does not exist yet!", severity="error")
152
+ return
153
+ try:
154
+ if sys.platform == "win32": os.startfile(path)
155
+ elif sys.platform == "darwin": subprocess.call(["open", path])
156
+ else: subprocess.call(["xdg-open", path])
157
+ except Exception as e:
158
+ self.notify(f"Could not open folder: {e}", severity="error")
159
+
160
+ # =========================================================================
161
+ # DOWNLOAD LOGIC & STRICT VALIDATION
162
+ # =========================================================================
163
+
164
+ @on(Button.Pressed)
165
+ def start_downloading(self, event: Button.Pressed):
166
+ if event.button.id != "download-btn":
167
+ return
168
+
169
+ format_input = self.query_one("#format-input", Input)
170
+ save_location = self.query_one("#save-location", Input)
171
+ single_input = self.query_one("#single-search-input", Input)
172
+ batch_input = self.query_one("#batch-search-input", TextArea)
173
+
174
+ format_input.remove_class("error")
175
+ save_location.remove_class("error")
176
+ single_input.remove_class("error")
177
+ batch_input.remove_class("error")
178
+
179
+ has_error = False
180
+
181
+ raw_format = format_input.value.strip()
182
+ is_valid_format = False
183
+
184
+ if raw_format.startswith(".") and "," in raw_format:
185
+ parts = raw_format.split(",")
186
+ ext = parts[0].strip()[1:]
187
+ try:
188
+ bitrate_str = ''.join(filter(str.isdigit, parts[1]))
189
+ bitrate = int(bitrate_str) if bitrate_str else 0
190
+
191
+ if 92 <= bitrate <= 320 and ext.isalnum():
192
+ is_valid_format = True
193
+ format_input.value = f".{ext.lower()}, {bitrate}"
194
+ except ValueError:
195
+ pass
196
+
197
+ if not is_valid_format:
198
+ format_input.add_class("error")
199
+ has_error = True
200
+ self.notify("Format must be '.ext, bitrate' (e.g. .mp3, 320). Bitrate limit: 92-320.", severity="error", timeout=6)
201
+ self.query_one("#download-settings", Collapsible).collapsed = False
202
+
203
+ if save_location.value.strip() == "":
204
+ save_location.add_class("error")
205
+ has_error = True
206
+ self.query_one("#download-settings", Collapsible).collapsed = False
207
+
208
+ active_tab = self.query_one("#tabs", TabbedContent).active
209
+ is_batch_mode = (active_tab == "batch-tab")
210
+
211
+ if is_batch_mode:
212
+ raw_text = batch_input.text
213
+ if not raw_text.strip():
214
+ batch_input.add_class("error")
215
+ has_error = True
216
+ songs_to_download = raw_text.strip().split("\n")
217
+ else:
218
+ raw_text = single_input.value
219
+ if not raw_text.strip():
220
+ single_input.add_class("error")
221
+ has_error = True
222
+ songs_to_download = [raw_text.strip()]
223
+
224
+ if has_error:
225
+ if is_valid_format:
226
+ self.notify("Oops! Please fill out all required fields.", severity="error")
227
+ return
228
+
229
+ save_settings(format_input.value, save_location.value, self.theme)
230
+
231
+ progress_bar = self.query_one("#my-progress-bar", ProgressBar)
232
+ progress_bar.display = True
233
+ progress_bar.update(total=100, progress=0)
234
+
235
+ self.notify("Starting your download process! Please wait...")
236
+ self.run_engine(songs_to_download, save_location.value, format_input.value, is_batch_mode)
237
+
238
+ def update_progress_ui(self, downloaded, total):
239
+ progress_bar = self.query_one("#my-progress-bar", ProgressBar)
240
+ progress_bar.update(total=total, progress=downloaded)
241
+
242
+ @work(thread=True)
243
+ def run_engine(self, songs, save_path, audio_format, is_batch):
244
+ try:
245
+ def progress_callback(downloaded, total):
246
+ self.app.call_from_thread(self.update_progress_ui, downloaded, total)
247
+
248
+ if is_batch and len(songs) > 1:
249
+ working_path = os.path.join(save_path, "Mooze_Temp_Batch")
250
+ os.makedirs(working_path, exist_ok=True)
251
+ else:
252
+ working_path = save_path
253
+
254
+ for song in songs:
255
+ if song.strip():
256
+ self.app.call_from_thread(self.update_progress_ui, 0, 100)
257
+ download_song(song.strip(), working_path, audio_format, progress_callback)
258
+
259
+ if is_batch and len(songs) > 1:
260
+ zip_filename = os.path.join(save_path, "Mooze_Batch_Archive")
261
+ shutil.make_archive(zip_filename, 'zip', working_path)
262
+ shutil.rmtree(working_path)
263
+
264
+ self.app.call_from_thread(self.finish_download, True, "All downloads completed successfully!")
265
+ except Exception as e:
266
+ self.app.call_from_thread(self.finish_download, False, str(e))
267
+
268
+ def finish_download(self, success: bool, message: str):
269
+ progress_bar = self.query_one("#my-progress-bar", ProgressBar)
270
+ progress_bar.display = False
271
+ if success:
272
+ self.notify(message, title="Success!")
273
+ else:
274
+ self.notify(f"Engine Error: {message}", title="Oops!", severity="error")
275
+
276
+ # =========================================================================
277
+ # DPI-AWARE SCREENSHOT TOOL
278
+ # =========================================================================
279
+ def action_save_png(self):
280
+ try:
281
+ from PIL import ImageGrab
282
+ import sys
283
+
284
+ self.notify("Capturing screenshot...", title="Processing")
285
+ filename = "mooze_screenshot.png"
286
+
287
+ if sys.platform == "win32":
288
+ import ctypes
289
+ from ctypes import wintypes
290
+
291
+ user32 = ctypes.windll.user32
292
+ user32.SetProcessDPIAware()
293
+
294
+ hwnd = user32.GetForegroundWindow()
295
+ rect = wintypes.RECT()
296
+ user32.GetWindowRect(hwnd, ctypes.byref(rect))
297
+
298
+ bbox = (rect.left, rect.top, rect.right, rect.bottom)
299
+ screenshot = ImageGrab.grab(bbox=bbox)
300
+ else:
301
+ screenshot = ImageGrab.grab()
302
+
303
+ screenshot.save(filename)
304
+ self.notify(f"Saved: {filename}", title="Screenshot", severity="information")
305
+
306
+ except ImportError:
307
+ self.notify("Missing dependency! Run: pip install pillow", severity="error", timeout=10)
308
+ except Exception as e:
309
+ self.notify(f"Screenshot failed: {str(e)}", title="Error", severity="error", timeout=15)
310
+
311
+ def start():
312
+ app = MoozeApp()
313
+ app.run()
314
+
315
+ if __name__ == "__main__":
316
+ start()
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: mooze
3
+ Version: 1.0.0
4
+ Summary: A modern, TUI-based Spotify and YouTube audio downloader.
5
+ Author-email: Reyhan Janus <reyhanjanus@gmail.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Environment :: Console
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: textual>=0.38.1
14
+ Requires-Dist: yt-dlp>=2023.10.13
15
+ Requires-Dist: pillow>=10.0.0
16
+ Dynamic: license-file
17
+
18
+ # 🎵 Mooze
19
+
20
+
21
+ <!-- PROJECT BADGES -->
22
+
23
+ <div align="left">
24
+ <img src="https://img.shields.io/badge/Python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python Version" />
25
+ <img src="https://img.shields.io/badge/UI-Textual%20TUI-22C55E?style=flat-square&logo=gnuterminal&logoColor=white" alt="Textual TUI" />
26
+ <img src="https://img.shields.io/badge/Engine-yt--dlp-FF0000?style=flat-square&logo=youtube&logoColor=white" alt="yt-dlp Engine" />
27
+ <img src="https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-4B5563?style=flat-square" alt="Platform Support" />
28
+ <img src="https://img.shields.io/badge/License-MIT-F59E0B?style=flat-square&logo=opensourceinitiative&logoColor=white" alt="MIT License" />
29
+ </div>
30
+
31
+ ---
32
+
33
+ ![1784285736694](image/README/1784285736694.png)
34
+
35
+ ---
36
+
37
+ ## ✨ Features
38
+
39
+ * **Dual Download Modes:**
40
+ * **Single Mode:** Paste a Spotify track link or type a raw search query (e.g., "Coldplay Yellow").
41
+ * **Batch Mode:** Paste a list of multiple Spotify links (one per line) to download them all at once. Batch downloads are automatically zipped into a neat archive!
42
+ * **Smart Spotify Translation:** Bypasses web-scrapers using oEmbed and Googlebot masking to accurately extract Track + Artist data.
43
+ * **Audiophile Quality:** Choose your preferred output:
44
+ * MP3 - High Quality (320kbps)
45
+ * MP3 - Normal (128kbps)
46
+ * WAV - Best Quality (Lossless)
47
+ * **Modern Terminal UI:** Built with Textual, featuring mouse support, smooth toggle switches, and background asynchronous workers to keep the UI perfectly responsive while downloading.
48
+
49
+ ---
50
+
51
+ ## 🛠️ Prerequisites
52
+
53
+ Mooze uses `yt-dlp` under the hood, which requires **FFmpeg** to convert video files into clean MP3/WAV audio.
54
+
55
+ **Windows:**
56
+
57
+ ```PowerShell
58
+ winget install ffmpeg
59
+ ```
60
+
61
+ **MacOS:**
62
+
63
+ ```PowerShell
64
+ brew install ffmpeg
65
+ ```
66
+
67
+ **Linux:**
68
+
69
+ ```PowerShell
70
+ sudo apt install ffmpeg
71
+ ```
72
+
73
+ *(Note: Restart your terminal after installing FFmpeg so your system recognizes it.)*
74
+
75
+ ---
76
+
77
+ ## 🚀 Installation
78
+
79
+ Because Mooze is packaged professionally, you can install it globally on your system.
80
+
81
+ 1. Clone the repository:
82
+
83
+ ```Shell
84
+ git clone [https://github.com/X0fx/mooze.git](https://github.com/X0fx/mooze.git)
85
+ cd mooze
86
+ ```
87
+
88
+ 2. Install the application:
89
+
90
+ ```Shell
91
+ pip install -e .
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 📖 User Manual
97
+
98
+ Once installed, simply type `mooze` in any terminal to launch the dashboard.
99
+
100
+ ### 📥 Single Song Download
101
+
102
+ 1. Ensure the **Batch Mode** switch is toggled **OFF** .
103
+ 2. In the input box, paste a **Spotify Track Link** (e.g., `https://open.spotify.com/track/...`) OR type a search term (e.g., `Mozart Requiem`).
104
+ 3. Select your desired audio format from the dropdown menu.
105
+ 4. Type your save folder path (e.g., `C:/Users/YourName/Downloads` or just `./` for the current folder).
106
+ 5. Click **Search & Download** .
107
+
108
+ ### 📦 Batch Downloading
109
+
110
+ 1. Toggle the **Batch Mode** switch to **ON** . The UI will dynamically expand.
111
+ 2. Paste multiple Spotify links into the text area. **You must put exactly one link per line.**
112
+ 3. Select your audio format and your save location.
113
+ 4. Click **Download Batch** .
114
+
115
+ *Note: Mooze will create a temporary folder, process all your songs, compress them into a `Mooze_Batch_Archive.zip` file in your save directory, and clean up the temporary files automatically.*
116
+
117
+ ---
118
+
119
+ ## ⚠️ Disclaimer
120
+
121
+ This tool is for educational purposes and personal use only. It relies on publicly available APIs and YouTube search mechanisms. Please respect digital rights and support the artists you listen to.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ mooze/__init__.py
5
+ mooze/downloader.py
6
+ mooze/main.py
7
+ mooze.egg-info/PKG-INFO
8
+ mooze.egg-info/SOURCES.txt
9
+ mooze.egg-info/dependency_links.txt
10
+ mooze.egg-info/entry_points.txt
11
+ mooze.egg-info/requires.txt
12
+ mooze.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mooze = mooze.main:start
@@ -0,0 +1,3 @@
1
+ textual>=0.38.1
2
+ yt-dlp>=2023.10.13
3
+ pillow>=10.0.0
@@ -0,0 +1 @@
1
+ mooze
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mooze"
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="Reyhan Janus", email="reyhanjanus@gmail.com" },
10
+ ]
11
+ description = "A modern, TUI-based Spotify and YouTube audio downloader."
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ license = {text = "MIT"}
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Environment :: Console",
19
+ ]
20
+ dependencies = [
21
+ "textual>=0.38.1",
22
+ "yt-dlp>=2023.10.13",
23
+ "pillow>=10.0.0"
24
+ ]
25
+
26
+ [project.scripts]
27
+ mooze = "mooze.main:start"
28
+
29
+ [tool.setuptools]
30
+ packages = ["mooze"]
mooze-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+