mirror of
https://github.com/riwiwa/muzi.git
synced 2026-03-04 00:51:59 -08:00
add multi-artist support for comma separated artists
This commit is contained in:
4
db/db.go
4
db/db.go
@@ -250,8 +250,10 @@ func AddHistoryEntityColumns() error {
|
||||
_, err := Pool.Exec(context.Background(),
|
||||
`ALTER TABLE history ADD COLUMN IF NOT EXISTS artist_id INTEGER REFERENCES artists(id) ON DELETE SET NULL;
|
||||
ALTER TABLE history ADD COLUMN IF NOT EXISTS song_id INTEGER REFERENCES songs(id) ON DELETE SET NULL;
|
||||
ALTER TABLE history ADD COLUMN IF NOT EXISTS artist_ids INTEGER[] DEFAULT '{}';
|
||||
CREATE INDEX IF NOT EXISTS idx_history_artist_id ON history(artist_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_song_id ON history(song_id);`)
|
||||
CREATE INDEX IF NOT EXISTS idx_history_song_id ON history(song_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_artist_ids ON history USING gin(artist_ids);`)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error adding history entity columns: %v\n", err)
|
||||
return err
|
||||
|
||||
@@ -499,7 +499,7 @@ func SearchSongs(userId int, query string) ([]Song, float64, error) {
|
||||
func GetArtistStats(userId, artistId int) (int, error) {
|
||||
var count int
|
||||
err := Pool.QueryRow(context.Background(),
|
||||
"SELECT COUNT(*) FROM history WHERE user_id = $1 AND artist_id = $2",
|
||||
"SELECT COUNT(*) FROM history WHERE user_id = $1 AND $2 = ANY(artist_ids)",
|
||||
userId, artistId).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
@@ -534,8 +534,9 @@ func MergeArtists(userId int, fromArtistId, toArtistId int) error {
|
||||
func GetHistoryForArtist(userId, artistId int, limit, offset int) ([]ScrobbleEntry, error) {
|
||||
rows, err := Pool.Query(context.Background(),
|
||||
`SELECT h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name
|
||||
FROM history h WHERE h.user_id = $1 AND h.artist_id = $2
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
||||
h.artist_ids
|
||||
FROM history h WHERE h.user_id = $1 AND $2 = ANY(h.artist_ids)
|
||||
ORDER BY h.timestamp DESC LIMIT $3 OFFSET $4`,
|
||||
userId, artistId, limit, offset)
|
||||
if err != nil {
|
||||
@@ -546,7 +547,7 @@ func GetHistoryForArtist(userId, artistId int, limit, offset int) ([]ScrobbleEnt
|
||||
var entries []ScrobbleEntry
|
||||
for rows.Next() {
|
||||
var e ScrobbleEntry
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName)
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -558,7 +559,8 @@ func GetHistoryForArtist(userId, artistId int, limit, offset int) ([]ScrobbleEnt
|
||||
func GetHistoryForSong(userId, songId int, limit, offset int) ([]ScrobbleEntry, error) {
|
||||
rows, err := Pool.Query(context.Background(),
|
||||
`SELECT h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
||||
h.artist_ids
|
||||
FROM history h WHERE h.user_id = $1 AND h.song_id = $2
|
||||
ORDER BY h.timestamp DESC LIMIT $3 OFFSET $4`,
|
||||
userId, songId, limit, offset)
|
||||
@@ -570,7 +572,7 @@ func GetHistoryForSong(userId, songId int, limit, offset int) ([]ScrobbleEntry,
|
||||
var entries []ScrobbleEntry
|
||||
for rows.Next() {
|
||||
var e ScrobbleEntry
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName)
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -586,6 +588,7 @@ type ScrobbleEntry struct {
|
||||
AlbumName string
|
||||
MsPlayed int
|
||||
Platform string
|
||||
ArtistIds []int
|
||||
}
|
||||
|
||||
func MigrateHistoryEntities() error {
|
||||
@@ -703,7 +706,8 @@ func GetAlbumStats(userId, albumId int) (int, error) {
|
||||
func GetHistoryForAlbum(userId, albumId int, limit, offset int) ([]ScrobbleEntry, error) {
|
||||
rows, err := Pool.Query(context.Background(),
|
||||
`SELECT h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
||||
h.artist_ids
|
||||
FROM history h
|
||||
JOIN songs s ON h.song_id = s.id
|
||||
WHERE h.user_id = $1 AND s.album_id = $2
|
||||
@@ -717,7 +721,7 @@ func GetHistoryForAlbum(userId, albumId int, limit, offset int) ([]ScrobbleEntry
|
||||
var entries []ScrobbleEntry
|
||||
for rows.Next() {
|
||||
var e ScrobbleEntry
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName)
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -743,7 +747,8 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
||||
}
|
||||
rows, err := Pool.Query(context.Background(),
|
||||
`SELECT h.timestamp, h.song_name, h.album_name, h.ms_played, h.platform,
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name
|
||||
(SELECT name FROM artists WHERE id = h.artist_id) as artist_name,
|
||||
h.artist_ids
|
||||
FROM history h WHERE h.user_id = $1 AND h.song_id = ANY($2)
|
||||
ORDER BY h.timestamp DESC LIMIT $3 OFFSET $4`,
|
||||
userId, songIds, limit, offset)
|
||||
@@ -755,7 +760,7 @@ func GetHistoryForSongs(userId int, songIds []int, limit, offset int) ([]Scrobbl
|
||||
var entries []ScrobbleEntry
|
||||
for rows.Next() {
|
||||
var e ScrobbleEntry
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName)
|
||||
err := rows.Scan(&e.Timestamp, &e.SongName, &e.AlbumName, &e.MsPlayed, &e.Platform, &e.ArtistName, &e.ArtistIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -236,20 +236,65 @@ func ImportLastFM(
|
||||
}
|
||||
|
||||
func insertBatch(tracks []LastFMTrack, totalImported *int) error {
|
||||
if len(tracks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
artistIdMap, err := resolveLastFMArtistIds(tracks)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving artist IDs: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
rows := make([][]any, 0, len(tracks))
|
||||
for _, t := range tracks {
|
||||
artistNames := parseArtistString(t.Artist)
|
||||
var artistIds []int
|
||||
for _, name := range artistNames {
|
||||
if ids, ok := artistIdMap[name]; ok {
|
||||
artistIds = append(artistIds, ids...)
|
||||
}
|
||||
}
|
||||
|
||||
primaryArtistId := 0
|
||||
if len(artistIds) > 0 {
|
||||
primaryArtistId = artistIds[0]
|
||||
}
|
||||
|
||||
rows = append(rows, []any{
|
||||
t.UserId, t.Timestamp, t.SongName, t.Artist,
|
||||
t.Album, 0, "lastfm", primaryArtistId, artistIds,
|
||||
})
|
||||
}
|
||||
|
||||
copyCount, err := db.Pool.CopyFrom(context.Background(),
|
||||
pgx.Identifier{"history"},
|
||||
[]string{
|
||||
"user_id", "timestamp", "song_name", "artist", "album_name",
|
||||
"ms_played", "platform",
|
||||
"ms_played", "platform", "artist_id", "artist_ids",
|
||||
},
|
||||
pgx.CopyFromSlice(len(tracks), func(i int) ([]any, error) {
|
||||
t := tracks[i]
|
||||
return []any{
|
||||
t.UserId, t.Timestamp, t.SongName, t.Artist,
|
||||
t.Album, 0, "lastfm",
|
||||
}, nil
|
||||
}),
|
||||
pgx.CopyFromRows(rows),
|
||||
)
|
||||
*totalImported += int(copyCount)
|
||||
return err
|
||||
}
|
||||
|
||||
func resolveLastFMArtistIds(tracks []LastFMTrack) (map[string][]int, error) {
|
||||
artistIdMap := make(map[string][]int)
|
||||
|
||||
for _, t := range tracks {
|
||||
artistNames := parseArtistString(t.Artist)
|
||||
for _, name := range artistNames {
|
||||
if _, exists := artistIdMap[name]; !exists {
|
||||
artistId, _, err := db.GetOrCreateArtist(t.UserId, name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating artist %s: %v\n", name, err)
|
||||
continue
|
||||
}
|
||||
artistIdMap[name] = []int{artistId}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artistIdMap, nil
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ type trackSource struct {
|
||||
tracksToSkip map[string]struct{} // Set of duplicate keys to skip
|
||||
idx int // Current position in tracks slice
|
||||
userId int // User ID to associate with imported tracks
|
||||
artistIdMap map[string][]int // Map of track key to artist IDs
|
||||
}
|
||||
|
||||
// Represents a track already stored in the database, used for duplicate
|
||||
@@ -107,11 +108,19 @@ func ImportSpotify(tracks []SpotifyTrack,
|
||||
continue
|
||||
}
|
||||
|
||||
artistIdMap, err := resolveArtistIds(userId, validTracks)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving artist IDs: %v\n", err)
|
||||
batchStart += batchSize
|
||||
continue
|
||||
}
|
||||
|
||||
src := &trackSource{
|
||||
tracks: validTracks,
|
||||
tracksToSkip: tracksToSkip,
|
||||
idx: 0,
|
||||
userId: userId,
|
||||
artistIdMap: artistIdMap,
|
||||
}
|
||||
|
||||
copyCount, err := db.Pool.CopyFrom(
|
||||
@@ -125,6 +134,8 @@ func ImportSpotify(tracks []SpotifyTrack,
|
||||
"album_name",
|
||||
"ms_played",
|
||||
"platform",
|
||||
"artist_id",
|
||||
"artist_ids",
|
||||
},
|
||||
src,
|
||||
)
|
||||
@@ -218,6 +229,43 @@ func getDupes(userId int, tracks []SpotifyTrack) (map[string]struct{}, error) {
|
||||
return duplicates, nil
|
||||
}
|
||||
|
||||
func resolveArtistIds(userId int, tracks []SpotifyTrack) (map[string][]int, error) {
|
||||
artistIdMap := make(map[string][]int)
|
||||
|
||||
for _, track := range tracks {
|
||||
trackKey := createTrackKey(track)
|
||||
artistNames := parseArtistString(track.Artist)
|
||||
|
||||
var artistIds []int
|
||||
for _, name := range artistNames {
|
||||
artistId, _, err := db.GetOrCreateArtist(userId, name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating artist %s: %v\n", name, err)
|
||||
continue
|
||||
}
|
||||
artistIds = append(artistIds, artistId)
|
||||
}
|
||||
|
||||
artistIdMap[trackKey] = artistIds
|
||||
}
|
||||
|
||||
return artistIdMap, nil
|
||||
}
|
||||
|
||||
func parseArtistString(artist string) []string {
|
||||
if artist == "" {
|
||||
return nil
|
||||
}
|
||||
var artists []string
|
||||
for _, a := range strings.Split(artist, ",") {
|
||||
a = strings.TrimSpace(a)
|
||||
if a != "" {
|
||||
artists = append(artists, a)
|
||||
}
|
||||
}
|
||||
return artists
|
||||
}
|
||||
|
||||
// Get the min/max timestamp range for a batch of tracks
|
||||
func findTimeRange(tracks []SpotifyTrack) (time.Time, time.Time) {
|
||||
var minTs, maxTs time.Time
|
||||
@@ -319,6 +367,14 @@ func (s *trackSource) Next() bool {
|
||||
func (s *trackSource) Values() ([]any, error) {
|
||||
// idx is already incremented in Next(), so use idx-1
|
||||
t := s.tracks[s.idx-1]
|
||||
trackKey := createTrackKey(t)
|
||||
artistIds := s.artistIdMap[trackKey]
|
||||
|
||||
primaryArtistId := 0
|
||||
if len(artistIds) > 0 {
|
||||
primaryArtistId = artistIds[0]
|
||||
}
|
||||
|
||||
return []any{
|
||||
s.userId,
|
||||
t.Timestamp,
|
||||
@@ -327,6 +383,8 @@ func (s *trackSource) Values() ([]any, error) {
|
||||
t.Album,
|
||||
t.Played,
|
||||
"spotify",
|
||||
primaryArtistId,
|
||||
artistIds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"muzi/db"
|
||||
@@ -118,33 +119,38 @@ func SaveScrobble(scrobble Scrobble) error {
|
||||
return fmt.Errorf("duplicate scrobble")
|
||||
}
|
||||
|
||||
artistId, _, err := db.GetOrCreateArtist(scrobble.UserId, scrobble.Artist)
|
||||
artistNames := parseArtistString(scrobble.Artist)
|
||||
artistIds, err := getOrCreateArtists(scrobble.UserId, artistNames)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting/creating artist: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
primaryArtistId := 0
|
||||
if len(artistIds) > 0 {
|
||||
primaryArtistId = artistIds[0]
|
||||
}
|
||||
|
||||
var albumId int
|
||||
if scrobble.Album != "" {
|
||||
albumId, _, err = db.GetOrCreateAlbum(scrobble.UserId, scrobble.Album, artistId)
|
||||
albumId, _, err = db.GetOrCreateAlbum(scrobble.UserId, scrobble.Album, primaryArtistId)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting/creating album: %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
songId, _, err := db.GetOrCreateSong(scrobble.UserId, scrobble.SongName, artistId, albumId)
|
||||
songId, _, err := db.GetOrCreateSong(scrobble.UserId, scrobble.SongName, primaryArtistId, albumId)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting/creating song: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Pool.Exec(context.Background(),
|
||||
`INSERT INTO history (user_id, timestamp, song_name, artist, album_name, ms_played, platform, artist_id, song_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`INSERT INTO history (user_id, timestamp, song_name, artist, album_name, ms_played, platform, artist_id, song_id, artist_ids)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (user_id, song_name, artist, timestamp) DO NOTHING`,
|
||||
scrobble.UserId, scrobble.Timestamp, scrobble.SongName, scrobble.Artist,
|
||||
scrobble.Album, scrobble.MsPlayed, scrobble.Platform, artistId, songId)
|
||||
scrobble.Album, scrobble.MsPlayed, scrobble.Platform, primaryArtistId, songId, artistIds)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error saving scrobble: %v\n", err)
|
||||
return err
|
||||
@@ -152,6 +158,33 @@ func SaveScrobble(scrobble Scrobble) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseArtistString(artist string) []string {
|
||||
if artist == "" {
|
||||
return nil
|
||||
}
|
||||
var artists []string
|
||||
for _, a := range strings.Split(artist, ",") {
|
||||
a = strings.TrimSpace(a)
|
||||
if a != "" {
|
||||
artists = append(artists, a)
|
||||
}
|
||||
}
|
||||
return artists
|
||||
}
|
||||
|
||||
func getOrCreateArtists(userId int, artistNames []string) ([]int, error) {
|
||||
var artistIds []int
|
||||
for _, name := range artistNames {
|
||||
id, _, err := db.GetOrCreateArtist(userId, name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting/creating artist: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
artistIds = append(artistIds, id)
|
||||
}
|
||||
return artistIds, nil
|
||||
}
|
||||
|
||||
func SaveScrobbles(scrobbles []Scrobble) (int, int, error) {
|
||||
if len(scrobbles) == 0 {
|
||||
return 0, 0, nil
|
||||
|
||||
@@ -334,6 +334,7 @@
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
width: 30%;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #111;
|
||||
@@ -341,6 +342,7 @@
|
||||
a {
|
||||
color: #AFA;
|
||||
text-decoration: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
a:hover {
|
||||
color: #FFF;
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
<button class="edit-btn" onclick="openEditModal()">Edit</button>
|
||||
{{end}}
|
||||
</h1>
|
||||
{{if .Artist.Name}}
|
||||
<h2><a href="/profile/{{.Username}}/artist/{{urlquery .Artist.Name}}">{{.Artist.Name}}</a></h2>
|
||||
{{if .ArtistNames}}
|
||||
<h2>
|
||||
{{- range $i, $name := .ArtistNames}}{{if $i}}, {{end}}<a href="/profile/{{$.Username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</h2>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="profile-top-blank">
|
||||
@@ -34,7 +36,10 @@
|
||||
{{$username := .Username}}
|
||||
{{range .Times}}
|
||||
<tr>
|
||||
<td><a href="/profile/{{$username}}/artist/{{urlquery .ArtistName}}">{{.ArtistName}}</a></td>
|
||||
<td>
|
||||
{{- $artistNames := getArtistNames .ArtistIds}}
|
||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</td>
|
||||
<td><a href="/profile/{{$username}}/song/{{urlquery .ArtistName}}/{{urlquery .SongName}}">{{.SongName}}</a></td>
|
||||
<td>{{.AlbumName}}</td>
|
||||
<td title="{{formatTimestampFull .Timestamp}}">{{formatTimestamp .Timestamp}}</td>
|
||||
|
||||
@@ -31,7 +31,10 @@
|
||||
{{$username := .Username}}
|
||||
{{range .Times}}
|
||||
<tr>
|
||||
<td><a href="/profile/{{$username}}/artist/{{urlquery .ArtistName}}">{{.ArtistName}}</a></td>
|
||||
<td>
|
||||
{{- $artistNames := getArtistNames .ArtistIds}}
|
||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</td>
|
||||
<td><a href="/profile/{{$username}}/song/{{urlquery .ArtistName}}/{{urlquery .SongName}}">{{.SongName}}</a></td>
|
||||
<td><a href="/profile/{{$username}}/album/{{urlquery .ArtistName}}/{{urlquery .AlbumName}}">{{.AlbumName}}</a></td>
|
||||
<td title="{{formatTimestampFull .Timestamp}}">{{formatTimestamp .Timestamp}}</td>
|
||||
|
||||
@@ -28,13 +28,17 @@
|
||||
<td>Now Playing</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{$artists := .Artists}}
|
||||
{{$artistIdsList := .ArtistIdsList}}
|
||||
{{$times := .Times}}
|
||||
{{$username := .Username}}
|
||||
{{range $index, $title := .Titles}}
|
||||
<tr>
|
||||
<td><a href="/profile/{{$username}}/artist/{{urlquery (index $artists $index)}}">{{index $artists $index}}</a></td>
|
||||
<td><a href="/profile/{{$username}}/song/{{urlquery (index $artists $index)}}/{{urlquery $title}}">{{$title}}</a></td>
|
||||
<td>
|
||||
{{- $artistIds := index $artistIdsList $index}}
|
||||
{{- $artistNames := getArtistNames $artistIds}}
|
||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</td>
|
||||
<td><a href="/profile/{{$username}}/song/{{urlquery (index $.Artists $index)}}/{{urlquery $title}}">{{$title}}</a></td>
|
||||
<td title="{{formatTimestampFull (index $times $index)}}">{{formatTimestamp (index $times $index)}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
<button class="edit-btn" onclick="openEditModal()">Edit</button>
|
||||
{{end}}
|
||||
</h1>
|
||||
{{if .Artist.Name}}
|
||||
<h2><a href="/profile/{{.Username}}/artist/{{urlquery .Artist.Name}}">{{.Artist.Name}}</a></h2>
|
||||
{{if .ArtistNames}}
|
||||
<h2>
|
||||
{{- range $i, $name := .ArtistNames}}{{if $i}}, {{end}}<a href="/profile/{{$.Username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</h2>
|
||||
{{end}}
|
||||
{{range .Albums}}
|
||||
<h3><a href="/profile/{{$.Username}}/album/{{urlquery $.Artist.Name}}/{{urlquery .Title}}">{{.Title}}</a></h3>
|
||||
@@ -32,7 +34,10 @@
|
||||
{{$username := .Username}}
|
||||
{{range .Times}}
|
||||
<tr>
|
||||
<td><a href="/profile/{{$username}}/artist/{{urlquery .ArtistName}}">{{.ArtistName}}</a></td>
|
||||
<td>
|
||||
{{- $artistNames := getArtistNames .ArtistIds}}
|
||||
{{- range $i, $name := $artistNames}}{{if $i}}, {{end}}<a href="/profile/{{$username}}/artist/{{urlquery $name}}">{{$name}}</a>{{end}}
|
||||
</td>
|
||||
<td><a href="/profile/{{$username}}/song/{{urlquery .ArtistName}}/{{urlquery .SongName}}">{{.SongName}}</a></td>
|
||||
<td><a href="/profile/{{$username}}/album/{{urlquery .ArtistName}}/{{urlquery .AlbumName}}">{{.AlbumName}}</a></td>
|
||||
<td title="{{formatTimestampFull .Timestamp}}">{{formatTimestamp .Timestamp}}</td>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -35,6 +36,7 @@ type SongData struct {
|
||||
Username string
|
||||
Song db.Song
|
||||
Artist db.Artist
|
||||
ArtistNames []string
|
||||
Albums []db.Album
|
||||
ListenCount int
|
||||
Times []db.ScrobbleEntry
|
||||
@@ -48,6 +50,7 @@ type AlbumData struct {
|
||||
Username string
|
||||
Album db.Album
|
||||
Artist db.Artist
|
||||
ArtistNames []string
|
||||
ListenCount int
|
||||
Times []db.ScrobbleEntry
|
||||
Page int
|
||||
@@ -169,8 +172,14 @@ func songPageHandler() http.HandlerFunc {
|
||||
var songIds []int
|
||||
var albums []db.Album
|
||||
seenAlbums := make(map[int]bool)
|
||||
seenArtistIds := make(map[int]bool)
|
||||
var allArtistIds []int
|
||||
for _, s := range songs {
|
||||
songIds = append(songIds, s.Id)
|
||||
if s.ArtistId > 0 && !seenArtistIds[s.ArtistId] {
|
||||
seenArtistIds[s.ArtistId] = true
|
||||
allArtistIds = append(allArtistIds, s.ArtistId)
|
||||
}
|
||||
if s.AlbumId > 0 && !seenAlbums[s.AlbumId] {
|
||||
seenAlbums[s.AlbumId] = true
|
||||
album, _ := db.GetAlbumById(s.AlbumId)
|
||||
@@ -178,6 +187,29 @@ func songPageHandler() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
var artistNames []string
|
||||
seenArtistIdsMap := make(map[int]bool)
|
||||
rows, err := db.Pool.Query(context.Background(),
|
||||
`SELECT DISTINCT artist_ids FROM history WHERE song_id = ANY($1)`,
|
||||
songIds)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var artistIds []int
|
||||
if err := rows.Scan(&artistIds); err == nil {
|
||||
for _, id := range artistIds {
|
||||
if !seenArtistIdsMap[id] {
|
||||
seenArtistIdsMap[id] = true
|
||||
a, err := db.GetArtistById(id)
|
||||
if err == nil {
|
||||
artistNames = append(artistNames, a.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageStr := r.URL.Query().Get("page")
|
||||
var pageInt int
|
||||
if pageStr == "" {
|
||||
@@ -208,6 +240,7 @@ func songPageHandler() http.HandlerFunc {
|
||||
Username: username,
|
||||
Song: song,
|
||||
Artist: artist,
|
||||
ArtistNames: artistNames,
|
||||
Albums: albums,
|
||||
ListenCount: listenCount,
|
||||
Times: entries,
|
||||
@@ -375,10 +408,25 @@ func albumPageHandler() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
var artistNames []string
|
||||
seenArtistIds := make(map[int]bool)
|
||||
for _, e := range entries {
|
||||
for _, artistId := range e.ArtistIds {
|
||||
if !seenArtistIds[artistId] {
|
||||
seenArtistIds[artistId] = true
|
||||
a, err := db.GetArtistById(artistId)
|
||||
if err == nil {
|
||||
artistNames = append(artistNames, a.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
albumData := AlbumData{
|
||||
Username: username,
|
||||
Album: album,
|
||||
Artist: artist,
|
||||
ArtistNames: artistNames,
|
||||
ListenCount: listenCount,
|
||||
Times: entries,
|
||||
Page: pageInt,
|
||||
|
||||
@@ -25,6 +25,7 @@ type ProfileData struct {
|
||||
TrackCount int
|
||||
ArtistCount int
|
||||
Artists []string
|
||||
ArtistIdsList [][]int
|
||||
Titles []string
|
||||
Times []time.Time
|
||||
Page int
|
||||
@@ -93,7 +94,7 @@ func profilePageHandler() http.HandlerFunc {
|
||||
|
||||
rows, err := db.Pool.Query(
|
||||
r.Context(),
|
||||
"SELECT artist, song_name, timestamp FROM history WHERE user_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3;",
|
||||
"SELECT artist_id, song_name, timestamp, artist_ids FROM history WHERE user_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3;",
|
||||
userId,
|
||||
lim,
|
||||
off,
|
||||
@@ -106,15 +107,27 @@ func profilePageHandler() http.HandlerFunc {
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var artist, title string
|
||||
var artistId int
|
||||
var title string
|
||||
var time pgtype.Timestamptz
|
||||
err = rows.Scan(&artist, &title, &time)
|
||||
var artistIds []int
|
||||
err = rows.Scan(&artistId, &title, &time, &artistIds)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Scanning history row failed: %v\n", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
profileData.Artists = append(profileData.Artists, artist)
|
||||
|
||||
var artistName string
|
||||
if artistId > 0 {
|
||||
artist, err := db.GetArtistById(artistId)
|
||||
if err == nil {
|
||||
artistName = artist.Name
|
||||
}
|
||||
}
|
||||
|
||||
profileData.Artists = append(profileData.Artists, artistName)
|
||||
profileData.ArtistIdsList = append(profileData.ArtistIdsList, artistIds)
|
||||
profileData.Titles = append(profileData.Titles, title)
|
||||
profileData.Times = append(profileData.Times, time.Time)
|
||||
}
|
||||
|
||||
17
web/utils.go
17
web/utils.go
@@ -5,6 +5,8 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"muzi/db"
|
||||
)
|
||||
|
||||
// Subtracts two integers
|
||||
@@ -56,3 +58,18 @@ func formatTimestamp(timestamp time.Time) string {
|
||||
func formatTimestampFull(timestamp time.Time) string {
|
||||
return timestamp.Format("Monday 2 Jan 2006, 3:04pm")
|
||||
}
|
||||
|
||||
// GetArtistNames takes artist IDs and returns a slice of artist names
|
||||
func GetArtistNames(artistIds []int) []string {
|
||||
if artistIds == nil {
|
||||
return nil
|
||||
}
|
||||
var names []string
|
||||
for _, id := range artistIds {
|
||||
artist, err := db.GetArtistById(id)
|
||||
if err == nil {
|
||||
names = append(names, artist.Name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ func init() {
|
||||
"formatTimestamp": formatTimestamp,
|
||||
"formatTimestampFull": formatTimestampFull,
|
||||
"urlquery": url.QueryEscape,
|
||||
"getArtistNames": GetArtistNames,
|
||||
}
|
||||
templates = template.Must(template.New("").Funcs(funcMap).ParseGlob("./templates/*.gohtml"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user