package albums
import (
"golang.org/x/net/html"
"io/ioutil"
"net/http"
"strconv"
"strings"
)
type Track struct {
Name string
Hours int
Minutes int
Seconds int
}
func readTrack(n *html.Node) Track {
var track Track
track.Name = strings.TrimSpace(n.FirstChild.Data)
stripedTime := strings.Split(n.NextSibling.NextSibling.FirstChild.Data, ":")
if len(stripedTime) == 2 {
track.Minutes, _ = strconv.Atoi(stripedTime[0])
track.Seconds, _ = strconv.Atoi(stripedTime[1])
} else {
track.Hours, _ = strconv.Atoi(stripedTime[0])
track.Minutes, _ = strconv.Atoi(stripedTime[1])
track.Seconds, _ = strconv.Atoi(stripedTime[2])
}
return track
}
func getCoverURL(n *html.Node) string {
var cover string
coverURL := n.FirstChild.NextSibling.Attr[3].Val
stripedCover := strings.Split(coverURL, "?")
cover = stripedCover[0]
return cover
}
func GetAlbumInfo(client http.Client, albumURL string) ([]Track, string, error) {
var albumTracks []Track
var coverURL string
req, err := http.NewRequest(http.MethodGet, albumURL, nil)
if err != nil {
return albumTracks, coverURL, err
}
req.Header.Set("User-Agent", "https://github.com/a-castellano/metal-archives-wrapper")
res, getErr := client.Do(req)
if getErr != nil {
return albumTracks, coverURL, err
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
return albumTracks, coverURL, err
}
stringBody := string(body)
doc, err := html.Parse(strings.NewReader(stringBody))
if err != nil {
return albumTracks, coverURL, err
}
var f func(*html.Node, *[]Track)
f = func(n *html.Node, albumTracks *[]Track) {
if n.Type == html.ElementNode && n.Data == "td" {
if len(n.Attr) == 1 && n.Attr[0].Val == "wrapWords" {
*albumTracks = append(*albumTracks, readTrack(n))
}
} else {
if n.Type == html.ElementNode && n.Data == "div" {
if len(n.Attr) == 1 && n.Attr[0].Val == "album_img" {
coverURL = getCoverURL(n)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c, albumTracks)
}
}
f(doc, &albumTracks)
return albumTracks, coverURL, nil
}
package albums
import (
"encoding/json"
"errors"
"fmt"
commontypes "github.com/a-castellano/music-manager-common-types/types"
types "github.com/a-castellano/music-manager-metal-archives-wrapper/types"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
)
type SearchAlbumData struct {
Name string
URL string
ID int
Year int
Cover string
Artist string
ArtistID int
ArtistURL string
Type commontypes.RecordType
Tracks []Track
}
func searchAlbumAjax(client http.Client, album string) ([][]string, error) {
var searchAlbumData [][]string
albumString := strings.Replace(album, " ", "+", -1)
url := fmt.Sprintf("https://www.metal-archives.com/search/ajax-album-search/?field=title&query=%s", albumString)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return searchAlbumData, err
}
req.Header.Set("User-Agent", "https://github.com/a-castellano/metal-archives-wrapper")
res, getErr := client.Do(req)
if getErr != nil {
return searchAlbumData, getErr
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
return searchAlbumData, readErr
}
searchAlbum := types.SearchAjaxData{}
jsonErr := json.Unmarshal(body, &searchAlbum)
if jsonErr != nil {
return searchAlbumData, jsonErr
}
searchAlbumData = searchAlbum.Data
return searchAlbumData, nil
}
func SearchAlbum(client http.Client, album string) (SearchAlbumData, []SearchAlbumData, error) {
var albumData SearchAlbumData
var albumExtraData []SearchAlbumData
artistDatare := regexp.MustCompile(`(?m)<[^/]*//[^/]*/[^/]*/[^/]*/([0-9]*)[^>]*>([^<]*)</a>$`)
albumDatare := regexp.MustCompile(`(?m)<a href="([^"]*)">([^<]*)</a> <!-- [0-9]*.[0-9]* -->$`)
artistURLre := regexp.MustCompile(`(?m)<a href="([^"]*)".*$`)
yearre := regexp.MustCompile(`(?m)([1|2][0-9]{3})`)
albumIDre := regexp.MustCompile(`(?m)[^/]*//[^/]*/[^/]*/[^/]*[^/]*/[^/]*/([0-9]*)`)
data, err := searchAlbumAjax(client, album)
var found bool = false
if err != nil {
return albumData, albumExtraData, err
} else {
for _, foundAlbumData := range data {
albumMatch := albumDatare.FindAllStringSubmatch(foundAlbumData[1], -1)
if strings.ToLower(albumMatch[0][2]) == strings.ToLower(album) {
if found == false {
found = true
albumData.URL = albumMatch[0][1]
albumData.Name = albumMatch[0][2]
albumIDMatch := albumIDre.FindAllStringSubmatch(albumData.URL, -1)
albumData.ID, _ = strconv.Atoi(albumIDMatch[0][1])
artistMatch := artistDatare.FindAllStringSubmatch(foundAlbumData[0], -1)
albumData.ArtistID, _ = strconv.Atoi(artistMatch[0][1])
albumData.Artist = artistMatch[0][2]
artistURLMatch := artistURLre.FindAllStringSubmatch(foundAlbumData[0], -1)
albumData.ArtistURL = artistURLMatch[0][1]
albumData.Type = types.SelectRecordType(foundAlbumData[2])
yearMatch := yearre.FindAllStringSubmatch(foundAlbumData[3], 1)
albumData.Year, _ = strconv.Atoi(yearMatch[0][0])
} else {
var extraAlbumData SearchAlbumData
extraAlbumData.URL = albumMatch[0][1]
extraAlbumData.Name = albumMatch[0][2]
albumIDMatch := albumIDre.FindAllStringSubmatch(extraAlbumData.URL, -1)
extraAlbumData.ID, _ = strconv.Atoi(albumIDMatch[0][1])
artistMatch := artistDatare.FindAllStringSubmatch(foundAlbumData[0], -1)
extraAlbumData.ArtistID, _ = strconv.Atoi(artistMatch[0][1])
extraAlbumData.Artist = artistMatch[0][2]
artistURLMatch := artistURLre.FindAllStringSubmatch(foundAlbumData[0], -1)
extraAlbumData.ArtistURL = artistURLMatch[0][1]
extraAlbumData.Type = types.SelectRecordType(foundAlbumData[2])
yearMatch := yearre.FindAllStringSubmatch(foundAlbumData[3], 1)
extraAlbumData.Year, _ = strconv.Atoi(yearMatch[0][0])
albumExtraData = append(albumExtraData, extraAlbumData)
}
}
}
}
if !found {
return albumData, albumExtraData, errors.New("No album was found.")
}
return albumData, albumExtraData, nil
}
package artists
import (
"errors"
"fmt"
commontypes "github.com/a-castellano/music-manager-common-types/types"
types "github.com/a-castellano/music-manager-metal-archives-wrapper/types"
"golang.org/x/net/html"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"strings"
)
func readRecord(n *html.Node) commontypes.Record {
recordIDre := regexp.MustCompile(`^[^\/]*\/\/[^\/]*\/albums\/[^\/]*\/[^\/]*\/([0-9]*)$`)
var newRecord commontypes.Record
RecordInfo := n.FirstChild.NextSibling.FirstChild
newRecord.URL = RecordInfo.Attr[0].Val
RecordNameInfo := RecordInfo.FirstChild
newRecord.Name = RecordNameInfo.Data
match := recordIDre.FindAllStringSubmatch(newRecord.URL, -1)
newRecord.ID = match[0][1]
RecordTypeInfo := n.FirstChild.NextSibling.NextSibling.NextSibling.FirstChild
newRecord.Type = types.SelectRecordType(RecordTypeInfo.Data)
RecordYearInfo := n.FirstChild.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.FirstChild
newRecord.Year, _ = strconv.Atoi(RecordYearInfo.Data)
return newRecord
}
func GetArtistRecords(client http.Client, artistData SearchArtistData) ([]commontypes.Record, error) {
var records []commontypes.Record
url := fmt.Sprintf("https://www.metal-archives.com/band/discography/id/%s/tab/all", artistData.ID)
trCounter := 0
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return records, err
}
req.Header.Set("User-Agent", "https://github.com/a-castellano/metal-archives-wrapper")
res, getErr := client.Do(req)
if getErr != nil {
return records, err
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
return records, err
}
stringBody := string(body)
doc, err := html.Parse(strings.NewReader(stringBody))
if err != nil {
return records, err
}
var f func(*html.Node, *[]commontypes.Record)
f = func(n *html.Node, records *[]commontypes.Record) {
if n.Type == html.ElementNode && n.Data == "tr" {
if trCounter != 0 {
newRecord := readRecord(n)
*records = append(*records, newRecord)
}
trCounter++
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c, records)
}
}
f(doc, &records)
if len(records) == 0 {
return records, errors.New("No records were found.")
}
return records, nil
}
package artists
import (
"encoding/json"
"errors"
"fmt"
commontypes "github.com/a-castellano/music-manager-common-types/types"
types "github.com/a-castellano/music-manager-metal-archives-wrapper/types"
"io/ioutil"
"net/http"
"regexp"
"strings"
)
type SearchArtistData commontypes.Artist
func searchArtistAjax(client http.Client, artist string) ([][]string, error) {
var searchArtistData [][]string
artistString := strings.Replace(artist, " ", "+", -1)
url := fmt.Sprintf("https://www.metal-archives.com/search/ajax-band-search/?field=name&query=%s", artistString)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return searchArtistData, err
}
req.Header.Set("User-Agent", "https://github.com/a-castellano/metal-archives-wrapper")
res, getErr := client.Do(req)
if getErr != nil {
return searchArtistData, getErr
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
return searchArtistData, readErr
}
searchArtist := types.SearchAjaxData{}
jsonErr := json.Unmarshal(body, &searchArtist)
if jsonErr != nil {
return searchArtistData, jsonErr
}
searchArtistData = searchArtist.Data
return searchArtistData, nil
}
func SearchArtist(client http.Client, artist string) (SearchArtistData, []SearchArtistData, error) {
var artistData SearchArtistData
var artistExtraData []SearchArtistData
artistDatare := regexp.MustCompile(`^<a href=\"([^\"]+)\">([^<]+)</a>`)
artistIDre := regexp.MustCompile(`^[^\/]*\/\/[^\/]*\/[^\/]*\/[^\/]*\/([0-9]*)`)
data, err := searchArtistAjax(client, artist)
var found bool = false
if err != nil {
return artistData, artistExtraData, err
} else {
for _, foundArtistData := range data {
match := artistDatare.FindAllStringSubmatch(foundArtistData[0], -1)
if strings.ToLower(match[0][2]) == strings.ToLower(artist) {
if !found {
artistData.URL = match[0][1]
artistData.Name = match[0][2]
artistData.Genre = foundArtistData[1]
artistData.Country = foundArtistData[2]
IDmatch := artistIDre.FindAllStringSubmatch(artistData.URL, -1)
artistData.ID = IDmatch[0][1]
found = true
} else {
extraData := SearchArtistData{}
extraData.URL = match[0][1]
extraData.Name = match[0][2]
extraData.Genre = foundArtistData[1]
extraData.Country = foundArtistData[2]
IDmatch := artistIDre.FindAllStringSubmatch(extraData.URL, -1)
extraData.ID = IDmatch[0][1]
artistExtraData = append(artistExtraData, extraData)
}
}
}
}
if !found {
return artistData, artistExtraData, errors.New("No artist was found.")
}
return artistData, artistExtraData, nil
}
package jobs
import (
"errors"
"fmt"
"net/http"
commontypes "github.com/a-castellano/music-manager-common-types/types"
"github.com/a-castellano/music-manager-metal-archives-wrapper/artists"
)
func ProcessJob(data []byte, origin string, client http.Client) (bool, []byte, error) {
receivedJob, decodeJobErr := commontypes.DecodeJob(data)
var job commontypes.Job
var die bool = false
var err error
job.ID = receivedJob.ID
job.Type = receivedJob.Type
job.Status = receivedJob.Status
job.LastOrigin = origin
if decodeJobErr == nil {
// Job has been successfully decoded
switch receivedJob.Type {
case commontypes.ArtistInfoRetrieval:
var retrievalData commontypes.InfoRetrieval
retrievalData, err = commontypes.DecodeInfoRetrieval(receivedJob.Data)
if err == nil {
switch retrievalData.Type {
case commontypes.ArtistName:
data, extraData, errSearchArtist := artists.SearchArtist(client, retrievalData.Artist)
// If there is no artist info job must return empty data, but it is not an error.
if errSearchArtist != nil {
err = errors.New(errors.New("Artist retrieval failed: ").Error() + errSearchArtist.Error())
job.Error = err.Error()
job.Status = false
} else {
artistData := commontypes.Artist{}
artistData.Name = data.Name
artistData.URL = data.URL
artistData.ID = data.ID
artistData.Country = data.Country
artistData.Genre = data.Genre
artistinfo := commontypes.ArtistInfo{}
artistinfo.Data = artistData
for _, extraArtist := range extraData {
var artist commontypes.Artist
artist.Name = extraArtist.Name
artist.URL = extraArtist.URL
artist.ID = extraArtist.ID
artist.Country = extraArtist.Country
artist.Genre = extraArtist.Genre
artistinfo.ExtraData = append(artistinfo.ExtraData, artist)
}
job.Result, _ = commontypes.EncodeArtistInfo(artistinfo)
job.Status = true
}
default:
err = errors.New("Music Manager Metal Archives Wrapper - ArtistInfoRetrieval type should be only ArtistName.")
job.Status = false
job.Error = err.Error()
}
}
case commontypes.RecordInfoRetrieval:
fmt.Println("RecordInfoRetrieval")
case commontypes.Die:
die = true
default:
err = errors.New("Unknown Job Type for this service.")
job.Status = false
}
} else {
err = errors.New("Empty job data received.")
job.Status = false
}
processedJob, _ := commontypes.EncodeJob(job)
return die, processedJob, err
}
package queues
import (
"fmt"
config "github.com/a-castellano/music-manager-config-reader/config_reader"
"github.com/a-castellano/music-manager-metal-archives-wrapper/jobs"
"github.com/streadway/amqp"
"net/http"
"strconv"
)
func StartJobManagement(config config.Config, client http.Client) error {
connection_string := "amqp://" + config.Server.User + ":" + config.Server.Password + "@" + config.Server.Host + ":" + strconv.Itoa(config.Server.Port) + "/"
conn, err := amqp.Dial(connection_string)
if err != nil {
return fmt.Errorf("Failed to stablish connection with RabbitMQ: %w", err)
}
defer conn.Close()
incoming_ch, err := conn.Channel()
defer incoming_ch.Close()
if err != nil {
return fmt.Errorf("Failed to open incoming channel: %w", err)
}
outgoing_ch, err := conn.Channel()
defer outgoing_ch.Close()
if err != nil {
return fmt.Errorf("Failed to open outgoing channel: %w", err)
}
incoming_q, err := incoming_ch.QueueDeclare(
config.Incoming.Name,
true, // Durable
false, // DeleteWhenUnused
false, // Exclusive
false, // NoWait
nil, // arguments
)
if err != nil {
return fmt.Errorf("Failed to declare incoming queue: %w", err)
}
outgoing_q, err := outgoing_ch.QueueDeclare(
config.Outgoing.Name,
true, // Durable
false, // DeleteWhenUnused
false, // Exclusive
false, // NoWait
nil, // arguments
)
if err != nil {
return fmt.Errorf("Failed to declare outgoing queue: %w", err)
}
err = incoming_ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
if err != nil {
return fmt.Errorf("Failed to set incoming QoS: %w", err)
}
jobsToProcess, err := incoming_ch.Consume(
incoming_q.Name,
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
return fmt.Errorf("Failed to register a consumer: %w", err)
}
processJobs := make(chan bool)
go func() {
for job := range jobsToProcess {
die, jobResult, _ := jobs.ProcessJob(job.Body, config.Origin, client)
if die {
job.Ack(false)
processJobs <- false
return
}
err = outgoing_ch.Publish(
"", // exchange
outgoing_q.Name, // routing key
false, // mandatory
false,
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: jobResult,
})
if err != nil {
fmt.Println(err)
return
}
job.Ack(false)
}
return
}()
<-processJobs
return nil
}
package types
import (
commontypes "github.com/a-castellano/music-manager-common-types/types"
)
type SearchAjaxData struct {
Error string `json:"error"`
TotalRecords int `json:"iTotalRecords"`
TotalDisplayRecords int `json:"iTotalDisplayRecords"`
Echo int `json:"sEcho"`
Data [][]string `json:"aaData"`
}
func SelectRecordType(record string) commontypes.RecordType {
var typeFound commontypes.RecordType
switch record {
case "Full-length":
typeFound = commontypes.FullLength
case "EP":
typeFound = commontypes.EP
case "Compilation":
typeFound = commontypes.Compilation
case "Demo":
typeFound = commontypes.Demo
case "Video":
typeFound = commontypes.Video
case "Single":
typeFound = commontypes.Single
case "Live album":
typeFound = commontypes.Live
case "Split":
typeFound = commontypes.Split
case "Boxed set":
typeFound = commontypes.BoxedSet
default:
typeFound = commontypes.Other
}
return typeFound
}