Compare commits

...

5 Commits

Author SHA1 Message Date
Hazel Noack
4a54048feb enabling gos comression negotiation 2025-10-08 11:10:26 +02:00
Hazel Noack
018dbe1ef0 implemented init function 2025-10-08 10:34:22 +02:00
Hazel Noack
19ec6d1571 allowing pointer 2025-10-08 10:33:13 +02:00
Hazel Noack
a99dee5104 allowing pointer 2025-10-08 10:32:43 +02:00
Hazel Noack
8587bdf360 shell setup 2025-10-08 10:27:16 +02:00
9 changed files with 331 additions and 40 deletions

2
go.mod
View File

@ -1,3 +1,5 @@
module gitea.elara.ws/Hazel/music-kraken
go 1.24.2
require golang.org/x/net v0.45.0 // indirect

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=

47
internal/cli/shell.go Normal file
View File

@ -0,0 +1,47 @@
package cli
import (
"bufio"
"fmt"
"log"
"os"
"gitea.elara.ws/Hazel/music-kraken/internal/data"
"gitea.elara.ws/Hazel/music-kraken/internal/plugin"
)
func printResults(musicObjects []data.MusicObject) {
for _, m := range musicObjects {
if a, ok := m.(data.Artist); ok {
fmt.Println("artist: " + a.Name)
} else if a, ok := m.(data.Album); ok {
fmt.Println("release: " + a.Name)
} else if a, ok := m.(data.Song); ok {
fmt.Println("track: " + a.Name)
}
}
}
func Shell() {
plugin.RegisterPlugin(&plugin.Musify{})
for {
fmt.Print("> ")
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
return
}
searchResults, err := plugin.Search(line, plugin.SearchConfig{IgnoreErrors: false})
if err != nil {
fmt.Println(err)
fmt.Println()
}
fmt.Println()
printResults(searchResults)
}
}

View File

@ -1,7 +1,11 @@
package common
import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
)
@ -116,3 +120,26 @@ func NewQuery(search string) (Query, error) {
return query, nil
}
func TestQueryParsing() {
for {
fmt.Print("> ")
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
return
}
query, err := NewQuery(line)
if err != nil {
fmt.Println(err)
}
fmt.Println("search: '" + query.Search + "'")
fmt.Println("artist: '" + query.Artist + "'")
fmt.Println("album: '" + query.Album + "'")
fmt.Println("song: '" + query.Song + "'")
fmt.Println()
}
}

View File

@ -15,6 +15,8 @@ type Plugin interface {
RegexAlbum() *regexp.Regexp
RegexSong() *regexp.Regexp
Init()
Search(query common.Query) ([]data.MusicObject, error)
FetchArtist(source data.Source) (data.Artist, error)
@ -41,6 +43,9 @@ func RegisterPlugin(plugin Plugin) error {
}
namePlugins[name] = plugin
plugin.Init()
return nil
}

View File

