feat: read, remove and clear listening history and switch recording over HTTP

This commit is contained in:
DiheChen committed 2026-11-11 00:06:11 +08:00
1 parent 112527db83
commit 17e486209e
14 files changed
+722 -18

No files matched your search

+7
View File
@@ -19,6 +19,7 @@ import (
"github.com/dihemoe/alisten/internal/app/admin"
"github.com/dihemoe/alisten/internal/app/catalog"
"github.com/dihemoe/alisten/internal/app/identity"
"github.com/dihemoe/alisten/internal/app/listen"
"github.com/dihemoe/alisten/internal/app/lobby"
"github.com/dihemoe/alisten/internal/app/media"
"github.com/dihemoe/alisten/internal/app/playlist"
@@ -52,6 +53,7 @@ type Services struct {
Media *media.Library
Playback *media.Playback
Site *site.Service
Listens *listen.Service
// 浏览器访问对象存储时使用的地址前缀,结尾没有 /。
StorageURL string
@@ -158,6 +160,11 @@ func New(s Services, metrics prometheus.Registerer, tracer trace.TracerProvider,
srv.handle(mux, protocol.RouteAdminCloseRegistration, srv.adminCloseRegistration)
srv.handle(mux, protocol.RouteAdminGetAccount, srv.adminGetAccount)
srv.handle(mux, protocol.RouteAdminResetAccountPassword, srv.adminResetAccountPassword)
srv.handle(mux, protocol.RouteListMyListens, srv.listMyListens)
srv.handle(mux, protocol.RouteRemoveMyListen, srv.removeMyListen)
srv.handle(mux, protocol.RouteClearMyListens, srv.clearMyListens)
srv.handle(mux, protocol.RouteEnableListenRecording, srv.enableListenRecording)
srv.handle(mux, protocol.RouteDisableListenRecording, srv.disableListenRecording)
mux.HandleFunc("GET /ws", srv.websocket)
mux.HandleFunc("GET /api/room/events", srv.roomEvents)
mux.HandleFunc("GET /media/{source}/{id}", srv.redirectMedia)
+3
View File
@@ -42,6 +42,9 @@ func (s *server) getMe(ctx context.Context, c call) (any, error) {
dto := accountDTO(account)
resp.Account = &dto
}
if resp.RecordListens, err = s.Listens.Recording(ctx, c.member.ID); err != nil {
return nil, err
}
return resp, nil
}
+42
View File
@@ -0,0 +1,42 @@
package api
import (
"context"
"github.com/dihemoe/alisten/protocol"
)
func (s *server) listMyListens(ctx context.Context, c call) (any, error) {
listens, err := s.Listens.List(ctx, c.member.ID)
if err != nil {
return nil, err
}
items := make([]protocol.MyListen, len(listens))
for i, l := range listens {
items[i] = protocol.MyListen{Track: trackDTO(l.Track), Meta: s.trackMeta(l.Meta), HeardAtMs: l.HeardAt.UnixMilli()}
}
return protocol.ListMyListensResponse{Items: items}, nil
}
func (s *server) removeMyListen(ctx context.Context, c call) (any, error) {
req, err := protocol.DecodeRemoveMyListenRequest(c.body)
if err != nil {
return nil, err
}
return nil, s.Listens.Remove(ctx, c.member.ID, trackOf(req.Track))
}
func (s *server) clearMyListens(ctx context.Context, c call) (any, error) {
return nil, s.Listens.Clear(ctx, c.member.ID)
}
func (s *server) enableListenRecording(ctx context.Context, c call) (any, error) {
if _, err := protocol.DecodeEnableListenRecordingRequest(c.body); err != nil {
return nil, err
}
return nil, s.Listens.SetRecording(ctx, c.member.ID, true)
}
func (s *server) disableListenRecording(ctx context.Context, c call) (any, error) {
return nil, s.Listens.SetRecording(ctx, c.member.ID, false)
}
+38
View File
@@ -0,0 +1,38 @@
package listen
import (
"context"
"github.com/dihemoe/alisten/internal/app/port"
"github.com/dihemoe/alisten/internal/domain/track"
)
// 游客和已注册成员都有播放历史,不像歌单要求注册。
type Service struct {
listens port.Listens
}
func New(listens port.Listens) *Service {
return &Service{listens: listens}
}
func (s *Service) List(ctx context.Context, memberID string) ([]port.Listen, error) {
return s.listens.MemberListens(ctx, memberID)
}
// 链接曲目的 t.ID 必须已经按 track.LinkID 规范化。
func (s *Service) Remove(ctx context.Context, memberID string, t track.Track) error {
return s.listens.DeleteListen(ctx, memberID, t)
}
func (s *Service) Clear(ctx context.Context, memberID string) error {
return s.listens.ClearListens(ctx, memberID)
}
func (s *Service) Recording(ctx context.Context, memberID string) (bool, error) {
return s.listens.ListenRecording(ctx, memberID)
}
func (s *Service) SetRecording(ctx context.Context, memberID string, on bool) error {
return s.listens.SetListenRecording(ctx, memberID, on)
}
+2
View File
@@ -27,6 +27,7 @@ import (
"github.com/dihemoe/alisten/internal/app/admin"
"github.com/dihemoe/alisten/internal/app/catalog"
"github.com/dihemoe/alisten/internal/app/identity"
"github.com/dihemoe/alisten/internal/app/listen"
"github.com/dihemoe/alisten/internal/app/lobby"
"github.com/dihemoe/alisten/internal/app/media"
"github.com/dihemoe/alisten/internal/app/playlist"
@@ -90,6 +91,7 @@ func newPublic(cfg serveConfig, pool *pgxpool.Pool, rdb *goredis.Client, instanc
Media: media.NewLibrary(store, store, objects),
Playback: media.NewPlayback(src.playback, store),
Site: site.New(store),
Listens: listen.New(store),
StorageURL: cfg.storageURL,
}
+6 -6
View File
@@ -22,7 +22,7 @@ GET /api/me
Authorization: Bearer $alice
-- response --
200
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}}
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}, "recordListens": true}
-- request --
PATCH /api/me
@@ -38,7 +38,7 @@ GET /api/me
Authorization: Bearer $alice
-- response --
200
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}}
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}, "recordListens": true}
-- request --
POST /api/account
@@ -62,7 +62,7 @@ GET /api/me
Authorization: Bearer $bob
-- response --
200
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}}
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}, "recordListens": true}
-- request --
PATCH /api/me
@@ -78,7 +78,7 @@ GET /api/me
Authorization: Bearer $bob
-- response --
200
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}}
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "recordListens": true}
-- request --
PATCH /api/me
@@ -139,7 +139,7 @@ GET /api/me
Authorization: Bearer $alice2
-- response --
200
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}}
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}, "account": {"username": "alice", "email": "Alice@Example.COM"}, "recordListens": true}
-- request --
PATCH /api/me
@@ -155,7 +155,7 @@ GET /api/me
Authorization: Bearer $alice
-- response --
200
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}}
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}, "recordListens": true}
-- request --
GET /api/me
+3 -3
View File
@@ -21,7 +21,7 @@ GET /api/me
Authorization: Bearer $alice
-- response --
200
{"member": {"id": "$aliceId", "name": "$guestName", "avatarHash": ""}}
{"member": {"id": "$aliceId", "name": "$guestName", "avatarHash": ""}, "recordListens": true}
-- request --
PATCH /api/me
@@ -73,11 +73,11 @@ GET /api/me
Authorization: Bearer $alice
-- response --
200
{"member": {"id": "$aliceId", "name": "甲", "avatarHash": ""}}
{"member": {"id": "$aliceId", "name": "甲", "avatarHash": ""}, "recordListens": true}
-- request --
GET /api/me
Authorization: Bearer $bob
-- response --
200
{"member": {"id": "$bobId", "name": "乙", "avatarHash": ""}}
{"member": {"id": "$bobId", "name": "乙", "avatarHash": ""}, "recordListens": true}
+249
View File
@@ -0,0 +1,249 @@
播放历史:开播时记给房间里的成员,开播之后进来的成员也记上正在播放的歌,关闭了记录的成员不记,已有的历史保留;
可以删除一首、清空;游客登录账号时,游客的历史并入账号。读取历史要有成员 token。
-- request --
POST /api/members
{}
-- response --
201
{"memberToken": "$alice", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}}
-- request --
POST /api/account
Authorization: Bearer $alice
{"username": "alice", "password": "correct horse"}
-- response --
201
{"account": {"username": "alice"}}
-- request --
GET /api/me/listens
Authorization: Bearer $alice
-- response --
200
{"items": []}
-- request --
POST /api/members
{}
-- response --
201
{"memberToken": "$bob", "member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}}
-- request --
DELETE /api/me/listen-recording
Authorization: Bearer $bob
-- response --
204
-- request --
GET /api/me
Authorization: Bearer $bob
-- response --
200
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}, "recordListens": false}
-- request --
POST /api/admin/rooms
Authorization: Bearer admin-secret
{"name": "大厅", "description": ""}
-- response --
201
Location: /api/rooms/$room
{"id": "$room"}
-- request --
POST /api/rooms/$room/tickets
Authorization: Bearer $alice
{}
-- response --
201
{"ticket": "$ticketA"}
-- connect alice --
-- send alice --
{"kind": "request", "type": "hello", "id": "h1", "data": {"roomId": "$room", "ticket": "$ticketA", "protocolVersion": "$protocolVersion"}}
-- receive alice --
{"kind": "response", "type": "hello", "id": "h1", "data": {
"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""},
"serverTimeMs": 1,
"room": {"term": 1, "seq": 1, "room": {"id": "$room", "name": "大厅", "description": "", "kind": "persistent"},
"playMode": "sequential", "nowPlaying": null, "pending": null, "items": [],
"members": {"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}], "onlineCount": 1}}}}
-- send alice --
{"kind": "request", "type": "queue.add", "id": "a1", "data": {"track": {"source": "netease", "id": "186016"}}}
-- receive alice --
{"kind": "event", "type": "room.activity", "term": 1, "seq": 2, "data": {"kind": "added", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""},
"entry": {"id": "1", "track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269000},
"by": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "auto": false, "likes": 0, "status": "preparing"},
"message": "点了《晴天》"}}
-- receive alice --
{"kind": "event", "type": "queue.updated", "term": 1, "seq": 3, "data": {"items": [],
"pending": {"id": "1", "track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269000},
"by": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "auto": false, "likes": 0, "status": "preparing"}}}
-- receive alice --
{"kind": "response", "type": "queue.add", "id": "a1", "data": {"entry": {"id": "1", "track": {"source": "netease", "id": "186016"},
"meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269000},
"by": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "auto": false, "likes": 0, "status": "preparing"}}}
-- receive alice --
{"kind": "event", "type": "queue.updated", "term": 1, "seq": 4, "data": {"pending": null, "items": []}}
-- receive alice --
{"kind": "event", "type": "playback.updated", "term": 1, "seq": 5, "data": {"nowPlaying": {
"entry": {"id": "1", "track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269500},
"by": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "auto": false, "likes": 0, "status": "ready"},
"mediaUrl": "/media/netease/186016", "startedAtMs": 1, "durationMs": 269500, "gain": 1, "lyric": "[00:01.00]故事的小黄花"}}}
-- request --
GET /api/me/listens
Authorization: Bearer $alice
-- response --
200
{"items": [{"track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269500}, "heardAtMs": 1}]}
-- request --
POST /api/rooms/$room/tickets
Authorization: Bearer $bob
{}
-- response --
201
{"ticket": "$ticketB"}
-- connect bob --
-- send bob --
{"kind": "request", "type": "hello", "id": "h1", "data": {"roomId": "$room", "ticket": "$ticketB", "protocolVersion": "$protocolVersion"}}
-- receive bob --
{"kind": "response", "type": "hello", "id": "h1", "data": {
"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""},
"serverTimeMs": 1,
"room": {"term": 1, "seq": 6, "room": {"id": "$room", "name": "大厅", "description": "", "kind": "persistent"},
"playMode": "sequential", "pending": null, "items": [],
"nowPlaying": {
"entry": {"id": "1", "track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269500},
"by": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "auto": false, "likes": 0, "status": "ready"},
"mediaUrl": "/media/netease/186016", "startedAtMs": 1, "durationMs": 269500, "gain": 1, "lyric": "[00:01.00]故事的小黄花"},
"members": {"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, {"id": "$bobId", "name": "$bobName", "avatarHash": ""}], "onlineCount": 2}}}}
-- receive alice --
{"kind": "event", "type": "members.updated", "term": 1, "seq": 6, "data": {
"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, {"id": "$bobId", "name": "$bobName", "avatarHash": ""}], "onlineCount": 2}}
-- request --
GET /api/me/listens
Authorization: Bearer $bob
-- response --
200
{"items": []}
-- request --
PUT /api/me/listen-recording
Authorization: Bearer $bob
{}
-- response --
204
-- request --
GET /api/me
Authorization: Bearer $bob
-- response --
200
{"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}, "recordListens": true}
-- disconnect bob --
-- receive alice --
{"kind": "event", "type": "members.updated", "term": 1, "seq": 7, "data": {
"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}], "onlineCount": 1}}
-- connect again --
-- send again --
{"kind": "request", "type": "hello", "id": "h1", "data": {"roomId": "$room", "ticket": "$ticketB", "protocolVersion": "$protocolVersion"}}
-- receive again --
{"kind": "response", "type": "hello", "id": "h1", "data": {
"member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""},
"serverTimeMs": 1,
"room": {"term": 1, "seq": 8, "room": {"id": "$room", "name": "大厅", "description": "", "kind": "persistent"},
"playMode": "sequential", "pending": null, "items": [],
"nowPlaying": {
"entry": {"id": "1", "track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269500},
"by": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "auto": false, "likes": 0, "status": "ready"},
"mediaUrl": "/media/netease/186016", "startedAtMs": 1, "durationMs": 269500, "gain": 1, "lyric": "[00:01.00]故事的小黄花"},
"members": {"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, {"id": "$bobId", "name": "$bobName", "avatarHash": ""}], "onlineCount": 2}}}}
-- receive alice --
{"kind": "event", "type": "members.updated", "term": 1, "seq": 8, "data": {
"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, {"id": "$bobId", "name": "$bobName", "avatarHash": ""}], "onlineCount": 2}}
-- request --
GET /api/me/listens
Authorization: Bearer $bob
-- response --
200
{"items": [{"track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269500}, "heardAtMs": 1}]}
-- request --
POST /api/me/listens/removals
Authorization: Bearer $alice
{"track": {"source": "netease", "id": "186016"}}
-- response --
204
-- request --
POST /api/me/listens/removals
Authorization: Bearer $alice
{"track": {"source": "qq", "id": "0039MnYb0qxYhV"}}
-- response --
204
-- request --
GET /api/me/listens
Authorization: Bearer $alice
-- response --
200
{"items": []}
-- request --
POST /api/sessions
Authorization: Bearer $bob
{"username": "alice", "password": "correct horse"}
-- response --
201
{"memberToken": "$alice2", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}, "account": {"username": "alice"}, "device": "$bobDevice"}
-- closed again --
4401
-- receive alice --
{"kind": "event", "type": "members.updated", "term": 1, "seq": 9, "data": {
"sessions": [{"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}], "onlineCount": 1}}
-- request --
GET /api/me/listens
Authorization: Bearer $alice2
-- response --
200
{"items": [{"track": {"source": "netease", "id": "186016"}, "meta": {"title": "晴天", "artist": "周杰伦", "album": "叶惠美", "coverUrl": "https://p1.music.126.net/example.jpg", "pageUrl": "https://music.163.com/#/song?id=186016", "durationMs": 269500}, "heardAtMs": 1}]}
-- request --
DELETE /api/me/listens
Authorization: Bearer $alice2
-- response --
204
-- request --
GET /api/me/listens
Authorization: Bearer $alice
-- response --
200
{"items": []}
-- request --
GET /api/me/listens
-- response --
401
{"error": {"code": "UNAUTHORIZED", "message": "成员 token 无效"}}