diff --git a/client/app/routes/MediaItems/Artist.tsx b/client/app/routes/MediaItems/Artist.tsx
index b698a27..20b4f88 100644
--- a/client/app/routes/MediaItems/Artist.tsx
+++ b/client/app/routes/MediaItems/Artist.tsx
@@ -50,6 +50,7 @@ export default function Artist() {
subContent={
diff --git a/client/app/routes/MediaItems/Track.tsx b/client/app/routes/MediaItems/Track.tsx
index 44ac3aa..5f61dcd 100644
--- a/client/app/routes/MediaItems/Track.tsx
+++ b/client/app/routes/MediaItems/Track.tsx
@@ -46,7 +46,8 @@ export default function Track() {
subContent={
appears on {album.title}
{track.listen_count &&
{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}
}
- {
{timeListenedString(track.time_listened)}
}
+ {
{timeListenedString(track.time_listened)}
}
+ {
Listening since {new Date(track.first_listen * 1000).toLocaleDateString()}
}
}
>
diff --git a/db/queries/listen.sql b/db/queries/listen.sql
index 6a1d9a6..fc8c502 100644
--- a/db/queries/listen.sql
+++ b/db/queries/listen.sql
@@ -29,6 +29,16 @@ WHERE at.artist_id = $5
ORDER BY l.listened_at DESC
LIMIT $3 OFFSET $4;
+-- name: GetFirstListenFromArtist :one
+SELECT
+ l.*
+FROM listens l
+JOIN tracks_with_title t ON l.track_id = t.id
+JOIN artist_tracks at ON t.id = at.track_id
+WHERE at.artist_id = $1
+ORDER BY l.listened_at ASC
+LIMIT 1;
+
-- name: GetLastListensFromReleasePaginated :many
SELECT
l.*,
@@ -42,6 +52,15 @@ WHERE l.listened_at BETWEEN $1 AND $2
ORDER BY l.listened_at DESC
LIMIT $3 OFFSET $4;
+-- name: GetFirstListenFromRelease :one
+SELECT
+ l.*
+FROM listens l
+JOIN tracks t ON l.track_id = t.id
+WHERE t.release_id = $1
+ORDER BY l.listened_at ASC
+LIMIT 1;
+
-- name: GetLastListensFromTrackPaginated :many
SELECT
l.*,
@@ -55,6 +74,15 @@ WHERE l.listened_at BETWEEN $1 AND $2
ORDER BY l.listened_at DESC
LIMIT $3 OFFSET $4;
+-- name: GetFirstListenFromTrack :one
+SELECT
+ l.*
+FROM listens l
+JOIN tracks t ON l.track_id = t.id
+WHERE t.id = $1
+ORDER BY l.listened_at ASC
+LIMIT 1;
+
-- name: CountListens :one
SELECT COUNT(*) AS total_count
FROM listens l
diff --git a/internal/db/psql/album.go b/internal/db/psql/album.go
index 2ac2fe0..eb0dcad 100644
--- a/internal/db/psql/album.go
+++ b/internal/db/psql/album.go
@@ -98,8 +98,14 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)
}
+ firstListen, err := d.q.GetFirstListenFromRelease(ctx, ret.ID)
+ if err != nil {
+ return nil, fmt.Errorf("GetAlbum: GetFirstListenFromRelease: %w", err)
+ }
+
ret.ListenCount = count
ret.TimeListened = seconds
+ ret.FirstListen = firstListen.ListenedAt.Unix()
return ret, nil
}
diff --git a/internal/db/psql/artist.go b/internal/db/psql/artist.go
index bfa31fc..9158d15 100644
--- a/internal/db/psql/artist.go
+++ b/internal/db/psql/artist.go
@@ -41,6 +41,10 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
if err != nil {
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
}
+ firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID)
+ if err != nil {
+ return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err)
+ }
return &models.Artist{
ID: row.ID,
MbzID: row.MusicBrainzID,
@@ -49,6 +53,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
Image: row.Image,
ListenCount: count,
TimeListened: seconds,
+ FirstListen: firstListen.ListenedAt.Unix(),
}, nil
} else if opts.MusicBrainzID != uuid.Nil {
l.Debug().Msgf("Fetching artist from DB with MusicBrainz ID %s", opts.MusicBrainzID)
@@ -71,14 +76,19 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
if err != nil {
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
}
+ firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID)
+ if err != nil {
+ return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err)
+ }
return &models.Artist{
ID: row.ID,
MbzID: row.MusicBrainzID,
Name: row.Name,
Aliases: row.Aliases,
Image: row.Image,
- TimeListened: seconds,
ListenCount: count,
+ TimeListened: seconds,
+ FirstListen: firstListen.ListenedAt.Unix(),
}, nil
} else if opts.Name != "" {
l.Debug().Msgf("Fetching artist from DB with name '%s'", opts.Name)
@@ -101,6 +111,10 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
if err != nil {
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
}
+ firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID)
+ if err != nil {
+ return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err)
+ }
return &models.Artist{
ID: row.ID,
MbzID: row.MusicBrainzID,
@@ -109,6 +123,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
Image: row.Image,
ListenCount: count,
TimeListened: seconds,
+ FirstListen: firstListen.ListenedAt.Unix(),
}, nil
} else {
return nil, errors.New("insufficient information to get artist")
diff --git a/internal/db/psql/track.go b/internal/db/psql/track.go
index 1614448..0660702 100644
--- a/internal/db/psql/track.go
+++ b/internal/db/psql/track.go
@@ -89,8 +89,14 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err)
}
+ firstListen, err := d.q.GetFirstListenFromTrack(ctx, track.ID)
+ if err != nil {
+ return nil, fmt.Errorf("GetAlbum: GetFirstListenFromRelease: %w", err)
+ }
+
track.ListenCount = count
track.TimeListened = seconds
+ track.FirstListen = firstListen.ListenedAt.Unix()
return &track, nil
}
diff --git a/internal/models/album.go b/internal/models/album.go
index a92a3aa..24948f9 100644
--- a/internal/models/album.go
+++ b/internal/models/album.go
@@ -11,6 +11,7 @@ type Album struct {
VariousArtists bool `json:"is_various_artists"`
ListenCount int64 `json:"listen_count"`
TimeListened int64 `json:"time_listened"`
+ FirstListen int64 `json:"first_listen"`
}
// type SimpleAlbum struct {
diff --git a/internal/models/artist.go b/internal/models/artist.go
index d9f62f0..7784e51 100644
--- a/internal/models/artist.go
+++ b/internal/models/artist.go
@@ -10,6 +10,7 @@ type Artist struct {
Image *uuid.UUID `json:"image"`
ListenCount int64 `json:"listen_count"`
TimeListened int64 `json:"time_listened"`
+ FirstListen int64 `json:"first_listen"`
IsPrimary bool `json:"is_primary,omitempty"`
}
@@ -27,5 +28,6 @@ type ArtistWithFullAliases struct {
ImageSource string `json:"image_source,omitempty"`
ListenCount int64 `json:"listen_count"`
TimeListened int64 `json:"time_listened"`
+ FirstListen int64 `json:"first_listen"`
IsPrimary bool `json:"is_primary,omitempty"`
}
diff --git a/internal/models/track.go b/internal/models/track.go
index 086813f..8eb802c 100644
--- a/internal/models/track.go
+++ b/internal/models/track.go
@@ -12,4 +12,5 @@ type Track struct {
Image *uuid.UUID `json:"image"`
AlbumID int32 `json:"album_id"`
TimeListened int64 `json:"time_listened"`
+ FirstListen int64 `json:"first_listen"`
}
diff --git a/internal/repository/listen.sql.go b/internal/repository/listen.sql.go
index 583dca6..027873a 100644
--- a/internal/repository/listen.sql.go
+++ b/internal/repository/listen.sql.go
@@ -190,6 +190,73 @@ func (q *Queries) DeleteListen(ctx context.Context, arg DeleteListenParams) erro
return err
}
+const getFirstListenFromArtist = `-- name: GetFirstListenFromArtist :one
+SELECT
+ l.track_id, l.listened_at, l.client, l.user_id
+FROM listens l
+JOIN tracks_with_title t ON l.track_id = t.id
+JOIN artist_tracks at ON t.id = at.track_id
+WHERE at.artist_id = $1
+ORDER BY l.listened_at ASC
+LIMIT 1
+`
+
+func (q *Queries) GetFirstListenFromArtist(ctx context.Context, artistID int32) (Listen, error) {
+ row := q.db.QueryRow(ctx, getFirstListenFromArtist, artistID)
+ var i Listen
+ err := row.Scan(
+ &i.TrackID,
+ &i.ListenedAt,
+ &i.Client,
+ &i.UserID,
+ )
+ return i, err
+}
+
+const getFirstListenFromRelease = `-- name: GetFirstListenFromRelease :one
+SELECT
+ l.track_id, l.listened_at, l.client, l.user_id
+FROM listens l
+JOIN tracks t ON l.track_id = t.id
+WHERE t.release_id = $1
+ORDER BY l.listened_at ASC
+LIMIT 1
+`
+
+func (q *Queries) GetFirstListenFromRelease(ctx context.Context, releaseID int32) (Listen, error) {
+ row := q.db.QueryRow(ctx, getFirstListenFromRelease, releaseID)
+ var i Listen
+ err := row.Scan(
+ &i.TrackID,
+ &i.ListenedAt,
+ &i.Client,
+ &i.UserID,
+ )
+ return i, err
+}
+
+const getFirstListenFromTrack = `-- name: GetFirstListenFromTrack :one
+SELECT
+ l.track_id, l.listened_at, l.client, l.user_id
+FROM listens l
+JOIN tracks t ON l.track_id = t.id
+WHERE t.id = $1
+ORDER BY l.listened_at ASC
+LIMIT 1
+`
+
+func (q *Queries) GetFirstListenFromTrack(ctx context.Context, id int32) (Listen, error) {
+ row := q.db.QueryRow(ctx, getFirstListenFromTrack, id)
+ var i Listen
+ err := row.Scan(
+ &i.TrackID,
+ &i.ListenedAt,
+ &i.Client,
+ &i.UserID,
+ )
+ return i, err
+}
+
const getLastListensFromArtistPaginated = `-- name: GetLastListensFromArtistPaginated :many
SELECT
l.track_id, l.listened_at, l.client, l.user_id,