Skip to content

Commit df4a7c6

Browse files
committed
v7.0.5
1 parent 37a59db commit df4a7c6

26 files changed

+265
-121
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Get Spotify tracks in MP3 and FLAC via spotidownloader.com
2222

2323
### [SpotiFLAC](https://github.com/afkarxyz/SpotiFLAC)
2424

25-
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.
25+
Get Spotify tracks in true FLAC from Tidal, Qobuz, Amazon Music & Deezer — no account required.
2626

2727
### [SpotiFLAC Next](https://github.com/spotiverse/SpotiFLAC-Next)
2828

app.go

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"os"
99

1010
"path/filepath"
11-
"strconv"
1211
"strings"
1312
"time"
1413

@@ -88,6 +87,7 @@ type DownloadRequest struct {
8887
PlaylistOwner string `json:"playlist_owner,omitempty"`
8988
UseFirstArtistOnly bool `json:"use_first_artist_only,omitempty"`
9089
UseSingleGenre bool `json:"use_single_genre,omitempty"`
90+
EmbedGenre bool `json:"embed_genre,omitempty"`
9191
}
9292

9393
type DownloadResponse struct {
@@ -367,6 +367,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
367367
req.PlaylistOwner,
368368
req.UseFirstArtistOnly,
369369
req.UseSingleGenre,
370+
req.EmbedGenre,
370371
)
371372

