diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5821642..b5a5a83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -233,22 +233,17 @@ jobs: - name: Build application run: wails build -platform linux/amd64 - - name: Download appimagetool + - name: Download linuxdeploy and appimagetool run: | - wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage - chmod +x appimagetool + wget -O linuxdeploy https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy - name: Create AppImage run: | - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - - # Copy binary - cp build/bin/SpotiFLAC AppDir/usr/bin/spotiflac + mkdir -p AppDir # Create desktop file - cat > AppDir/spotiflac.desktop << 'EOF' + cat > spotiflac.desktop << 'EOF' [Desktop Entry] Name=SpotiFLAC Exec=spotiflac @@ -258,38 +253,58 @@ jobs: Comment=Get Spotify tracks in true FLAC from Tidal/Deezer EOF - # Copy desktop file to usr/share/applications - cp AppDir/spotiflac.desktop AppDir/usr/share/applications/ - - # Use existing icon from build or convert SVG to PNG + # Create icon in multiple sizes for better compatibility if [ -f "build/appicon.png" ]; then - # Resize to 256x256 if needed - convert build/appicon.png -resize 256x256 AppDir/spotiflac.png + cp build/appicon.png spotiflac-256.png elif [ -f "frontend/public/icon.svg" ]; then - # Convert SVG to PNG - convert -background none -size 256x256 frontend/public/icon.svg AppDir/spotiflac.png + convert -background none -size 256x256 frontend/public/icon.svg spotiflac-256.png else - # Fallback: create simple icon - convert -size 256x256 radial-gradient:#FFD700-#FFA500 AppDir/spotiflac.png + echo "Warning: No icon found, building without icon" + touch spotiflac-256.png # Create empty file to prevent errors fi - cp AppDir/spotiflac.png AppDir/usr/share/icons/hicolor/256x256/apps/ - cp AppDir/spotiflac.png AppDir/.DirIcon + # Create additional icon sizes only if icon exists + if [ -s spotiflac-256.png ]; then + convert spotiflac-256.png -resize 128x128 spotiflac-128.png + convert spotiflac-256.png -resize 64x64 spotiflac-64.png + convert spotiflac-256.png -resize 48x48 spotiflac-48.png + convert spotiflac-256.png -resize 32x32 spotiflac-32.png + convert spotiflac-256.png -resize 16x16 spotiflac-16.png + fi - # Create AppRun - cat > AppDir/AppRun << 'EOF' - #!/bin/sh - SELF=$(readlink -f "$0") - HERE=${SELF%/*} - export PATH="${HERE}/usr/bin/:${PATH}" - export LD_LIBRARY_PATH="${HERE}/usr/lib/:${LD_LIBRARY_PATH}" - exec "${HERE}/usr/bin/spotiflac" "$@" - EOF - chmod +x AppDir/AppRun - - # Create AppImage + # Use linuxdeploy to create AppImage + # Bundle only uncommon libraries (webkit2gtk), exclude common ones (GTK, glib) mkdir -p dist - ARCH=x86_64 ./appimagetool --no-appstream AppDir dist/SpotiFLAC-${{ steps.version.outputs.version }}.AppImage + ./linuxdeploy \ + --appdir AppDir \ + --executable build/bin/SpotiFLAC \ + --desktop-file spotiflac.desktop \ + --icon-file spotiflac-256.png \ + --icon-file spotiflac-128.png \ + --icon-file spotiflac-64.png \ + --icon-file spotiflac-48.png \ + --icon-file spotiflac-32.png \ + --icon-file spotiflac-16.png \ + --exclude-library "libgtk-3.so*" \ + --exclude-library "libgdk-3.so*" \ + --exclude-library "libglib-2.0.so*" \ + --exclude-library "libgobject-2.0.so*" \ + --exclude-library "libgio-2.0.so*" \ + --exclude-library "libpango-1.0.so*" \ + --exclude-library "libcairo.so*" \ + --exclude-library "libX11.so*" \ + --exclude-library "libXext.so*" \ + --exclude-library "libXrender.so*" \ + --exclude-library "libfontconfig.so*" \ + --exclude-library "libfreetype.so*" \ + --exclude-library "libpthread.so*" \ + --exclude-library "libdl.so*" \ + --exclude-library "libm.so*" \ + --exclude-library "libc.so*" \ + --output appimage + + # Rename to match version + mv SpotiFLAC*.AppImage dist/SpotiFLAC-${{ steps.version.outputs.version }}.AppImage - name: Upload artifacts uses: actions/upload-artifact@v4 diff --git a/app.go b/app.go index b13aca9..7adeb9a 100644 --- a/app.go +++ b/app.go @@ -45,6 +45,7 @@ type DownloadRequest struct { AudioFormat string `json:"audio_format,omitempty"` FilenameFormat string `json:"filename_format,omitempty"` TrackNumber bool `json:"track_number,omitempty"` + Position int `json:"position,omitempty"` // Position in playlist/album (1-based) } // DownloadResponse represents the response structure for download operations @@ -125,20 +126,20 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) { if req.ApiURL == "" || req.ApiURL == "auto" { downloader := backend.NewTidalDownloader("") - filename, err = downloader.DownloadWithFallback(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) + filename, err = downloader.DownloadWithFallback(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName) } else { downloader := backend.NewTidalDownloader(req.ApiURL) - filename, err = downloader.Download(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) + filename, err = downloader.Download(searchQuery, req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName) } } else if req.Service == "qobuz" { downloader := backend.NewQobuzDownloader() - err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) + err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName) if err == nil { filename = "Downloaded via Qobuz" } } else { downloader := backend.NewDeezerDownloader() - err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.TrackName, req.ArtistName, req.AlbumName) + err = downloader.DownloadByISRC(req.ISRC, req.OutputDir, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName) if err == nil { filename = "Downloaded via Deezer" } @@ -188,3 +189,9 @@ func (a *App) GetDefaults() map[string]string { func (a *App) GetDownloadProgress() backend.ProgressInfo { return backend.GetDownloadProgress() } + +// Quit closes the application +func (a *App) Quit() { + // You can add cleanup logic here if needed + panic("quit") // This will trigger Wails to close the app +} diff --git a/backend/deezer.go b/backend/deezer.go index 2195c10..a861cbb 100644 --- a/backend/deezer.go +++ b/backend/deezer.go @@ -172,7 +172,7 @@ func sanitizeFilename(name string) string { return sanitized } -func buildFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool) string { +func buildFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int) string { var filename string // Build base filename based on format @@ -186,14 +186,15 @@ func buildFilename(title, artist string, trackNumber int, format string, include } // Add track number prefix if enabled - if includeTrackNumber && trackNumber > 0 { - filename = fmt.Sprintf("%02d. %s", trackNumber, filename) + // Only use track number for bulk downloads (when position > 0) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) } return filename + ".flac" } -func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) error { +func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) error { fmt.Printf("Fetching track info for ISRC: %s\n", isrc) track, err := d.GetTrackByISRC(isrc) @@ -241,7 +242,7 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string safeTitle := sanitizeFilename(trackTitle) // Build filename based on format settings - filename := buildFilename(safeTitle, safeArtist, track.TrackPos, filenameFormat, includeTrackNumber) + filename := buildFilename(safeTitle, safeArtist, track.TrackPos, filenameFormat, includeTrackNumber, position) filepath := filepath.Join(outputDir, filename) fmt.Println("Downloading FLAC file...") @@ -263,12 +264,18 @@ func (d *DeezerDownloader) DownloadByISRC(isrc, outputDir, filenameFormat string } fmt.Println("Embedding metadata and cover art...") + // Only use track number for bulk downloads (when position > 0) + trackNumberToEmbed := 0 + if position > 0 { + trackNumberToEmbed = position + } + metadata := Metadata{ Title: trackTitle, Artist: artists, Album: albumTitle, Date: track.ReleaseDate, - TrackNumber: track.TrackPos, + TrackNumber: trackNumberToEmbed, DiscNumber: track.DiskNumber, ISRC: track.ISRC, } diff --git a/backend/progress.go b/backend/progress.go index 7565f29..e9f8598 100644 --- a/backend/progress.go +++ b/backend/progress.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "sync" + "time" ) // Global progress tracker @@ -12,12 +13,15 @@ var ( currentProgressLock sync.RWMutex isDownloading bool downloadingLock sync.RWMutex + currentSpeed float64 + speedLock sync.RWMutex ) // ProgressInfo represents download progress information type ProgressInfo struct { IsDownloading bool `json:"is_downloading"` MBDownloaded float64 `json:"mb_downloaded"` + SpeedMBps float64 `json:"speed_mbps"` } // GetDownloadProgress returns current download progress @@ -30,12 +34,24 @@ func GetDownloadProgress() ProgressInfo { progress := currentProgress currentProgressLock.RUnlock() + speedLock.RLock() + speed := currentSpeed + speedLock.RUnlock() + return ProgressInfo{ IsDownloading: downloading, MBDownloaded: progress, + SpeedMBps: speed, } } +// SetDownloadSpeed updates the current download speed +func SetDownloadSpeed(mbps float64) { + speedLock.Lock() + currentSpeed = mbps + speedLock.Unlock() +} + // SetDownloadProgress updates the current download progress func SetDownloadProgress(mbDownloaded float64) { currentProgressLock.Lock() @@ -52,6 +68,7 @@ func SetDownloading(downloading bool) { if !downloading { // Reset progress when download completes SetDownloadProgress(0) + SetDownloadSpeed(0) } } @@ -60,16 +77,27 @@ type ProgressWriter struct { writer io.Writer total int64 lastPrinted int64 + startTime int64 + lastTime int64 + lastBytes int64 } func NewProgressWriter(writer io.Writer) *ProgressWriter { + now := getCurrentTimeMillis() return &ProgressWriter{ writer: writer, total: 0, lastPrinted: 0, + startTime: now, + lastTime: now, + lastBytes: 0, } } +func getCurrentTimeMillis() int64 { + return time.Now().UnixMilli() +} + func (pw *ProgressWriter) Write(p []byte) (int, error) { n, err := pw.writer.Write(p) pw.total += int64(n) @@ -77,12 +105,26 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) { // Report progress every 256KB for smoother updates if pw.total-pw.lastPrinted >= 256*1024 { mbDownloaded := float64(pw.total) / (1024 * 1024) - fmt.Printf("\rDownloaded: %.2f MB", mbDownloaded) + + // Calculate speed (MB/s) + now := getCurrentTimeMillis() + timeDiff := float64(now-pw.lastTime) / 1000.0 // seconds + bytesDiff := float64(pw.total - pw.lastBytes) + + if timeDiff > 0 { + speedMBps := (bytesDiff / (1024 * 1024)) / timeDiff + SetDownloadSpeed(speedMBps) + fmt.Printf("\rDownloaded: %.2f MB (%.2f MB/s)", mbDownloaded, speedMBps) + } else { + fmt.Printf("\rDownloaded: %.2f MB", mbDownloaded) + } // Update global progress SetDownloadProgress(mbDownloaded) pw.lastPrinted = pw.total + pw.lastTime = now + pw.lastBytes = pw.total } return n, err diff --git a/backend/qobuz.go b/backend/qobuz.go index b2defda..474e2b3 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -222,7 +222,7 @@ func (q *QobuzDownloader) DownloadCoverArt(coverURL, filepath string) error { return err } -func buildQobuzFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool) string { +func buildQobuzFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int) string { var filename string // Build base filename based on format @@ -236,14 +236,15 @@ func buildQobuzFilename(title, artist string, trackNumber int, format string, in } // Add track number prefix if enabled - if includeTrackNumber && trackNumber > 0 { - filename = fmt.Sprintf("%02d. %s", trackNumber, filename) + // Only use track number for bulk downloads (when position > 0) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) } return filename + ".flac" } -func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) error { +func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) error { fmt.Printf("Fetching track info for ISRC: %s\n", isrc) // Create output directory if it doesn't exist @@ -311,7 +312,7 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma safeTitle := sanitizeFilename(trackTitle) // Build filename based on format settings - filename := buildQobuzFilename(safeTitle, safeArtist, track.TrackNumber, filenameFormat, includeTrackNumber) + filename := buildQobuzFilename(safeTitle, safeArtist, track.TrackNumber, filenameFormat, includeTrackNumber, position) filepath := filepath.Join(outputDir, filename) fmt.Printf("Downloading FLAC file to: %s\n", filepath) @@ -339,12 +340,18 @@ func (q *QobuzDownloader) DownloadByISRC(isrc, outputDir, quality, filenameForma releaseYear = track.ReleaseDateOriginal[:4] } + // Only use track number for bulk downloads (when position > 0) + trackNumberToEmbed := 0 + if position > 0 { + trackNumberToEmbed = position + } + metadata := Metadata{ Title: trackTitle, Artist: artists, Album: albumTitle, Date: releaseYear, - TrackNumber: track.TrackNumber, + TrackNumber: trackNumberToEmbed, DiscNumber: track.MediaNumber, ISRC: track.ISRC, } diff --git a/backend/tidal.go b/backend/tidal.go index 7e6f66b..9c22a6b 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -333,7 +333,7 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error { return nil } -func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) (string, error) { +func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) (string, error) { if outputDir != "." { if err := os.MkdirAll(outputDir, 0755); err != nil { return "", fmt.Errorf("directory error: %w", err) @@ -386,7 +386,7 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm } // Build filename based on format settings - filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber) + filename := buildTidalFilename(trackTitle, artistName, trackInfo.TrackNumber, filenameFormat, includeTrackNumber, position) outputFilename := filepath.Join(outputDir, filename) if fileInfo, err := os.Stat(outputFilename); err == nil && fileInfo.Size() > 0 { @@ -427,12 +427,18 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm releaseYear = trackInfo.Album.ReleaseDate[:4] } + // Only use track number for bulk downloads (when position > 0) + trackNumberToEmbed := 0 + if position > 0 { + trackNumberToEmbed = position + } + metadata := Metadata{ Title: trackTitle, Artist: artistName, Album: albumTitle, Date: releaseYear, - TrackNumber: trackInfo.TrackNumber, + TrackNumber: trackNumberToEmbed, DiscNumber: trackInfo.VolumeNumber, ISRC: trackInfo.ISRC, } @@ -447,7 +453,7 @@ func (t *TidalDownloader) Download(query, isrc, outputDir, quality, filenameForm return outputFilename, nil } -func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) (string, error) { +func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName string) (string, error) { apis, err := t.GetAvailableAPIs() if err != nil { return "", fmt.Errorf("no APIs available for fallback: %w", err) @@ -459,7 +465,7 @@ func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, fallbackDownloader := NewTidalDownloader(apiURL) - result, err := fallbackDownloader.Download(query, isrc, outputDir, quality, filenameFormat, includeTrackNumber, spotifyTrackName, spotifyArtistName, spotifyAlbumName) + result, err := fallbackDownloader.Download(query, isrc, outputDir, quality, filenameFormat, includeTrackNumber, position, spotifyTrackName, spotifyArtistName, spotifyAlbumName) if err == nil { fmt.Printf("✓ Success with: %s\n", apiURL) return result, nil @@ -476,7 +482,7 @@ func (t *TidalDownloader) DownloadWithFallback(query, isrc, outputDir, quality, return "", fmt.Errorf("all %d APIs failed. Last error: %v", len(apis), lastError) } -func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool) string { +func buildTidalFilename(title, artist string, trackNumber int, format string, includeTrackNumber bool, position int) string { var filename string // Build base filename based on format @@ -490,8 +496,9 @@ func buildTidalFilename(title, artist string, trackNumber int, format string, in } // Add track number prefix if enabled - if includeTrackNumber && trackNumber > 0 { - filename = fmt.Sprintf("%02d. %s", trackNumber, filename) + // Only use track number for bulk downloads (when position > 0) + if includeTrackNumber && position > 0 { + filename = fmt.Sprintf("%02d. %s", position, filename) } return filename + ".flac" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fca5c2e..a5896f0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,7 @@ import { OpenFolder } from "../wailsjs/go/main/App"; import { toastWithSound as toast } from "@/lib/toast-with-sound"; // Components +import { TitleBar } from "@/components/TitleBar"; import { Header } from "@/components/Header"; import { SearchBar } from "@/components/SearchBar"; import { TrackInfo } from "@/components/TrackInfo"; @@ -259,9 +260,11 @@ function App() { return ( -
-
-
+
+ +
+
+
{/* Download Progress Toast */} @@ -344,7 +347,8 @@ function App() { onFetch={handleFetchMetadata} /> - {metadata.metadata && renderMetadata()} + {metadata.metadata && renderMetadata()} +
diff --git a/frontend/src/components/AlbumInfo.tsx b/frontend/src/components/AlbumInfo.tsx index 70056ac..10a385b 100644 --- a/frontend/src/components/AlbumInfo.tsx +++ b/frontend/src/components/AlbumInfo.tsx @@ -117,7 +117,7 @@ export function AlbumInfo({ )} {downloadedTracks.size > 0 && ( - diff --git a/frontend/src/components/ArtistInfo.tsx b/frontend/src/components/ArtistInfo.tsx index 82153cb..0d758db 100644 --- a/frontend/src/components/ArtistInfo.tsx +++ b/frontend/src/components/ArtistInfo.tsx @@ -173,7 +173,7 @@ export function ArtistInfo({ )} {downloadedTracks.size > 0 && ( - diff --git a/frontend/src/components/DownloadProgress.tsx b/frontend/src/components/DownloadProgress.tsx index 35bd701..a75ae9e 100644 --- a/frontend/src/components/DownloadProgress.tsx +++ b/frontend/src/components/DownloadProgress.tsx @@ -13,8 +13,8 @@ export function DownloadProgress({ progress, currentTrack, onStop }: DownloadPro
-
diff --git a/frontend/src/components/DownloadProgressToast.tsx b/frontend/src/components/DownloadProgressToast.tsx index 94645f1..4cac862 100644 --- a/frontend/src/components/DownloadProgressToast.tsx +++ b/frontend/src/components/DownloadProgressToast.tsx @@ -9,13 +9,20 @@ export function DownloadProgressToast() { } return ( -
+
-
+
-

- {progress.mb_downloaded.toFixed(2)} MB -

+
+

+ {progress.mb_downloaded.toFixed(2)} MB +

+ {progress.speed_mbps > 0 && ( +

+ {progress.speed_mbps.toFixed(2)} MB/s +

+ )} +
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index e9f1771..1365340 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -68,7 +68,7 @@ export function Header({ version, hasUpdate }: HeaderProps) { - +

Report bug or request feature

diff --git a/frontend/src/components/PlaylistInfo.tsx b/frontend/src/components/PlaylistInfo.tsx index 5d8a523..bc4a42d 100644 --- a/frontend/src/components/PlaylistInfo.tsx +++ b/frontend/src/components/PlaylistInfo.tsx @@ -123,7 +123,7 @@ export function PlaylistInfo({ )} {downloadedTracks.size > 0 && ( - diff --git a/frontend/src/components/SearchAndSort.tsx b/frontend/src/components/SearchAndSort.tsx index 3588fbc..a2e14c6 100644 --- a/frontend/src/components/SearchAndSort.tsx +++ b/frontend/src/components/SearchAndSort.tsx @@ -33,8 +33,8 @@ export function SearchAndSort({ />
handleDownloadPathChange(e.target.value)} - placeholder="C:\Users\YourUsername\Music" - /> - +
+ {/* Left Column */} +
+ {/* Download Path */} +
+ +
+ handleDownloadPathChange(e.target.value)} + placeholder="C:\Users\YourUsername\Music" + /> + +
+
+ + {/* Source Selection */} +
+ + +
+ + {/* Theme Mode Selection */} +
+ + +
+ + {/* Theme Color Selection */} +
+ +
- {/* Source Selection */} -
- - -
- - {/* File Settings */} -
-

File Settings

- + {/* Right Column */} +
{/* Filename Format */} -
+
setTempSettings(prev => ({ ...prev, filenameFormat: value as any }))} - className="flex flex-wrap gap-3" > -
+
- +
-
+
- +
-
+
- +
- {/* Subfolder Options */} +
+ + {/* Folder Settings */}
+

Folder Settings

setTempSettings(prev => ({ ...prev, trackNumber: checked as boolean }))} /> + + + + + +

Adds track numbers based on the order in the album, playlist, or discography list

+
+
- - {/* Theme Mode Selection */} -
- - -
- - {/* Theme Color Selection */} -
- - -
+ + +
+ ); +} diff --git a/frontend/src/components/TrackInfo.tsx b/frontend/src/components/TrackInfo.tsx index 8c624be..e5d8f4c 100644 --- a/frontend/src/components/TrackInfo.tsx +++ b/frontend/src/components/TrackInfo.tsx @@ -51,19 +51,20 @@ export function TrackInfo({
{isDownloaded && ( - diff --git a/frontend/src/components/TrackList.tsx b/frontend/src/components/TrackList.tsx index b445a1c..b1d5cc3 100644 --- a/frontend/src/components/TrackList.tsx +++ b/frontend/src/components/TrackList.tsx @@ -187,13 +187,14 @@ export function TrackList({ onDownloadTrack(track.isrc, track.name, track.artists, track.album_name) } size="sm" + className="gap-1.5" disabled={isDownloading || downloadingTrack === track.isrc} > {downloadingTrack === track.isrc ? ( ) : ( <> - + Download )} diff --git a/frontend/src/hooks/useDownload.ts b/frontend/src/hooks/useDownload.ts index dfade9f..4c7c322 100644 --- a/frontend/src/hooks/useDownload.ts +++ b/frontend/src/hooks/useDownload.ts @@ -24,7 +24,8 @@ export function useDownload() { artistName?: string, albumName?: string, playlistName?: string, - isArtistDiscography?: boolean + isArtistDiscography?: boolean, + position?: number ) => { let service = settings.downloader; @@ -64,6 +65,7 @@ export function useDownload() { output_dir: outputDir, filename_format: settings.filenameFormat, track_number: settings.trackNumber, + position, }); if (tidalResponse.success) { @@ -85,6 +87,7 @@ export function useDownload() { output_dir: outputDir, filename_format: settings.filenameFormat, track_number: settings.trackNumber, + position, }); if (deezerResponse.success) { @@ -108,6 +111,7 @@ export function useDownload() { output_dir: outputDir, filename_format: settings.filenameFormat, track_number: settings.trackNumber, + position, }); }; @@ -126,6 +130,7 @@ export function useDownload() { setDownloadingTrack(isrc); try { + // Single track download - no position parameter const response = await downloadWithAutoFallback( isrc, settings, @@ -133,7 +138,8 @@ export function useDownload() { artistName, albumName, undefined, - false + false, + undefined // Don't pass position for single track ); if (response.success) { @@ -187,6 +193,7 @@ export function useDownload() { } try { + // Use sequential numbering (1, 2, 3...) for selected tracks const response = await downloadWithAutoFallback( isrc, settings, @@ -194,7 +201,8 @@ export function useDownload() { track?.artists, track?.album_name, playlistName, - isArtistDiscography + isArtistDiscography, + i + 1 // Sequential position based on selection order ); if (response.success) { @@ -265,7 +273,8 @@ export function useDownload() { track.artists, track.album_name, playlistName, - isArtistDiscography + isArtistDiscography, + i + 1 ); if (response.success) { diff --git a/frontend/src/hooks/useDownloadProgress.ts b/frontend/src/hooks/useDownloadProgress.ts index 4137b52..b84ddde 100644 --- a/frontend/src/hooks/useDownloadProgress.ts +++ b/frontend/src/hooks/useDownloadProgress.ts @@ -4,12 +4,14 @@ import { GetDownloadProgress } from "../../wailsjs/go/main/App"; export interface DownloadProgressInfo { is_downloading: boolean; mb_downloaded: number; + speed_mbps: number; } export function useDownloadProgress() { const [progress, setProgress] = useState({ is_downloading: false, mb_downloaded: 0, + speed_mbps: 0, }); const intervalRef = useRef(null); diff --git a/frontend/src/lib/settings.ts b/frontend/src/lib/settings.ts index 5b89e99..16dc68c 100644 --- a/frontend/src/lib/settings.ts +++ b/frontend/src/lib/settings.ts @@ -12,6 +12,15 @@ export interface Settings { operatingSystem: "Windows" | "linux/MacOS" } +// Auto-detect operating system +function detectOS(): "Windows" | "linux/MacOS" { + const platform = window.navigator.platform.toLowerCase(); + if (platform.includes('win')) { + return "Windows"; + } + return "linux/MacOS"; +} + export const DEFAULT_SETTINGS: Settings = { downloadPath: "", downloader: "auto", @@ -21,7 +30,7 @@ export const DEFAULT_SETTINGS: Settings = { artistSubfolder: false, albumSubfolder: false, trackNumber: false, - operatingSystem: "Windows" + operatingSystem: detectOS() }; async function fetchDefaultPath(): Promise { @@ -46,6 +55,8 @@ export function getSettings(): Settings { parsed.themeMode = parsed.darkMode ? 'dark' : 'light'; delete parsed.darkMode; } + // Always use detected OS (don't persist it) + parsed.operatingSystem = detectOS(); return { ...DEFAULT_SETTINGS, ...parsed }; } } catch (error) { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index b8f698b..3bc6713 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -18,11 +18,23 @@ export function sanitizePath(input: string, os: string): string { export function joinPath(os: string, ...parts: string[]): string { const sep = os === "Windows" ? "\\" : "/"; - - return parts - .filter(Boolean) - .map(p => p.replace(/^[/\\]+|[/\\]+$/g, "")) + + const filtered = parts.filter(Boolean); + if (filtered.length === 0) return ""; + + const joined = filtered + .map((p, i) => { + // For first part, only remove trailing slashes (preserve leading slash for absolute paths) + if (i === 0) { + return p.replace(/[/\\]+$/g, ""); + } + // For other parts, remove both leading and trailing slashes + return p.replace(/^[/\\]+|[/\\]+$/g, ""); + }) + .filter(Boolean) // Remove empty strings after trimming .join(sep); + + return joined; } export function buildOutputPath(settings: Settings, folder?: string) { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 7aad6c5..f1c75e1 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -7,6 +7,6 @@ import { Toaster } from '@/components/ui/sonner' createRoot(document.getElementById('root')!).render( - + , ) diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index a6c6717..6daffd1 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -108,6 +108,7 @@ export interface DownloadRequest { folder_name?: string; filename_format?: string; track_number?: boolean; + position?: number; } export interface DownloadResponse { diff --git a/main.go b/main.go index e6fce0a..5cd2e96 100644 --- a/main.go +++ b/main.go @@ -19,9 +19,10 @@ func main() { // Create application with options err := wails.Run(&options.App{ - Title: "SpotiFLAC", - Width: 1024, - Height: 600, + Title: "SpotiFLAC", + Width: 1024, + Height: 600, + Frameless: true, AssetServer: &assetserver.Options{ Assets: assets, }, @@ -31,9 +32,10 @@ func main() { app, }, Windows: &windows.Options{ - WebviewIsTransparent: false, - WindowIsTranslucent: false, - DisableWindowIcon: false, + WebviewIsTransparent: false, + WindowIsTranslucent: false, + DisableWindowIcon: false, + DisableFramelessWindowDecorations: false, }, })