@ -1,11 +1,15 @@
package plugin
import (
"compress/gzip"
"fmt"
"io"
"regexp"
"strings"
"gitea.elara.ws/Hazel/music-kraken/internal/common"
"gitea.elara.ws/Hazel/music-kraken/internal/data"
"gitea.elara.ws/Hazel/music-kraken/internal/scraper"
)
func extractName(s string) string {
@ -17,6 +21,7 @@ func extractName(s string) string {
}
type Musify struct {
session *scraper.Session
}
func (m Musify) Name() string {
@ -35,10 +40,54 @@ func (m Musify) RegexAlbum() *regexp.Regexp {
return regexp.MustCompile(`(?i)https?://musify\.club/release/[a-z\-0-9]+`)
}
func (m *Musify) Init() {
m.session = scraper.NewSession()
}
func (m Musify) RegexSong() *regexp.Regexp {
return regexp.MustCompile(`(?i)https?://musify\.club/track/[a-z\-0-9]+`)
}
func (m *Musify) Search(query common.Query) ([]data.MusicObject, error) {
musicObjects := []data.MusicObject{}
resp, err := m.session.PostMultipartForm("https://musify.club/en/search", map[string]string{
"SearchText": query.Search, // alternatively I could also add year and genre
})
if err != nil {
return musicObjects, err
}
defer resp.Body.Close()
var bodyReader io.Reader = resp.Body
// Check if we need to decompress manually
if resp.Header.Get("Content-Encoding") == "gzip" && false {
fmt.Println("Response is gzipped, decompressing...")
gzReader, err := gzip.NewReader(resp.Body)
if err != nil {
panic(err)
}
defer gzReader.Close()
bodyReader = gzReader
}
body, err := io.ReadAll(bodyReader)
if err != nil {
panic(err)
}
fmt.Printf("Response length: %d bytes\n", len(body))
fmt.Println("Content:")
fmt.Println(string(body))
fmt.Println(resp.Header)
fmt.Println(resp.StatusCode)
return musicObjects, nil
}
func (m Musify) FetchSong(source data.Source) (data.Song, error) {
return data.Song{
Name: extractName(source.Url),
@ -56,7 +105,3 @@ func (m Musify) FetchArtist(source data.Source) (data.Artist, error) {
Name: extractName(source.Url),
}, nil
}
func (m Musify) Search(query common.Query) ([]data.MusicObject, error) {
return []data.MusicObject{}, nil
}

View File

@ -40,6 +40,10 @@ func (m MusifyTest) RegexSong() *regexp.Regexp {
return regexp.MustCompile(`(?i)https?://musify\.club/track/[a-z\-0-9]+`)
}
func (m *MusifyTest) Init() {
}
func (m MusifyTest) FetchSong(source data.Source) (data.Song, error) {
return data.Song{
Name: extractNameTest(source.Url),
@ -63,11 +67,11 @@ func (m MusifyTest) Search(query common.Query) ([]data.MusicObject, error) {
}
func TestRegister(t *testing.T) {
if RegisterPlugin(MusifyTest{}) != nil {
if RegisterPlugin(&MusifyTest{}) != nil {
t.Errorf(`registering first plugin shouldn't return an error`)
}
if RegisterPlugin(MusifyTest{}) == nil {
if RegisterPlugin(&MusifyTest{}) == nil {
t.Errorf(`registering same plugin twice should return an error`)
}
@ -81,7 +85,7 @@ func TestRegister(t *testing.T) {
}
func TestFetchSong(t *testing.T) {
RegisterPlugin(MusifyTest{})
RegisterPlugin(&MusifyTest{})
s, err := Fetch(data.Source{
Url: "https://musify.club/track/linkin-park-in-the-end-3058",
@ -102,7 +106,7 @@ func TestFetchSong(t *testing.T) {
}
func TestFetchAlbum(t *testing.T) {
RegisterPlugin(MusifyTest{})
RegisterPlugin(&MusifyTest{})
a, err := Fetch(data.Source{
Url: "https://musify.club/release/linkin-park-hybrid-theory-2000-188",
@ -123,7 +127,7 @@ func TestFetchAlbum(t *testing.T) {
}
func TestFetchArtist(t *testing.T) {
RegisterPlugin(MusifyTest{})
RegisterPlugin(&MusifyTest{})
a, err := Fetch(data.Source{
Url: "https://musify.club/artist/linkin-park-5",
@ -144,7 +148,7 @@ func TestFetchArtist(t *testing.T) {
}
func TestFetchWrongUrl(t *testing.T) {
RegisterPlugin(MusifyTest{})
RegisterPlugin(&MusifyTest{})
_, err := Fetch(data.Source{
Url: "https://musify.club/",
@ -156,7 +160,7 @@ func TestFetchWrongUrl(t *testing.T) {
}
func TestNonExistentSourceType(t *testing.T) {
RegisterPlugin(MusifyTest{})
RegisterPlugin(&MusifyTest{})
_, err := Fetch(data.Source{
Url: "https://musify.club/",

185
internal/scraper/session.go Normal file
View File

@ -0,0 +1,185 @@
package scraper
import (
"bytes"
"encoding/json"
"log"
"mime/multipart"
"net/http"
"net/http/cookiejar"
"net/url"
"time"
"golang.org/x/net/publicsuffix"
)
// Session represents a persistent HTTP session
type Session struct {
client *http.Client
headers map[string]string
baseURL string
UserAgent string
}
// NewSession creates a new session with browser-like headers
func NewSession() *Session {
// Create cookie jar first
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
log.Fatal(err)
}
return &Session{
client: &http.Client{
Timeout: 30 * time.Second,
Jar: jar, // Set the cookie jar
},
headers: map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
},
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
}
}
// SetHeader sets a header for all subsequent requests
func (s *Session) SetHeader(key, value string) {
s.headers[key] = value
}
// SetHeaders sets multiple headers at once
func (s *Session) SetHeaders(headers map[string]string) {
for key, value := range headers {
s.headers[key] = value
}
}
// SetBaseURL sets the base URL for relative paths
func (s *Session) SetBaseURL(baseURL string) {
s.baseURL = baseURL
}
// Get performs a GET request
func (s *Session) Get(url string, headers ...map[string]string) (*http.Response, error) {
// Use base URL if set and url is relative
fullURL := s.buildURL(url)
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, err
}
s.setDefaultHeaders(req)
// Add any additional headers provided
if len(headers) > 0 {
for key, value := range headers[0] {
req.Header.Set(key, value)
}
}
return s.client.Do(req)
}
// Post performs a POST request with form data
func (s *Session) PostMultipartForm(url string, data map[string]string, headers ...map[string]string) (*http.Response, error) {
fullURL := s.buildURL(url)
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
for k, v := range data {
err := writer.WriteField(k, v)
if err != nil {
return nil, err
}
}
writer.Close()
req, err := http.NewRequest("POST", fullURL, &requestBody)
if err != nil {
return nil, err
}
s.setDefaultHeaders(req)
req.Header.Set("Content-Type", writer.FormDataContentType())
// Add any additional headers provided
if len(headers) > 0 {
for key, value := range headers[0] {
req.Header.Set(key, value)
}
}
return s.client.Do(req)
}
// PostJSON performs a POST request with JSON data
func (s *Session) PostJSON(url string, data interface{}, headers ...map[string]string) (*http.Response, error) {
fullURL := s.buildURL(url)
jsonData, err := json.Marshal(data)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", fullURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
s.setDefaultHeaders(req)
req.Header.Set("Content-Type", "application/json")
// Add any additional headers provided
if len(headers) > 0 {
for key, value := range headers[0] {
req.Header.Set(key, value)
}
}
return s.client.Do(req)
}
// buildURL constructs the full URL using baseURL if set
func (s *Session) buildURL(path string) string {
if s.baseURL != "" && !isAbsoluteURL(path) {
return s.baseURL + path
}
return path
}
// isAbsoluteURL checks if the URL is absolute
func isAbsoluteURL(urlStr string) bool {
u, err := url.Parse(urlStr)
return err == nil && u.Scheme != "" && u.Host != ""
}
// setDefaultHeaders sets the default browser-like headers
func (s *Session) setDefaultHeaders(req *http.Request) {
for key, value := range s.headers {
req.Header.Set(key, value)
}
}
// GetCookies returns cookies for a given URL
func (s *Session) GetCookies(urlStr string) []*http.Cookie {
u, err := url.Parse(urlStr)
if err != nil {
return nil
}
return s.client.Jar.Cookies(u)
}
// SetCookies sets cookies for a given URL
func (s *Session) SetCookies(urlStr string, cookies []*http.Cookie) {
u, err := url.Parse(urlStr)
if err != nil {
return
}
s.client.Jar.SetCookies(u, cookies)
}

32
main.go
View File

@ -1,39 +1,13 @@
package main
import (
"bufio"
"fmt"
"log"
"os"
"gitea.elara.ws/Hazel/music-kraken/internal/common"
"gitea.elara.ws/Hazel/music-kraken/internal/cli"
)
func testQuery() {
for {
fmt.Print("> ")
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
return
}
query, err := common.NewQuery(line)
if err != nil {
fmt.Println(err)
}
fmt.Println("search: '" + query.Search + "'")
fmt.Println("artist: '" + query.Artist + "'")
fmt.Println("album: '" + query.Album + "'")
fmt.Println("song: '" + query.Song + "'")
fmt.Println()
}
}
func main() {
fmt.Println("music kraken")
fmt.Println("welcome to music kraken")
testQuery()
cli.Shell()
}