22 |
goffy
23 |
Simply click below to download and enjoy your music.
24 |
YourMusic
25 |
26 |
27 |
28 | `
29 | w.Header().Set("Content-Type", "text/html")
30 | fmt.Fprintf(w, html, zipFile)
31 | })
32 |
33 | http.HandleFunc(zipFile, func(w http.ResponseWriter, r *http.Request) {
34 | http.ServeFile(w, r, zipFile)
35 | })
36 |
37 | http.ListenAndServe(":8080", nil)
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/zip"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 | "net/url"
10 | "os"
11 | "os/signal"
12 | "path/filepath"
13 | "runtime"
14 | "strings"
15 | "unicode"
16 |
17 | "golang.org/x/text/runes"
18 | "golang.org/x/text/transform"
19 | "golang.org/x/text/unicode/norm"
20 | )
21 |
22 | func EncodeParam(s string) string {
23 | return url.QueryEscape(s)
24 | }
25 |
26 | func isPathValid(path string) bool {
27 | dir, err := os.Stat(path)
28 | if err != nil {
29 | fmt.Println(err)
30 | return false
31 | }
32 |
33 | if !dir.IsDir() {
34 | return false
35 | }
36 |
37 | return true
38 | }
39 |
40 | func IsTxt(file string) bool {
41 | parts := strings.Index(file, ".")
42 | if parts != -1 {
43 | extension := file[parts+1:]
44 | if strings.ToLower(extension) != "txt" {
45 | return false
46 | }
47 | }
48 | return true
49 | }
50 |
51 | func NewDir(path string) (string, error) {
52 | if !isPathValid(path) {
53 | return "", errors.New("invalid path")
54 | }
55 |
56 | dirName := "YourMusic"
57 | fullPath := filepath.Join(path, dirName)
58 |
59 | if runtime.GOOS == "windows" {
60 | fullPath = fullPath + "\\"
61 | } else {
62 | fullPath = fullPath + "/"
63 | }
64 |
65 | err := os.Mkdir(fullPath, 0700)
66 | if err != nil {
67 | fmt.Sprintln("Error: %w", err)
68 | return "", err
69 | }
70 |
71 | return fullPath, nil
72 | }
73 |
74 | func ToZip(dir, zipPath string) error {
75 | zipFile, err := os.Create(zipPath)
76 | if err != nil {
77 | return err
78 | }
79 | defer zipFile.Close()
80 |
81 | zipWriter := zip.NewWriter(zipFile)
82 | defer zipWriter.Close()
83 |
84 | err = filepath.Walk(dir, func(filePath string, info os.FileInfo, err error) error {
85 | if err != nil {
86 | return err
87 | }
88 |
89 | if filePath == dir {
90 | return nil
91 | }
92 |
93 | header, err := zip.FileInfoHeader(info)
94 | if err != nil {
95 | return err
96 | }
97 | header.Name, _ = filepath.Rel(dir, filePath)
98 |
99 | writer, err := zipWriter.CreateHeader(header)
100 | if err != nil {
101 | return err
102 | }
103 |
104 | if !info.IsDir() {
105 | file, err := os.Open(filePath)
106 | if err != nil {
107 | return err
108 | }
109 | defer file.Close()
110 |
111 | _, err = io.Copy(writer, file)
112 | if err != nil {
113 | return err
114 | }
115 | }
116 | return nil
117 | })
118 |
119 | return err
120 | }
121 |
122 | func GetFileSize(file string) (int64, error) {
123 | fileInfo, err := os.Stat(file)
124 | if err != nil {
125 | return 0, err
126 | }
127 |
128 | size := int64(fileInfo.Size())
129 | return size, nil
130 | }
131 |
132 | func RemoveAccents(s string) string {
133 | t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
134 | output, _, e := transform.String(t, s)
135 | if e != nil {
136 | panic(e)
137 | }
138 |
139 | return output
140 | }
141 |
142 | func RemoveInvalidChars(input string, invalidChars []byte) string {
143 | filter := func(r rune) rune {
144 | for _, c := range invalidChars {
145 | if byte(r) == c {
146 | return -1 /* remove the char */
147 | }
148 | }
149 | return r
150 | }
151 |
152 | return strings.Map(filter, input)
153 | }
154 |
155 | func GetLocalIP() string {
156 | addrs, err := net.InterfaceAddrs()
157 | if err != nil {
158 | return ""
159 | }
160 | for _, address := range addrs {
161 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
162 | if ipnet.IP.To4() != nil {
163 | return ipnet.IP.String()
164 | }
165 | }
166 | }
167 |
168 | return ""
169 | }
170 |
171 | func GetCurrentDir() string {
172 | workingDir, err := os.Getwd()
173 | if err != nil {
174 | fmt.Println("Error:", err)
175 | return ""
176 | }
177 |
178 | return workingDir
179 | }
180 |
181 | // handle ctrl + c (delete music folder and zip file)
182 | func InterruptHandler(resource string) (err error) {
183 | c := make(chan os.Signal, 1)
184 | signal.Notify(c, os.Interrupt)
185 | go func() {
186 | <-c
187 | DeleteResource(resource)
188 | DeleteResource(resource)
189 | os.Exit(1)
190 | }()
191 |
192 | return err
193 | }
194 |
195 | func DeleteResource(resource string) {
196 | if _, err := os.Stat(resource); err == nil {
197 | if err := os.RemoveAll(resource); err != nil {
198 | fmt.Println("Error deleting resource:", err)
199 | }
200 | }
201 | }
202 |
203 | /* used for the last validation in the Match function */
204 | func ExtractFirstWord(value string) string {
205 | for i := range value {
206 | if value[i] == ' ' {
207 | return value[0:i]
208 | }
209 | }
210 | return value
211 | }
212 |
213 | /*
214 | i don't know why, but there are artists who,
215 | due to their name, they add a hyphen
216 | between some words of their names
217 | on one platform and not on the other
218 | */
219 | func CleanAndNormalize(s string) string {
220 | cleaned := strings.ReplaceAll(s, "-", "")
221 | cleaned = strings.ReplaceAll(cleaned, " ", "")
222 | return cleaned
223 | }
224 |
--------------------------------------------------------------------------------
/youtube.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/adrg/strutil"
9 | "github.com/adrg/strutil/metrics"
10 | "github.com/raitonoberu/ytmusic"
11 | "github.com/tidwall/gjson"
12 | )
13 |
14 | type TrackMatch struct {
15 | Id string
16 | Ratio float64
17 | }
18 |
19 | type YTResult struct {
20 | Title, Artist, Album, Id string
21 | }
22 |
23 | type Ratio struct {
24 | Title, Artist, Album, Total float64
25 | }
26 |
27 | /* select the best result on YouTube Music */
28 | func Match(results []YTResult, spTrack *Track) string {
29 | var trackMatch TrackMatch
30 | spTrack.Title = RemoveAccents(strings.ToLower(spTrack.Title))
31 | spTrack.Artist = RemoveAccents(strings.ToLower(spTrack.Artist))
32 | spTrack.Album = RemoveAccents(strings.ToLower(spTrack.Album))
33 |
34 | for _, result := range results {
35 | ratio := calculateMatchRatio(spTrack, result)
36 | if ratio > trackMatch.Ratio && isPartialMatch(result, spTrack) {
37 | trackMatch.Id = result.Id
38 | trackMatch.Ratio = ratio
39 | }
40 | }
41 |
42 | return trackMatch.Id
43 | }
44 |
45 | func calculateMatchRatio(spTrack *Track, result YTResult) float64 {
46 | var ratio Ratio
47 |
48 | ratio.Title = map[bool]float64{result.Title != "": strutil.Similarity(result.Title, spTrack.Title, metrics.NewLevenshtein()), true: 0}[true]
49 | ratio.Artist = map[bool]float64{result.Artist != "" && strings.Contains(CleanAndNormalize(result.Artist), CleanAndNormalize(spTrack.Artist)): strutil.Similarity(result.Artist, spTrack.Artist, metrics.NewLevenshtein()), true: 0}[true]
50 | ratio.Album = map[bool]float64{result.Album == result.Title && result.Album == spTrack.Title: 1, true: strutil.Similarity(result.Album, spTrack.Album, metrics.NewLevenshtein())}[true]
51 | ratio.Total = (ratio.Title + ratio.Artist + ratio.Album) / 3
52 |
53 | return ratio.Total
54 | }
55 |
56 | /* last validation before returning the most precise ID from the Match function */
57 | func isPartialMatch(result YTResult, spTrack *Track) bool {
58 | ytTitle, spTitle := RemoveAccents(strings.ToLower(result.Title)), RemoveAccents(strings.ToLower(spTrack.Title))
59 | ytTitleSeparated, spTitleSeparated := strings.Fields(ytTitle), strings.Fields(spTitle)
60 |
61 | for _, ytField := range ytTitleSeparated {
62 | for _, spField := range spTitleSeparated {
63 | if strings.Contains(ytField, spField) {
64 | return true
65 | }
66 | }
67 | }
68 |
69 | return false
70 | }
71 |
72 | /* construct each YouTube result into a structured track and return a two-element slice */
73 | func (yt YTResult) buildResults(jsonResponse string) []YTResult {
74 | var ytResults []YTResult
75 | jsonResults := gjson.Get(jsonResponse, "tracks").Array()
76 | limit := 2
77 |
78 | for _, result := range jsonResults {
79 | if len(ytResults) >= limit {
80 | break
81 | }
82 |
83 | title := result.Get("title").String()
84 | artist := result.Get("artists.#.name").String()
85 | album := result.Get("album.name").String()
86 | id := result.Get("videoId").String()
87 |
88 | item := YTResult{
89 | Title: RemoveAccents(strings.ToLower(title)),
90 | Artist: RemoveAccents(strings.ToLower(artist)),
91 | Album: RemoveAccents(strings.ToLower(album)),
92 | Id: id,
93 | }
94 |
95 | ytResults = append(ytResults, item)
96 | }
97 |
98 | return ytResults
99 | }
100 |
101 | func VideoID(spTrack Track) (string, error) {
102 | var ytResult YTResult
103 | query := fmt.Sprintf("'%s' %s %s", spTrack.Title, spTrack.Artist, spTrack.Album)
104 | search := ytmusic.TrackSearch(query) /* github.com/raitonoberu/ytmusic */
105 | result, err := search.Next()
106 | if err != nil {
107 | return "", err
108 | }
109 |
110 | jsonStr, _ := json.Marshal(result)
111 | ytResults := ytResult.buildResults(string(jsonStr))
112 | id := Match(ytResults, &spTrack)
113 |
114 | return id, nil
115 | }
116 |
--------------------------------------------------------------------------------