mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-16 02:45:54 -07:00
chore: initial public commit
This commit is contained in:
commit
fc9054b78c
250 changed files with 32809 additions and 0 deletions
57
internal/mbz/artist.go
Normal file
57
internal/mbz/artist.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package mbz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/gabehf/koito/internal/logger"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MusicBrainzArtist struct {
|
||||
Name string `json:"name"`
|
||||
SortName string `json:"sort_name"`
|
||||
Gender string `json:"gender"`
|
||||
Area MusicBrainzArea `json:"area"`
|
||||
Aliases []MusicBrainzArtistAlias `json:"aliases"`
|
||||
}
|
||||
type MusicBrainzArtistAlias struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Primary bool `json:"primary"`
|
||||
}
|
||||
|
||||
const artistAliasFmtStr = "%s/ws/2/artist/%s?inc=aliases"
|
||||
|
||||
func (c *MusicBrainzClient) getArtist(ctx context.Context, id uuid.UUID) (*MusicBrainzArtist, error) {
|
||||
var mbzArtist *MusicBrainzArtist
|
||||
err := c.getEntity(ctx, artistAliasFmtStr, id, mbzArtist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mbzArtist, nil
|
||||
}
|
||||
|
||||
// Returns the artist name at index 0, and all primary aliases after.
|
||||
func (c *MusicBrainzClient) GetArtistPrimaryAliases(ctx context.Context, id uuid.UUID) ([]string, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
artist, err := c.getArtist(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if artist == nil {
|
||||
return nil, errors.New("artist could not be found by musicbrainz")
|
||||
}
|
||||
used := make(map[string]bool)
|
||||
ret := make([]string, 1)
|
||||
ret[0] = artist.Name
|
||||
used[artist.Name] = true
|
||||
for _, alias := range artist.Aliases {
|
||||
if alias.Primary && !slices.Contains(ret, alias.Name) {
|
||||
l.Debug().Msgf("Found primary alias '%s' for artist '%s'", alias.Name, artist.Name)
|
||||
ret = append(ret, alias.Name)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
92
internal/mbz/mbz.go
Normal file
92
internal/mbz/mbz.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// package mbz provides functions for interacting with the musicbrainz api
|
||||
package mbz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gabehf/koito/internal/cfg"
|
||||
"github.com/gabehf/koito/internal/logger"
|
||||
"github.com/gabehf/koito/queue"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MusicBrainzArea struct {
|
||||
Name string `json:"name"`
|
||||
Iso3166_1Codes []string `json:"iso-3166-1-codes"`
|
||||
}
|
||||
|
||||
type MusicBrainzClient struct {
|
||||
url string
|
||||
userAgent string
|
||||
requestQueue *queue.RequestQueue
|
||||
}
|
||||
|
||||
type MusicBrainzCaller interface {
|
||||
GetArtistPrimaryAliases(ctx context.Context, id uuid.UUID) ([]string, error)
|
||||
GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error)
|
||||
GetTrack(ctx context.Context, id uuid.UUID) (*MusicBrainzTrack, error)
|
||||
GetReleaseGroup(ctx context.Context, id uuid.UUID) (*MusicBrainzReleaseGroup, error)
|
||||
GetRelease(ctx context.Context, id uuid.UUID) (*MusicBrainzRelease, error)
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
func NewMusicBrainzClient() *MusicBrainzClient {
|
||||
ret := new(MusicBrainzClient)
|
||||
ret.url = cfg.MusicBrainzUrl()
|
||||
ret.userAgent = "Koito/0.0.1 (contact@koito.app)"
|
||||
ret.requestQueue = queue.NewRequestQueue(1, 1)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *MusicBrainzClient) Shutdown() {
|
||||
c.requestQueue.Shutdown()
|
||||
}
|
||||
|
||||
func (c *MusicBrainzClient) getEntity(ctx context.Context, fmtStr string, id uuid.UUID, result any) error {
|
||||
l := logger.FromContext(ctx)
|
||||
url := fmt.Sprintf(fmtStr, c.url, id.String())
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Debug().Msg("Adding MusicBrainz request to queue")
|
||||
body, err := c.queue(ctx, req)
|
||||
if err != nil {
|
||||
l.Debug().Err(err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, result)
|
||||
if err != nil {
|
||||
l.Debug().Err(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MusicBrainzClient) queue(ctx context.Context, req *http.Request) ([]byte, error) {
|
||||
l := logger.FromContext(ctx)
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resultChan := c.requestQueue.Enqueue(func(client *http.Client, done chan<- queue.RequestResult) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
l.Debug().Err(err).Str("url", req.RequestURI).Msg("Failed to contact MusicBrainz")
|
||||
done <- queue.RequestResult{Err: err}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
done <- queue.RequestResult{Body: body, Err: err}
|
||||
})
|
||||
|
||||
result := <-resultChan
|
||||
return result.Body, result.Err
|
||||
}
|
||||
93
internal/mbz/mock.go
Normal file
93
internal/mbz/mock.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package mbz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// implements a mock caller
|
||||
|
||||
type MbzMockCaller struct {
|
||||
Artists map[uuid.UUID]*MusicBrainzArtist
|
||||
ReleaseGroups map[uuid.UUID]*MusicBrainzReleaseGroup
|
||||
Releases map[uuid.UUID]*MusicBrainzRelease
|
||||
Tracks map[uuid.UUID]*MusicBrainzTrack
|
||||
}
|
||||
|
||||
func (m *MbzMockCaller) GetReleaseGroup(ctx context.Context, id uuid.UUID) (*MusicBrainzReleaseGroup, error) {
|
||||
releaseGroup, exists := m.ReleaseGroups[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("release group with ID %s not found", id)
|
||||
}
|
||||
return releaseGroup, nil
|
||||
}
|
||||
|
||||
func (m *MbzMockCaller) GetRelease(ctx context.Context, id uuid.UUID) (*MusicBrainzRelease, error) {
|
||||
release, exists := m.Releases[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("release group with ID %s not found", id)
|
||||
}
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (m *MbzMockCaller) GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error) {
|
||||
rg, exists := m.ReleaseGroups[RGID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("release with ID %s not found", RGID)
|
||||
}
|
||||
|
||||
var titles []string
|
||||
for _, release := range rg.Releases {
|
||||
if !slices.Contains(titles, release.Title) {
|
||||
titles = append(titles, release.Title)
|
||||
}
|
||||
}
|
||||
return titles, nil
|
||||
}
|
||||
|
||||
func (m *MbzMockCaller) GetTrack(ctx context.Context, id uuid.UUID) (*MusicBrainzTrack, error) {
|
||||
track, exists := m.Tracks[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("track with ID %s not found", id)
|
||||
}
|
||||
return track, nil
|
||||
}
|
||||
|
||||
func (m *MbzMockCaller) GetArtistPrimaryAliases(ctx context.Context, id uuid.UUID) ([]string, error) {
|
||||
name := m.Artists[id].Name
|
||||
ss := make([]string, len(m.Artists[id].Aliases)+1)
|
||||
ss[0] = name
|
||||
for i, alias := range m.Artists[id].Aliases {
|
||||
ss[i+1] = alias.Name
|
||||
}
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
func (m *MbzMockCaller) Shutdown() {}
|
||||
|
||||
type MbzErrorCaller struct{}
|
||||
|
||||
func (m *MbzErrorCaller) GetReleaseGroup(ctx context.Context, id uuid.UUID) (*MusicBrainzReleaseGroup, error) {
|
||||
return nil, fmt.Errorf("error: GetReleaseGroup not implemented")
|
||||
}
|
||||
|
||||
func (m *MbzErrorCaller) GetRelease(ctx context.Context, id uuid.UUID) (*MusicBrainzRelease, error) {
|
||||
return nil, fmt.Errorf("error: GetRelease not implemented")
|
||||
}
|
||||
|
||||
func (m *MbzErrorCaller) GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error) {
|
||||
return nil, fmt.Errorf("error: GetReleaseTitles not implemented")
|
||||
}
|
||||
|
||||
func (m *MbzErrorCaller) GetTrack(ctx context.Context, id uuid.UUID) (*MusicBrainzTrack, error) {
|
||||
return nil, fmt.Errorf("error: GetTrack not implemented")
|
||||
}
|
||||
|
||||
func (m *MbzErrorCaller) GetArtistPrimaryAliases(ctx context.Context, id uuid.UUID) ([]string, error) {
|
||||
return nil, fmt.Errorf("error: GetArtistPrimaryAliases not implemented")
|
||||
}
|
||||
|
||||
func (m *MbzErrorCaller) Shutdown() {}
|
||||
92
internal/mbz/release.go
Normal file
92
internal/mbz/release.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package mbz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MusicBrainzReleaseGroup struct {
|
||||
Title string `json:"title"`
|
||||
Type string `json:"primary_type"`
|
||||
ArtistCredit []MusicBrainzArtistCredit `json:"artist-credit"`
|
||||
Releases []MusicBrainzRelease `json:"releases"`
|
||||
}
|
||||
type MusicBrainzRelease struct {
|
||||
Title string `json:"title"`
|
||||
ID string `json:"id"`
|
||||
ArtistCredit []MusicBrainzArtistCredit `json:"artist-credit"`
|
||||
Status string `json:"status"`
|
||||
TextRepresentation TextRepresentation `json:"text-representation"`
|
||||
}
|
||||
type MusicBrainzArtistCredit struct {
|
||||
Artist MusicBrainzArtist `json:"artist"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type TextRepresentation struct {
|
||||
Language string `json:"language"`
|
||||
Script string `json:"script"`
|
||||
}
|
||||
|
||||
const releaseGroupFmtStr = "%s/ws/2/release-group/%s?inc=releases+artists"
|
||||
const releaseFmtStr = "%s/ws/2/release/%s?inc=artists"
|
||||
|
||||
func (c *MusicBrainzClient) GetReleaseGroup(ctx context.Context, id uuid.UUID) (*MusicBrainzReleaseGroup, error) {
|
||||
mbzRG := new(MusicBrainzReleaseGroup)
|
||||
err := c.getEntity(ctx, releaseGroupFmtStr, id, mbzRG)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mbzRG, nil
|
||||
}
|
||||
|
||||
func (c *MusicBrainzClient) GetRelease(ctx context.Context, id uuid.UUID) (*MusicBrainzRelease, error) {
|
||||
mbzRelease := new(MusicBrainzRelease)
|
||||
err := c.getEntity(ctx, releaseFmtStr, id, mbzRelease)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mbzRelease, nil
|
||||
}
|
||||
|
||||
func (c *MusicBrainzClient) GetReleaseTitles(ctx context.Context, RGID uuid.UUID) ([]string, error) {
|
||||
releaseGroup, err := c.GetReleaseGroup(ctx, RGID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var titles []string
|
||||
for _, release := range releaseGroup.Releases {
|
||||
if !slices.Contains(titles, release.Title) {
|
||||
titles = append(titles, release.Title)
|
||||
}
|
||||
}
|
||||
|
||||
return titles, nil
|
||||
}
|
||||
|
||||
func ReleaseGroupToTitles(rg *MusicBrainzReleaseGroup) []string {
|
||||
var titles []string
|
||||
for _, release := range rg.Releases {
|
||||
if !slices.Contains(titles, release.Title) {
|
||||
titles = append(titles, release.Title)
|
||||
}
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
// Searches for Pseudo-Releases of release groups with Latin script, and returns them as an array
|
||||
func (c *MusicBrainzClient) GetLatinTitles(ctx context.Context, id uuid.UUID) ([]string, error) {
|
||||
rg, err := c.GetReleaseGroup(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
titles := make([]string, 0)
|
||||
for _, r := range rg.Releases {
|
||||
if r.Status == "Pseudo-Release" && r.TextRepresentation.Script == "Latn" { // not a typo
|
||||
titles = append(titles, r.Title)
|
||||
}
|
||||
}
|
||||
return titles, nil
|
||||
}
|
||||
23
internal/mbz/track.go
Normal file
23
internal/mbz/track.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package mbz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MusicBrainzTrack struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
const recordingFmtStr = "%s/ws/2/recording/%s"
|
||||
|
||||
// Returns the artist name at index 0, and all primary aliases after.
|
||||
func (c *MusicBrainzClient) GetTrack(ctx context.Context, id uuid.UUID) (*MusicBrainzTrack, error) {
|
||||
track := new(MusicBrainzTrack)
|
||||
err := c.getEntity(ctx, recordingFmtStr, id, track)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return track, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue