├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── taglib.go ├── taglib_test.go └── test.mp3 /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | before_install: 3 | - sudo apt-get update -qq 4 | - sudo apt-get install -qq libtagc0-dev 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2021 William Trevor Olson 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-taglib 2 | ========= 3 | 4 | Go wrapper for [taglib](http://taglib.github.com/) 5 | [![Build Status](https://travis-ci.org/wtolson/go-taglib.png)](https://travis-ci.org/wtolson/go-taglib) 6 | 7 | Dependencies 8 | ------------ 9 | 10 | You must have the static [taglib](http://taglib.github.com/) libraries installed 11 | in order to compile go-taglib. 12 | 13 | ### OSX: 14 | 15 | brew install taglib 16 | 17 | ### Ubuntu: 18 | 19 | sudo apt-get install libtagc0-dev 20 | 21 | Install 22 | ------- 23 | 24 | go get github.com/wtolson/go-taglib 25 | 26 | Documentation 27 | ------------- 28 | 29 | Checkout the documentation at http://godoc.org/github.com/wtolson/go-taglib 30 | -------------------------------------------------------------------------------- /taglib.go: -------------------------------------------------------------------------------- 1 | // Go wrapper for taglib 2 | 3 | // Generate stringer method for types 4 | //go:generate stringer -type=TagName 5 | 6 | package taglib 7 | 8 | // #cgo pkg-config: taglib 9 | // #cgo LDFLAGS: -ltag_c 10 | // #include 11 | // #include 12 | import "C" 13 | 14 | import ( 15 | "errors" 16 | "strconv" 17 | "sync" 18 | "time" 19 | "unsafe" 20 | ) 21 | 22 | type TagName int 23 | 24 | // Tag names 25 | const ( 26 | Album TagName = iota 27 | Artist 28 | Bitrate 29 | Channels 30 | Comments 31 | Genre 32 | Length 33 | Samplerate 34 | Title 35 | Track 36 | Year 37 | ) 38 | 39 | var ( 40 | ErrInvalid = errors.New("invalid file") 41 | glock = sync.Mutex{} 42 | ) 43 | 44 | func init() { 45 | // Make everything utf-8 46 | C.taglib_id3v2_set_default_text_encoding(3) 47 | } 48 | 49 | // Returns a string with this tag's comment. 50 | func (file *File) Tag(tagname TagName) (tagvalue string) { 51 | switch tagname { 52 | case Album: 53 | return file.Album() 54 | case Artist: 55 | return file.Artist() 56 | case Bitrate: 57 | return strconv.Itoa(file.Bitrate()) 58 | case Channels: 59 | return strconv.Itoa(file.Channels()) 60 | case Comments: 61 | return file.Comment() 62 | case Genre: 63 | return file.Genre() 64 | case Length: 65 | return file.Length().String() 66 | case Samplerate: 67 | return strconv.Itoa(file.Samplerate()) 68 | case Title: 69 | return file.Title() 70 | case Track: 71 | return strconv.Itoa(file.Track()) 72 | case Year: 73 | return strconv.Itoa(file.Year()) 74 | } 75 | return "" 76 | } 77 | 78 | // Sets the tag. 79 | func (file *File) SetTag(tagname TagName, tagvalue string) { 80 | switch tagname { 81 | case Album: 82 | file.SetAlbum(tagvalue) 83 | case Artist: 84 | file.SetArtist(tagvalue) 85 | case Comments: 86 | file.SetComment(tagvalue) 87 | case Genre: 88 | file.SetGenre(tagvalue) 89 | case Title: 90 | file.SetTitle(tagvalue) 91 | case Track: 92 | intValue, convErr := strconv.Atoi(tagvalue) 93 | if convErr == nil { 94 | file.SetTrack(intValue) 95 | } 96 | case Year: 97 | intValue, convErr := strconv.Atoi(tagvalue) 98 | if convErr == nil { 99 | file.SetYear(intValue) 100 | } 101 | } 102 | } 103 | 104 | type File struct { 105 | fp *C.TagLib_File 106 | tag *C.TagLib_Tag 107 | props *C.TagLib_AudioProperties 108 | } 109 | 110 | // Reads and parses a music file. Returns an error if the provided filename is 111 | // not a valid file. 112 | func Read(filename string) (*File, error) { 113 | glock.Lock() 114 | defer glock.Unlock() 115 | 116 | cs := C.CString(filename) 117 | defer C.free(unsafe.Pointer(cs)) 118 | 119 | fp := C.taglib_file_new(cs) 120 | if fp == nil || C.taglib_file_is_valid(fp) == 0 { 121 | return nil, ErrInvalid 122 | } 123 | 124 | return &File{ 125 | fp: fp, 126 | tag: C.taglib_file_tag(fp), 127 | props: C.taglib_file_audioproperties(fp), 128 | }, nil 129 | } 130 | 131 | // Close and free the file. 132 | func (file *File) Close() { 133 | glock.Lock() 134 | defer glock.Unlock() 135 | 136 | C.taglib_file_free(file.fp) 137 | file.fp = nil 138 | file.tag = nil 139 | file.props = nil 140 | } 141 | 142 | func convertAndFree(cs *C.char) string { 143 | if cs == nil { 144 | return "" 145 | } 146 | 147 | defer C.free(unsafe.Pointer(cs)) 148 | return C.GoString(cs) 149 | } 150 | 151 | // Returns a string with this tag's title. 152 | func (file *File) Title() string { 153 | glock.Lock() 154 | defer glock.Unlock() 155 | 156 | return convertAndFree(C.taglib_tag_title(file.tag)) 157 | } 158 | 159 | // Returns a string with this tag's artist. 160 | func (file *File) Artist() string { 161 | glock.Lock() 162 | defer glock.Unlock() 163 | 164 | return convertAndFree(C.taglib_tag_artist(file.tag)) 165 | } 166 | 167 | // Returns a string with this tag's album name. 168 | func (file *File) Album() string { 169 | glock.Lock() 170 | defer glock.Unlock() 171 | 172 | return convertAndFree(C.taglib_tag_album(file.tag)) 173 | } 174 | 175 | // Returns a string with this tag's comment. 176 | func (file *File) Comment() string { 177 | glock.Lock() 178 | defer glock.Unlock() 179 | 180 | return convertAndFree(C.taglib_tag_comment(file.tag)) 181 | } 182 | 183 | // Returns a string with this tag's genre. 184 | func (file *File) Genre() string { 185 | glock.Lock() 186 | defer glock.Unlock() 187 | 188 | return convertAndFree(C.taglib_tag_genre(file.tag)) 189 | } 190 | 191 | // Returns the tag's year or 0 if year is not set. 192 | func (file *File) Year() int { 193 | glock.Lock() 194 | defer glock.Unlock() 195 | 196 | return int(C.taglib_tag_year(file.tag)) 197 | } 198 | 199 | // Returns the tag's track number or 0 if track number is not set. 200 | func (file *File) Track() int { 201 | glock.Lock() 202 | defer glock.Unlock() 203 | 204 | return int(C.taglib_tag_track(file.tag)) 205 | } 206 | 207 | // Returns the length of the file. 208 | func (file *File) Length() time.Duration { 209 | glock.Lock() 210 | defer glock.Unlock() 211 | 212 | length := C.taglib_audioproperties_length(file.props) 213 | return time.Duration(length) * time.Second 214 | } 215 | 216 | // Returns the bitrate of the file in kb/s. 217 | func (file *File) Bitrate() int { 218 | glock.Lock() 219 | defer glock.Unlock() 220 | 221 | return int(C.taglib_audioproperties_bitrate(file.props)) 222 | } 223 | 224 | // Returns the sample rate of the file in Hz. 225 | func (file *File) Samplerate() int { 226 | glock.Lock() 227 | defer glock.Unlock() 228 | 229 | return int(C.taglib_audioproperties_samplerate(file.props)) 230 | } 231 | 232 | // Returns the number of channels in the audio stream. 233 | func (file *File) Channels() int { 234 | glock.Lock() 235 | defer glock.Unlock() 236 | 237 | return int(C.taglib_audioproperties_channels(file.props)) 238 | } 239 | 240 | func init() { 241 | glock.Lock() 242 | defer glock.Unlock() 243 | 244 | C.taglib_set_string_management_enabled(0) 245 | } 246 | 247 | // Saves the \a file to disk. 248 | func (file *File) Save() error { 249 | var err error 250 | glock.Lock() 251 | defer glock.Unlock() 252 | if C.taglib_file_save(file.fp) != 1 { 253 | err = errors.New("Cannot save file") 254 | } 255 | return err 256 | } 257 | 258 | // Sets the tag's title. 259 | func (file *File) SetTitle(s string) { 260 | glock.Lock() 261 | defer glock.Unlock() 262 | cs := GetCCharPointer(s) 263 | defer C.free(unsafe.Pointer(cs)) 264 | C.taglib_tag_set_title(file.tag, cs) 265 | 266 | } 267 | 268 | // Sets the tag's artist. 269 | func (file *File) SetArtist(s string) { 270 | glock.Lock() 271 | defer glock.Unlock() 272 | cs := GetCCharPointer(s) 273 | defer C.free(unsafe.Pointer(cs)) 274 | C.taglib_tag_set_artist(file.tag, cs) 275 | } 276 | 277 | // Sets the tag's album. 278 | func (file *File) SetAlbum(s string) { 279 | glock.Lock() 280 | defer glock.Unlock() 281 | cs := GetCCharPointer(s) 282 | defer C.free(unsafe.Pointer(cs)) 283 | C.taglib_tag_set_album(file.tag, cs) 284 | } 285 | 286 | // Sets the tag's comment. 287 | func (file *File) SetComment(s string) { 288 | glock.Lock() 289 | defer glock.Unlock() 290 | cs := GetCCharPointer(s) 291 | defer C.free(unsafe.Pointer(cs)) 292 | C.taglib_tag_set_comment(file.tag, cs) 293 | } 294 | 295 | // Sets the tag's genre. 296 | func (file *File) SetGenre(s string) { 297 | glock.Lock() 298 | defer glock.Unlock() 299 | cs := GetCCharPointer(s) 300 | defer C.free(unsafe.Pointer(cs)) 301 | C.taglib_tag_set_genre(file.tag, cs) 302 | } 303 | 304 | // Sets the tag's year. 0 indicates that this field should be cleared. 305 | func (file *File) SetYear(i int) { 306 | glock.Lock() 307 | defer glock.Unlock() 308 | ci := C.uint(i) 309 | C.taglib_tag_set_year(file.tag, ci) 310 | } 311 | 312 | // Sets the tag's track number. 0 indicates that this field should be cleared. 313 | func (file *File) SetTrack(i int) { 314 | glock.Lock() 315 | defer glock.Unlock() 316 | ci := C.uint(i) 317 | C.taglib_tag_set_track(file.tag, ci) 318 | } 319 | 320 | func GetCCharPointer(s string) *C.char { 321 | // Add a 0x00 to end 322 | b := append([]byte(s), 0) 323 | return (*C.char)(C.CBytes(b)) 324 | } 325 | -------------------------------------------------------------------------------- /taglib_test.go: -------------------------------------------------------------------------------- 1 | package taglib 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "strconv" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestReadNothing(t *testing.T) { 14 | file, err := Read("doesnotexist.mp3") 15 | 16 | if file != nil { 17 | t.Fatal("Returned non nil file struct.") 18 | } 19 | 20 | if err == nil { 21 | t.Fatal("Returned nil err.") 22 | } 23 | 24 | if err != ErrInvalid { 25 | t.Fatal("Didn't return ErrInvalid") 26 | } 27 | } 28 | 29 | func TestReadDirectory(t *testing.T) { 30 | file, err := Read("/") 31 | 32 | if file != nil { 33 | t.Fatal("Returned non nil file struct.") 34 | } 35 | 36 | if err == nil { 37 | t.Fatal("Returned nil err.") 38 | } 39 | 40 | if err != ErrInvalid { 41 | t.Fatal("Didn't return ErrInvalid") 42 | } 43 | } 44 | 45 | func TestTagLib(t *testing.T) { 46 | file, err := Read("test.mp3") 47 | 48 | if err != nil { 49 | panic(err) 50 | t.Fatalf("Read returned error: %s", err) 51 | } 52 | 53 | defer file.Close() 54 | 55 | // Test the Tags 56 | if title := file.Title(); title != "The Title" { 57 | t.Errorf("Got wrong title: %s", title) 58 | } 59 | 60 | if artist := file.Artist(); artist != "The Artist" { 61 | t.Errorf("Got wrong artist: %s", artist) 62 | } 63 | 64 | if album := file.Album(); album != "The Album" { 65 | t.Errorf("Got wrong album: %s", album) 66 | } 67 | 68 | if comment := file.Comment(); comment != "A Comment" { 69 | t.Errorf("Got wrong comment: %s", comment) 70 | } 71 | 72 | if genre := file.Genre(); genre != "Booty Bass" { 73 | t.Errorf("Got wrong genre: %s", genre) 74 | } 75 | 76 | if year := file.Year(); year != 1942 { 77 | t.Errorf("Got wrong year: %d", year) 78 | } 79 | 80 | if track := file.Track(); track != 42 { 81 | t.Errorf("Got wrong track: %d", track) 82 | } 83 | 84 | // Test the properties 85 | if length := file.Length(); length != 42*time.Second { 86 | t.Errorf("Got wrong length: %s", length) 87 | } 88 | 89 | if bitrate := file.Bitrate(); bitrate != 128 { 90 | t.Errorf("Got wrong bitrate: %d", bitrate) 91 | } 92 | 93 | if samplerate := file.Samplerate(); samplerate != 44100 { 94 | t.Errorf("Got wrong samplerate: %d", samplerate) 95 | } 96 | 97 | if channels := file.Channels(); channels != 2 { 98 | t.Errorf("Got wrong channels: %d", channels) 99 | } 100 | } 101 | 102 | func TestWriteTagLib(t *testing.T) { 103 | fileName := "test.mp3" 104 | file, err := Read(fileName) 105 | 106 | if err != nil { 107 | panic(err) 108 | t.Fatalf("Read returned error: %s", err) 109 | } 110 | tempDir, err := ioutil.TempDir("", "go-taglib-test") 111 | 112 | if err != nil { 113 | panic(err) 114 | t.Fatalf("Cannot create temporary file for writing tests: %s", err) 115 | } 116 | 117 | tempFileName := path.Join(tempDir, "go-taglib-test.mp3") 118 | 119 | defer file.Close() 120 | defer os.RemoveAll(tempDir) 121 | 122 | err = cp(tempFileName, fileName) 123 | 124 | if err != nil { 125 | panic(err) 126 | t.Fatalf("Cannot copy file for writing tests: %s", err) 127 | } 128 | 129 | modifiedFile, err := Read(tempFileName) 130 | if err != nil { 131 | panic(err) 132 | t.Fatalf("Read returned error: %s", err) 133 | } 134 | modifiedFile.SetAlbum(getModifiedString(file.Album())) 135 | modifiedFile.SetComment(getModifiedString(file.Comment())) 136 | modifiedFile.SetGenre(getModifiedString(file.Genre())) 137 | modifiedFile.SetTrack(file.Track() + 1) 138 | modifiedFile.SetYear(file.Year() + 1) 139 | modifiedFile.SetArtist(getModifiedString(file.Artist())) 140 | modifiedFile.SetTitle(getModifiedString(file.Title())) 141 | err = modifiedFile.Save() 142 | if err != nil { 143 | panic(err) 144 | t.Fatalf("Cannot save file : %s", err) 145 | } 146 | modifiedFile.Close() 147 | //Re-open the modified file 148 | modifiedFile, err = Read(tempFileName) 149 | if err != nil { 150 | panic(err) 151 | t.Fatalf("Read returned error: %s", err) 152 | } 153 | 154 | // Test the Tags 155 | if title := modifiedFile.Title(); title != getModifiedString("The Title") { 156 | t.Errorf("Got wrong modified title: %s", title) 157 | } 158 | 159 | if artist := modifiedFile.Artist(); artist != getModifiedString("The Artist") { 160 | t.Errorf("Got wrong modified artist: %s", artist) 161 | } 162 | 163 | if album := modifiedFile.Album(); album != getModifiedString("The Album") { 164 | t.Errorf("Got wrong modified album: %s", album) 165 | } 166 | 167 | if comment := modifiedFile.Comment(); comment != getModifiedString("A Comment") { 168 | t.Errorf("Got wrong modified comment: %s", comment) 169 | } 170 | 171 | if genre := modifiedFile.Genre(); genre != getModifiedString("Booty Bass") { 172 | t.Errorf("Got wrong modified genre: %s", genre) 173 | } 174 | 175 | if year := modifiedFile.Year(); year != getModifiedInt(1942) { 176 | t.Errorf("Got wrong modified year: %d", year) 177 | } 178 | 179 | if track := modifiedFile.Track(); track != getModifiedInt(42) { 180 | t.Errorf("Got wrong modified track: %d", track) 181 | } 182 | } 183 | 184 | func TestGenericWriteTagLib(t *testing.T) { 185 | fileName := "test.mp3" 186 | file, err := Read(fileName) 187 | 188 | if err != nil { 189 | panic(err) 190 | t.Fatalf("Read returned error: %s", err) 191 | } 192 | tempDir, err := ioutil.TempDir("", "go-taglib-test") 193 | 194 | if err != nil { 195 | panic(err) 196 | t.Fatalf("Cannot create temporary file for writing tests: %s", err) 197 | } 198 | 199 | tempFileName := path.Join(tempDir, "go-taglib-test.mp3") 200 | 201 | defer file.Close() 202 | defer os.RemoveAll(tempDir) 203 | 204 | err = cp(tempFileName, fileName) 205 | 206 | if err != nil { 207 | panic(err) 208 | t.Fatalf("Cannot copy file for writing tests: %s", err) 209 | } 210 | 211 | modifiedFile, err := Read(tempFileName) 212 | if err != nil { 213 | panic(err) 214 | t.Fatalf("Read returned error: %s", err) 215 | } 216 | modifiedFile.SetTag(Album, getModifiedString(file.Album())) 217 | modifiedFile.SetTag(Comments, getModifiedString(file.Comment())) 218 | modifiedFile.SetTag(Genre, getModifiedString(file.Genre())) 219 | modifiedFile.SetTag(Track, strconv.Itoa(file.Track()+1)) 220 | modifiedFile.SetTag(Year, strconv.Itoa(file.Year()+1)) 221 | modifiedFile.SetTag(Artist, getModifiedString(file.Artist())) 222 | modifiedFile.SetTag(Title, getModifiedString(file.Title())) 223 | err = modifiedFile.Save() 224 | if err != nil { 225 | panic(err) 226 | t.Fatalf("Cannot save file : %s", err) 227 | } 228 | modifiedFile.Close() 229 | //Re-open the modified file 230 | modifiedFile, err = Read(tempFileName) 231 | if err != nil { 232 | panic(err) 233 | t.Fatalf("Read returned error: %s", err) 234 | } 235 | 236 | // Test the Tags 237 | if title := modifiedFile.Tag(Title); title != getModifiedString("The Title") { 238 | t.Errorf("Got wrong modified title: %s", title) 239 | } 240 | 241 | if artist := modifiedFile.Tag(Artist); artist != getModifiedString("The Artist") { 242 | t.Errorf("Got wrong modified artist: %s", artist) 243 | } 244 | 245 | if album := modifiedFile.Tag(Album); album != getModifiedString("The Album") { 246 | t.Errorf("Got wrong modified album: %s", album) 247 | } 248 | 249 | if comment := modifiedFile.Tag(Comments); comment != getModifiedString("A Comment") { 250 | t.Errorf("Got wrong modified comment: %s", comment) 251 | } 252 | 253 | if genre := modifiedFile.Tag(Genre); genre != getModifiedString("Booty Bass") { 254 | t.Errorf("Got wrong modified genre: %s", genre) 255 | } 256 | 257 | if year := modifiedFile.Tag(Year); year != strconv.Itoa(getModifiedInt((1942))) { 258 | t.Errorf("Got wrong modified year: %s", year) 259 | } 260 | 261 | if track := modifiedFile.Tag(Track); track != strconv.Itoa(getModifiedInt((42))) { 262 | t.Errorf("Got wrong modified track: %s", track) 263 | } 264 | } 265 | 266 | func checkModified(original string, modified string) bool { 267 | return modified == getModifiedString(original) 268 | } 269 | 270 | func getModifiedString(s string) string { 271 | return s + " MODIFIED" 272 | } 273 | 274 | func getModifiedInt(i int) int { 275 | return i + 1 276 | } 277 | 278 | func cp(dst, src string) error { 279 | s, err := os.Open(src) 280 | if err != nil { 281 | return err 282 | } 283 | // no need to check errors on read only file, we already got everything 284 | // we need from the filesystem, so nothing can go wrong now. 285 | defer s.Close() 286 | d, err := os.Create(dst) 287 | if err != nil { 288 | return err 289 | } 290 | if _, err := io.Copy(d, s); err != nil { 291 | d.Close() 292 | return err 293 | } 294 | return d.Close() 295 | } 296 | -------------------------------------------------------------------------------- /test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtolson/go-taglib/79209c28005838ca690bc23d0e434202c9103892/test.mp3 --------------------------------------------------------------------------------