mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-15 10:25:55 -07:00
feat: add first listened to dates for media items
This commit is contained in:
parent
300bac0e19
commit
cb32cd4509
12 changed files with 134 additions and 2 deletions
|
|
@ -256,6 +256,7 @@ type Track = {
|
||||||
album_id: number
|
album_id: number
|
||||||
musicbrainz_id: string
|
musicbrainz_id: string
|
||||||
time_listened: number
|
time_listened: number
|
||||||
|
first_listen: number
|
||||||
}
|
}
|
||||||
type Artist = {
|
type Artist = {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -265,6 +266,7 @@ type Artist = {
|
||||||
listen_count: number
|
listen_count: number
|
||||||
musicbrainz_id: string
|
musicbrainz_id: string
|
||||||
time_listened: number
|
time_listened: number
|
||||||
|
first_listen: number
|
||||||
is_primary: boolean
|
is_primary: boolean
|
||||||
}
|
}
|
||||||
type Album = {
|
type Album = {
|
||||||
|
|
@ -276,6 +278,7 @@ type Album = {
|
||||||
artists: SimpleArtists[]
|
artists: SimpleArtists[]
|
||||||
musicbrainz_id: string
|
musicbrainz_id: string
|
||||||
time_listened: number
|
time_listened: number
|
||||||
|
first_listen: number
|
||||||
}
|
}
|
||||||
type Alias = {
|
type Alias = {
|
||||||
id: number
|
id: number
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export default function Album() {
|
||||||
subContent={<div className="flex flex-col gap-2 items-start">
|
subContent={<div className="flex flex-col gap-2 items-start">
|
||||||
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
|
{album.listen_count && <p>{album.listen_count} play{ album.listen_count > 1 ? 's' : ''}</p>}
|
||||||
{<p title={Math.floor(album.time_listened / 60) + " minutes"}>{timeListenedString(album.time_listened)}</p>}
|
{<p title={Math.floor(album.time_listened / 60) + " minutes"}>{timeListenedString(album.time_listened)}</p>}
|
||||||
|
{<p title={new Date(album.first_listen * 1000).toLocaleString()}>Listening since {new Date(album.first_listen * 1000).toLocaleDateString()}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export default function Artist() {
|
||||||
subContent={<div className="flex flex-col gap-2 items-start">
|
subContent={<div className="flex flex-col gap-2 items-start">
|
||||||
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
|
{artist.listen_count && <p>{artist.listen_count} play{ artist.listen_count > 1 ? 's' : ''}</p>}
|
||||||
{<p title={Math.floor(artist.time_listened / 60) + " minutes"}>{timeListenedString(artist.time_listened)}</p>}
|
{<p title={Math.floor(artist.time_listened / 60) + " minutes"}>{timeListenedString(artist.time_listened)}</p>}
|
||||||
|
{<p title={new Date(artist.first_listen * 1000).toLocaleString()}>Listening since {new Date(artist.first_listen * 1000).toLocaleDateString()}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ export default function Track() {
|
||||||
subContent={<div className="flex flex-col gap-2 items-start">
|
subContent={<div className="flex flex-col gap-2 items-start">
|
||||||
<Link to={`/album/${track.album_id}`}>appears on {album.title}</Link>
|
<Link to={`/album/${track.album_id}`}>appears on {album.title}</Link>
|
||||||
{track.listen_count && <p>{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}</p>}
|
{track.listen_count && <p>{track.listen_count} play{ track.listen_count > 1 ? 's' : ''}</p>}
|
||||||
{<p title={Math.floor(track.time_listened / 60) + " minutes"}>{timeListenedString(track.time_listened)}</p>}
|
{<p title={Math.floor(track.time_listened / 60) + " minutes"}>{timeListenedString(track.time_listened)}</p>}
|
||||||
|
{<p title={new Date(track.first_listen * 1000).toLocaleString()}>Listening since {new Date(track.first_listen * 1000).toLocaleDateString()}</p>}
|
||||||
</div>}
|
</div>}
|
||||||
>
|
>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,16 @@ WHERE at.artist_id = $5
|
||||||
ORDER BY l.listened_at DESC
|
ORDER BY l.listened_at DESC
|
||||||
LIMIT $3 OFFSET $4;
|
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
|
-- name: GetLastListensFromReleasePaginated :many
|
||||||
SELECT
|
SELECT
|
||||||
l.*,
|
l.*,
|
||||||
|
|
@ -42,6 +52,15 @@ WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
ORDER BY l.listened_at DESC
|
ORDER BY l.listened_at DESC
|
||||||
LIMIT $3 OFFSET $4;
|
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
|
-- name: GetLastListensFromTrackPaginated :many
|
||||||
SELECT
|
SELECT
|
||||||
l.*,
|
l.*,
|
||||||
|
|
@ -55,6 +74,15 @@ WHERE l.listened_at BETWEEN $1 AND $2
|
||||||
ORDER BY l.listened_at DESC
|
ORDER BY l.listened_at DESC
|
||||||
LIMIT $3 OFFSET $4;
|
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
|
-- name: CountListens :one
|
||||||
SELECT COUNT(*) AS total_count
|
SELECT COUNT(*) AS total_count
|
||||||
FROM listens l
|
FROM listens l
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,14 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu
|
||||||
return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err)
|
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.ListenCount = count
|
||||||
ret.TimeListened = seconds
|
ret.TimeListened = seconds
|
||||||
|
ret.FirstListen = firstListen.ListenedAt.Unix()
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
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{
|
return &models.Artist{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
MbzID: row.MusicBrainzID,
|
MbzID: row.MusicBrainzID,
|
||||||
|
|
@ -49,6 +53,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
||||||
Image: row.Image,
|
Image: row.Image,
|
||||||
ListenCount: count,
|
ListenCount: count,
|
||||||
TimeListened: seconds,
|
TimeListened: seconds,
|
||||||
|
FirstListen: firstListen.ListenedAt.Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
} else if opts.MusicBrainzID != uuid.Nil {
|
} else if opts.MusicBrainzID != uuid.Nil {
|
||||||
l.Debug().Msgf("Fetching artist from DB with MusicBrainz ID %s", opts.MusicBrainzID)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
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{
|
return &models.Artist{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
MbzID: row.MusicBrainzID,
|
MbzID: row.MusicBrainzID,
|
||||||
Name: row.Name,
|
Name: row.Name,
|
||||||
Aliases: row.Aliases,
|
Aliases: row.Aliases,
|
||||||
Image: row.Image,
|
Image: row.Image,
|
||||||
TimeListened: seconds,
|
|
||||||
ListenCount: count,
|
ListenCount: count,
|
||||||
|
TimeListened: seconds,
|
||||||
|
FirstListen: firstListen.ListenedAt.Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
} else if opts.Name != "" {
|
} else if opts.Name != "" {
|
||||||
l.Debug().Msgf("Fetching artist from DB with name '%s'", 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err)
|
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{
|
return &models.Artist{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
MbzID: row.MusicBrainzID,
|
MbzID: row.MusicBrainzID,
|
||||||
|
|
@ -109,6 +123,7 @@ func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Ar
|
||||||
Image: row.Image,
|
Image: row.Image,
|
||||||
ListenCount: count,
|
ListenCount: count,
|
||||||
TimeListened: seconds,
|
TimeListened: seconds,
|
||||||
|
FirstListen: firstListen.ListenedAt.Unix(),
|
||||||
}, nil
|
}, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("insufficient information to get artist")
|
return nil, errors.New("insufficient information to get artist")
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,14 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac
|
||||||
return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err)
|
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.ListenCount = count
|
||||||
track.TimeListened = seconds
|
track.TimeListened = seconds
|
||||||
|
track.FirstListen = firstListen.ListenedAt.Unix()
|
||||||
|
|
||||||
return &track, nil
|
return &track, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ type Album struct {
|
||||||
VariousArtists bool `json:"is_various_artists"`
|
VariousArtists bool `json:"is_various_artists"`
|
||||||
ListenCount int64 `json:"listen_count"`
|
ListenCount int64 `json:"listen_count"`
|
||||||
TimeListened int64 `json:"time_listened"`
|
TimeListened int64 `json:"time_listened"`
|
||||||
|
FirstListen int64 `json:"first_listen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// type SimpleAlbum struct {
|
// type SimpleAlbum struct {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ type Artist struct {
|
||||||
Image *uuid.UUID `json:"image"`
|
Image *uuid.UUID `json:"image"`
|
||||||
ListenCount int64 `json:"listen_count"`
|
ListenCount int64 `json:"listen_count"`
|
||||||
TimeListened int64 `json:"time_listened"`
|
TimeListened int64 `json:"time_listened"`
|
||||||
|
FirstListen int64 `json:"first_listen"`
|
||||||
IsPrimary bool `json:"is_primary,omitempty"`
|
IsPrimary bool `json:"is_primary,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,5 +28,6 @@ type ArtistWithFullAliases struct {
|
||||||
ImageSource string `json:"image_source,omitempty"`
|
ImageSource string `json:"image_source,omitempty"`
|
||||||
ListenCount int64 `json:"listen_count"`
|
ListenCount int64 `json:"listen_count"`
|
||||||
TimeListened int64 `json:"time_listened"`
|
TimeListened int64 `json:"time_listened"`
|
||||||
|
FirstListen int64 `json:"first_listen"`
|
||||||
IsPrimary bool `json:"is_primary,omitempty"`
|
IsPrimary bool `json:"is_primary,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,5 @@ type Track struct {
|
||||||
Image *uuid.UUID `json:"image"`
|
Image *uuid.UUID `json:"image"`
|
||||||
AlbumID int32 `json:"album_id"`
|
AlbumID int32 `json:"album_id"`
|
||||||
TimeListened int64 `json:"time_listened"`
|
TimeListened int64 `json:"time_listened"`
|
||||||
|
FirstListen int64 `json:"first_listen"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,73 @@ func (q *Queries) DeleteListen(ctx context.Context, arg DeleteListenParams) erro
|
||||||
return err
|
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
|
const getLastListensFromArtistPaginated = `-- name: GetLastListensFromArtistPaginated :many
|
||||||
SELECT
|
SELECT
|
||||||
l.track_id, l.listened_at, l.client, l.user_id,
|
l.track_id, l.listened_at, l.client, l.user_id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue