v7.1.1
This commit is contained in:
+1
-1
@@ -117,7 +117,7 @@ func buildCoverFilename(trackName, artistName, albumName, albumArtist, releaseDa
|
||||
}
|
||||
}
|
||||
|
||||
return filename + ".cover.jpg"
|
||||
return filename + ".jpg"
|
||||
}
|
||||
|
||||
func convertSmallToMedium(imageURL string) string {
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeezerDownloader struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewDeezerDownloader() *DeezerDownloader {
|
||||
return &DeezerDownloader{
|
||||
client: &http.Client{
|
||||
Timeout: 300 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type YoinkifyRequest struct {
|
||||
URL string `json:"url"`
|
||||
Format string `json:"format"`
|
||||
GenreSource string `json:"genreSource"`
|
||||
}
|
||||
|
||||
func (d *DeezerDownloader) DownloadFromYoinkify(spotifyURL, outputDir string) (string, error) {
|
||||
apiURL := "https://yoinkify.lol/api/download"
|
||||
|
||||
payload := YoinkifyRequest{
|
||||
URL: spotifyURL,
|
||||
Format: "flac",
|
||||
GenreSource: "spotify",
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||
|
||||
fmt.Printf("Fetching from Deezer API (Yoinkify)...\n")
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("Deezer API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
tempFileName := fmt.Sprintf("deezer_%d.flac", time.Now().UnixNano())
|
||||
filePath := filepath.Join(outputDir, tempFileName)
|
||||
|
||||
out, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
fmt.Printf("Downloading track from Deezer...\n")
|
||||
pw := NewProgressWriter(out)
|
||||
_, err = io.Copy(pw, resp.Body)
|
||||
if err != nil {
|
||||
out.Close()
|
||||
os.Remove(filePath)
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Printf("\rDownloaded: %.2f MB (Complete)\n", float64(pw.GetTotal())/(1024*1024))
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
func (d *DeezerDownloader) Download(spotifyID, outputDir, filenameFormat, playlistName, playlistOwner string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate, spotifyCoverURL string, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, embedMaxQualityCover bool, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyURL string, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
|
||||
|
||||
if outputDir != "." {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||
filenameArtist := spotifyArtistName
|
||||
filenameAlbumArtist := spotifyAlbumArtist
|
||||
if useFirstArtistOnly {
|
||||
filenameArtist = GetFirstArtist(spotifyArtistName)
|
||||
filenameAlbumArtist = GetFirstArtist(spotifyAlbumArtist)
|
||||
}
|
||||
expectedFilename := BuildExpectedFilename(spotifyTrackName, filenameArtist, spotifyAlbumName, filenameAlbumArtist, spotifyReleaseDate, filenameFormat, playlistName, playlistOwner, includeTrackNumber, position, spotifyDiscNumber, false)
|
||||
expectedPath := filepath.Join(outputDir, expectedFilename)
|
||||
|
||||
if fileInfo, err := os.Stat(expectedPath); err == nil && fileInfo.Size() > 0 {
|
||||
fmt.Printf("File already exists: %s (%.2f MB)\n", expectedPath, float64(fileInfo.Size())/(1024*1024))
|
||||
return "EXISTS:" + expectedPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
type mbResult struct {
|
||||
ISRC string
|
||||
Metadata Metadata
|
||||
}
|
||||
|
||||
metaChan := make(chan mbResult, 1)
|
||||
if (embedGenre || true) && spotifyURL != "" {
|
||||
go func() {
|
||||
res := mbResult{}
|
||||
var isrc string
|
||||
parts := strings.Split(spotifyURL, "/")
|
||||
if len(parts) > 0 {
|
||||
sID := strings.Split(parts[len(parts)-1], "?")[0]
|
||||
if sID != "" {
|
||||
client := NewSongLinkClient()
|
||||
if val, err := client.GetISRC(sID); err == nil {
|
||||
isrc = val
|
||||
}
|
||||
}
|
||||
}
|
||||
res.ISRC = isrc
|
||||
if isrc != "" && embedGenre {
|
||||
fmt.Println("Fetching MusicBrainz metadata...")
|
||||
if fetchedMeta, err := FetchMusicBrainzMetadata(isrc, spotifyTrackName, spotifyArtistName, spotifyAlbumName, useSingleGenre, embedGenre); err == nil {
|
||||
res.Metadata = fetchedMeta
|
||||
fmt.Println("✓ MusicBrainz metadata fetched")
|
||||
} else {
|
||||
fmt.Printf("Warning: Failed to fetch MusicBrainz metadata: %v\n", err)
|
||||
}
|
||||
}
|
||||
metaChan <- res
|
||||
}()
|
||||
} else {
|
||||
close(metaChan)
|
||||
}
|
||||
|
||||
filePath, err := d.DownloadFromYoinkify(spotifyURL, outputDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var isrc string
|
||||
var mbMeta Metadata
|
||||
if spotifyURL != "" {
|
||||
result := <-metaChan
|
||||
isrc = result.ISRC
|
||||
mbMeta = result.Metadata
|
||||
}
|
||||
|
||||
if spotifyTrackName != "" && spotifyArtistName != "" {
|
||||
safeArtist := sanitizeFilename(spotifyArtistName)
|
||||
safeAlbumArtist := sanitizeFilename(spotifyAlbumArtist)
|
||||
|
||||
if useFirstArtistOnly {
|
||||
safeArtist = sanitizeFilename(GetFirstArtist(spotifyArtistName))
|
||||
safeAlbumArtist = sanitizeFilename(GetFirstArtist(spotifyAlbumArtist))
|
||||
}
|
||||
|
||||
safeTitle := sanitizeFilename(spotifyTrackName)
|
||||
safeAlbum := sanitizeFilename(spotifyAlbumName)
|
||||
|
||||
year := ""
|
||||
if len(spotifyReleaseDate) >= 4 {
|
||||
year = spotifyReleaseDate[:4]
|
||||
}
|
||||
|
||||
var newFilename string
|
||||
|
||||
if strings.Contains(filenameFormat, "{") {
|
||||
newFilename = filenameFormat
|
||||
newFilename = strings.ReplaceAll(newFilename, "{title}", safeTitle)
|
||||
newFilename = strings.ReplaceAll(newFilename, "{artist}", safeArtist)
|
||||
newFilename = strings.ReplaceAll(newFilename, "{album}", safeAlbum)
|
||||
newFilename = strings.ReplaceAll(newFilename, "{album_artist}", safeAlbumArtist)
|
||||
newFilename = strings.ReplaceAll(newFilename, "{year}", year)
|
||||
newFilename = strings.ReplaceAll(newFilename, "{date}", SanitizeFilename(spotifyReleaseDate))
|
||||
|
||||
if spotifyDiscNumber > 0 {
|
||||
newFilename = strings.ReplaceAll(newFilename, "{disc}", fmt.Sprintf("%d", spotifyDiscNumber))
|
||||
} else {
|
||||
newFilename = strings.ReplaceAll(newFilename, "{disc}", "")
|
||||
}
|
||||
|
||||
if position > 0 {
|
||||
newFilename = strings.ReplaceAll(newFilename, "{track}", fmt.Sprintf("%02d", position))
|
||||
} else {
|
||||
newFilename = strings.ReplaceAll(newFilename, "{track}", "")
|
||||
}
|
||||
} else {
|
||||
switch filenameFormat {
|
||||
case "artist-title":
|
||||
newFilename = fmt.Sprintf("%s - %s", safeArtist, safeTitle)
|
||||
case "title":
|
||||
newFilename = safeTitle
|
||||
default:
|
||||
newFilename = fmt.Sprintf("%s - %s", safeTitle, safeArtist)
|
||||
}
|
||||
|
||||
if includeTrackNumber && position > 0 {
|
||||
newFilename = fmt.Sprintf("%02d. %s", position, newFilename)
|
||||
}
|
||||
}
|
||||
|
||||
ext := ".flac"
|
||||
newFilename = newFilename + ext
|
||||
newFilePath := filepath.Join(outputDir, newFilename)
|
||||
|
||||
if err := os.Rename(filePath, newFilePath); err != nil {
|
||||
fmt.Printf("Warning: Failed to rename file: %v\n", err)
|
||||
} else {
|
||||
filePath = newFilePath
|
||||
fmt.Printf("Renamed to: %s\n", newFilename)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Embedding Spotify metadata...")
|
||||
|
||||
coverPath := ""
|
||||
if spotifyCoverURL != "" {
|
||||
coverPath = filePath + ".cover.jpg"
|
||||
coverClient := NewCoverClient()
|
||||
if err := coverClient.DownloadCoverToPath(spotifyCoverURL, coverPath, embedMaxQualityCover); err != nil {
|
||||
fmt.Printf("Warning: Failed to download Spotify cover: %v\n", err)
|
||||
coverPath = ""
|
||||
} else {
|
||||
defer os.Remove(coverPath)
|
||||
fmt.Println("Spotify cover downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
trackNumberToEmbed := spotifyTrackNumber
|
||||
if trackNumberToEmbed == 0 {
|
||||
trackNumberToEmbed = 1
|
||||
}
|
||||
|
||||
metadata := Metadata{
|
||||
Title: spotifyTrackName,
|
||||
Artist: spotifyArtistName,
|
||||
Album: spotifyAlbumName,
|
||||
AlbumArtist: spotifyAlbumArtist,
|
||||
Date: spotifyReleaseDate,
|
||||
TrackNumber: trackNumberToEmbed,
|
||||
TotalTracks: spotifyTotalTracks,
|
||||
DiscNumber: spotifyDiscNumber,
|
||||
TotalDiscs: spotifyTotalDiscs,
|
||||
URL: spotifyURL,
|
||||
Copyright: spotifyCopyright,
|
||||
Publisher: spotifyPublisher,
|
||||
Description: "https://github.com/afkarxyz/SpotiFLAC",
|
||||
ISRC: isrc,
|
||||
Genre: mbMeta.Genre,
|
||||
}
|
||||
|
||||
if err := EmbedMetadataToConvertedFile(filePath, metadata, coverPath); err != nil {
|
||||
fmt.Printf("Warning: Failed to embed metadata: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Metadata embedded successfully")
|
||||
}
|
||||
|
||||
fmt.Println("Done")
|
||||
fmt.Println("✓ Downloaded successfully from Deezer")
|
||||
return filePath, nil
|
||||
}
|
||||
+1
-45
@@ -81,28 +81,6 @@ func GetFFmpegPath() (string, error) {
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
homebrewPath := "/opt/homebrew/bin/" + ffmpegName
|
||||
if _, err := os.Stat(homebrewPath); err == nil {
|
||||
return homebrewPath, nil
|
||||
}
|
||||
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
|
||||
homebrewPath := "/usr/local/bin/" + ffmpegName
|
||||
if _, err := os.Stat(homebrewPath); err == nil {
|
||||
return homebrewPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
path, err := exec.Command("which", ffmpegName).Output()
|
||||
if err == nil {
|
||||
trimmed := strings.TrimSpace(string(path))
|
||||
if trimmed != "" {
|
||||
return trimmed, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(ffmpegName)
|
||||
if err == nil {
|
||||
return path, nil
|
||||
@@ -127,28 +105,6 @@ func GetFFprobePath() (string, error) {
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
homebrewPath := "/opt/homebrew/bin/" + ffprobeName
|
||||
if _, err := os.Stat(homebrewPath); err == nil {
|
||||
return homebrewPath, nil
|
||||
}
|
||||
} else if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
|
||||
homebrewPath := "/usr/local/bin/" + ffprobeName
|
||||
if _, err := os.Stat(homebrewPath); err == nil {
|
||||
return homebrewPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
path, err := exec.Command("which", ffprobeName).Output()
|
||||
if err == nil {
|
||||
trimmed := strings.TrimSpace(string(path))
|
||||
if trimmed != "" {
|
||||
return trimmed, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(ffprobeName)
|
||||
if err == nil {
|
||||
return path, nil
|
||||
@@ -295,7 +251,7 @@ func downloadAndExtract(url, destDir string, progressCallback func(int), progres
|
||||
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/120.0.0.0 Safari/537.36")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
+27
-1
@@ -1,7 +1,9 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -133,10 +135,34 @@ func GetFirstArtist(artistString string) string {
|
||||
}
|
||||
|
||||
func NormalizePath(folderPath string) string {
|
||||
|
||||
return strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
||||
}
|
||||
|
||||
func GetSeparator() string {
|
||||
dir, err := GetFFmpegDir()
|
||||
if err != nil {
|
||||
return "; "
|
||||
}
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return "; "
|
||||
}
|
||||
|
||||
var settings map[string]interface{}
|
||||
if err := json.Unmarshal(data, &settings); err == nil {
|
||||
if sep, ok := settings["separator"].(string); ok {
|
||||
if sep == "comma" {
|
||||
return ", "
|
||||
}
|
||||
if sep == "semicolon" {
|
||||
return "; "
|
||||
}
|
||||
}
|
||||
}
|
||||
return "; "
|
||||
}
|
||||
|
||||
func SanitizeFolderPath(folderPath string) string {
|
||||
|
||||
normalizedPath := strings.ReplaceAll(folderPath, "/", string(filepath.Separator))
|
||||
|
||||
+99
-116
@@ -37,17 +37,6 @@ type LyricsResponse struct {
|
||||
Lines []LyricsLine `json:"lines"`
|
||||
}
|
||||
|
||||
type SpotifyLyricsLine struct {
|
||||
TimeTag string `json:"timeTag"`
|
||||
Words string `json:"words"`
|
||||
}
|
||||
|
||||
type SpotifyLyricsAPIResponse struct {
|
||||
Error bool `json:"error"`
|
||||
SyncType string `json:"syncType"`
|
||||
Lines []SpotifyLyricsLine `json:"lines"`
|
||||
}
|
||||
|
||||
type LyricsDownloadRequest struct {
|
||||
SpotifyID string `json:"spotify_id"`
|
||||
TrackName string `json:"track_name"`
|
||||
@@ -81,12 +70,16 @@ func NewLyricsClient() *LyricsClient {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string, duration int) (*LyricsResponse, error) {
|
||||
func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName, albumName string, duration int) (*LyricsResponse, error) {
|
||||
|
||||
apiURL := fmt.Sprintf("https://lrclib.net/api/get?artist_name=%s&track_name=%s",
|
||||
url.QueryEscape(artistName),
|
||||
url.QueryEscape(trackName))
|
||||
|
||||
if albumName != "" {
|
||||
apiURL = fmt.Sprintf("%s&album_name=%s", apiURL, url.QueryEscape(albumName))
|
||||
}
|
||||
|
||||
if duration > 0 {
|
||||
apiURL = fmt.Sprintf("%s&duration=%d", apiURL, duration)
|
||||
}
|
||||
@@ -111,6 +104,10 @@ func (c *LyricsClient) FetchLyricsWithMetadata(trackName, artistName string, dur
|
||||
return nil, fmt.Errorf("failed to parse LRCLIB response: %v", err)
|
||||
}
|
||||
|
||||
if lrcLibResp.SyncedLyrics == "" && lrcLibResp.PlainLyrics == "" {
|
||||
return nil, fmt.Errorf("LRCLIB returned empty lyrics")
|
||||
}
|
||||
|
||||
return c.convertLRCLibToLyricsResponse(&lrcLibResp), nil
|
||||
}
|
||||
|
||||
@@ -174,8 +171,10 @@ func lrcTimestampToMs(timestamp string) int64 {
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
apiURL := fmt.Sprintf("https://lrclib.net/api/search?artist_name=%s&track_name=%s",
|
||||
url.QueryEscape(artistName),
|
||||
url.QueryEscape(trackName))
|
||||
|
||||
resp, err := c.httpClient.Get(apiURL)
|
||||
if err != nil {
|
||||
@@ -201,79 +200,35 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(trackName, artistName string)
|
||||
return nil, fmt.Errorf("no results found")
|
||||
}
|
||||
|
||||
var best *LRCLibResponse
|
||||
var bestSynced *LRCLibResponse
|
||||
var bestPlain *LRCLibResponse
|
||||
for i := range results {
|
||||
if results[i].SyncedLyrics != "" {
|
||||
best = &results[i]
|
||||
break
|
||||
if results[i].SyncedLyrics != "" && bestSynced == nil {
|
||||
bestSynced = &results[i]
|
||||
}
|
||||
if best == nil && results[i].PlainLyrics != "" {
|
||||
best = &results[i]
|
||||
if results[i].PlainLyrics != "" && bestPlain == nil {
|
||||
bestPlain = &results[i]
|
||||
}
|
||||
if bestSynced != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
best := bestSynced
|
||||
if best == nil {
|
||||
best = bestPlain
|
||||
}
|
||||
if best == nil {
|
||||
best = &results[0]
|
||||
}
|
||||
|
||||
if best.SyncedLyrics == "" && best.PlainLyrics == "" {
|
||||
return nil, fmt.Errorf("no lyrics found in search results")
|
||||
}
|
||||
|
||||
return c.convertLRCLibToLyricsResponse(best), nil
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsFromSpotifyAPI(spotifyID string) (*LyricsResponse, error) {
|
||||
if spotifyID == "" {
|
||||
return nil, fmt.Errorf("spotify ID is empty")
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("https://spotify-lyrics-api-pi.vercel.app/?trackid=%s&format=lrc", spotifyID)
|
||||
|
||||
resp, err := c.httpClient.Get(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch from Spotify Lyrics API: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Spotify Lyrics API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Spotify Lyrics API response: %v", err)
|
||||
}
|
||||
|
||||
var apiResp SpotifyLyricsAPIResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Spotify Lyrics API response: %v", err)
|
||||
}
|
||||
|
||||
if apiResp.Error {
|
||||
return nil, fmt.Errorf("Spotify Lyrics API returned error")
|
||||
}
|
||||
|
||||
result := &LyricsResponse{
|
||||
Error: false,
|
||||
SyncType: apiResp.SyncType,
|
||||
Lines: []LyricsLine{},
|
||||
}
|
||||
|
||||
for _, line := range apiResp.Lines {
|
||||
if line.TimeTag == "" && line.Words == "" {
|
||||
continue
|
||||
}
|
||||
ms := lrcTimestampToMs(line.TimeTag)
|
||||
result.Lines = append(result.Lines, LyricsLine{
|
||||
StartTimeMs: fmt.Sprintf("%d", ms),
|
||||
Words: line.Words,
|
||||
})
|
||||
}
|
||||
|
||||
if len(result.Lines) == 0 {
|
||||
return nil, fmt.Errorf("Spotify Lyrics API returned empty lines")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func simplifyTrackName(name string) string {
|
||||
|
||||
if idx := strings.Index(name, "("); idx > 0 {
|
||||
@@ -286,41 +241,88 @@ func simplifyTrackName(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string, duration int) (*LyricsResponse, string, error) {
|
||||
func isSynced(resp *LyricsResponse) bool {
|
||||
return resp != nil && !resp.Error && resp.SyncType == "LINE_SYNCED" && len(resp.Lines) > 0
|
||||
}
|
||||
|
||||
resp, err := c.FetchLyricsFromSpotifyAPI(spotifyID)
|
||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||
return resp, "Spotify", nil
|
||||
}
|
||||
fmt.Printf(" Spotify Lyrics API: %v\n", err)
|
||||
func hasLyrics(resp *LyricsResponse) bool {
|
||||
return resp != nil && !resp.Error && len(resp.Lines) > 0
|
||||
}
|
||||
|
||||
resp, err = c.FetchLyricsWithMetadata(trackName, artistName, duration)
|
||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||
return resp, "LRCLIB", nil
|
||||
}
|
||||
fmt.Printf(" LRCLIB exact: %v\n", err)
|
||||
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName, albumName string, duration int) (*LyricsResponse, string, error) {
|
||||
|
||||
resp, err = c.FetchLyricsFromLRCLibSearch(trackName, artistName)
|
||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||
return resp, "LRCLIB Search", nil
|
||||
var unsyncedFallback *LyricsResponse
|
||||
var unsyncedSource string
|
||||
|
||||
check := func(resp *LyricsResponse, err error, source string) (*LyricsResponse, string, bool) {
|
||||
if err != nil || resp == nil || resp.Error || len(resp.Lines) == 0 {
|
||||
return nil, "", false
|
||||
}
|
||||
if isSynced(resp) {
|
||||
return resp, source, true
|
||||
}
|
||||
|
||||
if unsyncedFallback == nil {
|
||||
unsyncedFallback = resp
|
||||
unsyncedSource = source
|
||||
}
|
||||
return nil, "", false
|
||||
}
|
||||
fmt.Printf(" LRCLIB search: %v\n", err)
|
||||
|
||||
var resp *LyricsResponse
|
||||
var src string
|
||||
var found bool
|
||||
|
||||
resp, _ = c.FetchLyricsWithMetadata(trackName, artistName, albumName, duration)
|
||||
resp, src, found = check(resp, nil, "LRCLIB")
|
||||
if found {
|
||||
fmt.Printf(" [LRCLIB] Synced found via exact match (with album)\n")
|
||||
return resp, src, nil
|
||||
}
|
||||
fmt.Printf(" LRCLIB exact (with album): no synced\n")
|
||||
|
||||
if albumName != "" {
|
||||
resp, _ = c.FetchLyricsWithMetadata(trackName, artistName, "", duration)
|
||||
resp, src, found = check(resp, nil, "LRCLIB (no album)")
|
||||
if found {
|
||||
fmt.Printf(" [LRCLIB] Synced found via exact match (no album)\n")
|
||||
return resp, src, nil
|
||||
}
|
||||
fmt.Printf(" LRCLIB exact (no album): no synced\n")
|
||||
}
|
||||
|
||||
resp, _ = c.FetchLyricsFromLRCLibSearch(trackName, artistName)
|
||||
resp, src, found = check(resp, nil, "LRCLIB Search")
|
||||
if found {
|
||||
fmt.Printf(" [LRCLIB] Synced found via search\n")
|
||||
return resp, src, nil
|
||||
}
|
||||
fmt.Printf(" LRCLIB search: no synced\n")
|
||||
|
||||
simplifiedTrack := simplifyTrackName(trackName)
|
||||
if simplifiedTrack != trackName {
|
||||
fmt.Printf(" Trying simplified name: %s\n", simplifiedTrack)
|
||||
|
||||
resp, err = c.FetchLyricsWithMetadata(simplifiedTrack, artistName, duration)
|
||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||
return resp, "LRCLIB (simplified)", nil
|
||||
resp, _ = c.FetchLyricsWithMetadata(simplifiedTrack, artistName, albumName, duration)
|
||||
resp, src, found = check(resp, nil, "LRCLIB (simplified)")
|
||||
if found {
|
||||
fmt.Printf(" [LRCLIB] Synced found via simplified exact\n")
|
||||
return resp, src, nil
|
||||
}
|
||||
|
||||
resp, err = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName)
|
||||
if err == nil && resp != nil && !resp.Error && len(resp.Lines) > 0 {
|
||||
return resp, "LRCLIB Search (simplified)", nil
|
||||
resp, _ = c.FetchLyricsFromLRCLibSearch(simplifiedTrack, artistName)
|
||||
resp, src, found = check(resp, nil, "LRCLIB Search (simplified)")
|
||||
if found {
|
||||
fmt.Printf(" [LRCLIB] Synced found via simplified search\n")
|
||||
return resp, src, nil
|
||||
}
|
||||
}
|
||||
|
||||
if unsyncedFallback != nil {
|
||||
fmt.Printf(" [LRCLIB] No synced found, using unsynced from: %s\n", unsyncedSource)
|
||||
return unsyncedFallback, unsyncedSource + " (unsynced)", nil
|
||||
}
|
||||
|
||||
return nil, "", fmt.Errorf("lyrics not found in any source")
|
||||
}
|
||||
|
||||
@@ -472,25 +474,6 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
|
||||
outputDir = NormalizePath(outputDir)
|
||||
}
|
||||
|
||||
safeArtist := sanitizeFilename(req.AlbumArtist)
|
||||
if safeArtist == "" {
|
||||
safeArtist = sanitizeFilename(req.ArtistName)
|
||||
}
|
||||
safeAlbum := sanitizeFilename(req.AlbumName)
|
||||
|
||||
if safeArtist != "" && safeAlbum != "" {
|
||||
artistAlbumPath := filepath.Join(outputDir, safeArtist, safeAlbum)
|
||||
if info, err := os.Stat(artistAlbumPath); err == nil && info.IsDir() {
|
||||
outputDir = artistAlbumPath
|
||||
} else {
|
||||
|
||||
artistPath := filepath.Join(outputDir, safeArtist)
|
||||
if info, err := os.Stat(artistPath); err == nil && info.IsDir() {
|
||||
outputDir = artistPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return &LyricsDownloadResponse{
|
||||
Success: false,
|
||||
@@ -524,7 +507,7 @@ func (c *LyricsClient) DownloadLyrics(req LyricsDownloadRequest) (*LyricsDownloa
|
||||
}
|
||||
}
|
||||
|
||||
lyrics, _, err := c.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, audioDuration)
|
||||
lyrics, _, err := c.FetchLyricsAllSources(req.SpotifyID, req.TrackName, req.ArtistName, req.AlbumName, audioDuration)
|
||||
if err != nil {
|
||||
return &LyricsDownloadResponse{
|
||||
Success: false,
|
||||
|
||||
@@ -146,7 +146,7 @@ func FetchMusicBrainzMetadata(isrc, title, artist, album string, useSingleGenre
|
||||
if len(genres) > 5 {
|
||||
genres = genres[:5]
|
||||
}
|
||||
meta.Genre = strings.Join(genres, "; ")
|
||||
meta.Genre = strings.Join(genres, GetSeparator())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -118,8 +118,15 @@ func (q *QobuzDownloader) searchByISRC(isrc string) (*QobuzTrack, error) {
|
||||
return &searchResp.Tracks.Items[0], nil
|
||||
}
|
||||
|
||||
func buildQobuzAPIURL(apiBase string, trackID int64, quality string) string {
|
||||
if strings.Contains(apiBase, "qbz.afkarxyz.fun") {
|
||||
return fmt.Sprintf("%s%d?quality=%s", apiBase, trackID, quality)
|
||||
}
|
||||
return fmt.Sprintf("%s%d&quality=%s", apiBase, trackID, quality)
|
||||
}
|
||||
|
||||
func (q *QobuzDownloader) DownloadFromStandard(apiBase string, trackID int64, quality string) (string, error) {
|
||||
apiURL := fmt.Sprintf("%s%d&quality=%s", apiBase, trackID, quality)
|
||||
apiURL := buildQobuzAPIURL(apiBase, trackID, quality)
|
||||
resp, err := q.client.Get(apiURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -167,6 +174,7 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string, allowFal
|
||||
standardAPIs := []string{
|
||||
"https://dab.yeet.su/api/stream?trackId=",
|
||||
"https://dabmusic.xyz/api/stream?trackId=",
|
||||
"https://qbz.afkarxyz.fun/api/track/",
|
||||
}
|
||||
|
||||
downloadFunc := func(qual string) (string, error) {
|
||||
|
||||
+10
-10
@@ -665,7 +665,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
||||
for _, artist := range albumArtists {
|
||||
albumArtistNames = append(albumArtistNames, getString(artist, "name"))
|
||||
}
|
||||
albumArtistsString = strings.Join(albumArtistNames, ", ")
|
||||
albumArtistsString = strings.Join(albumArtistNames, GetSeparator())
|
||||
}
|
||||
if albumArtistsString == "" {
|
||||
albumArtistsString = getString(albumUnionData, "artists")
|
||||
@@ -681,7 +681,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
||||
for _, artist := range albumArtists {
|
||||
albumArtistNames = append(albumArtistNames, getString(artist, "name"))
|
||||
}
|
||||
albumArtistsString = strings.Join(albumArtistNames, ", ")
|
||||
albumArtistsString = strings.Join(albumArtistNames, GetSeparator())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,13 +715,13 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
|
||||
for _, artist := range artists {
|
||||
artistNames = append(artistNames, getString(artist, "name"))
|
||||
}
|
||||
artistsString := strings.Join(artistNames, ", ")
|
||||
artistsString := strings.Join(artistNames, GetSeparator())
|
||||
|
||||
copyrightTexts := []string{}
|
||||
for _, item := range copyrightInfo {
|
||||
copyrightTexts = append(copyrightTexts, getString(item, "text"))
|
||||
}
|
||||
copyrightString := strings.Join(copyrightTexts, ", ")
|
||||
copyrightString := strings.Join(copyrightTexts, GetSeparator())
|
||||
|
||||
discNumber := int(getFloat64(trackData, "discNumber"))
|
||||
if discNumber == 0 {
|
||||
@@ -814,7 +814,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
||||
for _, artist := range artists {
|
||||
artistNames = append(artistNames, getString(artist, "name"))
|
||||
}
|
||||
albumArtistsString := strings.Join(artistNames, ", ")
|
||||
albumArtistsString := strings.Join(artistNames, GetSeparator())
|
||||
|
||||
coverObj := extractCoverImage(getMap(albumData, "coverArt"))
|
||||
var cover interface{}
|
||||
@@ -875,7 +875,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
|
||||
for _, artist := range trackArtists {
|
||||
trackArtistNames = append(trackArtistNames, getString(artist, "name"))
|
||||
}
|
||||
trackArtistsString := strings.Join(trackArtistNames, ", ")
|
||||
trackArtistsString := strings.Join(trackArtistNames, GetSeparator())
|
||||
|
||||
trackURI := getString(track, "uri")
|
||||
trackID := ""
|
||||
@@ -1075,7 +1075,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} {
|
||||
for _, artist := range trackArtists {
|
||||
trackArtistNames = append(trackArtistNames, getString(artist, "name"))
|
||||
}
|
||||
artistsString := strings.Join(trackArtistNames, ", ")
|
||||
artistsString := strings.Join(trackArtistNames, GetSeparator())
|
||||
|
||||
trackDurationMs := getFloat64(getMap(trackData, "trackDuration"), "totalMilliseconds")
|
||||
durationObj := extractDuration(trackDurationMs)
|
||||
@@ -1121,7 +1121,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} {
|
||||
for _, artist := range albumArtists {
|
||||
albumArtistNames = append(albumArtistNames, getString(artist, "name"))
|
||||
}
|
||||
albumArtistsString = strings.Join(albumArtistNames, ", ")
|
||||
albumArtistsString = strings.Join(albumArtistNames, GetSeparator())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1514,7 +1514,7 @@ func FilterSearch(data map[string]interface{}) map[string]interface{} {
|
||||
for _, artist := range trackArtists {
|
||||
trackArtistNames = append(trackArtistNames, getString(artist, "name"))
|
||||
}
|
||||
trackArtistsString := strings.Join(trackArtistNames, ", ")
|
||||
trackArtistsString := strings.Join(trackArtistNames, GetSeparator())
|
||||
|
||||
durationString := getString(trackDuration, "formatted")
|
||||
|
||||
@@ -1586,7 +1586,7 @@ func FilterSearch(data map[string]interface{}) map[string]interface{} {
|
||||
for _, artist := range albumArtists {
|
||||
albumArtistNames = append(albumArtistNames, getString(artist, "name"))
|
||||
}
|
||||
albumArtistsString := strings.Join(albumArtistNames, ", ")
|
||||
albumArtistsString := strings.Join(albumArtistNames, GetSeparator())
|
||||
|
||||
dateInfo := getMap(album, "date")
|
||||
var year interface{}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetOSInfo() (string, error) {
|
||||
osType := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
switch osType {
|
||||
case "darwin":
|
||||
out, err := exec.Command("sw_vers", "-productVersion").Output()
|
||||
if err != nil {
|
||||
return fmt.Sprintf("macOS %s", arch), nil
|
||||
}
|
||||
version := strings.TrimSpace(string(out))
|
||||
return fmt.Sprintf("macOS %s (%s)", version, arch), nil
|
||||
|
||||
case "linux":
|
||||
out, err := exec.Command("cat", "/etc/os-release").Output()
|
||||
if err == nil {
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "PRETTY_NAME=") {
|
||||
name := strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"")
|
||||
return fmt.Sprintf("%s (%s)", name, arch), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Linux %s", arch), nil
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%s %s", osType, arch), nil
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func GetOSInfo() (string, error) {
|
||||
arch := runtime.GOARCH
|
||||
|
||||
cmd := exec.Command("wmic", "os", "get", "Caption,Version", "/value")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
cmdVer := exec.Command("cmd", "/c", "ver")
|
||||
cmdVer.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
outVer, errVer := cmdVer.Output()
|
||||
if errVer != nil {
|
||||
return fmt.Sprintf("Windows %s", arch), nil
|
||||
}
|
||||
return strings.TrimSpace(string(outVer)), nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
var caption, version string
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Caption=") {
|
||||
caption = strings.TrimPrefix(line, "Caption=")
|
||||
} else if strings.HasPrefix(line, "Version=") {
|
||||
version = strings.TrimPrefix(line, "Version=")
|
||||
}
|
||||
}
|
||||
if caption != "" && version != "" {
|
||||
return fmt.Sprintf("%s (%s, %s)", caption, version, arch), nil
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
+5
-1
@@ -79,9 +79,13 @@ func NewTidalDownloader(apiURL string) *TidalDownloader {
|
||||
|
||||
func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) {
|
||||
apis := []string{
|
||||
"https://triton.squid.wtf",
|
||||
"https://hifi-one.spotisaver.net",
|
||||
"https://hifi-two.spotisaver.net",
|
||||
"https://eu-central.monochrome.tf",
|
||||
"https://us-west.monochrome.tf",
|
||||
"https://api.monochrome.tf",
|
||||
"https://monochrome-api.samidy.com",
|
||||
"https://tidal.kinoplus.online",
|
||||
}
|
||||
return apis, nil
|
||||
}
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SendNowResponse []struct {
|
||||
FileCode string `json:"file_code"`
|
||||
}
|
||||
|
||||
func UploadToSendNow(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return uploadToService(filepath.Base(filePath), file)
|
||||
}
|
||||
|
||||
func UploadBytesToSendNow(filename string, data []byte) (string, error) {
|
||||
return uploadToService(filename, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
func uploadToService(filename string, fileReader io.Reader) (string, error) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
fields := map[string]string{
|
||||
"sess_id": "",
|
||||
"utype": "anon",
|
||||
"hidden": "",
|
||||
"enableemail": "",
|
||||
"link_rcpt": "",
|
||||
"link_pass": "",
|
||||
"file_expire_time": "",
|
||||
"file_expire_unit": "DAY",
|
||||
"file_max_dl": "1",
|
||||
"file_public": "1",
|
||||
"keepalive": "1",
|
||||
}
|
||||
|
||||
for key, val := range fields {
|
||||
if err := writer.WriteField(key, val); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile("file_0", filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(part, fileReader); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
|
||||
req.Header.Set("Origin", "https://send.now")
|
||||
req.Header.Set("Referer", "https://send.now/")
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("upload failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
respBytes, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("server error %d: %s", resp.StatusCode, string(respBytes))
|
||||
}
|
||||
|
||||
respBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var result SendNowResponse
|
||||
if err := json.Unmarshal(respBytes, &result); err != nil {
|
||||
return "", fmt.Errorf("failed to parse response: %v, raw: %s", err, string(respBytes))
|
||||
}
|
||||
|
||||
if len(result) == 0 || result[0].FileCode == "" {
|
||||
return "", fmt.Errorf("invalid response format")
|
||||
}
|
||||
|
||||
fileCode := result[0].FileCode
|
||||
downloadLink := fmt.Sprintf("https://send.now/%s", fileCode)
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
if ext == ".mp4" || ext == ".mov" || ext == ".mkv" || ext == ".webm" || ext == ".avi" {
|
||||
return fmt.Sprintf("[Video](%s)", downloadLink), nil
|
||||
}
|
||||
|
||||
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/145.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 {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.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()
|
||||
|
||||
htmlBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
htmlStr := string(htmlBytes)
|
||||
|
||||
reFullRes := regexp.MustCompile(`(?i)<a[^>]+href=["']([^"']+)["'][^>]*title=["']Open image on new tab["']`)
|
||||
matchesFull := reFullRes.FindStringSubmatch(htmlStr)
|
||||
if len(matchesFull) > 1 {
|
||||
return fmt.Sprintf("", matchesFull[1]), nil
|
||||
}
|
||||
|
||||
reClipboard := regexp.MustCompile(`(?s)data-clipboard-text=['"]<a href="[^"]+".*?><img src="([^"]+)"`)
|
||||
matches := reClipboard.FindStringSubmatch(htmlStr)
|
||||
if len(matches) > 1 {
|
||||
return fmt.Sprintf("", matches[1]), nil
|
||||
}
|
||||
|
||||
reImg := regexp.MustCompile(`(?i)<img[^>]+src=["']([^"']*?\.send\.now/i/[^"']+)["']`)
|
||||
matchesImg := reImg.FindStringSubmatch(htmlStr)
|
||||
if len(matchesImg) > 1 {
|
||||
return fmt.Sprintf("", matchesImg[1]), nil
|
||||
}
|
||||
|
||||
reAnchor := regexp.MustCompile(`(?i)<a[^>]+href=["']([^"']+\.(?:jpg|jpeg|png|gif|webp))["']`)
|
||||
matchesAnchor := reAnchor.FindStringSubmatch(htmlStr)
|
||||
if len(matchesAnchor) > 1 {
|
||||
return fmt.Sprintf("", matchesAnchor[1]), nil
|
||||
}
|
||||
|
||||
reGeneric := regexp.MustCompile(`(?i)<img[^>]+src=["']([^"']+\.(?:jpg|jpeg|png|gif|webp))["']`)
|
||||
matchesGeneric := reGeneric.FindAllStringSubmatch(htmlStr, -1)
|
||||
for _, match := range matchesGeneric {
|
||||
if len(match) > 1 {
|
||||
link := match[1]
|
||||
|
||||
if !regexp.MustCompile(`(?i)(logo|icon|button|assets)`).MatchString(filepath.Base(link)) {
|
||||
return fmt.Sprintf("", link), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[View File](%s)", url), nil
|
||||
}
|
||||
Reference in New Issue
Block a user