v7.0.8
This commit is contained in:
+129
-25
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -25,13 +26,9 @@ type SongLinkResponse struct {
|
||||
} `json:"linksByPlatform"`
|
||||
}
|
||||
|
||||
type AfkarXYZResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
DirectLink string `json:"direct_link"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
} `json:"data"`
|
||||
type AmazonStreamResponse struct {
|
||||
StreamURL string `json:"streamUrl"`
|
||||
DecryptionKey string `json:"decryptionKey"`
|
||||
}
|
||||
|
||||
func NewAmazonDownloader() *AmazonDownloader {
|
||||
@@ -55,6 +52,7 @@ func (a *AmazonDownloader) GetAmazonURLFromSpotify(spotifyTrackID string) (strin
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
fmt.Println("Getting Amazon URL...")
|
||||
|
||||
@@ -108,13 +106,21 @@ func (a *AmazonDownloader) GetAmazonURLFromSpotify(spotifyTrackID string) (strin
|
||||
}
|
||||
|
||||
func (a *AmazonDownloader) DownloadFromAfkarXYZ(amazonURL, outputDir, quality string) (string, error) {
|
||||
apiURL := "https://amazon.afkarxyz.fun/convert?url=" + url.QueryEscape(amazonURL)
|
||||
|
||||
asinRegex := regexp.MustCompile(`(B[0-9A-Z]{9})`)
|
||||
asin := asinRegex.FindString(amazonURL)
|
||||
if asin == "" {
|
||||
return "", fmt.Errorf("failed to extract ASIN from URL: %s", amazonURL)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("https://amazon.afkarxyz.fun/api/track/%s", asin)
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
fmt.Printf("Fetching from AfkarXYZ...\n")
|
||||
fmt.Printf("Fetching from Amazon API (ASIN: %s)...\n", asin)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -122,27 +128,25 @@ func (a *AmazonDownloader) DownloadFromAfkarXYZ(amazonURL, outputDir, quality st
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("AfkarXYZ API returned status %d", resp.StatusCode)
|
||||
return "", fmt.Errorf("Amazon API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
var apiResp AfkarXYZResponse
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var apiResp AmazonStreamResponse
|
||||
if err := json.Unmarshal(bodyBytes, &apiResp); err != nil {
|
||||
return "", fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
if !apiResp.Success || apiResp.Data.DirectLink == "" {
|
||||
return "", fmt.Errorf("AfkarXYZ failed or no link found")
|
||||
if apiResp.StreamURL == "" {
|
||||
return "", fmt.Errorf("no stream URL found in response")
|
||||
}
|
||||
|
||||
downloadURL := apiResp.Data.DirectLink
|
||||
fileName := apiResp.Data.FileName
|
||||
if fileName == "" {
|
||||
fileName = "track.flac"
|
||||
}
|
||||
|
||||
reg := regexp.MustCompile(`[<>:"/\\|?*]`)
|
||||
fileName = reg.ReplaceAllString(fileName, "")
|
||||
downloadURL := apiResp.StreamURL
|
||||
fileName := fmt.Sprintf("%s.m4a", asin)
|
||||
filePath := filepath.Join(outputDir, fileName)
|
||||
|
||||
out, err := os.Create(filePath)
|
||||
@@ -152,6 +156,7 @@ func (a *AmazonDownloader) DownloadFromAfkarXYZ(amazonURL, outputDir, quality st
|
||||
defer out.Close()
|
||||
|
||||
dlReq, _ := http.NewRequest("GET", downloadURL, nil)
|
||||
dlReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
dlResp, err := a.client.Do(dlReq)
|
||||
if err != nil {
|
||||
@@ -159,7 +164,7 @@ func (a *AmazonDownloader) DownloadFromAfkarXYZ(amazonURL, outputDir, quality st
|
||||
}
|
||||
defer dlResp.Body.Close()
|
||||
|
||||
fmt.Printf("Downloading from AfkarXYZ: %s\n", fileName)
|
||||
fmt.Printf("Downloading track: %s\n", fileName)
|
||||
pw := NewProgressWriter(out)
|
||||
_, err = io.Copy(pw, dlResp.Body)
|
||||
if err != nil {
|
||||
@@ -169,6 +174,86 @@ func (a *AmazonDownloader) DownloadFromAfkarXYZ(amazonURL, outputDir, quality st
|
||||
}
|
||||
|
||||
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
|
||||
|
||||
if apiResp.DecryptionKey != "" {
|
||||
fmt.Printf("Decrypting file...\n")
|
||||
|
||||
ffprobePath, err := GetFFprobePath()
|
||||
var codec string
|
||||
if err == nil {
|
||||
cmdProbe := exec.Command(ffprobePath,
|
||||
"-v", "quiet",
|
||||
"-select_streams", "a:0",
|
||||
"-show_entries", "stream=codec_name",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
filePath,
|
||||
)
|
||||
setHideWindow(cmdProbe)
|
||||
codecOutput, _ := cmdProbe.Output()
|
||||
codec = strings.TrimSpace(string(codecOutput))
|
||||
fmt.Printf("Detected codec: %s\n", codec)
|
||||
}
|
||||
|
||||
targetExt := ".m4a"
|
||||
if codec == "flac" {
|
||||
targetExt = ".flac"
|
||||
}
|
||||
|
||||
decryptedFilename := "dec_" + fileName + targetExt
|
||||
|
||||
if targetExt == ".flac" && strings.HasSuffix(fileName, ".m4a") {
|
||||
decryptedFilename = "dec_" + strings.TrimSuffix(fileName, ".m4a") + ".flac"
|
||||
}
|
||||
|
||||
decryptedPath := filepath.Join(outputDir, decryptedFilename)
|
||||
|
||||
ffmpegPath, err := GetFFmpegPath()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ffmpeg not found for decryption: %w", err)
|
||||
}
|
||||
|
||||
if err := ValidateExecutable(ffmpegPath); err != nil {
|
||||
return "", fmt.Errorf("invalid ffmpeg executable: %w", err)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(apiResp.DecryptionKey)
|
||||
|
||||
cmd := exec.Command(ffmpegPath,
|
||||
"-decryption_key", key,
|
||||
"-i", filePath,
|
||||
"-c", "copy",
|
||||
"-y",
|
||||
decryptedPath,
|
||||
)
|
||||
|
||||
setHideWindow(cmd)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
|
||||
outStr := string(output)
|
||||
if len(outStr) > 500 {
|
||||
outStr = outStr[len(outStr)-500:]
|
||||
}
|
||||
return "", fmt.Errorf("ffmpeg decryption failed: %v\nTail Output: %s", err, outStr)
|
||||
}
|
||||
|
||||
if info, err := os.Stat(decryptedPath); err != nil || info.Size() == 0 {
|
||||
return "", fmt.Errorf("decrypted file missing or empty")
|
||||
}
|
||||
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
fmt.Printf("Warning: Failed to remove encrypted file: %v\n", err)
|
||||
}
|
||||
|
||||
finalPath := filepath.Join(outputDir, strings.TrimPrefix(decryptedFilename, "dec_"))
|
||||
if err := os.Rename(decryptedPath, finalPath); err != nil {
|
||||
return "", fmt.Errorf("failed to rename decrypted file: %w", err)
|
||||
}
|
||||
filePath = finalPath
|
||||
|
||||
fmt.Println("Decryption successful")
|
||||
}
|
||||
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
@@ -201,6 +286,9 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
||||
return "", err
|
||||
}
|
||||
|
||||
originalFileDir := filepath.Dir(filePath)
|
||||
originalFileBase := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
|
||||
|
||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||
safeArtist := sanitizeFilename(spotifyArtistName)
|
||||
safeTitle := sanitizeFilename(spotifyTrackName)
|
||||
@@ -252,7 +340,11 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
||||
}
|
||||
}
|
||||
|
||||
newFilename = newFilename + ".flac"
|
||||
ext := filepath.Ext(filePath)
|
||||
if ext == "" {
|
||||
ext = ".flac"
|
||||
}
|
||||
newFilename = newFilename + ext
|
||||
newFilePath := filepath.Join(outputDir, newFilename)
|
||||
|
||||
if err := os.Rename(filePath, newFilePath); err != nil {
|
||||
@@ -300,12 +392,24 @@ func (a *AmazonDownloader) DownloadByURL(amazonURL, outputDir, quality, filename
|
||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||
}
|
||||
|
||||
if err := EmbedMetadata(filePath, metadata, coverPath); err != nil {
|
||||
if err := EmbedMetadataToConvertedFile(filePath, metadata, coverPath); err != nil {
|
||||
fmt.Printf("Warning: Failed to embed metadata: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Metadata embedded successfully")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(strings.ToLower(filePath), ".flac") {
|
||||
|
||||
originalM4aPath := filepath.Join(originalFileDir, originalFileBase+".m4a")
|
||||
if _, err := os.Stat(originalM4aPath); err == nil {
|
||||
if err := os.Remove(originalM4aPath); err != nil {
|
||||
fmt.Printf("Warning: Failed to remove M4A file: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Cleaned up original M4A file: %s\n", filepath.Base(originalM4aPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Done")
|
||||
fmt.Println("✓ Downloaded successfully from Amazon Music")
|
||||
return filePath, nil
|
||||
|
||||
+25
-15
@@ -145,10 +145,19 @@ func (q *QobuzDownloader) mapJumoQuality(quality string) int {
|
||||
func (q *QobuzDownloader) DownloadFromJumo(trackID int64, quality string) (string, error) {
|
||||
formatID := q.mapJumoQuality(quality)
|
||||
region := "US"
|
||||
url := fmt.Sprintf("https://jumo-dl.pages.dev/file?track_id=%d&format_id=%d®ion=%s", trackID, formatID, region)
|
||||
url := fmt.Sprintf("https://jumo-dl.pages.dev/get?track_id=%d&format_id=%d®ion=%s", trackID, formatID, region)
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Referer", "https://jumo-dl.pages.dev/")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -163,7 +172,9 @@ func (q *QobuzDownloader) DownloadFromJumo(trackID int64, quality string) (strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
var result struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
|
||||
@@ -173,18 +184,8 @@ func (q *QobuzDownloader) DownloadFromJumo(trackID int64, quality string) (strin
|
||||
}
|
||||
}
|
||||
|
||||
if urlVal, ok := result["url"].(string); ok && urlVal != "" {
|
||||
return urlVal, nil
|
||||
}
|
||||
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if urlVal, ok := data["url"].(string); ok && urlVal != "" {
|
||||
return urlVal, nil
|
||||
}
|
||||
}
|
||||
|
||||
if linkVal, ok := result["link"].(string); ok && linkVal != "" {
|
||||
return linkVal, nil
|
||||
if result.URL != "" {
|
||||
return result.URL, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("URL not found in Jumo response")
|
||||
@@ -216,6 +217,15 @@ func (q *QobuzDownloader) DownloadFromStandard(apiBase string, trackID int64, qu
|
||||
return streamResp.URL, nil
|
||||
}
|
||||
|
||||
var nestedResp struct {
|
||||
Data struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &nestedResp); err == nil && nestedResp.Data.URL != "" {
|
||||
return nestedResp.Data.URL, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid response")
|
||||
}
|
||||
|
||||
|
||||
+72
-2
@@ -767,8 +767,57 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
||||
if discNumber == 0 {
|
||||
discNumber = 1
|
||||
}
|
||||
|
||||
maxDiscFromAlbum := 0
|
||||
totalDiscsFromAlbum := 0
|
||||
|
||||
if len(albumFetchData) > 0 && albumFetchData[0] != nil {
|
||||
albumUnion := getMap(getMap(albumFetchData[0], "data"), "albumUnion")
|
||||
if len(albumUnion) > 0 {
|
||||
discsData := getMap(albumUnion, "discs")
|
||||
if len(discsData) > 0 {
|
||||
totalDiscsFromAlbum = int(getFloat64(discsData, "totalCount"))
|
||||
}
|
||||
|
||||
albumTracks := getMap(albumUnion, "tracks")
|
||||
if len(albumTracks) > 0 {
|
||||
albumTrackItems := getSlice(albumTracks, "items")
|
||||
currentTrackID := getString(trackData, "id")
|
||||
for idx, item := range albumTrackItems {
|
||||
itemMap, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
trackItem := getMap(itemMap, "track")
|
||||
if len(trackItem) > 0 {
|
||||
dNum := int(getFloat64(trackItem, "discNumber"))
|
||||
if dNum > maxDiscFromAlbum {
|
||||
maxDiscFromAlbum = dNum
|
||||
}
|
||||
|
||||
trackURI := getString(trackItem, "uri")
|
||||
if strings.Contains(trackURI, currentTrackID) || getString(trackItem, "id") == currentTrackID {
|
||||
if dNum > 0 {
|
||||
discNumber = dNum
|
||||
}
|
||||
}
|
||||
|
||||
trackNum := int(getFloat64(trackData, "trackNumber"))
|
||||
itemTrackNum := idx + 1
|
||||
if trackNum == itemTrackNum && dNum > 0 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalDiscs := 1
|
||||
if discInfo["totalDiscs"] != nil {
|
||||
if totalDiscsFromAlbum > 0 {
|
||||
totalDiscs = totalDiscsFromAlbum
|
||||
} else if maxDiscFromAlbum > 0 {
|
||||
totalDiscs = maxDiscFromAlbum
|
||||
} else if discInfo["totalDiscs"] != nil {
|
||||
totalDiscs = discInfo["totalDiscs"].(int)
|
||||
}
|
||||
|
||||
@@ -878,6 +927,11 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
||||
contentRating := getMap(track, "contentRating")
|
||||
isExplicit := getString(contentRating, "label") == "EXPLICIT"
|
||||
|
||||
discNumber := int(getFloat64(track, "discNumber"))
|
||||
if discNumber == 0 {
|
||||
discNumber = 1
|
||||
}
|
||||
|
||||
trackInfo := map[string]interface{}{
|
||||
"id": trackID,
|
||||
"name": getString(track, "name"),
|
||||
@@ -886,6 +940,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
||||
"duration": durationString,
|
||||
"plays": getString(track, "playcount"),
|
||||
"is_explicit": isExplicit,
|
||||
"disc_number": discNumber,
|
||||
}
|
||||
tracks = append(tracks, trackInfo)
|
||||
}
|
||||
@@ -905,6 +960,12 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
||||
albumID = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
totalDiscs := 1
|
||||
discsData := getMap(albumData, "discs")
|
||||
if len(discsData) > 0 {
|
||||
totalDiscs = int(getFloat64(discsData, "totalCount"))
|
||||
}
|
||||
|
||||
filtered := map[string]interface{}{
|
||||
"id": albumID,
|
||||
"name": getString(albumData, "name"),
|
||||
@@ -913,6 +974,9 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
||||
"releaseDate": releaseDate,
|
||||
"count": len(tracks),
|
||||
"tracks": tracks,
|
||||
"discs": map[string]interface{}{
|
||||
"totalCount": totalDiscs,
|
||||
},
|
||||
}
|
||||
|
||||
return filtered
|
||||
@@ -1103,10 +1167,15 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} {
|
||||
contentRating := getMap(trackData, "contentRating")
|
||||
isExplicit := getString(contentRating, "label") == "EXPLICIT"
|
||||
|
||||
trackName := getString(trackData, "name")
|
||||
if trackName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
trackInfo := map[string]interface{}{
|
||||
"id": trackID,
|
||||
"cover": trackCover,
|
||||
"title": getString(trackData, "name"),
|
||||
"title": trackName,
|
||||
"artist": artistsString,
|
||||
"artistIds": artistIDs,
|
||||
"plays": rank,
|
||||
@@ -1116,6 +1185,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} {
|
||||
"albumId": albumID,
|
||||
"duration": durationString,
|
||||
"is_explicit": isExplicit,
|
||||
"disc_number": int(getFloat64(trackData, "discNumber")),
|
||||
}
|
||||
tracks = append(tracks, trackInfo)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetSpotifyDataWithAPI(ctx context.Context, spotifyURL string, useAPI bool, apiBaseURL string, batch bool, delay time.Duration) (interface{}, error) {
|
||||
if !useAPI || apiBaseURL == "" {
|
||||
|
||||
return GetFilteredSpotifyData(ctx, spotifyURL, batch, delay)
|
||||
}
|
||||
|
||||
spotifyType, id := parseSpotifyURLToTypeAndID(spotifyURL)
|
||||
if spotifyType == "" || id == "" {
|
||||
return nil, fmt.Errorf("invalid Spotify URL: %s", spotifyURL)
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s/%s/%s", strings.TrimSuffix(apiBaseURL, "/"), spotifyType, id)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create API request: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SpotFetch API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("SpotFetch API error: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read API response: %w", err)
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
|
||||
switch spotifyType {
|
||||
case "track":
|
||||
var trackResp TrackResponse
|
||||
if err := json.Unmarshal(bodyBytes, &trackResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode track response: %w", err)
|
||||
}
|
||||
data = trackResp
|
||||
case "album":
|
||||
var albumResp AlbumResponsePayload
|
||||
if err := json.Unmarshal(bodyBytes, &albumResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode album response: %w", err)
|
||||
}
|
||||
data = &albumResp
|
||||
case "playlist":
|
||||
var playlistResp PlaylistResponsePayload
|
||||
if err := json.Unmarshal(bodyBytes, &playlistResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode playlist response: %w", err)
|
||||
}
|
||||
data = playlistResp
|
||||
case "artist":
|
||||
var artistResp ArtistDiscographyPayload
|
||||
if err := json.Unmarshal(bodyBytes, &artistResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode artist response: %w", err)
|
||||
}
|
||||
data = &artistResp
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Spotify type: %s", spotifyType)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func parseSpotifyURLToTypeAndID(url string) (string, string) {
|
||||
|
||||
if strings.HasPrefix(url, "spotify:") {
|
||||
parts := strings.Split(url, ":")
|
||||
if len(parts) >= 3 {
|
||||
return parts[1], parts[2]
|
||||
}
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`spotify\.com/(track|album|playlist|artist)/([a-zA-Z0-9]+)`)
|
||||
matches := re.FindStringSubmatch(url)
|
||||
if len(matches) == 3 {
|
||||
return matches[1], matches[2]
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
+47
-19
@@ -210,7 +210,10 @@ type apiAlbumResponse struct {
|
||||
Cover string `json:"cover"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Count int `json:"count"`
|
||||
Tracks []struct {
|
||||
Discs struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
} `json:"discs"`
|
||||
Tracks []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Artists string `json:"artists"`
|
||||
@@ -218,6 +221,7 @@ type apiAlbumResponse struct {
|
||||
Duration string `json:"duration"`
|
||||
Plays string `json:"plays"`
|
||||
IsExplicit bool `json:"is_explicit"`
|
||||
DiscNumber int `json:"disc_number"`
|
||||
} `json:"tracks"`
|
||||
}
|
||||
|
||||
@@ -245,6 +249,7 @@ type apiPlaylistResponse struct {
|
||||
AlbumID string `json:"albumId"`
|
||||
Duration string `json:"duration"`
|
||||
IsExplicit bool `json:"is_explicit"`
|
||||
DiscNumber int `json:"disc_number"`
|
||||
} `json:"tracks"`
|
||||
}
|
||||
|
||||
@@ -432,22 +437,45 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
|
||||
}
|
||||
|
||||
if albumID != "" {
|
||||
albumPayload := map[string]interface{}{
|
||||
"variables": map[string]interface{}{
|
||||
"uri": fmt.Sprintf("spotify:album:%s", albumID),
|
||||
"locale": "",
|
||||
"offset": 0,
|
||||
"limit": 1,
|
||||
},
|
||||
"operationName": "getAlbum",
|
||||
"extensions": map[string]interface{}{
|
||||
"persistedQuery": map[string]interface{}{
|
||||
"version": 1,
|
||||
"sha256Hash": "b9bfabef66ed756e5e13f68a942deb60bd4125ec1f1be8cc42769dc0259b4b10",
|
||||
|
||||
albumResponse, err := c.fetchAlbumWithClient(ctx, client, albumID)
|
||||
if err == nil && albumResponse != nil {
|
||||
|
||||
albumJSON, _ := json.Marshal(albumResponse)
|
||||
var albumMap map[string]interface{}
|
||||
json.Unmarshal(albumJSON, &albumMap)
|
||||
|
||||
tracksItems := []interface{}{}
|
||||
if albumMap["tracks"] != nil {
|
||||
if trackList, ok := albumMap["tracks"].([]interface{}); ok {
|
||||
for _, t := range trackList {
|
||||
if trackMap, ok := t.(map[string]interface{}); ok {
|
||||
tracksItems = append(tracksItems, map[string]interface{}{
|
||||
"track": map[string]interface{}{
|
||||
"discNumber": trackMap["disc_number"],
|
||||
"id": trackMap["id"],
|
||||
"uri": fmt.Sprintf("spotify:track:%s", trackMap["id"]),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
albumFetchData = map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"albumUnion": map[string]interface{}{
|
||||
"discs": map[string]interface{}{
|
||||
"totalCount": albumResponse.Discs.TotalCount,
|
||||
},
|
||||
"tracks": map[string]interface{}{
|
||||
"items": tracksItems,
|
||||
"totalCount": albumResponse.Count,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
albumFetchData, _ = client.Query(albumPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -914,8 +942,8 @@ func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumRe
|
||||
ReleaseDate: raw.ReleaseDate,
|
||||
TrackNumber: trackNumber,
|
||||
TotalTracks: raw.Count,
|
||||
DiscNumber: 1,
|
||||
TotalDiscs: 0,
|
||||
DiscNumber: item.DiscNumber,
|
||||
TotalDiscs: raw.Discs.TotalCount,
|
||||
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
|
||||
ISRC: item.ID,
|
||||
AlbumID: raw.ID,
|
||||
@@ -974,7 +1002,7 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) Pla
|
||||
ReleaseDate: "",
|
||||
TrackNumber: 0,
|
||||
TotalTracks: 0,
|
||||
DiscNumber: 1,
|
||||
DiscNumber: item.DiscNumber,
|
||||
TotalDiscs: 0,
|
||||
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", item.ID),
|
||||
ISRC: item.ID,
|
||||
@@ -1094,7 +1122,7 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
|
||||
ReleaseDate: albumData.ReleaseDate,
|
||||
TrackNumber: trackNumber,
|
||||
TotalTracks: albumData.Count,
|
||||
DiscNumber: 1,
|
||||
DiscNumber: tr.DiscNumber,
|
||||
ExternalURL: fmt.Sprintf("https://open.spotify.com/track/%s", tr.ID),
|
||||
ISRC: tr.ID,
|
||||
AlbumID: albumID,
|
||||
|
||||
+129
-70
@@ -101,6 +101,8 @@ func (t *TidalDownloader) GetTidalURLFromSpotify(spotifyTrackID string) (string,
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
fmt.Println("Getting Tidal URL...")
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
@@ -157,7 +159,15 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (string,
|
||||
url := fmt.Sprintf("%s/track/?id=%d&quality=%s", t.apiURL, trackID, quality)
|
||||
fmt.Printf("Tidal API URL: %s\n", url)
|
||||
|
||||
resp, err := t.client.Get(url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("✗ failed to create request: %v\n", err)
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("✗ Tidal API request failed: %v\n", err)
|
||||
return "", fmt.Errorf("failed to get download URL: %w", err)
|
||||
@@ -214,7 +224,14 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
|
||||
return t.DownloadFromManifest(strings.TrimPrefix(url, "MANIFEST:"), filepath)
|
||||
}
|
||||
|
||||
resp, err := t.client.Get(url)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download file: %w", err)
|
||||
@@ -244,7 +261,7 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
|
||||
}
|
||||
|
||||
func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) error {
|
||||
directURL, initURL, mediaURLs, err := parseManifest(manifestB64)
|
||||
directURL, initURL, mediaURLs, mimeType, err := parseManifest(manifestB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse manifest: %w", err)
|
||||
}
|
||||
@@ -253,10 +270,19 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
|
||||
Timeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
if directURL != "" {
|
||||
doRequest := func(url string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
if directURL != "" && (strings.Contains(strings.ToLower(mimeType), "flac") || mimeType == "") {
|
||||
fmt.Println("Downloading file...")
|
||||
|
||||
resp, err := client.Get(directURL)
|
||||
resp, err := doRequest(directURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download file: %w", err)
|
||||
}
|
||||
@@ -283,83 +309,116 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading %d segments...\n", len(mediaURLs)+1)
|
||||
|
||||
tempPath := outputPath + ".m4a.tmp"
|
||||
out, err := os.Create(tempPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print("Downloading init segment... ")
|
||||
resp, err := client.Get(initURL)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to download init segment: %w", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("init segment download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to write init segment: %w", err)
|
||||
}
|
||||
fmt.Println("OK")
|
||||
if directURL != "" {
|
||||
fmt.Printf("Downloading non-FLAC file (%s)...\n", mimeType)
|
||||
|
||||
totalSegments := len(mediaURLs)
|
||||
var totalBytes int64
|
||||
lastTime := time.Now()
|
||||
var lastBytes int64
|
||||
for i, mediaURL := range mediaURLs {
|
||||
resp, err := client.Get(mediaURL)
|
||||
resp, err := doRequest(directURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download file: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
out, err := os.Create(tempPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
pw := NewProgressWriter(out)
|
||||
_, err = io.Copy(pw, resp.Body)
|
||||
out.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to write temp file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
|
||||
|
||||
} else {
|
||||
|
||||
fmt.Printf("Downloading %d segments...\n", len(mediaURLs)+1)
|
||||
|
||||
out, err := os.Create(tempPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print("Downloading init segment... ")
|
||||
resp, err := doRequest(initURL)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to download segment %d: %w", i+1, err)
|
||||
return fmt.Errorf("failed to download init segment: %w", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("segment %d download failed with status %d", i+1, resp.StatusCode)
|
||||
return fmt.Errorf("init segment download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
n, err := io.Copy(out, resp.Body)
|
||||
totalBytes += n
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to write segment %d: %w", i+1, err)
|
||||
return fmt.Errorf("failed to write init segment: %w", err)
|
||||
}
|
||||
fmt.Println("OK")
|
||||
|
||||
totalSegments := len(mediaURLs)
|
||||
var totalBytes int64
|
||||
lastTime := time.Now()
|
||||
var lastBytes int64
|
||||
for i, mediaURL := range mediaURLs {
|
||||
resp, err := doRequest(mediaURL)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to download segment %d: %w", i+1, err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("segment %d download failed with status %d", i+1, resp.StatusCode)
|
||||
}
|
||||
n, err := io.Copy(out, resp.Body)
|
||||
totalBytes += n
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(tempPath)
|
||||
return fmt.Errorf("failed to write segment %d: %w", i+1, err)
|
||||
}
|
||||
|
||||
mbDownloaded := float64(totalBytes) / (1024 * 1024)
|
||||
now := time.Now()
|
||||
timeDiff := now.Sub(lastTime).Seconds()
|
||||
var speedMBps float64
|
||||
if timeDiff > 0.1 {
|
||||
bytesDiff := float64(totalBytes - lastBytes)
|
||||
speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff
|
||||
SetDownloadSpeed(speedMBps)
|
||||
lastTime = now
|
||||
lastBytes = totalBytes
|
||||
}
|
||||
SetDownloadProgress(mbDownloaded)
|
||||
|
||||
fmt.Printf("\rDownloading: %.2f MB (%d/%d segments)", mbDownloaded, i+1, totalSegments)
|
||||
}
|
||||
|
||||
mbDownloaded := float64(totalBytes) / (1024 * 1024)
|
||||
now := time.Now()
|
||||
timeDiff := now.Sub(lastTime).Seconds()
|
||||
var speedMBps float64
|
||||
if timeDiff > 0.1 {
|
||||
bytesDiff := float64(totalBytes - lastBytes)
|
||||
speedMBps = (bytesDiff / (1024 * 1024)) / timeDiff
|
||||
SetDownloadSpeed(speedMBps)
|
||||
lastTime = now
|
||||
lastBytes = totalBytes
|
||||
}
|
||||
SetDownloadProgress(mbDownloaded)
|
||||
out.Close()
|
||||
|
||||
fmt.Printf("\rDownloading: %.2f MB (%d/%d segments)", mbDownloaded, i+1, totalSegments)
|
||||
tempInfo, _ := os.Stat(tempPath)
|
||||
fmt.Printf("\rDownloaded: %.2f MB (Complete) \n", float64(tempInfo.Size())/(1024*1024))
|
||||
}
|
||||
|
||||
out.Close()
|
||||
|
||||
tempInfo, _ := os.Stat(tempPath)
|
||||
fmt.Printf("\rDownloaded: %.2f MB (Complete) \n", float64(tempInfo.Size())/(1024*1024))
|
||||
|
||||
fmt.Println("Converting to FLAC...")
|
||||
ffmpegPath, err := GetFFmpegPath()
|
||||
if err != nil {
|
||||
@@ -633,10 +692,10 @@ type MPD struct {
|
||||
} `xml:"Period"`
|
||||
}
|
||||
|
||||
func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, err error) {
|
||||
func parseManifest(manifestB64 string) (directURL string, initURL string, mediaURLs []string, mimeType string, err error) {
|
||||
manifestBytes, err := base64.StdEncoding.DecodeString(manifestB64)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to decode manifest: %w", err)
|
||||
return "", "", nil, "", fmt.Errorf("failed to decode manifest: %w", err)
|
||||
}
|
||||
|
||||
manifestStr := string(manifestBytes)
|
||||
@@ -644,15 +703,15 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
if strings.HasPrefix(strings.TrimSpace(manifestStr), "{") {
|
||||
var btsManifest TidalBTSManifest
|
||||
if err := json.Unmarshal(manifestBytes, &btsManifest); err != nil {
|
||||
return "", "", nil, fmt.Errorf("failed to parse BTS manifest: %w", err)
|
||||
return "", "", nil, "", fmt.Errorf("failed to parse BTS manifest: %w", err)
|
||||
}
|
||||
|
||||
if len(btsManifest.URLs) == 0 {
|
||||
return "", "", nil, fmt.Errorf("no URLs in BTS manifest")
|
||||
return "", "", nil, "", fmt.Errorf("no URLs in BTS manifest")
|
||||
}
|
||||
|
||||
fmt.Printf("Manifest: BTS format (%s, %s)\n", btsManifest.MimeType, btsManifest.Codecs)
|
||||
return btsManifest.URLs[0], "", nil, nil
|
||||
return btsManifest.URLs[0], "", nil, btsManifest.MimeType, nil
|
||||
}
|
||||
|
||||
fmt.Println("Manifest: DASH format")
|
||||
@@ -717,7 +776,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
mediaURL := strings.ReplaceAll(mediaTemplate, "$Number$", fmt.Sprintf("%d", i))
|
||||
mediaURLs = append(mediaURLs, mediaURL)
|
||||
}
|
||||
return "", initURL, mediaURLs, nil
|
||||
return "", initURL, mediaURLs, "", nil
|
||||
}
|
||||
|
||||
fmt.Println("Using regex fallback for DASH manifest...")
|
||||
@@ -733,7 +792,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
}
|
||||
|
||||
if initURL == "" {
|
||||
return "", "", nil, fmt.Errorf("no initialization URL found in manifest")
|
||||
return "", "", nil, "", fmt.Errorf("no initialization URL found in manifest")
|
||||
}
|
||||
|
||||
initURL = strings.ReplaceAll(initURL, "&", "&")
|
||||
@@ -754,7 +813,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
}
|
||||
|
||||
if segmentCount == 0 {
|
||||
return "", "", nil, fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches))
|
||||
return "", "", nil, "", fmt.Errorf("no segments found in manifest (XML: %d, Regex: 0)", len(matches))
|
||||
}
|
||||
|
||||
fmt.Printf("Parsed manifest via Regex: %d segments\n", segmentCount)
|
||||
@@ -764,7 +823,7 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
|
||||
mediaURLs = append(mediaURLs, mediaURL)
|
||||
}
|
||||
|
||||
return "", initURL, mediaURLs, nil
|
||||
return "", initURL, mediaURLs, "", nil
|
||||
}
|
||||
|
||||
func getDownloadURLRotated(apis []string, trackID int64, quality string) (string, string, error) {
|
||||
|
||||
+45
-1
@@ -66,7 +66,12 @@ func uploadToService(filename string, fileReader io.Reader) (string, error) {
|
||||
|
||||
writer.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", "https://u1112.send.now/cgi-bin/upload.cgi?upload_type=file&utype=anon", body)
|
||||
uploadURL, err := getUploadURL()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get upload server: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", uploadURL, body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -113,6 +118,45 @@ func uploadToService(filename string, fileReader io.Reader) (string, error) {
|
||||
return fetchDirectImageLink(downloadLink)
|
||||
}
|
||||
|
||||
func getUploadURL() (string, error) {
|
||||
req, err := http.NewRequest("GET", "https://send.now/", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36")
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("failed to fetch main page: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
body := string(bodyBytes)
|
||||
|
||||
re := regexp.MustCompile(`action=["'](https://[^"']+/cgi-bin/upload\.cgi\?upload_type=file[^"']*)["']`)
|
||||
matches := re.FindStringSubmatch(body)
|
||||
if len(matches) > 1 {
|
||||
return matches[1], nil
|
||||
}
|
||||
|
||||
reFallback := regexp.MustCompile(`action=["'](https://[^"']+/cgi-bin/upload\.cgi)`)
|
||||
matchesFallback := reFallback.FindStringSubmatch(body)
|
||||
if len(matchesFallback) > 1 {
|
||||
return matchesFallback[1] + "?upload_type=file&utype=anon", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("upload URL not found in main page")
|
||||
}
|
||||
|
||||
func fetchDirectImageLink(url string) (string, error) {
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user