/* Copyright © 2024 Philipp Wolfer Scotty is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Scotty is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Scotty. If not, see . */ package spotifyhistory import ( "encoding/json" "fmt" "io" "strings" "time" "go.uploadedlobster.com/scotty/internal/models" ) type StreamingHistory []HistoryItem type ListenListOptions struct { IgnoreIncognito bool IgnoreSkipped bool skippedMinSeconds int } type HistoryItem struct { Timestamp time.Time `json:"ts"` UserName string `json:"username"` Platform string `json:"platform"` MillisecondsPlayed int `json:"ms_played"` ConnCountry string `json:"conn_country"` IpAddrDecrypted string `json:"ip_addr_decrypted"` UserAgentDecrypted string `json:"user_agent_decrypted"` MasterMetadataTrackName string `json:"master_metadata_track_name"` MasterMetadataAlbumArtistName string `json:"master_metadata_album_artist_name"` MasterMetadataAlbumName string `json:"master_metadata_album_album_name"` SpotifyTrackUri string `json:"spotify_track_uri"` EpisodeName string `json:"episode_name"` EpisodeShowName string `json:"episode_show_name"` SpotifyEpisodeUri string `json:"spotify_episode_uri"` ReasonStart string `json:"reason_start"` ReasonEnd string `json:"reason_end"` Shuffle bool `json:"shuffle"` Skipped bool `json:"skipped"` Offline bool `json:"offline"` OfflineTimestamp int `json:"offline_timestamp"` IncognitoMode bool `json:"incognito_mode"` } func (j *StreamingHistory) Read(in io.Reader) error { bytes, err := io.ReadAll(in) if err != nil { return err } err = json.Unmarshal(bytes, j) return err } func (h *StreamingHistory) AsListenList(opt ListenListOptions) models.ListensList { listens := make(models.ListensList, 0, len(*h)) for _, item := range *h { if item.MasterMetadataTrackName == "" || (opt.IgnoreIncognito && item.IncognitoMode) || (opt.IgnoreSkipped && item.Skipped) || (item.Skipped && item.MillisecondsPlayed < opt.skippedMinSeconds*1000) { continue } listens = append(listens, item.AsListen()) } return listens } func (i HistoryItem) AsListen() models.Listen { listen := models.Listen{ Track: models.Track{ TrackName: i.MasterMetadataTrackName, ReleaseName: i.MasterMetadataAlbumName, ArtistNames: []string{i.MasterMetadataAlbumArtistName}, AdditionalInfo: models.AdditionalInfo{}, }, ListenedAt: i.Timestamp, PlaybackDuration: time.Duration(i.MillisecondsPlayed * int(time.Millisecond)), UserName: i.UserName, } if trackUrl, err := formatSpotifyUri(i.SpotifyTrackUri); err != nil { listen.AdditionalInfo["spotify_id"] = trackUrl } return listen } // Returns a Spotify ID like "spotify:track:5jzma6gCzYtKB1DbEwFZKH" into an // URL like "https://open.spotify.com/track/5jzma6gCzYtKB1DbEwFZKH" func formatSpotifyUri(id string) (string, error) { parts := strings.Split(id, ":") if len(parts) == 3 && parts[0] == "spotify" { return fmt.Sprintf("https://opem.spotify.com/%s/%s", parts[1], parts[2]), nil } else { return "", fmt.Errorf("Invalid Spotify ID \"%v\"", id) } }