1package song
2
3import (
4	"fmt"
5	"sort"
6	"strconv"
7	"strings"
8
9	"github.com/ambientsound/pms/utils"
10
11	"github.com/ambientsound/gompd/mpd"
12)
13
14// Song represents a combined view of a song from both MPD and PMS' perspectives.
15type Song struct {
16	ID         int
17	Position   int
18	Time       int
19	Tags       Taglist
20	StringTags StringTaglist
21	SortTags   StringTaglist
22}
23
24type Tag []rune
25
26type Taglist map[string]Tag
27
28type StringTaglist map[string]string
29
30const NullID int = -1
31const NullPosition int = -1
32
33func New() (s *Song) {
34	s = &Song{}
35	s.Tags = make(Taglist)
36	s.StringTags = make(StringTaglist)
37	s.SortTags = make(StringTaglist)
38	return
39}
40
41func (s *Song) SetTags(tags mpd.Attrs) {
42	s.Tags = make(Taglist)
43	for key := range tags {
44		lowKey := strings.ToLower(key)
45		s.Tags[lowKey] = []rune(tags[key])
46		s.StringTags[lowKey] = tags[key]
47	}
48	s.AutoFill()
49	s.FillSortTags()
50}
51
52// NullID returns true if the song's ID is not present.
53func (s *Song) NullID() bool {
54	return s.ID == NullID
55}
56
57// NullPosition returns true if the song's osition is not present.
58func (s *Song) NullPosition() bool {
59	return s.Position == NullPosition
60}
61
62// AutoFill post-processes and caches song tags.
63func (s *Song) AutoFill() {
64	var err error
65
66	s.ID, err = strconv.Atoi(s.StringTags["id"])
67	if err != nil {
68		s.ID = NullID
69	}
70	s.Position, err = strconv.Atoi(s.StringTags["pos"])
71	if err != nil {
72		s.Position = NullPosition
73	}
74
75	s.Time, err = strconv.Atoi(s.StringTags["time"])
76	if err == nil {
77		s.Tags["time"] = utils.TimeRunes(s.Time)
78	} else {
79		s.Tags["time"] = utils.TimeRunes(-1)
80	}
81	if len(s.Tags["date"]) >= 4 {
82		s.Tags["year"] = s.Tags["date"][:4]
83		s.StringTags["year"] = string(s.Tags["year"])
84	}
85	if len(s.Tags["originaldate"]) >= 4 {
86		s.Tags["originalyear"] = s.Tags["originaldate"][:4]
87		s.StringTags["originalyear"] = string(s.Tags["originalyear"])
88	}
89}
90
91// FillSortTags post-processes tags, and saves them as strings for sorting purposes later on.
92func (s *Song) FillSortTags() {
93	for i := range s.Tags {
94		s.SortTags[i] = strings.ToLower(s.StringTags[i])
95	}
96
97	if t, ok := s.SortTags["track"]; ok {
98		s.SortTags["track"] = trackSort(t)
99	}
100
101	if _, ok := s.SortTags["artistsort"]; !ok {
102		s.SortTags["artistsort"] = s.SortTags["artist"]
103	}
104
105	if _, ok := s.SortTags["albumartist"]; !ok {
106		s.SortTags["albumartist"] = s.SortTags["artist"]
107	}
108
109	if _, ok := s.SortTags["albumartistsort"]; !ok {
110		s.SortTags["albumartistsort"] = s.SortTags["albumartist"]
111	}
112}
113
114// HasOneOfTags returns true if the song contains at least one of the tags mentioned.
115func (s *Song) HasOneOfTags(tags ...string) bool {
116	for _, tag := range tags {
117		if _, ok := s.Tags[tag]; ok {
118			return true
119		}
120	}
121	return false
122}
123
124// TagKeys returns a string slice with all tag keys, sorted in alphabetical order.
125func (s *Song) TagKeys() []string {
126	keys := make(sort.StringSlice, 0, len(s.StringTags))
127	for tag := range s.StringTags {
128		keys = append(keys, tag)
129	}
130	keys.Sort()
131	return keys
132}
133
134func trackSort(s string) string {
135	tracks := strings.Split(s, "/")
136	if len(tracks) == 0 {
137		return s
138	}
139	trackNum, err := strconv.Atoi(tracks[0])
140	if err != nil {
141		return s
142	}
143	// Assume no release has more than 999 tracks.
144	return fmt.Sprintf("%03d", trackNum)
145}
146