druvx13 commited on
Commit
ae3df94
Β·
verified Β·
1 Parent(s): 7802eef

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +344 -0
app.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py β€” Neon Wave Music Player (Gradio / Hugging Face Spaces edition)
3
+
4
+ A full-featured music player built with Gradio that mirrors the PHP version.
5
+ Run locally:
6
+ python app.py
7
+ Deploy on HF Spaces:
8
+ Push this folder to a Gradio Space β€” no additional config required.
9
+
10
+ Compatible with Gradio 4.44+ and Gradio 6.x.
11
+ """
12
+
13
+ import os
14
+ import secrets
15
+ import shutil
16
+ from pathlib import Path
17
+
18
+ import gradio as gr
19
+
20
+ from database import fetch_songs, init_db, insert_song
21
+
22
+ # ── Paths ──────────────────────────────────────────────────────────────────────
23
+ BASE_DIR = Path(__file__).parent
24
+ UPLOAD_DIR = BASE_DIR / "uploads"
25
+ UPLOAD_DIR.mkdir(exist_ok=True)
26
+
27
+ # Initialise database on startup
28
+ init_db()
29
+
30
+ # ── Gradio theme (neon-wave dark) ──────────────────────────────────────────────
31
+ # In Gradio 6.x, theme and css are passed to demo.launch() instead of gr.Blocks().
32
+ _theme = gr.themes.Base(
33
+ primary_hue=gr.themes.colors.sky,
34
+ secondary_hue=gr.themes.colors.purple,
35
+ neutral_hue=gr.themes.colors.zinc,
36
+ font=gr.themes.GoogleFont("Space Mono"),
37
+ font_mono=gr.themes.GoogleFont("Space Mono"),
38
+ ).set(
39
+ body_background_fill="linear-gradient(135deg, hsl(220,13%,18%), hsl(220,13%,25%))",
40
+ body_text_color="#ffffff",
41
+ block_background_fill="rgba(255,255,255,0.08)",
42
+ block_border_color="rgba(255,255,255,0.10)",
43
+ block_border_width="1px",
44
+ block_radius="1rem",
45
+ block_shadow="0 8px 32px rgba(0,0,0,0.25), 0 0 20px rgba(64,160,212,0.12)",
46
+ block_title_text_color="rgba(255,255,255,0.90)",
47
+ block_label_text_color="rgba(255,255,255,0.70)",
48
+ input_background_fill="rgba(255,255,255,0.10)",
49
+ input_border_color="rgba(255,255,255,0.20)",
50
+ input_placeholder_color="rgba(255,255,255,0.30)",
51
+ input_shadow="none",
52
+ button_primary_background_fill="linear-gradient(90deg, hsl(201,63%,54%), hsl(261,77%,57%))",
53
+ button_primary_background_fill_hover="linear-gradient(90deg, hsl(201,63%,60%), hsl(261,77%,63%))",
54
+ button_primary_text_color="#ffffff",
55
+ button_primary_shadow="0 4px 15px rgba(64,160,212,0.35)",
56
+ button_secondary_background_fill="rgba(255,255,255,0.10)",
57
+ button_secondary_background_fill_hover="rgba(255,255,255,0.18)",
58
+ button_secondary_border_color="rgba(255,255,255,0.20)",
59
+ button_secondary_text_color="#ffffff",
60
+ checkbox_background_color="rgba(255,255,255,0.10)",
61
+ table_even_background_fill="rgba(255,255,255,0.04)",
62
+ table_odd_background_fill="rgba(255,255,255,0.02)",
63
+ table_row_focus="rgba(64,160,212,0.15)",
64
+ )
65
+
66
+ # ── Extra CSS (neon accents, cover art, etc.) ──────────────────────────────────
67
+ _CSS = """
68
+ /* Remove default white Gradio container background */
69
+ .gradio-container { background: transparent !important; }
70
+ footer { display: none !important; }
71
+
72
+ /* Header */
73
+ #app-header { text-align: center; padding: 1.5rem 0 0.5rem; }
74
+ #app-header h1 {
75
+ font-size: 2rem !important;
76
+ font-weight: 700 !important;
77
+ background: linear-gradient(135deg, hsl(201,63%,65%), hsl(261,77%,70%));
78
+ -webkit-background-clip: text;
79
+ -webkit-text-fill-color: transparent;
80
+ text-shadow: none !important;
81
+ }
82
+ #app-header p { color: rgba(255,255,255,0.55) !important; font-size: 0.85rem; }
83
+
84
+ /* Now-playing title */
85
+ #now-playing-title textarea,
86
+ #now-playing-title input {
87
+ font-size: 1.3rem !important;
88
+ font-weight: 700 !important;
89
+ color: #ffffff !important;
90
+ text-shadow: 0 0 10px rgba(64,160,212,0.6);
91
+ background: transparent !important;
92
+ border: none !important;
93
+ padding: 0 !important;
94
+ }
95
+ #now-playing-artist textarea,
96
+ #now-playing-artist input {
97
+ color: rgba(255,255,255,0.65) !important;
98
+ background: transparent !important;
99
+ border: none !important;
100
+ padding: 0 !important;
101
+ }
102
+
103
+ /* Cover image */
104
+ #cover-art img {
105
+ border-radius: 0.75rem !important;
106
+ box-shadow: 0 0 25px rgba(64,160,212,0.40) !important;
107
+ object-fit: cover !important;
108
+ }
109
+ #cover-art .image-container { background: rgba(44,62,80,0.9) !important; border-radius: 0.75rem; }
110
+
111
+ /* Playlist table */
112
+ #playlist-table table { font-size: 0.85rem !important; }
113
+ #playlist-table thead { background: rgba(64,160,212,0.18) !important; }
114
+ #playlist-table th { color: rgba(255,255,255,0.80) !important; }
115
+ #playlist-table td { color: rgba(255,255,255,0.90) !important; cursor: pointer; }
116
+ #playlist-table tbody tr:hover { background: rgba(255,255,255,0.09) !important; }
117
+
118
+ /* Lyrics */
119
+ #lyrics-display textarea {
120
+ line-height: 1.8 !important;
121
+ text-align: center !important;
122
+ color: rgba(255,255,255,0.80) !important;
123
+ background: rgba(255,255,255,0.04) !important;
124
+ font-size: 0.95rem !important;
125
+ }
126
+
127
+ /* Upload panel */
128
+ #upload-panel {
129
+ border: 1px dashed rgba(255,255,255,0.20) !important;
130
+ border-radius: 1rem !important;
131
+ padding: 0.5rem !important;
132
+ }
133
+
134
+ /* Status messages */
135
+ #upload-status textarea { font-size: 0.85rem !important; }
136
+
137
+ /* Footer */
138
+ .footer-credit {
139
+ text-align: center;
140
+ color: rgba(255,255,255,0.35);
141
+ font-size: 0.78rem;
142
+ padding: 1rem 0;
143
+ }
144
+ .footer-credit span { color: #f87171; }
145
+ """
146
+
147
+ # ── Handlers ───────────────────────────────────────────────────────────────────
148
+
149
+ def load_playlist() -> tuple[list[dict], list[list[str]]]:
150
+ """Fetch songs from DB; return (state, dataframe rows)."""
151
+ songs = fetch_songs()
152
+ table = [[s["title"], s["artist"]] for s in songs]
153
+ return songs, table
154
+
155
+
156
+ def on_song_select(
157
+ songs: list[dict], evt: gr.SelectData
158
+ ) -> tuple:
159
+ """Called when the user clicks a row in the playlist table."""
160
+ row = evt.index[0]
161
+ if not songs or row >= len(songs):
162
+ return None, "Select a song", "β€”", "No lyrics available.", None
163
+
164
+ song = songs[row]
165
+ audio = song["file"] if os.path.isfile(song["file"]) else None
166
+ cover = song["cover"] if song.get("cover") and os.path.isfile(song["cover"]) else None
167
+ lyrics = song.get("lyrics") or "No lyrics available for this song."
168
+
169
+ return audio, song["title"], song["artist"], lyrics, cover
170
+
171
+
172
+ def upload_song(
173
+ song_file,
174
+ cover_file,
175
+ title: str,
176
+ artist: str,
177
+ lyrics: str,
178
+ ) -> tuple:
179
+ """Handle song upload: validate, save files, insert into DB, refresh list."""
180
+ if song_file is None:
181
+ return "❌ Please select an MP3 file.", gr.update(), gr.update()
182
+
183
+ src_path = Path(song_file.name)
184
+ if src_path.suffix.lower() != ".mp3":
185
+ return "❌ Only MP3 files are allowed.", gr.update(), gr.update()
186
+
187
+ # ── Save audio ──────────────────────────────────────────────────────────
188
+ dest_name = secrets.token_hex(16) + ".mp3"
189
+ dest_path = UPLOAD_DIR / dest_name
190
+ shutil.copy2(src_path, dest_path)
191
+
192
+ # ── Save optional cover ─────────────────────────────────────────────────
193
+ cover_path: str | None = None
194
+ if cover_file is not None:
195
+ cover_src = Path(cover_file.name)
196
+ if cover_src.suffix.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp"}:
197
+ cover_name = secrets.token_hex(16) + cover_src.suffix.lower()
198
+ cover_dest = UPLOAD_DIR / cover_name
199
+ shutil.copy2(cover_src, cover_dest)
200
+ cover_path = str(cover_dest)
201
+
202
+ clean_title = title.strip() or src_path.stem
203
+ clean_artist = artist.strip() or "Unknown Artist"
204
+ clean_lyrics = lyrics.strip()
205
+
206
+ insert_song(clean_title, clean_artist, str(dest_path), cover_path, clean_lyrics)
207
+
208
+ songs = fetch_songs()
209
+ table = [[s["title"], s["artist"]] for s in songs]
210
+ return f"βœ… '{clean_title}' added to the playlist!", songs, table
211
+
212
+
213
+ # ── UI ─────────────────────────────────────────────────────────────────────────
214
+ with gr.Blocks(title="Neon Wave Music Player") as demo:
215
+
216
+ # ── Header ──────────────────────────────────────────────────────────────
217
+ gr.HTML("""
218
+ <div id="app-header">
219
+ <h1>🎡 Neon Wave Music Player</h1>
220
+ <p>Browse &middot; Upload &middot; Play</p>
221
+ </div>
222
+ """)
223
+
224
+ # Shared state: full list of song dicts (not exposed in the UI)
225
+ songs_state = gr.State([])
226
+
227
+ # ── Main row: Player (left) + Playlist (right) ───────────────────────
228
+ with gr.Row(equal_height=False):
229
+
230
+ # Left column β€” album art + player controls
231
+ with gr.Column(scale=2, min_width=280):
232
+ cover_art = gr.Image(
233
+ value=None,
234
+ label="Album Art",
235
+ height=240,
236
+ elem_id="cover-art",
237
+ interactive=False,
238
+ show_label=False,
239
+ )
240
+ song_title_disp = gr.Textbox(
241
+ value="Select a song",
242
+ label="Now Playing",
243
+ interactive=False,
244
+ elem_id="now-playing-title",
245
+ )
246
+ artist_disp = gr.Textbox(
247
+ value="β€”",
248
+ label="Artist",
249
+ interactive=False,
250
+ elem_id="now-playing-artist",
251
+ )
252
+ audio_player = gr.Audio(
253
+ value=None,
254
+ label="Player",
255
+ type="filepath",
256
+ interactive=False,
257
+ autoplay=True,
258
+ elem_id="audio-player",
259
+ )
260
+
261
+ # Right column β€” playlist + action buttons
262
+ with gr.Column(scale=3):
263
+ gr.Markdown("### 🎢 Playlist")
264
+ playlist_df = gr.Dataframe(
265
+ headers=["Title", "Artist"],
266
+ column_count=2,
267
+ interactive=False,
268
+ label="",
269
+ wrap=True,
270
+ elem_id="playlist-table",
271
+ )
272
+ with gr.Row():
273
+ refresh_btn = gr.Button("πŸ”„ Refresh", variant="secondary", scale=1)
274
+ upload_open_btn = gr.Button("⬆️ Upload Song", variant="primary", scale=2)
275
+
276
+ # ── Lyrics accordion ──────────────────────────────────────────────────
277
+ with gr.Accordion("πŸ“œ Lyrics", open=False):
278
+ lyrics_disp = gr.Textbox(
279
+ value="",
280
+ label="",
281
+ lines=7,
282
+ interactive=False,
283
+ elem_id="lyrics-display",
284
+ placeholder="Select a song to view lyrics…",
285
+ )
286
+
287
+ # ── Upload panel (hidden until button clicked) ────────────────────────
288
+ with gr.Group(visible=False, elem_id="upload-panel") as upload_panel:
289
+ gr.Markdown("### ⬆️ Upload New Song")
290
+ with gr.Row():
291
+ with gr.Column():
292
+ up_title = gr.Textbox(label="Song Title *", placeholder="Enter song title")
293
+ up_artist = gr.Textbox(label="Artist *", placeholder="Enter artist name")
294
+ up_lyrics = gr.Textbox(label="Lyrics (optional)", lines=4,
295
+ placeholder="Enter song lyrics…")
296
+ with gr.Column():
297
+ up_song = gr.File(
298
+ label="Song File (MP3) *",
299
+ file_types=[".mp3"],
300
+ file_count="single",
301
+ )
302
+ up_cover = gr.File(
303
+ label="Cover Art (optional)",
304
+ file_types=[".jpg", ".jpeg", ".png", ".gif", ".webp"],
305
+ file_count="single",
306
+ )
307
+ with gr.Row():
308
+ up_submit = gr.Button("Upload", variant="primary", scale=3)
309
+ up_cancel = gr.Button("Cancel", variant="secondary", scale=1)
310
+ up_status = gr.Textbox(
311
+ label="", interactive=False, show_label=False,
312
+ elem_id="upload-status", placeholder="",
313
+ )
314
+
315
+ # ── Footer ────────────────────────────────────────────────────────────
316
+ gr.HTML('<p class="footer-credit">Made with <span>❀️</span> by DK &nbsp;&middot;&nbsp; Neon Wave Music Player</p>')
317
+
318
+ # ── Event wiring ──────────────────────────────────────────────────────
319
+ demo.load(load_playlist, outputs=[songs_state, playlist_df])
320
+
321
+ playlist_df.select(
322
+ on_song_select,
323
+ inputs=[songs_state],
324
+ outputs=[audio_player, song_title_disp, artist_disp, lyrics_disp, cover_art],
325
+ )
326
+
327
+ refresh_btn.click(load_playlist, outputs=[songs_state, playlist_df])
328
+
329
+ upload_open_btn.click(lambda: gr.update(visible=True), outputs=[upload_panel])
330
+ up_cancel.click( lambda: gr.update(visible=False), outputs=[upload_panel])
331
+
332
+ up_submit.click(
333
+ upload_song,
334
+ inputs=[up_song, up_cover, up_title, up_artist, up_lyrics],
335
+ outputs=[up_status, songs_state, playlist_df],
336
+ )
337
+
338
+ # ── Launch ─────────────────────────────────────────────────────────────────────
339
+ if __name__ == "__main__":
340
+ demo.launch(
341
+ theme=_theme,
342
+ css=_CSS,
343
+ allowed_paths=[str(UPLOAD_DIR)],
344
+ )