├── .gitignore ├── LICENSE ├── README.md ├── crates.go ├── crates_test.go ├── databasev2.go ├── databasev2_test.go ├── entities.go ├── go.mod ├── go.sum ├── history.go ├── history_test.go ├── parser.go ├── reader.go └── reader_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 SpinTools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serato Parser 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/SpinTools/seratoparser)](https://goreportcard.com/report/github.com/SpinTools/seratoparser) 4 | 5 | A GoLang library for reading Serato database files. 6 | 7 | Data Types Supported: 8 | 9 | - [x] Database V2 10 | - [x] Crates 11 | - [ ] History Database 12 | - [x] History Sessions 13 | 14 | ## Installation 15 | 16 | This package can be installed with the go get command: 17 | 18 | ```bash 19 | go get -u github.com/SpinTools/seratoparser 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```go 25 | func main() { 26 | // Provide Serato Folder 27 | p := seratoparser.New("/Users/Stoyvo/Music/_Serato_") 28 | 29 | // Get All Tracks in Serato Database 30 | Tracks := p.GetAllTracks() 31 | log.Println("Database V2:", Tracks) 32 | 33 | // Get all Crates 34 | crates := p.GetCrates() 35 | log.Println("Crates:", crates) 36 | 37 | // Read crate and get all Tracks 38 | mediaEntities := p.GetCrateTracks(crates[0].Name()) 39 | log.Println("Crate Tracks:", mediaEntities) 40 | 41 | // Get all session files 42 | sessions := p.GetHistorySessions() 43 | log.Println("History Sessions:", sessions) 44 | 45 | // Read History Session 46 | historyEntities := p.ReadHistorySession(sessions[0].Name()) 47 | log.Println("History Tracks:", historyEntities) 48 | } 49 | ``` 50 | 51 | ## Contributing 52 | 53 | Pull requests are welcome, update tests as appropriate. 54 | 55 | ## License 56 | 57 | [MIT](https://github.com/SpinTools/seratoparser/LICENSE) 58 | -------------------------------------------------------------------------------- /crates.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "sort" 8 | ) 9 | 10 | // GetCrates returns files of all crates found in Serato Path 11 | // TODO: Should we parse meta data of theses files, or only provide OS level elements. Weird to be a parser library and no parsing. 12 | // TODO: Is there Serato Crate meta data? 13 | func (p Parser) GetCrates() []os.FileInfo { 14 | var crateFiles []os.FileInfo 15 | 16 | seratoFolder := filepath.FromSlash(p.FilePath + "/Subcrates") 17 | seratoFiles, _ := ioutil.ReadDir(seratoFolder) 18 | for _, seratoFile := range seratoFiles { 19 | fileExt := filepath.Ext(seratoFile.Name()) 20 | if fileExt != ".crate" { 21 | continue 22 | } 23 | 24 | crateFiles = append(crateFiles, seratoFile) 25 | } 26 | 27 | sort.Slice(crateFiles, func(i, j int) bool { 28 | return len(crateFiles[i].Name()) < len(crateFiles[j].Name()) 29 | }) 30 | 31 | return crateFiles 32 | } 33 | 34 | // GetCrateTracks takes a filename and returns all the tracks/entities inside the crate 35 | func (p Parser) GetCrateTracks(fileName string) []MediaEntity { 36 | return readMediaEntities(filepath.FromSlash(p.FilePath + "/Subcrates/" + fileName)) 37 | } 38 | -------------------------------------------------------------------------------- /crates_test.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestReadCrates(t *testing.T) { 10 | p := New(SeratoDir) 11 | crates := p.GetCrates() 12 | if len(crates) == 0 { 13 | t.Errorf("GetCrates() = %q, want %q", strconv.Itoa(len(crates)), ">0") 14 | log.Println(crates) 15 | } 16 | 17 | foundTracks := false 18 | for _, crate := range crates { 19 | mediaEntities := p.GetCrateTracks(crate.Name()) 20 | if len(mediaEntities) > 0 { 21 | foundTracks = true 22 | break 23 | } 24 | } 25 | if !foundTracks { 26 | t.Errorf("GetCrateTracks() = %q, want %q", "0", ">0") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /databasev2.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import "path/filepath" 4 | 5 | // GetAllTracks returns all the tracks/entities inside the Database 6 | func (p Parser) GetAllTracks() []MediaEntity { 7 | return readMediaEntities(filepath.FromSlash(p.FilePath + "/database V2")) 8 | } 9 | -------------------------------------------------------------------------------- /databasev2_test.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestReadDatabase(t *testing.T) { 10 | p := New(SeratoDir) 11 | mediaEntities := p.GetAllTracks() 12 | if len(mediaEntities) == 0 { 13 | t.Errorf("GetAllTracks() = %q, want %q", strconv.Itoa(len(mediaEntities)), ">0") 14 | log.Println(mediaEntities) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /entities.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | var seratoVolume string 4 | 5 | // MediaEntity defines Tracks/Songs entities in a Database or Crate 6 | type MediaEntity struct { 7 | // META 8 | DVOL string // volume 9 | 10 | // UTFSTR 11 | PTRK string // filetrack 12 | PFIL string // filebase 13 | 14 | // INT1 15 | BMIS bool // missing 16 | BCRT bool // corrupt 17 | 18 | // INT4 19 | UADD int // timeadded 20 | 21 | // BYTE SLICE 22 | ULBL []byte // color - track colour 23 | } 24 | 25 | // HistoryEntity defines Tracks/Songs entities in a History Session 26 | type HistoryEntity struct { 27 | RROW int // rrow 28 | RDIR string // rfullpath 29 | TTMS int // rstarttime 30 | TTME int // rendtime 31 | TDCK int // rdeck 32 | RDTE string // rdate* 33 | RSRT int // rstart* 34 | REND int // rend* 35 | TPTM int // rplaytime 36 | RSES int // rsessionId 37 | RPLY int // rplayed = 1 38 | RADD int // radded 39 | RUPD int // rupdatedAt 40 | RSWR string // rsoftware* 41 | RSWB int // rsoftwareBuild* 42 | RDEV string // rdevice 43 | } 44 | 45 | // SeratoAdatMap Defines all the known keys with their integer key found in Serato Databases 46 | // TODO: Identify all fields of an ADAT object 47 | var SeratoAdatMap = map[int]string{ 48 | 1: "RROW", // rrow 49 | 2: "RDIR", // rfullpath 50 | 28: "TTMS", // rstarttime 51 | 29: "TTME", // rendtime 52 | 31: "TDCK", // rdeck 53 | 54 | 41: "RDTE", // rdate 55 | 43: "RSRT", // rstart 56 | 44: "REND", // rend 57 | 58 | 45: "TPTM", // rplaytime 59 | 48: "RSES", // rsessionId 60 | 50: "RPLY", // rplayed 61 | 52: "RADD", // radded 62 | 53: "RUPD", // rupdatedAt 63 | 54: "RUNK", // rr54unknownTimestamp 64 | 65 | 57: "RSWR", // rsoftware 66 | 58: "RSWB", // rsoftwareBuild 67 | 68 | 63: "RDEV", // rdevice 69 | } 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/SpinTools/seratoparser 2 | 3 | go 1.16 4 | 5 | require github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 h1:jkvpcEatpwuMF5O5LVxTnehj6YZ/aEZN4NWD/Xml4pI= 2 | github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7/go.mod h1:KTrHyWpO1sevuXPZwyeZc72ddWRFqNSKDFl7uVWKpg0= 3 | -------------------------------------------------------------------------------- /history.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "bufio" 5 | "github.com/romana/rlog" 6 | "io/fs" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | // HistoryPath is the path inside the _Serato_ folder that contains History data 15 | var HistoryPath = "/History" 16 | 17 | // SessionPath is the path inside the _Serato_/History folder that contains all the played Sessions. 18 | var SessionPath = HistoryPath + "/Sessions" 19 | 20 | // GetHistorySessions returns a list of all Serato History session files in the users Serato directory. 21 | func (p Parser) GetHistorySessions() []fs.FileInfo { 22 | var sessionFiles []fs.FileInfo 23 | var err error 24 | 25 | //historySessionDir := currentUser.HomeDir + "/Music/_Serato_/History/Sessions" 26 | historySessionDir := filepath.FromSlash(p.FilePath + SessionPath) 27 | sessionFiles, err = ioutil.ReadDir(historySessionDir) 28 | 29 | // Remove .DS_STORE files. 30 | for i := 0; i < len(sessionFiles); i++ { 31 | if !strings.HasSuffix(sessionFiles[i].Name(), ".session") { 32 | sessionFiles = append(sessionFiles[:i], sessionFiles[i+1:]...) 33 | i-- 34 | } 35 | } 36 | 37 | if err != nil || len(sessionFiles) == 0 { 38 | rlog.Critical(err) 39 | return sessionFiles 40 | } 41 | 42 | sort.Slice(sessionFiles, func(i, j int) bool { 43 | return sessionFiles[i].ModTime().Unix() > sessionFiles[j].ModTime().Unix() 44 | }) 45 | 46 | return sessionFiles 47 | } 48 | 49 | // ReadHistorySession returns all track entities within the provided filepath. 50 | func (p Parser) ReadHistorySession(fileName string) []HistoryEntity { 51 | var historyEntities []HistoryEntity 52 | 53 | historySessionFilepath := filepath.FromSlash(p.FilePath + SessionPath + "/" + fileName) 54 | seratoFile, err := filepath.Abs(historySessionFilepath) 55 | if err != nil || seratoFile == "" { 56 | rlog.Critical(err) 57 | return historyEntities 58 | } 59 | 60 | // Only read files that exist, and then report errors if we cant read it 61 | _, err = os.Stat(seratoFile) 62 | if err != nil { 63 | rlog.Critical(err) 64 | return historyEntities 65 | } 66 | if os.IsNotExist(err) { 67 | rlog.Critical(err) 68 | return historyEntities 69 | } 70 | 71 | ioFile, err := os.Open(seratoFile) 72 | if err != nil { 73 | rlog.Critical(err) 74 | return historyEntities 75 | } 76 | defer ioFile.Close() 77 | 78 | seratoVolume = volumeName(seratoFile) 79 | 80 | fileBuffer := bufio.NewReader(ioFile) 81 | fileExt := filepath.Ext(seratoFile) 82 | if fileExt == ".session" { 83 | if !fileHeader(fileBuffer, "<1.0", "/Serato Scratch LIVE Review") { 84 | rlog.Critical("ReadFile: Unable to parse history |", seratoFile) 85 | } 86 | } 87 | 88 | defer func() { 89 | if r := recover(); r != nil { 90 | rlog.Warn("Recovered in fileReader", r) 91 | } 92 | }() 93 | 94 | for { 95 | nextTag1, eof := parseFilePeek(fileBuffer, 1) 96 | nextTag4, _ := parseFilePeek(fileBuffer, 4) 97 | if eof || string(nextTag1) == "" { 98 | break 99 | } else if string(nextTag4) == "osrt" { 100 | fileCrateColumns(fileBuffer) 101 | } else if string(nextTag4) == "otrk" || string(nextTag4) == "oent" { //|| string(nextTag4) == "oses" { 102 | for { 103 | name, data, eof := parseField(fileBuffer) 104 | 105 | // break if we don't need to be here 106 | // TODO: What is oses? 107 | if eof || (name != "otrk" && name != "oent") { //&& name != "oses") { 108 | break 109 | } 110 | 111 | dataBuffer := *bufio.NewReader(strings.NewReader(data)) 112 | if name == "oent" { 113 | for { 114 | dataName, dataValue, eof := parseField(&dataBuffer) 115 | if eof || dataName != "adat" { 116 | break 117 | } 118 | 119 | historyEntity := HistoryEntity{} 120 | parseAdat(dataValue, &historyEntity) 121 | historyEntities = append(historyEntities, historyEntity) 122 | } 123 | } else if name == "oses" { 124 | for { 125 | dataName, dataValue, eof := parseField(&dataBuffer) 126 | if eof || dataName != "adat" { 127 | break 128 | } 129 | 130 | rlog.Println(dataName) 131 | rlog.Println(dataValue) 132 | rlog.Println("--------") 133 | 134 | historyEntity := HistoryEntity{} 135 | parseAdat(dataValue, &historyEntity) 136 | } 137 | } 138 | } 139 | } else { 140 | parseFileLen(fileBuffer, 1) 141 | } 142 | } 143 | 144 | return historyEntities 145 | } 146 | -------------------------------------------------------------------------------- /history_test.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "log" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestReadHistorySession(t *testing.T) { 10 | p := New(SeratoDir) 11 | sessions := p.GetHistorySessions() 12 | if len(sessions) == 0 { 13 | t.Errorf("GetHistorySessions() = %q, want %q", strconv.Itoa(len(sessions)), ">0") 14 | log.Println(sessions) 15 | } 16 | 17 | historyEntities := p.ReadHistorySession(sessions[0].Name()) 18 | if len(historyEntities) == 0 { 19 | t.Errorf("ReadHistorySession() = %q, want %q", strconv.Itoa(len(historyEntities)), ">0") 20 | log.Println(historyEntities) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "math" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "unicode/utf16" 12 | "unicode/utf8" 13 | ) 14 | 15 | func parseFilePeek(b *bufio.Reader, n int) ([]byte, bool) { 16 | bPeek, err := b.Peek(n) 17 | if err != nil { 18 | return nil, true 19 | } 20 | 21 | return bPeek, false 22 | } 23 | func parseFileByte(b *bufio.Reader) (byte, bool) { 24 | bByte, err := b.ReadByte() 25 | if err != nil { 26 | return '\000', true 27 | } 28 | 29 | return bByte, false 30 | } 31 | func parseFileLen(b *bufio.Reader, n int) (string, bool) { 32 | var buffer bytes.Buffer 33 | counter := 0 34 | for { 35 | char, eof := parseFileByte(b) 36 | if eof { 37 | return "", true 38 | } 39 | buffer.WriteByte(char) 40 | counter++ 41 | if counter == n { 42 | break 43 | } 44 | } 45 | 46 | return buffer.String(), false 47 | } 48 | 49 | func parseCString(b *bufio.Reader) (string, bool) { 50 | /* 51 | * From the passed string, find the nearest \0 byte and return everything before it 52 | */ 53 | var buffer bytes.Buffer 54 | for { 55 | char, eof := parseFileByte(b) 56 | if eof { 57 | return "", true 58 | } 59 | if char == '\000' { 60 | break 61 | } 62 | buffer.WriteByte(char) 63 | } 64 | 65 | return buffer.String(), false 66 | } 67 | 68 | func parseField(b *bufio.Reader) (string, string, bool) { 69 | /* 70 | * 71 | */ 72 | name, eof := parseFileLen(b, 4) 73 | if eof { 74 | return "", "", true 75 | } 76 | rawlen, eof := parseFileLen(b, 4) 77 | if eof { 78 | return "", "", true 79 | } 80 | length := int(hexBin2Float(rawlen)) 81 | 82 | data, eof := parseFileLen(b, length) 83 | if eof { 84 | return "", "", true 85 | } 86 | 87 | return name, data, false 88 | } 89 | 90 | func matchUtf16(b *bufio.Reader, s string) bool { 91 | /* 92 | * Match utf16 string with next len() bytes 93 | */ 94 | chars, _ := parseFileLen(b, len(s)) 95 | if chars == s { 96 | return true 97 | } 98 | 99 | return false 100 | } 101 | 102 | // utf16BytesToString converts UTF-16 encoded bytes, in big or little endian byte order, to a UTF-8 encoded string. 103 | func utf16BytesToString(b []byte) string { 104 | utf := make([]uint16, (len(b)+(2-1))/2) 105 | for i := 0; i+(2-1) < len(b); i += 2 { 106 | utf[i/2] = binary.BigEndian.Uint16(b[i:]) 107 | } 108 | if len(b)/2 < len(utf) { 109 | utf[len(utf)-1] = utf8.RuneError 110 | } 111 | return string(utf16.Decode(utf)) 112 | } 113 | 114 | func makeUtf8(s string) string { 115 | /* 116 | * Convert the passed string s to UTF8 format 117 | */ 118 | var buffer bytes.Buffer 119 | for i := 0; i < len(s); i++ { 120 | if s[i] == '\000' { 121 | continue 122 | } 123 | buffer.WriteByte(s[i]) 124 | } 125 | 126 | return buffer.String() 127 | } 128 | 129 | func makeUtf16(s string) string { 130 | /* 131 | * Convert the passed string s to serato UTF16 format 132 | */ 133 | var buffer bytes.Buffer 134 | for i := 0; i < len(s); i++ { 135 | buffer.WriteByte(0) 136 | buffer.WriteByte(s[i]) 137 | } 138 | 139 | return buffer.String() 140 | } 141 | 142 | func hexBin2Int(raw string) int { 143 | return int(hexBin2Float(raw)) 144 | } 145 | 146 | func hexBin2Float(raw string) (val float64) { 147 | for i := 0; i < len(raw); i++ { 148 | fl1 := math.Pow(2, 8) 149 | fl2 := float64((len(raw) - 1) - i) 150 | val += float64(raw[i]) * math.Pow(fl1, fl2) 151 | } 152 | 153 | return val 154 | } 155 | 156 | func parseOtrk(dataBuffer *bufio.Reader, newEntity *MediaEntity) { 157 | elem := reflect.ValueOf(newEntity).Elem() 158 | for { 159 | dataName, dataValue, eof := parseField(dataBuffer) 160 | if eof { 161 | break 162 | } 163 | 164 | newEntity.DVOL = seratoVolume 165 | 166 | v := elem.FieldByName(strings.ToUpper(dataName)) 167 | reflectValue(&v, dataValue) 168 | } 169 | } 170 | 171 | func parseAdat(dataValue string, newEntity interface{}) { 172 | adatBuffer := bufio.NewReader(strings.NewReader(dataValue)) 173 | elem := reflect.ValueOf(newEntity).Elem() 174 | for { 175 | adatFieldHex, adatValue, eof := parseField(adatBuffer) 176 | if eof { 177 | break 178 | } 179 | 180 | adatFieldID := hexBin2Int(adatFieldHex) 181 | adatName := SeratoAdatMap[adatFieldID] 182 | 183 | v := elem.FieldByName(strings.ToUpper(adatName)) 184 | reflectValue(&v, adatValue) 185 | if v.IsValid() && v.Type().String() == "string" { 186 | tmpStringVal := v.String() 187 | v.SetString(tmpStringVal[:len(tmpStringVal)-1]) 188 | } 189 | } 190 | } 191 | 192 | func reflectValue(v *reflect.Value, dataValue string) { 193 | if v.IsValid() { 194 | t := v.Type().String() 195 | switch t { 196 | case "string": 197 | //rlog.Debug("%s \n", utf16BytesToString([]byte(dataValue))) 198 | v.SetString(utf16BytesToString([]byte(dataValue))) 199 | case "float": 200 | newFloat := hexBin2Float(dataValue) // convert hexbin to int 201 | //rlog.Debug("%d(%d) \n", newFloat, int64(newFloat)) 202 | v.SetFloat(newFloat) 203 | case "int": 204 | newFloat := hexBin2Int(dataValue) // convert hexbin to int 205 | //rlog.Debug("%d(%d) \n", newFloat, int64(newFloat)) 206 | v.SetInt(int64(newFloat)) 207 | case "bool": 208 | newBool, _ := strconv.ParseBool(dataValue) 209 | //rlog.Debug("%t \n", newBool) 210 | v.SetBool(newBool) 211 | case "[]uint8": 212 | newBytes := make([]byte, 4) 213 | binary.LittleEndian.PutUint32(newBytes[:], uint32(hexBin2Int(dataValue))) 214 | //rlog.Debug("%t \n", newBytes) 215 | v.SetBytes(newBytes[:]) 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "bufio" 5 | "github.com/romana/rlog" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | // Parser holds the filepath of all databases 13 | type Parser struct { 14 | FilePath string 15 | } 16 | 17 | // SeratoParser is the Parser for this Serato Database module. Exported for future option of override 18 | var SeratoParser Parser 19 | 20 | // New creates a new object with the provided Serato Database Path 21 | func New(seratoPath string) Parser { 22 | SeratoParser = Parser{} 23 | SeratoParser.FilePath = strings.TrimSuffix(seratoPath, "/") 24 | return SeratoParser 25 | } 26 | 27 | func fileHeader(fileBuffer *bufio.Reader, key string, value string) bool { 28 | // get first key 29 | firstKey, _ := parseCString(fileBuffer) 30 | if firstKey == "vrsn" { 31 | // Skip over next \0 32 | parseCString(fileBuffer) 33 | } 34 | 35 | // match vrsn 36 | key16 := makeUtf16(key) 37 | if !matchUtf16(fileBuffer, key16) { 38 | rlog.Error("fileHeader: vrsn value mismatch |", key16) 39 | return false 40 | } 41 | 42 | // match version type 43 | value16 := makeUtf16(value) 44 | if !matchUtf16(fileBuffer, value16) { 45 | rlog.Error("fileHeader: vrsn type mismatch |", value16) 46 | return false 47 | } 48 | 49 | return true 50 | } 51 | 52 | func fileCrateColumns(fileBuffer *bufio.Reader) { 53 | /* 54 | Field # TAG Description 55 | ============================== 56 | 57 | Header 58 | 01 vrsn 4 byte string/tag denoting version start 59 | 02 68 Byte null padded string representing the DB version. 60 | Decodes to 81.0/Serato ScratchLive Crate 61 | Column Sorting data 62 | 03 osrt 4 byte string/tag denoting sorting config start 63 | 04 4 bytes / 32bit int 64 | 05 tvcn 4 byte string/tag denoting column name 65 | 06 32bit int + Variable length string 66 | 07 brev 4 byte string/tag, ??? 67 | 08 5 bytes 0x 00 00 00 01 00 68 | 69 | Column Details - repeated for all columns 70 | 09 ovct 4 byte string/tag, ??? 71 | 10 4 bytes, 32bit int 72 | 11 tvcn byte string/tag denoting column name 73 | 12 32bit int + Variable length string 74 | 13 tvcw 4 byte string/tag, column width? 75 | 14 6 bytes of ??? 76 | 77 | Song/Track details - repeated for each track 78 | XX otrk 4 byte string/tag stores track length 79 | XX ptrk 32bit int + variable length track name 80 | */ 81 | 82 | for { 83 | nextTag, _ := parseFilePeek(fileBuffer, 4) 84 | if string(nextTag) == "otrk" || string(nextTag) == "oent" { 85 | break 86 | } 87 | // do stuff here for crate columns 88 | _, eof := parseFileLen(fileBuffer, 1) 89 | if eof { 90 | break 91 | } 92 | } 93 | } 94 | 95 | func volumeName(filePath string) string { 96 | volume := filepath.VolumeName(filePath) 97 | 98 | if volume == "" && runtime.GOOS == "darwin" { 99 | if strings.HasPrefix(filePath, "/Volumes/") { 100 | splitFilePath := strings.Split(filePath, "/") 101 | if len(splitFilePath) >= 2 { 102 | return "/Volumes/" + splitFilePath[2] 103 | } 104 | } 105 | } 106 | 107 | return volume 108 | } 109 | 110 | func readMediaEntities(fileName string) []MediaEntity { 111 | var mediaEntities []MediaEntity 112 | seratoFile, err := filepath.Abs(fileName) 113 | if err != nil || seratoFile == "" { 114 | rlog.Critical(err) 115 | return mediaEntities 116 | } 117 | 118 | // Only read files that exist, and then report errors if we cant read it 119 | _, err = os.Stat(seratoFile) 120 | if err != nil { 121 | rlog.Critical(err) 122 | return mediaEntities 123 | } 124 | if os.IsNotExist(err) { 125 | rlog.Critical(err) 126 | return mediaEntities 127 | } 128 | 129 | ioFile, err := os.Open(seratoFile) 130 | if err != nil { 131 | rlog.Critical(err) 132 | return mediaEntities 133 | } 134 | defer ioFile.Close() 135 | 136 | seratoVolume = volumeName(seratoFile) 137 | 138 | fileBuffer := bufio.NewReader(ioFile) 139 | fileExt := filepath.Ext(seratoFile) 140 | fileType := filepath.Base(seratoFile) 141 | if fileExt == ".crate" { 142 | if !fileHeader(fileBuffer, "81.0", "/Serato ScratchLive Crate") { 143 | rlog.Critical("ReadFile: Unable to parse crate |", seratoFile) 144 | } 145 | } else if fileType == "database V2" { 146 | if !fileHeader(fileBuffer, "@2.0", "/Serato Scratch LIVE Database") { 147 | rlog.Critical("ReadFile: Unable to parse database v2 |", seratoFile) 148 | } 149 | } else if fileType == "history.database" || fileExt == ".session" { 150 | if !fileHeader(fileBuffer, "<1.0", "/Serato Scratch LIVE Review") { 151 | rlog.Critical("ReadFile: Unable to parse history |", seratoFile) 152 | } 153 | } 154 | 155 | defer func() { 156 | if r := recover(); r != nil { 157 | rlog.Warn("Recovered in fileReader", r) 158 | } 159 | }() 160 | 161 | for { 162 | nextTag1, eof := parseFilePeek(fileBuffer, 1) 163 | nextTag4, _ := parseFilePeek(fileBuffer, 4) 164 | if eof || string(nextTag1) == "" { 165 | break 166 | } else if string(nextTag4) == "osrt" { 167 | fileCrateColumns(fileBuffer) 168 | } else if string(nextTag4) == "otrk" || string(nextTag4) == "oent" { //|| string(nextTag4) == "oses" { 169 | for { 170 | name, data, eof := parseField(fileBuffer) 171 | 172 | // break if we don't need to be here 173 | // TODO: What is oses? 174 | if eof || (name != "otrk" && name != "oent") { //&& name != "oses") { 175 | break 176 | } 177 | 178 | dataBuffer := *bufio.NewReader(strings.NewReader(data)) 179 | if name == "otrk" { 180 | mediaEntity := MediaEntity{} 181 | parseOtrk(&dataBuffer, &mediaEntity) 182 | mediaEntities = append(mediaEntities, mediaEntity) 183 | } 184 | } 185 | } else { 186 | parseFileLen(fileBuffer, 1) 187 | } 188 | } 189 | 190 | return mediaEntities 191 | } 192 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package seratoparser 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | ) 7 | 8 | var UserHomeDir string 9 | var SeratoDir string 10 | 11 | func init() { 12 | currentUser, err := user.Current() 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | UserHomeDir = filepath.FromSlash(currentUser.HomeDir) 18 | SeratoDir = filepath.FromSlash(UserHomeDir + "/Music/_Serato_") 19 | } 20 | --------------------------------------------------------------------------------