mirror of
https://github.com/riwiwa/muzi.git
synced 2026-02-28 11:56:57 -08:00
add scrobbling through listenbrainz-like endpoint and lastfm-like endpoint
This commit is contained in:
306
scrobble/scrobble.go
Normal file
306
scrobble/scrobble.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package scrobble
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"muzi/db"
|
||||
|
||||
"github.com/jackc/pgtype"
|
||||
)
|
||||
|
||||
const DuplicateToleranceSeconds = 20
|
||||
|
||||
type Scrobble struct {
|
||||
UserId int
|
||||
Timestamp time.Time
|
||||
SongName string
|
||||
Artist string
|
||||
Album string
|
||||
MsPlayed int
|
||||
Platform string
|
||||
Source string
|
||||
}
|
||||
|
||||
type NowPlaying struct {
|
||||
UserId int
|
||||
SongName string
|
||||
Artist string
|
||||
Album string
|
||||
MsPlayed int
|
||||
Platform string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
var CurrentNowPlaying = make(map[int]NowPlaying)
|
||||
|
||||
func GenerateAPIKey() (string, error) {
|
||||
bytes := make([]byte, 16)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GenerateAPISecret() (string, error) {
|
||||
bytes := make([]byte, 16)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GenerateSessionKey() (string, error) {
|
||||
bytes := make([]byte, 16)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GetUserByAPIKey(apiKey string) (int, string, error) {
|
||||
if apiKey == "" {
|
||||
return 0, "", fmt.Errorf("empty API key")
|
||||
}
|
||||
|
||||
var userId int
|
||||
var username string
|
||||
err := db.Pool.QueryRow(context.Background(),
|
||||
"SELECT pk, username FROM users WHERE api_key = $1", apiKey).Scan(&userId, &username)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return userId, username, nil
|
||||
}
|
||||
|
||||
func GetUserBySessionKey(sessionKey string) (int, string, error) {
|
||||
if sessionKey == "" {
|
||||
return 0, "", fmt.Errorf("empty session key")
|
||||
}
|
||||
|
||||
var userId int
|
||||
var username string
|
||||
err := db.Pool.QueryRow(context.Background(),
|
||||
"SELECT pk, username FROM users WHERE api_secret = $1", sessionKey).Scan(&userId, &username)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return userId, username, nil
|
||||
}
|
||||
|
||||
func SaveScrobble(scrobble Scrobble) error {
|
||||
exists, err := checkDuplicate(scrobble.UserId, scrobble.Artist, scrobble.SongName, scrobble.Timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("duplicate scrobble")
|
||||
}
|
||||
|
||||
_, err = db.Pool.Exec(context.Background(),
|
||||
`INSERT INTO history (user_id, timestamp, song_name, artist, album_name, ms_played, platform)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (user_id, song_name, artist, timestamp) DO NOTHING`,
|
||||
scrobble.UserId, scrobble.Timestamp, scrobble.SongName, scrobble.Artist,
|
||||
scrobble.Album, scrobble.MsPlayed, scrobble.Platform)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error saving scrobble: %v\n", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveScrobbles(scrobbles []Scrobble) (int, int, error) {
|
||||
if len(scrobbles) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
accepted := 0
|
||||
ignored := 0
|
||||
|
||||
batchSize := 100
|
||||
for i := 0; i < len(scrobbles); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(scrobbles) {
|
||||
end = len(scrobbles)
|
||||
}
|
||||
|
||||
for _, scrobble := range scrobbles[i:end] {
|
||||
err := SaveScrobble(scrobble)
|
||||
if err != nil {
|
||||
if err.Error() == "duplicate scrobble" {
|
||||
ignored++
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error saving scrobble: %v\n", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
accepted++
|
||||
}
|
||||
}
|
||||
|
||||
return accepted, ignored, nil
|
||||
}
|
||||
|
||||
func checkDuplicate(userId int, artist, songName string, timestamp time.Time) (bool, error) {
|
||||
var exists bool
|
||||
err := db.Pool.QueryRow(context.Background(),
|
||||
`SELECT EXISTS(
|
||||
SELECT 1 FROM history
|
||||
WHERE user_id = $1
|
||||
AND artist = $2
|
||||
AND song_name = $3
|
||||
AND ABS(EXTRACT(EPOCH FROM (timestamp - $4))) < $5
|
||||
)`,
|
||||
userId, artist, songName, timestamp, DuplicateToleranceSeconds).Scan(&exists)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
func UpdateNowPlaying(np NowPlaying) {
|
||||
CurrentNowPlaying[np.UserId] = np
|
||||
}
|
||||
|
||||
func GetNowPlaying(userId int) (NowPlaying, bool) {
|
||||
np, ok := CurrentNowPlaying[userId]
|
||||
return np, ok
|
||||
}
|
||||
|
||||
func ClearNowPlaying(userId int) {
|
||||
delete(CurrentNowPlaying, userId)
|
||||
}
|
||||
|
||||
func GetUserSpotifyCredentials(userId int) (clientId, clientSecret, accessToken, refreshToken string, expiresAt time.Time, err error) {
|
||||
err = db.Pool.QueryRow(context.Background(),
|
||||
`SELECT spotify_client_id, spotify_client_secret, spotify_access_token,
|
||||
spotify_refresh_token, spotify_token_expires
|
||||
FROM users WHERE pk = $1`,
|
||||
userId).Scan(&clientId, &clientSecret, &accessToken, &refreshToken, &expiresAt)
|
||||
if err != nil {
|
||||
return "", "", "", "", time.Time{}, err
|
||||
}
|
||||
return clientId, clientSecret, accessToken, refreshToken, expiresAt, nil
|
||||
}
|
||||
|
||||
func UpdateUserSpotifyTokens(userId int, accessToken, refreshToken string, expiresIn int) error {
|
||||
expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||
_, err := db.Pool.Exec(context.Background(),
|
||||
`UPDATE users SET
|
||||
spotify_access_token = $1,
|
||||
spotify_refresh_token = $2,
|
||||
spotify_token_expires = $3
|
||||
WHERE pk = $4`,
|
||||
accessToken, refreshToken, expiresAt, userId)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateUserSpotifyCheck(userId int) error {
|
||||
_, err := db.Pool.Exec(context.Background(),
|
||||
`UPDATE users SET last_spotify_check = $1 WHERE pk = $2`,
|
||||
time.Now(), userId)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetUsersWithSpotify() ([]int, error) {
|
||||
rows, err := db.Pool.Query(context.Background(),
|
||||
`SELECT pk FROM users WHERE spotify_client_id IS NOT NULL AND spotify_client_secret IS NOT NULL`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userIds []int
|
||||
for rows.Next() {
|
||||
var userId int
|
||||
if err := rows.Scan(&userId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIds = append(userIds, userId)
|
||||
}
|
||||
return userIds, nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Pk int
|
||||
Username string
|
||||
Bio string
|
||||
Pfp string
|
||||
AllowDuplicateEdits bool
|
||||
ApiKey *string
|
||||
ApiSecret *string
|
||||
SpotifyClientId *string
|
||||
SpotifyClientSecret *string
|
||||
}
|
||||
|
||||
func GetUserById(userId int) (User, error) {
|
||||
var user User
|
||||
var apiKey, apiSecret, spotifyClientId, spotifyClientSecret pgtype.Text
|
||||
err := db.Pool.QueryRow(context.Background(),
|
||||
`SELECT pk, username, bio, pfp, allow_duplicate_edits, api_key, api_secret,
|
||||
spotify_client_id, spotify_client_secret
|
||||
FROM users WHERE pk = $1`,
|
||||
userId).Scan(&user.Pk, &user.Username, &user.Bio, &user.Pfp,
|
||||
&user.AllowDuplicateEdits, &apiKey, &apiSecret, &spotifyClientId, &spotifyClientSecret)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
|
||||
if apiKey.Status == pgtype.Present {
|
||||
user.ApiKey = &apiKey.String
|
||||
}
|
||||
if apiSecret.Status == pgtype.Present {
|
||||
user.ApiSecret = &apiSecret.String
|
||||
}
|
||||
if spotifyClientId.Status == pgtype.Present {
|
||||
user.SpotifyClientId = &spotifyClientId.String
|
||||
}
|
||||
if spotifyClientSecret.Status == pgtype.Present {
|
||||
user.SpotifyClientSecret = &spotifyClientSecret.String
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func UpdateUserAPIKey(userId int, apiKey, apiSecret string) error {
|
||||
_, err := db.Pool.Exec(context.Background(),
|
||||
`UPDATE users SET api_key = $1, api_secret = $2 WHERE pk = $3`,
|
||||
apiKey, apiSecret, userId)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateUserSpotifyCredentials(userId int, clientId, clientSecret string) error {
|
||||
_, err := db.Pool.Exec(context.Background(),
|
||||
`UPDATE users SET spotify_client_id = $1, spotify_client_secret = $2 WHERE pk = $3`,
|
||||
clientId, clientSecret, userId)
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteUserSpotifyCredentials(userId int) error {
|
||||
_, err := db.Pool.Exec(context.Background(),
|
||||
`UPDATE users SET
|
||||
spotify_client_id = NULL,
|
||||
spotify_client_secret = NULL,
|
||||
spotify_access_token = NULL,
|
||||
spotify_refresh_token = NULL,
|
||||
spotify_token_expires = NULL
|
||||
WHERE pk = $1`,
|
||||
userId)
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *User) IsSpotifyConnected() bool {
|
||||
_, _, accessToken, _, expiresAt, err := GetUserSpotifyCredentials(u.Pk)
|
||||
if err != nil || accessToken == "" {
|
||||
return false
|
||||
}
|
||||
return time.Now().Before(expiresAt)
|
||||
}
|
||||
Reference in New Issue
Block a user