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 无效"}}
+29
View File
@@ -187,6 +187,14 @@ func DecodeAdminResetAccountPasswordRequest(body []byte) (AdminResetAccountPassw
return decodeAdminResetAccountPasswordRequest(body, "")
}
func DecodeRemoveMyListenRequest(body []byte) (RemoveMyListenRequest, error) {
return decodeRemoveMyListenRequest(body, "")
}
func DecodeEnableListenRecordingRequest(body []byte) (EnableListenRecordingRequest, error) {
return decodeEnableListenRecordingRequest(body, "")
}
func decodeAddMyPlaylistItemRequest(raw json.RawMessage, path string) (AddMyPlaylistItemRequest, error) {
var v AddMyPlaylistItemRequest
m, err := decodeObject(raw, path, "track")
@@ -570,6 +578,12 @@ func decodeCreateTicketRequest(raw json.RawMessage, path string) (CreateTicketRe
return v, nil
}
func decodeEnableListenRecordingRequest(raw json.RawMessage, path string) (EnableListenRecordingRequest, error) {
var v EnableListenRecordingRequest
_, err := decodeObject(raw, path)
return v, err
}
func decodeHelloRequest(raw json.RawMessage, path string) (HelloRequest, error) {
var v HelloRequest
m, err := decodeObject(raw, path, "protocolVersion", "roomId", "ticket")
@@ -967,6 +981,21 @@ func decodeRegisterRequest(raw json.RawMessage, path string) (RegisterRequest, e
return v, nil
}
func decodeRemoveMyListenRequest(raw json.RawMessage, path string) (RemoveMyListenRequest, error) {
var v RemoveMyListenRequest
m, err := decodeObject(raw, path, "track")
if err != nil {
return v, err
}
if err = requireFields(m, path, "track"); err != nil {
return v, err
}
if v.Track, err = decodeTrack(m["track"], field(path, "track")); err != nil {
return v, err
}
return v, nil
}
func decodeRemoveMyPlaylistItemsRequest(raw json.RawMessage, path string) (RemoveMyPlaylistItemsRequest, error) {
var v RemoveMyPlaylistItemsRequest
m, err := decodeObject(raw, path, "tracks")
+73 -2
View File
@@ -352,6 +352,16 @@
"message": {"type": "string", "description": "平台给出的说明,可能为空"}
},
"required": ["status", "message"]
},
"MyListen": {
"type": "object",
"additionalProperties": false,
"properties": {
"track": {"$ref": "#/$defs/Track"},
"meta": {"$ref": "#/$defs/TrackMeta", "description": "听到时的元数据"},
"heardAtMs": {"type": "integer", "description": "最近一次听到这首歌时它的开播时间"}
},
"required": ["track", "meta", "heardAtMs"]
}
},
"x-ws": {
@@ -741,9 +751,10 @@
"additionalProperties": false,
"properties": {
"member": {"$ref": "#/$defs/Member"},
"account": {"$ref": "#/$defs/Account"}
"account": {"$ref": "#/$defs/Account"},
"recordListens": {"type": "boolean", "description": "是否把在房间里听到的歌记进播放历史"}
},
"required": ["member"]
"required": ["member", "recordListens"]
}
},
{
@@ -1553,6 +1564,66 @@
},
"required": ["password"]
}
},
{
"name": "listMyListens",
"description": "我的播放历史:在房间里听到过的歌,每首只留最近一次,最近听到的在前,最多 500 首;同一账号的所有设备看到同一份",
"method": "GET",
"path": "/api/me/listens",
"auth": "member",
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"items": {"type": "array", "items": {"$ref": "#/$defs/MyListen"}}
},
"required": ["items"]
}
},
{
"name": "removeMyListen",
"description": "从播放历史中删除一首歌;不在历史里的忽略",
"method": "POST",
"path": "/api/me/listens/removals",
"auth": "member",
"status": 204,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"track": {"$ref": "#/$defs/Track"}
},
"required": ["track"]
}
},
{
"name": "clearMyListens",
"description": "清空播放历史",
"method": "DELETE",
"path": "/api/me/listens",
"auth": "member",
"status": 204
},
{
"name": "enableListenRecording",
"description": "开始把在房间里听到的歌记进播放历史",
"method": "PUT",
"path": "/api/me/listen-recording",
"auth": "member",
"status": 204,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
},
{
"name": "disableListenRecording",
"description": "不再记录播放历史,已有的历史保留",
"method": "DELETE",
"path": "/api/me/listen-recording",
"auth": "member",
"status": 204
}
]
}
+213 -2
View File
@@ -570,6 +570,10 @@
"additionalProperties": false,
"type": "object"
},
"EnableListenRecordingRequest": {
"additionalProperties": false,
"type": "object"
},
"Entry": {
"additionalProperties": false,
"properties": {
@@ -690,10 +694,15 @@
},
"member": {
"$ref": "#/components/schemas/Member"
},
"recordListens": {
"description": "是否把在房间里听到的歌记进播放历史",
"type": "boolean"
}
},
"required": [
"member"
"member",
"recordListens"
],
"type": "object"
},
@@ -829,6 +838,21 @@
],
"type": "object"
},
"ListMyListensResponse": {
"additionalProperties": false,
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/MyListen"
},
"type": "array"
}
},
"required": [
"items"
],
"type": "object"
},
"ListMyPlaylistItemsResponse": {
"additionalProperties": false,
"properties": {
@@ -1014,6 +1038,28 @@
],
"type": "object"
},
"MyListen": {
"additionalProperties": false,
"properties": {
"heardAtMs": {
"description": "最近一次听到这首歌时它的开播时间",
"type": "integer"
},
"meta": {
"$ref": "#/components/schemas/TrackMeta",
"description": "听到时的元数据"
},
"track": {
"$ref": "#/components/schemas/Track"
}
},
"required": [
"track",
"meta",
"heardAtMs"
],
"type": "object"
},
"MyPlaylist": {
"additionalProperties": false,
"properties": {
@@ -1386,6 +1432,18 @@
],
"type": "object"
},
"RemoveMyListenRequest": {
"additionalProperties": false,
"properties": {
"track": {
"$ref": "#/components/schemas/Track"
}
},
"required": [
"track"
],
"type": "object"
},
"RemoveMyPlaylistItemsRequest": {
"additionalProperties": false,
"properties": {
@@ -1898,7 +1956,7 @@
},
"info": {
"title": "Alisten",
"version": "ed36759d403f9894"
"version": "ae9a632d648f3840"
},
"openapi": "3.1.0",
"paths": {
@@ -3035,6 +3093,159 @@
"summary": "修改昵称和头像;email 用来计算头像哈希,已注册的账号同时保存它,去掉首尾空白后应形如 name@example.com,空字符串表示清除"
}
},
"/api/me/listen-recording": {
"delete": {
"operationId": "disableListenRecording",
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "不再记录播放历史,已有的历史保留"
},
"put": {
"operationId": "enableListenRecording",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EnableListenRecordingRequest"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "开始把在房间里听到的歌记进播放历史"
}
},
"/api/me/listens": {
"delete": {
"operationId": "clearMyListens",
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "清空播放历史"
},
"get": {
"operationId": "listMyListens",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListMyListensResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "我的播放历史:在房间里听到过的歌,每首只留最近一次,最近听到的在前,最多 500 首;同一账号的所有设备看到同一份"
}
},
"/api/me/listens/removals": {
"post": {
"operationId": "removeMyListen",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RemoveMyListenRequest"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "从播放历史中删除一首歌;不在历史里的忽略"
}
},
"/api/me/playlist-links": {
"post": {
"operationId": "resolveMyPlaylistLink",
+33 -3
View File
@@ -4,7 +4,7 @@ package protocol
import "encoding/json"
const Version = "ed36759d403f9894"
const Version = "ae9a632d648f3840"
type APIKey struct {
Key string `json:"key"`
@@ -212,6 +212,8 @@ type CreateTicketResponse struct {
type Empty struct{}
type EnableListenRecordingRequest struct{}
type Entry struct {
Auto bool `json:"auto"`
By *Member `json:"by"`
@@ -253,8 +255,9 @@ type ErrorInfo struct {
}
type GetMeResponse struct {
Account *Account `json:"account,omitempty"`
Member Member `json:"member"`
Account *Account `json:"account,omitempty"`
Member Member `json:"member"`
RecordListens bool `json:"recordListens"`
}
type GetSiteResponse struct {
@@ -313,6 +316,18 @@ type ImportedTrack struct {
Track Track `json:"track"`
}
type ListMyListensResponse struct {
Items []MyListen `json:"items"`
}
func (v ListMyListensResponse) MarshalJSON() ([]byte, error) {
type plain ListMyListensResponse
if v.Items == nil {
v.Items = []MyListen{}
}
return json.Marshal(plain(v))
}
type ListMyPlaylistItemsResponse struct {
Items []MyPlaylistItem `json:"items"`
}
@@ -410,6 +425,12 @@ func (v MoveMyPlaylistItemsRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(plain(v))
}
type MyListen struct {
HeardAtMs int64 `json:"heardAtMs"`
Meta TrackMeta `json:"meta"`
Track Track `json:"track"`
}
type MyPlaylist struct {
CoverURL *string `json:"coverUrl"`
CreatedAtMs int64 `json:"createdAtMs"`
@@ -581,6 +602,10 @@ type RegisterResponse struct {
Account Account `json:"account"`
}
type RemoveMyListenRequest struct {
Track Track `json:"track"`
}
type RemoveMyPlaylistItemsRequest struct {
Tracks []Track `json:"tracks"`
}
@@ -865,6 +890,11 @@ var (
RouteAdminCloseRegistration = Route{Pattern: "DELETE /api/admin/registration", Auth: AuthAdmin, Status: 204}
RouteAdminGetAccount = Route{Pattern: "GET /api/admin/accounts/{username}", Auth: AuthAdmin, Status: 200}
RouteAdminResetAccountPassword = Route{Pattern: "POST /api/admin/accounts/{username}/password", Auth: AuthAdmin, Status: 200}
RouteListMyListens = Route{Pattern: "GET /api/me/listens", Auth: AuthMember, Status: 200}
RouteRemoveMyListen = Route{Pattern: "POST /api/me/listens/removals", Auth: AuthMember, Status: 204}
RouteClearMyListens = Route{Pattern: "DELETE /api/me/listens", Auth: AuthMember, Status: 204}
RouteEnableListenRecording = Route{Pattern: "PUT /api/me/listen-recording", Auth: AuthMember, Status: 204}
RouteDisableListenRecording = Route{Pattern: "DELETE /api/me/listen-recording", Auth: AuthMember, Status: 204}
)
var RoomRoutes = []RoomRoute{
+17 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
import type { APIKey, Account, AdminAccount, ErrorInfo, ImportedTrack, Member, MyPlaylist, MyPlaylistItem, QQLoginType, QRLogin, QRLoginResult, RoomListing, Source, SourceAccount, StoredMedia, Track } from '../realtime/protocol.gen'
import type { APIKey, Account, AdminAccount, ErrorInfo, ImportedTrack, Member, MyListen, MyPlaylist, MyPlaylistItem, QQLoginType, QRLogin, QRLoginResult, RoomListing, Source, SourceAccount, StoredMedia, Track } from '../realtime/protocol.gen'
export interface AddMyPlaylistItemRequest {
track: Track
@@ -147,9 +147,12 @@ export interface CreateTicketResponse {
ticket: string
}
export type EnableListenRecordingRequest = Record<string, never>
export interface GetMeResponse {
account?: Account
member: Member
recordListens: boolean
}
export interface GetSiteResponse {
@@ -178,6 +181,10 @@ export interface ImportMyPlaylistResponse {
playlist: MyPlaylist
}
export interface ListMyListensResponse {
items: MyListen[]
}
export interface ListMyPlaylistItemsResponse {
items: MyPlaylistItem[]
}
@@ -225,6 +232,10 @@ export interface RegisterResponse {
account: Account
}
export interface RemoveMyListenRequest {
track: Track
}
export interface RemoveMyPlaylistItemsRequest {
tracks: Track[]
}
@@ -318,5 +329,10 @@ export function httpClient(send: HttpSend) {
adminCloseRegistration: () => send<void>({ method: 'DELETE', path: '/api/admin/registration', auth: 'admin' }),
adminGetAccount: (username: string) => send<AdminAccount>({ method: 'GET', path: `/api/admin/accounts/${encodeURIComponent(username)}`, auth: 'admin' }),
adminResetAccountPassword: (username: string) => send<AdminResetAccountPasswordResponse>({ method: 'POST', path: `/api/admin/accounts/${encodeURIComponent(username)}/password`, auth: 'admin', body: {} }),
listMyListens: () => send<ListMyListensResponse>({ method: 'GET', path: '/api/me/listens', auth: 'member' }),
removeMyListen: (body: RemoveMyListenRequest) => send<void>({ method: 'POST', path: '/api/me/listens/removals', auth: 'member', body }),
clearMyListens: () => send<void>({ method: 'DELETE', path: '/api/me/listens', auth: 'member' }),
enableListenRecording: () => send<void>({ method: 'PUT', path: '/api/me/listen-recording', auth: 'member', body: {} }),
disableListenRecording: () => send<void>({ method: 'DELETE', path: '/api/me/listen-recording', auth: 'member' }),
}
}
+7 -1
View File
@@ -1,6 +1,6 @@
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
export const PROTOCOL_VERSION = 'ed36759d403f9894'
export const PROTOCOL_VERSION = 'ae9a632d648f3840'
export interface APIKey {
key: string
@@ -82,6 +82,12 @@ export interface Members {
sessions: Member[]
}
export interface MyListen {
heardAtMs: number
meta: TrackMeta
track: Track
}
export interface MyPlaylist {
coverUrl: string | null
createdAtMs: number