feat: expose member playlists over HTTP

This commit is contained in:
DiheChen committed 2026-11-09 21:27:41 +08:00
1 parent 95a56267a4
commit fafde9041c
13 files changed
+1280 -33

No files matched your search

+82
View File
@@ -87,6 +87,22 @@ func DecodeLoginRequest(body []byte) (LoginRequest, error) {
return decodeLoginRequest(body, "")
}
func DecodeCreateMyPlaylistRequest(body []byte) (CreateMyPlaylistRequest, error) {
return decodeCreateMyPlaylistRequest(body, "")
}
func DecodeRenameMyPlaylistRequest(body []byte) (RenameMyPlaylistRequest, error) {
return decodeRenameMyPlaylistRequest(body, "")
}
func DecodeAddMyPlaylistItemRequest(body []byte) (AddMyPlaylistItemRequest, error) {
return decodeAddMyPlaylistItemRequest(body, "")
}
func DecodeRemoveMyPlaylistItemRequest(body []byte) (RemoveMyPlaylistItemRequest, error) {
return decodeRemoveMyPlaylistItemRequest(body, "")
}
func DecodeCreateRoomRequest(body []byte) (CreateRoomRequest, error) {
return decodeCreateRoomRequest(body, "")
}
@@ -147,6 +163,21 @@ func DecodeAdminResetAccountPasswordRequest(body []byte) (AdminResetAccountPassw
return decodeAdminResetAccountPasswordRequest(body, "")
}
func decodeAddMyPlaylistItemRequest(raw json.RawMessage, path string) (AddMyPlaylistItemRequest, error) {
var v AddMyPlaylistItemRequest
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 decodeAdminCheckBilibiliLoginRequest(raw json.RawMessage, path string) (AdminCheckBilibiliLoginRequest, error) {
var v AdminCheckBilibiliLoginRequest
m, err := decodeObject(raw, path, "key")
@@ -426,6 +457,24 @@ func decodeCreateMemberRequest(raw json.RawMessage, path string) (CreateMemberRe
return v, err
}
func decodeCreateMyPlaylistRequest(raw json.RawMessage, path string) (CreateMyPlaylistRequest, error) {
var v CreateMyPlaylistRequest
m, err := decodeObject(raw, path, "name")
if err != nil {
return v, err
}
if err = requireFields(m, path, "name"); err != nil {
return v, err
}
if v.Name, err = decodeString(m["name"], field(path, "name")); err != nil {
return v, err
}
if err = checkMaxLength(v.Name, field(path, "name"), 200); err != nil {
return v, err
}
return v, nil
}
func decodeCreateRoomRequest(raw json.RawMessage, path string) (CreateRoomRequest, error) {
var v CreateRoomRequest
m, err := decodeObject(raw, path, "description", "name", "password")
@@ -712,6 +761,39 @@ func decodeRegisterRequest(raw json.RawMessage, path string) (RegisterRequest, e
return v, nil
}
func decodeRemoveMyPlaylistItemRequest(raw json.RawMessage, path string) (RemoveMyPlaylistItemRequest, error) {
var v RemoveMyPlaylistItemRequest
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 decodeRenameMyPlaylistRequest(raw json.RawMessage, path string) (RenameMyPlaylistRequest, error) {
var v RenameMyPlaylistRequest
m, err := decodeObject(raw, path, "name")
if err != nil {
return v, err
}
if err = requireFields(m, path, "name"); err != nil {
return v, err
}
if v.Name, err = decodeString(m["name"], field(path, "name")); err != nil {
return v, err
}
if err = checkMaxLength(v.Name, field(path, "name"), 200); err != nil {
return v, err
}
return v, nil
}
func decodeRoomSyncRequest(raw json.RawMessage, path string) (RoomSyncRequest, error) {
var v RoomSyncRequest
_, err := decodeObject(raw, path)
+135
View File
@@ -175,6 +175,30 @@
},
"required": ["member", "username"]
},
"PlaylistName": {"type": "string", "description": "歌单名:去掉首尾空白后 1 到 50 个字符", "maxLength": 200},
"MyPlaylist": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"favorites": {"type": "boolean", "description": "是否是“我喜欢”"},
"trackCount": {"type": "integer"},
"createdAtMs": {"type": "integer"},
"updatedAtMs": {"type": "integer"},
"coverUrl": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "歌单里排在最前、有封面的那首歌的封面;没有时为 null"}
},
"required": ["id", "name", "favorites", "trackCount", "createdAtMs", "updatedAtMs", "coverUrl"]
},
"MyPlaylistItem": {
"type": "object",
"additionalProperties": false,
"properties": {
"track": {"$ref": "#/$defs/Track"},
"meta": {"$ref": "#/$defs/TrackMeta"}
},
"required": ["track", "meta"]
},
"Members": {
"type": "object",
"additionalProperties": false,
@@ -810,6 +834,117 @@
"auth": "member",
"status": 204
},
{
"name": "listMyPlaylists",
"description": "我的歌单:“我喜欢”在最前,其余按创建的先后;只有已注册的账号能使用歌单",
"method": "GET",
"path": "/api/me/playlists",
"auth": "member",
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"items": {"type": "array", "items": {"$ref": "#/$defs/MyPlaylist"}}
},
"required": ["items"]
}
},
{
"name": "createMyPlaylist",
"description": "新建歌单:最多 50 个(含“我喜欢”),可以重名",
"method": "POST",
"path": "/api/me/playlists",
"auth": "member",
"status": 201,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {"$ref": "#/$defs/PlaylistName"}
},
"required": ["name"]
},
"response": {"$ref": "#/$defs/MyPlaylist"}
},
{
"name": "renameMyPlaylist",
"description": "重命名歌单;“我喜欢”不能重命名",
"method": "PATCH",
"path": "/api/me/playlists/{id}",
"auth": "member",
"status": 204,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {"$ref": "#/$defs/PlaylistName"}
},
"required": ["name"]
}
},
{
"name": "deleteMyPlaylist",
"description": "删除歌单;“我喜欢”不能删除",
"method": "DELETE",
"path": "/api/me/playlists/{id}",
"auth": "member",
"status": 204
},
{
"name": "listMyPlaylistItems",
"description": "歌单里的歌,新加入的在前",
"method": "GET",
"path": "/api/me/playlists/{id}/items",
"auth": "member",
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"items": {"type": "array", "items": {"$ref": "#/$defs/MyPlaylistItem"}}
},
"required": ["items"]
}
},
{
"name": "addMyPlaylistItem",
"description": "把一首歌加入歌单:元数据由服务端查询,链接曲目要先准备好;已经在歌单里时不变;一个歌单最多 1000 首",
"method": "POST",
"path": "/api/me/playlists/{id}/items",
"auth": "member",
"status": 201,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"track": {"$ref": "#/$defs/Track"}
},
"required": ["track"]
},
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"item": {"$ref": "#/$defs/MyPlaylistItem"}
},
"required": ["item"]
}
},
{
"name": "removeMyPlaylistItem",
"description": "把一首歌移出歌单;不在歌单里也算成功",
"method": "POST",
"path": "/api/me/playlists/{id}/removals",
"auth": "member",
"status": 204,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"track": {"$ref": "#/$defs/Track"}
},
"required": ["track"]
}
},
{
"name": "listRooms",
"description": "房间列表",
+450 -1
View File
@@ -41,6 +41,30 @@
],
"type": "string"
},
"AddMyPlaylistItemRequest": {
"additionalProperties": false,
"properties": {
"track": {
"$ref": "#/components/schemas/Track"
}
},
"required": [
"track"
],
"type": "object"
},
"AddMyPlaylistItemResponse": {
"additionalProperties": false,
"properties": {
"item": {
"$ref": "#/components/schemas/MyPlaylistItem"
}
},
"required": [
"item"
],
"type": "object"
},
"AdminAccount": {
"additionalProperties": false,
"properties": {
@@ -437,6 +461,20 @@
],
"type": "object"
},
"CreateMyPlaylistRequest": {
"additionalProperties": false,
"properties": {
"name": {
"description": "歌单名:去掉首尾空白后 1 到 50 个字符",
"maxLength": 200,
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"CreateRoomRequest": {
"additionalProperties": false,
"properties": {
@@ -641,6 +679,36 @@
],
"type": "object"
},
"ListMyPlaylistItemsResponse": {
"additionalProperties": false,
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/MyPlaylistItem"
},
"type": "array"
}
},
"required": [
"items"
],
"type": "object"
},
"ListMyPlaylistsResponse": {
"additionalProperties": false,
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/MyPlaylist"
},
"type": "array"
}
},
"required": [
"items"
],
"type": "object"
},
"ListRoomsResponse": {
"additionalProperties": false,
"properties": {
@@ -749,6 +817,67 @@
],
"type": "object"
},
"MyPlaylist": {
"additionalProperties": false,
"properties": {
"coverUrl": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "歌单里排在最前、有封面的那首歌的封面;没有时为 null"
},
"createdAtMs": {
"type": "integer"
},
"favorites": {
"description": "是否是“我喜欢”",
"type": "boolean"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"trackCount": {
"type": "integer"
},
"updatedAtMs": {
"type": "integer"
}
},
"required": [
"id",
"name",
"favorites",
"trackCount",
"createdAtMs",
"updatedAtMs",
"coverUrl"
],
"type": "object"
},
"MyPlaylistItem": {
"additionalProperties": false,
"properties": {
"meta": {
"$ref": "#/components/schemas/TrackMeta"
},
"track": {
"$ref": "#/components/schemas/Track"
}
},
"required": [
"track",
"meta"
],
"type": "object"
},
"NowPlaying": {
"additionalProperties": false,
"properties": {
@@ -1060,6 +1189,32 @@
],
"type": "object"
},
"RemoveMyPlaylistItemRequest": {
"additionalProperties": false,
"properties": {
"track": {
"$ref": "#/components/schemas/Track"
}
},
"required": [
"track"
],
"type": "object"
},
"RenameMyPlaylistRequest": {
"additionalProperties": false,
"properties": {
"name": {
"description": "歌单名:去掉首尾空白后 1 到 50 个字符",
"maxLength": 200,
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"RoomInfo": {
"additionalProperties": false,
"properties": {
@@ -1511,7 +1666,7 @@
},
"info": {
"title": "Alisten",
"version": "000a0b37ea0f0adc"
"version": "7380d77bc52d7f95"
},
"openapi": "3.1.0",
"paths": {
@@ -2648,6 +2803,300 @@
"summary": "修改昵称和头像;email 用来计算头像哈希,已注册的账号同时保存它,去掉首尾空白后应形如 name@example.com,空字符串表示清除"
}
},
"/api/me/playlists": {
"get": {
"operationId": "listMyPlaylists",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListMyPlaylistsResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "我的歌单:“我喜欢”在最前,其余按创建的先后;只有已注册的账号能使用歌单"
},
"post": {
"operationId": "createMyPlaylist",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateMyPlaylistRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MyPlaylist"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "新建歌单:最多 50 个(含“我喜欢”),可以重名"
}
},
"/api/me/playlists/{id}": {
"delete": {
"operationId": "deleteMyPlaylist",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "删除歌单;“我喜欢”不能删除"
},
"patch": {
"operationId": "renameMyPlaylist",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RenameMyPlaylistRequest"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "重命名歌单;“我喜欢”不能重命名"
}
},
"/api/me/playlists/{id}/items": {
"get": {
"operationId": "listMyPlaylistItems",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListMyPlaylistItemsResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "歌单里的歌,新加入的在前"
},
"post": {
"operationId": "addMyPlaylistItem",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddMyPlaylistItemRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AddMyPlaylistItemResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "把一首歌加入歌单:元数据由服务端查询,链接曲目要先准备好;已经在歌单里时不变;一个歌单最多 1000 首"
}
},
"/api/me/playlists/{id}/removals": {
"post": {
"operationId": "removeMyPlaylistItem",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RemoveMyPlaylistItemRequest"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "把一首歌移出歌单;不在歌单里也算成功"
}
},
"/api/members": {
"post": {
"operationId": "createMember",
+67 -1
View File
@@ -4,7 +4,7 @@ package protocol
import "encoding/json"
const Version = "000a0b37ea0f0adc"
const Version = "7380d77bc52d7f95"
type APIKey struct {
Key string `json:"key"`
@@ -26,6 +26,14 @@ const (
ActivityKindFailed ActivityKind = "failed"
)
type AddMyPlaylistItemRequest struct {
Track Track `json:"track"`
}
type AddMyPlaylistItemResponse struct {
Item MyPlaylistItem `json:"item"`
}
type AdminAccount struct {
Member Member `json:"member"`
Username string `json:"username"`
@@ -163,6 +171,10 @@ type CreateMemberResponse struct {
MemberToken string `json:"memberToken"`
}
type CreateMyPlaylistRequest struct {
Name string `json:"name"`
}
type CreateRoomRequest struct {
Description string `json:"description"`
Name string `json:"name"`
@@ -245,6 +257,30 @@ type HelloResponse struct {
ServerTimeMs int64 `json:"serverTimeMs"`
}
type ListMyPlaylistItemsResponse struct {
Items []MyPlaylistItem `json:"items"`
}
func (v ListMyPlaylistItemsResponse) MarshalJSON() ([]byte, error) {
type plain ListMyPlaylistItemsResponse
if v.Items == nil {
v.Items = []MyPlaylistItem{}
}
return json.Marshal(plain(v))
}
type ListMyPlaylistsResponse struct {
Items []MyPlaylist `json:"items"`
}
func (v ListMyPlaylistsResponse) MarshalJSON() ([]byte, error) {
type plain ListMyPlaylistsResponse
if v.Items == nil {
v.Items = []MyPlaylist{}
}
return json.Marshal(plain(v))
}
type ListRoomsResponse struct {
Items []RoomListing `json:"items"`
}
@@ -289,6 +325,21 @@ func (v Members) MarshalJSON() ([]byte, error) {
return json.Marshal(plain(v))
}
type MyPlaylist struct {
CoverURL *string `json:"coverUrl"`
CreatedAtMs int64 `json:"createdAtMs"`
Favorites bool `json:"favorites"`
ID string `json:"id"`
Name string `json:"name"`
TrackCount int64 `json:"trackCount"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
type MyPlaylistItem struct {
Meta TrackMeta `json:"meta"`
Track Track `json:"track"`
}
type NowPlaying struct {
DurationMs int64 `json:"durationMs"`
Entry Entry `json:"entry"`
@@ -445,6 +496,14 @@ type RegisterResponse struct {
Account Account `json:"account"`
}
type RemoveMyPlaylistItemRequest struct {
Track Track `json:"track"`
}
type RenameMyPlaylistRequest struct {
Name string `json:"name"`
}
type RoomActivityEvent struct {
Entry *Entry `json:"entry,omitempty"`
Kind ActivityKind `json:"kind"`
@@ -661,6 +720,13 @@ var (
RouteChangePassword = Route{Pattern: "PUT /api/account/password", Auth: AuthMember, Status: 204}
RouteLogin = Route{Pattern: "POST /api/sessions", Auth: AuthMember, Status: 201}
RouteLogout = Route{Pattern: "DELETE /api/sessions/current", Auth: AuthMember, Status: 204}
RouteListMyPlaylists = Route{Pattern: "GET /api/me/playlists", Auth: AuthMember, Status: 200}
RouteCreateMyPlaylist = Route{Pattern: "POST /api/me/playlists", Auth: AuthMember, Status: 201}
RouteRenameMyPlaylist = Route{Pattern: "PATCH /api/me/playlists/{id}", Auth: AuthMember, Status: 204}
RouteDeleteMyPlaylist = Route{Pattern: "DELETE /api/me/playlists/{id}", Auth: AuthMember, Status: 204}
RouteListMyPlaylistItems = Route{Pattern: "GET /api/me/playlists/{id}/items", Auth: AuthMember, Status: 200}
RouteAddMyPlaylistItem = Route{Pattern: "POST /api/me/playlists/{id}/items", Auth: AuthMember, Status: 201}
RouteRemoveMyPlaylistItem = Route{Pattern: "POST /api/me/playlists/{id}/removals", Auth: AuthMember, Status: 204}
RouteListRooms = Route{Pattern: "GET /api/rooms", Auth: AuthNone, Status: 200}
RouteGetRoom = Route{Pattern: "GET /api/rooms/{ref}", Auth: AuthNone, Status: 200}
RouteCreateRoom = Route{Pattern: "POST /api/rooms", Auth: AuthNone, Status: 201, Location: true}