372373
if err != nil {
@@ -427,29 +428,19 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
427428
quality := "Unknown"
428429
durationStr := "--:--"
429430

430-
props, err := backend.GetAudioProperties(fPath)
431-
if err == nil && props != nil {
432-
433-
if br, err := strconv.Atoi(props.BitRate); err == nil {
434-
quality = fmt.Sprintf("%dkbps", br/1000)
435-
} else {
436-
quality = props.BitRate
437-
}
438-
439-
if val, err := strconv.ParseFloat(props.Duration, 64); err == nil {
440-
d := int(val)
441-
durationStr = fmt.Sprintf("%d:%02d", d/60, d%60)
442-
}
443-
444-
if props.Format != "" {
445-
itemFormat := props.Format
446-
if itemFormat == "mp3" {
447-
format = "MP3"
448-
} else if itemFormat == "flac" {
449-
format = "FLAC"
450-
quality = "16-bit/44.1kHz"
451-
}
431+
meta, err := backend.GetTrackMetadata(fPath)
432+
if err == nil && meta != nil {
433+
if meta.BitsPerSample > 0 {
434+
quality = fmt.Sprintf("%d-bit/%.1fkHz", meta.BitsPerSample, float64(meta.SampleRate)/1000.0)
435+
} else if meta.Bitrate > 0 {
436+
quality = fmt.Sprintf("%dkbps/%.1fkHz", meta.Bitrate/1000, float64(meta.SampleRate)/1000.0)
437+
} else if meta.SampleRate > 0 {
438+
quality = fmt.Sprintf("%.1fkHz", float64(meta.SampleRate)/1000.0)
452439
}
440+
d := int(meta.Duration)
441+
durationStr = fmt.Sprintf("%d:%02d", d/60, d%60)
442+
} else if err != nil {
443+
fmt.Printf("[History] Failed to get metadata for %s: %v\n", fPath, err)
453444
} else {
454445

455446
}

backend/analysis.go

Lines changed: 86 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import (
44
"fmt"
55
"math"
66
"os"
7+
"os/exec"
8+
"strconv"
9+
"strings"
10+
"time"
711

8-
"github.com/go-flac/go-flac"
912
mewflac "github.com/mewkiz/flac"
1013
)
1114

@@ -17,6 +20,7 @@ type AnalysisResult struct {
1720
BitsPerSample uint8 `json:"bits_per_sample"`
1821
TotalSamples uint64 `json:"total_samples"`
1922
Duration float64 `json:"duration"`
23+
Bitrate int `json:"bit_rate"`
2024
BitDepth string `json:"bit_depth"`
2125
DynamicRange float64 `json:"dynamic_range"`
2226
PeakAmplitude float64 `json:"peak_amplitude"`
@@ -29,53 +33,97 @@ func AnalyzeTrack(filepath string) (*AnalysisResult, error) {
2933
return nil, fmt.Errorf("file does not exist: %s", filepath)
3034
}
3135

32-
fileInfo, err := os.Stat(filepath)
33-
if err != nil {
34-
return nil, fmt.Errorf("failed to get file info: %w", err)
36+
return GetMetadataWithFFprobe(filepath)
37+
}
38+
39+
func GetTrackMetadata(filepath string) (*AnalysisResult, error) {
40+
if !fileExists(filepath) {
41+
return nil, fmt.Errorf("file does not exist: %s", filepath)
3542
}
3643

37-
f, err := flac.ParseFile(filepath)
44+
return GetMetadataWithFFprobe(filepath)
45+
}
46+
47+
func GetMetadataWithFFprobe(filePath string) (*AnalysisResult, error) {
48+
ffprobePath, err := GetFFprobePath()
3849
if err != nil {
39-
return nil, fmt.Errorf("failed to parse FLAC file: %w", err)
40-
}
41-
42-
result := &AnalysisResult{
43-
FilePath: filepath,
44-
FileSize: fileInfo.Size(),
45-
}
46-
47-
if len(f.Meta) > 0 {
48-
streamInfo := f.Meta[0]
49-
if streamInfo.Type == flac.StreamInfo {
50-
data := streamInfo.Data
51-
if len(data) >= 18 {
52-
result.SampleRate = uint32(data[10])<<12 | uint32(data[11])<<4 | uint32(data[12])>>4
53-
result.Channels = ((data[12] >> 1) & 0x07) + 1
54-
result.BitsPerSample = ((data[12]&0x01)<<4 | data[13]>>4) + 1
55-
result.TotalSamples = uint64(data[13]&0x0F)<<32 |
56-
uint64(data[14])<<24 |
57-
uint64(data[15])<<16 |
58-
uint64(data[16])<<8 |
59-
uint64(data[17])
60-
61-
if result.SampleRate > 0 {
62-
result.Duration = float64(result.TotalSamples) / float64(result.SampleRate)
63-
}
64-
}
50+
return nil, err
51+
}
52+
53+
for i := 0; i < 5; i++ {
54+
if f, err := os.Open(filePath); err == nil {
55+
f.Close()
56+
break
6557
}
58+
time.Sleep(200 * time.Millisecond)
6659
}
6760

68-
spectrum, err := AnalyzeSpectrum(filepath)
69-
if err != nil {
70-
fmt.Printf("Warning: failed to analyze spectrum: %v\n", err)
61+
infoMap := make(map[string]string)
62+
args := []string{
63+
"-v", "error",
64+
"-select_streams", "a:0",
65+
"-show_entries", "stream=sample_rate,channels,bits_per_raw_sample,bits_per_sample,duration,bit_rate",
66+
"-of", "default=noprint_wrappers=0",
67+
filePath,
68+
}
69+
cmd := exec.Command(ffprobePath, args...)
70+
setHideWindow(cmd)
71+
output, err := cmd.CombinedOutput()
72+
if err == nil {
73+
lines := strings.Split(string(output), "\n")
74+
for _, line := range lines {
75+
if strings.Contains(line, "=") {
76+
parts := strings.SplitN(line, "=", 2)
77+
infoMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
78+
}
79+
}
7180
} else {
72-
result.Spectrum = spectrum
73-
calculateRealAudioMetrics(result, filepath)
81+
return nil, fmt.Errorf("ffprobe failed: %v - %s", err, string(output))
7482
}
7583

76-
result.BitDepth = fmt.Sprintf("%d-bit", result.BitsPerSample)
84+
res := &AnalysisResult{
85+
FilePath: filePath,
86+
}
87+
88+
if info, err := os.Stat(filePath); err == nil {
89+
res.FileSize = info.Size()
90+
}
91+
92+
if val, ok := infoMap["sample_rate"]; ok {
93+
s, _ := strconv.Atoi(val)
94+
res.SampleRate = uint32(s)
95+
}
96+
if val, ok := infoMap["channels"]; ok {
97+
c, _ := strconv.Atoi(val)
98+
res.Channels = uint8(c)
99+
}
100+
if val, ok := infoMap["duration"]; ok {
101+
d, _ := strconv.ParseFloat(val, 64)
102+
res.Duration = d
103+
}
104+
if val, ok := infoMap["bit_rate"]; ok && val != "N/A" {
105+
br, _ := strconv.Atoi(val)
106+
res.Bitrate = br
107+
}
108+
109+
bits := 0
110+
if val, ok := infoMap["bits_per_raw_sample"]; ok && val != "N/A" {
111+
bits, _ = strconv.Atoi(val)
112+
}
113+
if bits == 0 {
114+
if val, ok := infoMap["bits_per_sample"]; ok && val != "N/A" {
115+
bits, _ = strconv.Atoi(val)
116+
}
117+
}
118+
119+
res.BitsPerSample = uint8(bits)
120+
if bits > 0 {
121+
res.BitDepth = fmt.Sprintf("%d-bit", bits)
122+
} else {
123+
res.BitDepth = "Unknown"
124+
}
77125

78-
return result, nil
126+
return res, nil
79127
}
80128

81129
func calculateRealAudioMetrics(result *AnalysisResult, filepath string) {

backend/cover.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func buildCoverFilename(trackName, artistName, albumName, albumArtist, releaseDa
6969
filename = strings.ReplaceAll(filename, "{album}", safeAlbum)
7070
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
7171
filename = strings.ReplaceAll(filename, "{year}", year)
72+
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
7273

7374
if discNumber > 0 {
7475
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))

backend/filemanager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ func GenerateFilename(metadata *AudioMetadata, format string, ext string) string
328328
result = strings.ReplaceAll(result, "{album}", sanitizeFilename(metadata.Album))
329329
result = strings.ReplaceAll(result, "{album_artist}", sanitizeFilename(metadata.AlbumArtist))
330330
result = strings.ReplaceAll(result, "{year}", sanitizeFilename(year))
331+
result = strings.ReplaceAll(result, "{date}", sanitizeFilename(metadata.Year))
331332

332333
if metadata.TrackNumber > 0 {
333334
result = strings.ReplaceAll(result, "{track}", fmt.Sprintf("%02d", metadata.TrackNumber))

backend/filename.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func BuildFilename(trackName, artistName, albumName, albumArtist, releaseDate st
3131
filename = strings.ReplaceAll(filename, "{album}", safeAlbum)
3232
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
3333
filename = strings.ReplaceAll(filename, "{year}", year)
34+
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
3435
filename = strings.ReplaceAll(filename, "{playlist}", safePlaylist)
3536
filename = strings.ReplaceAll(filename, "{creator}", safeCreator)
3637

backend/lyrics.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ func buildLyricsFilename(trackName, artistName, albumName, albumArtist, releaseD
385385
filename = strings.ReplaceAll(filename, "{album}", safeAlbum)
386386
filename = strings.ReplaceAll(filename, "{album_artist}", safeAlbumArtist)
387387
filename = strings.ReplaceAll(filename, "{year}", year)
388+
filename = strings.ReplaceAll(filename, "{date}", SanitizeFilename(releaseDate))
388389

389390
if discNumber > 0 {
390391
filename = strings.ReplaceAll(filename, "{disc}", fmt.Sprintf("%d", discNumber))

backend/musicbrainz.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ type MusicBrainzRecordingResponse struct {
5454
} `json:"recordings"`
5555
}
5656

57-
func FetchMusicBrainzMetadata(isrc, title, artist, album string, useSingleGenre bool) (Metadata, error) {
57+
func FetchMusicBrainzMetadata(isrc, title, artist, album string, useSingleGenre bool, embedGenre bool) (Metadata, error) {
5858
var meta Metadata
5959

60+
if !embedGenre {
61+
return meta, nil
62+
}
63+
6064
if isrc == "" {
6165
return meta, fmt.Errorf("no ISRC provided")
6266
}

backend/spotidownloader.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ func (s *SpotiDownloader) DownloadTrack(
219219
playlistOwner string,
220220
useFirstArtistOnly bool,
221221
useSingleGenre bool,
222+
embedGenre bool,
222223
) (string, error) {
223224

224225
outputDir = NormalizePath(outputDir)
@@ -266,26 +267,30 @@ func (s *SpotiDownloader) DownloadTrack(
266267
}
267268

268269
metaChan := make(chan mbResult, 1)
269-
go func() {
270-
client := NewSongLinkClient()
271-
res := mbResult{}
272-
if val, err := client.GetISRC(trackID); err == nil {
273-
res.ISRC = val
274-
if val != "" {
275-
fmt.Println("Fetching MusicBrainz metadata...")
276-
277-
if fetchedMeta, err := FetchMusicBrainzMetadata(val, trackName, artistName, albumName, useSingleGenre); err == nil {
278-
res.Metadata = fetchedMeta
279-
fmt.Println("✓ MusicBrainz metadata fetched")
280-
} else {
281-
fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err)
270+
if embedGenre {
271+
go func() {
272+
client := NewSongLinkClient()
273+
res := mbResult{}
274+
if val, err := client.GetISRC(trackID); err == nil {
275+
res.ISRC = val
276+
if val != "" {
277+
fmt.Println("Fetching MusicBrainz metadata...")
278+
279+
if fetchedMeta, err := FetchMusicBrainzMetadata(val, trackName, artistName, albumName, useSingleGenre, embedGenre); err == nil {
280+
res.Metadata = fetchedMeta
281+
fmt.Println("✓ MusicBrainz metadata fetched")
282+
} else {
283+
fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err)
284+
}
282285
}
283-
}
284-
} else {
286+
} else {
285287

286-
}
287-
metaChan <- res
288-
}()
288+
}
289+
metaChan <- res
290+
}()
291+
} else {
292+
metaChan <- mbResult{}
293+
}
289294

290295
if err := s.DownloadFile(downloadURL, outputPath); err != nil {
291296
return "", fmt.Errorf("failed to download file: %v", err)

backend/token_fetcher.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package backend
33
import (
44
"fmt"
55
"os"
6-
"os/exec"
76
"path/filepath"
87
"strings"
98
"time"
@@ -68,7 +67,7 @@ func FetchSessionTokenWithParams(timeout int, retry int) (string, error) {
6867
for attempt := 1; attempt <= maxAttempts; attempt++ {
6968
fmt.Printf("[TokenFetcher] Attempt %d/%d (timeout: %ds)\n", attempt, maxAttempts, timeout)
7069

71-
cmd := exec.Command(exePath, "--timeout", fmt.Sprintf("%d", timeout), "--retry", "1")
70+
cmd := newTokenCmd(exePath, "--timeout", fmt.Sprintf("%d", timeout), "--retry", "1")
7271
output, err := cmd.CombinedOutput()
7372
outputStr := strings.TrimSpace(string(output))
7473

0 commit comments

Comments
 (0)