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