diff --git a/backends/scrobblerlog.go b/backends/scrobblerlog.go index 3fa8f17..3724df6 100644 --- a/backends/scrobblerlog.go +++ b/backends/scrobblerlog.go @@ -22,6 +22,14 @@ THE SOFTWARE. package backends import ( + "bufio" + "encoding/csv" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" "time" "github.com/spf13/viper" @@ -39,5 +47,124 @@ func (b ScrobblerLogBackend) FromConfig(config *viper.Viper) Backend { } func (b ScrobblerLogBackend) ExportListens(oldestTimestamp time.Time) ([]Listen, error) { - return nil, nil + file, err := os.Open(b.filePath) + if err != nil { + return nil, err + } + + defer file.Close() + + reader := bufio.NewReader(file) + client, err := readHeader(reader) + if err != nil { + return nil, err + } + + csvReader := csv.NewReader(reader) + csvReader.Comma = '\t' + // Row length is often flexible + csvReader.FieldsPerRecord = -1 + + listens := make([]Listen, 0) + for { + // A row is: + // artistName releaseName trackName trackNumber duration rating timestamp recordingMbid + row, err := csvReader.Read() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + // fmt.Printf("row: %v\n", row) + + // We consider only the last field (recording MBID) optional + if len(row) < 7 { + line, _ := csvReader.FieldPos(0) + return nil, errors.New(fmt.Sprintf( + "Invalid record in %s line %v", b.filePath, line)) + } + + rating := row[5] + if !b.includeSkipped && rating == "S" { + continue + } + + listen, err := rowToListen(row, client) + if err != nil { + return nil, err + } + + listens = append(listens, listen) + } + + return listens, nil +} + +func readHeader(reader *bufio.Reader) (client string, err error) { + // Skip header + for i := 0; i < 3; i++ { + line, _, err := reader.ReadLine() + if err != nil { + return client, err + } + + if len(line) == 0 || line[0] != '#' { + err = errors.New(fmt.Sprintf("Unexpected header (line %v)", i)) + } else { + text := string(line) + if i == 0 && !strings.HasPrefix(text, "#AUDIOSCROBBLER/1") { + err = errors.New(fmt.Sprintf("Not a scrobbler log file")) + } + + after, found := strings.CutPrefix(text, "#CLIENT/") + if found { + client = strings.Split(after, " ")[0] + } + } + + if err != nil { + return client, err + } + } + return client, nil +} + +func rowToListen(row []string, client string) (Listen, error) { + var listen Listen + trackNumber, err := strconv.Atoi(row[3]) + if err != nil { + return listen, err + } + + duration, err := strconv.Atoi(row[4]) + if err != nil { + return listen, err + } + + timestamp, err := strconv.Atoi(row[6]) + if err != nil { + return listen, err + } + + listen = Listen{ + Track: Track{ + ArtistNames: []string{row[0]}, + ReleaseName: row[1], + TrackName: row[2], + TrackNumber: trackNumber, + Duration: time.Duration(duration), + AdditionalInfo: AdditionalInfo{ + "rockbox_rating": row[5], + "media_player": client, + }, + }, + ListenedAt: time.Unix(int64(timestamp), 0), + } + + if len(row) > 7 { + listen.Track.RecordingMbid = MBID(row[7]) + } + + return listen, nil }