Add FLAC lyrics embedding with LRCLIB fallback (#151)
* Update lyrics code * Refactor DownloadFile to use default HTTP client
This commit is contained in:
@@ -299,6 +299,58 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
filename = strings.TrimPrefix(filename, "EXISTS:")
|
filename = strings.TrimPrefix(filename, "EXISTS:")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Embed lyrics after successful download (only for new downloads with Spotify ID)
|
||||||
|
if !alreadyExists && req.SpotifyID != "" && strings.HasSuffix(filename, ".flac") {
|
||||||
|
go func(filePath, spotifyID, trackName, artistName string) {
|
||||||
|
fmt.Printf("\n========== LYRICS FETCH START ==========\n")
|
||||||
|
fmt.Printf("Spotify ID: %s\n", spotifyID)
|
||||||
|
fmt.Printf("Track: %s\n", trackName)
|
||||||
|
fmt.Printf("Artist: %s\n", artistName)
|
||||||
|
fmt.Println("Searching all sources...")
|
||||||
|
|
||||||
|
lyricsClient := backend.NewLyricsClient()
|
||||||
|
|
||||||
|
// Try all sources with fallbacks
|
||||||
|
lyricsResp, source, err := lyricsClient.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("❌ All sources failed: %v\n", err)
|
||||||
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if lyricsResp == nil || len(lyricsResp.Lines) == 0 {
|
||||||
|
fmt.Println("❌ No lyrics content found")
|
||||||
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ Lyrics found from: %s\n", source)
|
||||||
|
fmt.Printf("✓ Sync type: %s\n", lyricsResp.SyncType)
|
||||||
|
fmt.Printf("✓ Total lines: %d\n", len(lyricsResp.Lines))
|
||||||
|
|
||||||
|
lyrics := lyricsClient.ConvertToLRC(lyricsResp, trackName, artistName)
|
||||||
|
if lyrics == "" {
|
||||||
|
fmt.Println("❌ No lyrics content to embed")
|
||||||
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show full lyrics in console for debugging
|
||||||
|
fmt.Printf("\n--- Full LRC Content ---\n")
|
||||||
|
fmt.Println(lyrics)
|
||||||
|
fmt.Printf("--- End LRC Content ---\n\n")
|
||||||
|
|
||||||
|
fmt.Printf("Embedding into: %s\n", filePath)
|
||||||
|
if err := backend.EmbedLyricsOnly(filePath, lyrics); err != nil {
|
||||||
|
fmt.Printf("❌ Failed to embed lyrics: %v\n", err)
|
||||||
|
fmt.Printf("========== LYRICS FETCH END (FAILED) ==========\n\n")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Lyrics embedded successfully!\n")
|
||||||
|
fmt.Printf("========== LYRICS FETCH END (SUCCESS) ==========\n\n")
|
||||||
|
}
|
||||||
|
}(filename, req.SpotifyID, req.TrackName, req.ArtistName)
|
||||||
|
}
|
||||||
|
|
||||||
message := "Download completed successfully"
|
message := "Download completed successfully"
|
||||||
if alreadyExists {
|
if alreadyExists {
|
||||||
message = "File already exists"
|
message = "File already exists"
|
||||||
|
|||||||
+7
-1
@@ -173,7 +173,13 @@ func (d *DeezerDownloader) GetDownloadURL(trackID int64) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DeezerDownloader) DownloadFile(url, filepath string) error {
|
func (d *DeezerDownloader) DownloadFile(url, filepath string) error {
|
||||||
resp, err := d.client.Get(url)
|
// Use a separate client with a longer timeout. The default client's 60s limit
|
||||||
|
// causes downloads to fail on slow connections or for large Hi-Res files.
|
||||||
|
downloadClient := &http.Client{
|
||||||
|
Timeout: 5 * time.Minute, // 5 minutes for large files
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := downloadClient.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download file: %w", err)
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+219
-3
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -13,6 +14,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LRCLibResponse represents the LRCLIB API response
|
||||||
|
type LRCLibResponse struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
TrackName string `json:"trackName"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
AlbumName string `json:"albumName"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
Instrumental bool `json:"instrumental"`
|
||||||
|
PlainLyrics string `json:"plainLyrics"`
|
||||||
|
SyncedLyrics string `json:"syncedLyrics"`
|
||||||
|
}
|
||||||
|
|
||||||
// LyricsLine represents a single line of lyrics
|
// LyricsLine represents a single line of lyrics
|
||||||
type LyricsLine struct {
|
type LyricsLine struct {
|
||||||
StartTimeMs string `json:"startTimeMs"`
|
StartTimeMs string `json:"startTimeMs"`
|
||||||
@@ -60,13 +74,215 @@ func NewLyricsClient() *LyricsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchLyrics fetches lyrics from the Spotify Lyrics API
|
// FetchLyricsWithMetadata fetches lyrics using track name and artist (for LRCLIB fallback)
|
||||||
|
func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string) (*LyricsResponse, error) {
|
||||||
|
// Try LRCLIB API
|
||||||
|
apiURL := fmt.Sprintf("https://lrclib.net/api/get?artist_name=%s&track_name=%s",
|
||||||
|
url.QueryEscape(artistName),
|
||||||
|
url.QueryEscape(trackName))
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Get(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch from LRCLIB: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("LRCLIB returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read LRCLIB response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lrcLibResp LRCLibResponse
|
||||||
|
if err := json.Unmarshal(body, &lrcLibResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse LRCLIB response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert LRCLIB response to our LyricsResponse format
|
||||||
|
return c.convertLRCLibToLyricsResponse(&lrcLibResp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertLRCLibToLyricsResponse converts LRCLIB response to our standard format
|
||||||
|
func (c *LyricsClient) convertLRCLibToLyricsResponse(lrcLib *LRCLibResponse) *LyricsResponse {
|
||||||
|
resp := &LyricsResponse{
|
||||||
|
Error: false,
|
||||||
|
SyncType: "LINE_SYNCED",
|
||||||
|
Lines: []LyricsLine{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer synced lyrics, fall back to plain
|
||||||
|
lyricsText := lrcLib.SyncedLyrics
|
||||||
|
if lyricsText == "" {
|
||||||
|
lyricsText = lrcLib.PlainLyrics
|
||||||
|
resp.SyncType = "UNSYNCED"
|
||||||
|
}
|
||||||
|
|
||||||
|
if lyricsText == "" {
|
||||||
|
resp.Error = true
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse synced lyrics format [mm:ss.xx] text
|
||||||
|
lines := strings.Split(lyricsText, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if line has timestamp [mm:ss.xx]
|
||||||
|
if strings.HasPrefix(line, "[") && len(line) > 10 {
|
||||||
|
closeBracket := strings.Index(line, "]")
|
||||||
|
if closeBracket > 0 {
|
||||||
|
timestamp := line[1:closeBracket]
|
||||||
|
words := strings.TrimSpace(line[closeBracket+1:])
|
||||||
|
|
||||||
|
// Convert [mm:ss.xx] to milliseconds
|
||||||
|
ms := lrcTimestampToMs(timestamp)
|
||||||
|
resp.Lines = append(resp.Lines, LyricsLine{
|
||||||
|
StartTimeMs: fmt.Sprintf("%d", ms),
|
||||||
|
Words: words,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain lyrics line (no timestamp)
|
||||||
|
resp.Lines = append(resp.Lines, LyricsLine{
|
||||||
|
StartTimeMs: "0",
|
||||||
|
Words: line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// lrcTimestampToMs converts LRC timestamp [mm:ss.xx] to milliseconds
|
||||||
|
func lrcTimestampToMs(timestamp string) int64 {
|
||||||
|
var minutes, seconds, centiseconds int64
|
||||||
|
// Try parsing mm:ss.xx format
|
||||||
|
n, _ := fmt.Sscanf(timestamp, "%d:%d.%d", &minutes, &seconds, ¢iseconds)
|
||||||
|
if n >= 2 {
|
||||||
|
return minutes*60*1000 + seconds*1000 + centiseconds*10
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchLyricsFromLRCLibSearch fetches lyrics using LRCLIB search API
|
||||||
|
func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string) (*LyricsResponse, error) {
|
||||||
|
query := fmt.Sprintf("%s %s", artistName, trackName)
|
||||||
|
apiURL := fmt.Sprintf("https://lrclib.net/api/search?q=%s", url.QueryEscape(query))
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Get(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []LRCLibResponse
|
||||||
|
if err := json.Unmarshal(body, &results); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return nil, fmt.Errorf("no results found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find best match - prefer one with synced lyrics
|
||||||
|
var best *LRCLibResponse
|
||||||
|
for i := range results {
|
||||||
|
if results[i].SyncedLyrics != "" {
|
||||||
|
best = &results[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if best == nil && results[i].PlainLyrics != "" {
|
||||||
|
best = &results[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if best == nil {
|
||||||
|
best = &results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.convertLRCLibToLyricsResponse(best), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// simplifyTrackName removes common suffixes like "(feat. X)", "(Remastered)", etc.
|
||||||
|
func simplifyTrackName(name string) string {
|
||||||
|
// Remove content in parentheses
|
||||||
|
if idx := strings.Index(name, "("); idx > 0 {
|
||||||
|
name = strings.TrimSpace(name[:idx])
|
||||||
|
}
|
||||||
|
// Remove content after " - " (like "From the Motion Picture")
|
||||||
|
if idx := strings.Index(name, " - "); idx > 0 {
|
||||||
|
name = strings.TrimSpace(name[:idx])
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchLyricsAllSources tries all sources to get lyrics
|
||||||
|
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string) (*LyricsResponse, string, error) {
|
||||||
|
// 1. Try Spotify API
|
||||||
|
if spotifyID != "" {
|
||||||
|
resp, err := c.FetchLyrics(spotifyID)
|
||||||
|
if err == nil && resp != nil && len(resp.Lines) > 0 {
|
||||||
|
return resp, "Spotify", nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" ↳ Spotify API: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try LRCLIB exact match
|
||||||
|
resp, err := c.FetchLyricsWithMetadata(trackName, artistName)
|
||||||
|
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||||
|
return resp, "LRCLIB", nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" ↳ LRCLIB exact: %v\n", err)
|
||||||
|
|
||||||
|
// 3. Try LRCLIB search
|
||||||
|
resp, err = c.FetchLyricsFromLRCLibSearch(trackName, artistName)
|
||||||
|
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||||
|
return resp, "LRCLIB Search", nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" ↳ LRCLIB search: %v\n", err)
|
||||||
|
|
||||||
|
// 4. Try with simplified track name (remove parentheses, subtitles)
|
||||||
|
simplifiedTrack := simplifyTrackName(trackName)
|
||||||
|
if simplifiedTrack != trackName {
|
||||||
|
fmt.Printf(" ↳ Trying simplified name: %s\n", simplifiedTrack)
|
||||||
|
|
||||||
|
resp, err = c.FetchLyricsWithMetadata(simplifiedTrack, artistName)
|
||||||
|
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||||
|
return resp, "LRCLIB (simplified)", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName)
|
||||||
|
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||||
|
return resp, "LRCLIB Search (simplified)", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, "", fmt.Errorf("lyrics not found in any source")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchLyrics fetches lyrics from the Spotify Lyrics API with LRCLIB fallback
|
||||||
func (c *LyricsClient) FetchLyrics(spotifyID string) (*LyricsResponse, error) {
|
func (c *LyricsClient) FetchLyrics(spotifyID string) (*LyricsResponse, error) {
|
||||||
// Decode base64 API URL
|
// Decode base64 API URL
|
||||||
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9zcG90aWZ5LWx5cmljcy1hcGktcGkudmVyY2VsLmFwcC8/dHJhY2tpZD0=")
|
apiBase, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9zcG90aWZ5LWx5cmljcy1hcGktcGkudmVyY2VsLmFwcC8/dHJhY2tpZD0=")
|
||||||
url := fmt.Sprintf("%s%s", string(apiBase), spotifyID)
|
apiURL := fmt.Sprintf("%s%s", string(apiBase), spotifyID)
|
||||||
|
|
||||||
resp, err := c.httpClient.Get(url)
|
resp, err := c.httpClient.Get(apiURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch lyrics: %v", err)
|
return nil, fmt.Errorf("failed to fetch lyrics: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Metadata struct {
|
|||||||
TrackNumber int
|
TrackNumber int
|
||||||
DiscNumber int
|
DiscNumber int
|
||||||
ISRC string
|
ISRC string
|
||||||
|
Lyrics string
|
||||||
}
|
}
|
||||||
|
|
||||||
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
||||||
@@ -58,6 +59,9 @@ func EmbedMetadata(filepath string, metadata Metadata, coverPath string) error {
|
|||||||
if metadata.ISRC != "" {
|
if metadata.ISRC != "" {
|
||||||
_ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC)
|
_ = cmt.Add(flacvorbis.FIELD_ISRC, metadata.ISRC)
|
||||||
}
|
}
|
||||||
|
if metadata.Lyrics != "" {
|
||||||
|
_ = cmt.Add("LYRICS", metadata.Lyrics) // Or "UNSYNCEDLYRICS" for unsynced
|
||||||
|
}
|
||||||
|
|
||||||
cmtBlock := cmt.Marshal()
|
cmtBlock := cmt.Marshal()
|
||||||
if cmtIdx < 0 {
|
if cmtIdx < 0 {
|
||||||
@@ -113,6 +117,62 @@ func fileExists(path string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmbedLyricsOnly adds lyrics to a FLAC file while preserving existing metadata
|
||||||
|
func EmbedLyricsOnly(filepath string, lyrics string) error {
|
||||||
|
if lyrics == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := flac.ParseFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse FLAC file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmtIdx = -1
|
||||||
|
var existingCmt *flacvorbis.MetaDataBlockVorbisComment
|
||||||
|
for idx, block := range f.Meta {
|
||||||
|
if block.Type == flac.VorbisComment {
|
||||||
|
cmtIdx = idx
|
||||||
|
existingCmt, err = flacvorbis.ParseFromMetaDataBlock(*block)
|
||||||
|
if err != nil {
|
||||||
|
existingCmt = nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new comment block, preserving existing comments
|
||||||
|
cmt := flacvorbis.New()
|
||||||
|
|
||||||
|
// Copy existing comments except LYRICS
|
||||||
|
if existingCmt != nil {
|
||||||
|
for _, comment := range existingCmt.Comments {
|
||||||
|
parts := strings.SplitN(comment, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
fieldName := strings.ToUpper(parts[0])
|
||||||
|
if fieldName != "LYRICS" && fieldName != "UNSYNCEDLYRICS" && fieldName != "SYNCEDLYRICS" {
|
||||||
|
_ = cmt.Add(parts[0], parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lyrics
|
||||||
|
_ = cmt.Add("LYRICS", lyrics)
|
||||||
|
|
||||||
|
cmtBlock := cmt.Marshal()
|
||||||
|
if cmtIdx < 0 {
|
||||||
|
f.Meta = append(f.Meta, &cmtBlock)
|
||||||
|
} else {
|
||||||
|
f.Meta[cmtIdx] = &cmtBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Save(filepath); err != nil {
|
||||||
|
return fmt.Errorf("failed to save FLAC file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReadISRCFromFile reads ISRC metadata from a FLAC file
|
// ReadISRCFromFile reads ISRC metadata from a FLAC file
|
||||||
func ReadISRCFromFile(filepath string) (string, error) {
|
func ReadISRCFromFile(filepath string) (string, error) {
|
||||||
if !fileExists(filepath) {
|
if !fileExists(filepath) {
|
||||||
|
|||||||
+7
-1
@@ -169,7 +169,13 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
|||||||
|
|
||||||
func (q *QobuzDownloader) DownloadFile(url, filepath string) error {
|
func (q *QobuzDownloader) DownloadFile(url, filepath string) error {
|
||||||
fmt.Println("Starting file download...")
|
fmt.Println("Starting file download...")
|
||||||
resp, err := q.client.Get(url)
|
// Use a separate client with a longer timeout. The default client's 60s limit
|
||||||
|
// causes downloads to fail on slow connections or for large Hi-Res files.
|
||||||
|
downloadClient := &http.Client{
|
||||||
|
Timeout: 5 * time.Minute, // 5 minutes for large files
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := downloadClient.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download file: %w", err)
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := t.client.Get(url)
|
resp, err := t.client.Get(url)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download file: %w", err)
|
return fmt.Errorf("failed to download file: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user