feat: expose playlist sharing over HTTP

This commit is contained in:
DiheChen committed 2026-11-11 00:08:57 +08:00
1 parent eed25578c7
commit 8b941764b1
13 files changed
+743 -19

No files matched your search

+32
View File
@@ -195,6 +195,14 @@ func DecodeEnableListenRecordingRequest(body []byte) (EnableListenRecordingReque
return decodeEnableListenRecordingRequest(body, "")
}
func DecodeShareMyPlaylistRequest(body []byte) (ShareMyPlaylistRequest, error) {
return decodeShareMyPlaylistRequest(body, "")
}
func DecodeCopySharedPlaylistRequest(body []byte) (CopySharedPlaylistRequest, error) {
return decodeCopySharedPlaylistRequest(body, "")
}
func decodeAddMyPlaylistItemRequest(raw json.RawMessage, path string) (AddMyPlaylistItemRequest, error) {
var v AddMyPlaylistItemRequest
m, err := decodeObject(raw, path, "track")
@@ -504,6 +512,24 @@ func decodeCopyMyPlaylistItemsRequest(raw json.RawMessage, path string) (CopyMyP
return v, nil
}
func decodeCopySharedPlaylistRequest(raw json.RawMessage, path string) (CopySharedPlaylistRequest, error) {
var v CopySharedPlaylistRequest
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 decodeCreateMemberRequest(raw json.RawMessage, path string) (CreateMemberRequest, error) {
var v CreateMemberRequest
_, err := decodeObject(raw, path)
@@ -1152,6 +1178,12 @@ func decodeSettingsSetPlayModeRequest(raw json.RawMessage, path string) (Setting
return v, nil
}
func decodeShareMyPlaylistRequest(raw json.RawMessage, path string) (ShareMyPlaylistRequest, error) {
var v ShareMyPlaylistRequest
_, err := decodeObject(raw, path)
return v, err
}
func decodeSource(raw json.RawMessage, path string) (Source, error) {
s, err := decodeString(raw, path)
if err != nil {
+64 -2
View File
@@ -186,9 +186,10 @@
"trackCount": {"type": "integer"},
"createdAtMs": {"type": "integer"},
"updatedAtMs": {"type": "integer"},
"coverUrl": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "歌单里排在最前、有封面的那首歌的封面;没有时为 null"}
"coverUrl": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "歌单里排在最前、有封面的那首歌的封面;没有时为 null"},
"shareId": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "分享链接 /s/<shareId> 中的 ID;没有分享时为 null"}
},
"required": ["id", "name", "favorites", "trackCount", "createdAtMs", "updatedAtMs", "coverUrl"]
"required": ["id", "name", "favorites", "trackCount", "createdAtMs", "updatedAtMs", "coverUrl", "shareId"]
},
"MyPlaylistItem": {
"type": "object",
@@ -362,6 +363,18 @@
"heardAtMs": {"type": "integer", "description": "最近一次听到这首歌时它的开播时间"}
},
"required": ["track", "meta", "heardAtMs"]
},
"SharedPlaylist": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {"type": "string"},
"favorites": {"type": "boolean", "description": "是否是“我喜欢”"},
"ownerName": {"type": "string", "description": "所有者的昵称"},
"ownerAvatarHash": {"type": "string", "description": "所有者邮箱的 MD5,未设置邮箱时为空字符串"},
"items": {"type": "array", "items": {"$ref": "#/$defs/MyPlaylistItem"}, "description": "新加入的在前"}
},
"required": ["name", "favorites", "ownerName", "ownerAvatarHash", "items"]
}
},
"x-ws": {
@@ -1624,6 +1637,55 @@
"path": "/api/me/listen-recording",
"auth": "member",
"status": 204
},
{
"name": "shareMyPlaylist",
"description": "开启分享或重新生成分享链接:每次都生成新的 shareId,之前的链接随即失效;“我喜欢”也能分享",
"method": "POST",
"path": "/api/me/playlists/{id}/share",
"auth": "member",
"request": {"type": "object", "additionalProperties": false, "properties": {}},
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"shareId": {"type": "string"}
},
"required": ["shareId"]
}
},
{
"name": "unshareMyPlaylist",
"description": "停止分享:分享链接随即失效;没有分享时也算成功",
"method": "DELETE",
"path": "/api/me/playlists/{id}/share",
"auth": "member",
"status": 204
},
{
"name": "getSharedPlaylist",
"description": "读取分享的歌单,不需要身份;不返回所有者的成员 ID 和用户名;停止分享、重新生成链接或删除歌单之后返回 NOT_FOUND",
"method": "GET",
"path": "/api/shared-playlists/{shareId}",
"auth": "none",
"response": {"$ref": "#/$defs/SharedPlaylist"}
},
{
"name": "copySharedPlaylist",
"description": "把分享的歌单复制成自己的新歌单,带上全部曲目和顺序;新歌单受“最多 50 个”的限制;分享已失效时返回 NOT_FOUND",
"method": "POST",
"path": "/api/shared-playlists/{shareId}/copies",
"auth": "member",
"status": 201,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {"$ref": "#/$defs/PlaylistName"}
},
"required": ["name"]
},
"response": {"$ref": "#/$defs/MyPlaylist"}
}
]
}
+258 -2
View File
@@ -475,6 +475,20 @@
],
"type": "object"
},
"CopySharedPlaylistRequest": {
"additionalProperties": false,
"properties": {
"name": {
"description": "歌单名:去掉首尾空白后 1 到 50 个字符",
"maxLength": 200,
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"CreateMemberRequest": {
"additionalProperties": false,
"type": "object"
@@ -1087,6 +1101,17 @@
"name": {
"type": "string"
},
"shareId": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "分享链接 /s/<shareId> 中的 ID;没有分享时为 null"
},
"trackCount": {
"type": "integer"
},
@@ -1101,7 +1126,8 @@
"trackCount",
"createdAtMs",
"updatedAtMs",
"coverUrl"
"coverUrl",
"shareId"
],
"type": "object"
},
@@ -1700,6 +1726,57 @@
],
"type": "object"
},
"ShareMyPlaylistRequest": {
"additionalProperties": false,
"type": "object"
},
"ShareMyPlaylistResponse": {
"additionalProperties": false,
"properties": {
"shareId": {
"type": "string"
}
},
"required": [
"shareId"
],
"type": "object"
},
"SharedPlaylist": {
"additionalProperties": false,
"properties": {
"favorites": {
"description": "是否是“我喜欢”",
"type": "boolean"
},
"items": {
"description": "新加入的在前",
"items": {
"$ref": "#/components/schemas/MyPlaylistItem"
},
"type": "array"
},
"name": {
"type": "string"
},
"ownerAvatarHash": {
"description": "所有者邮箱的 MD5,未设置邮箱时为空字符串",
"type": "string"
},
"ownerName": {
"description": "所有者的昵称",
"type": "string"
}
},
"required": [
"name",
"favorites",
"ownerName",
"ownerAvatarHash",
"items"
],
"type": "object"
},
"Source": {
"enum": [
"netease",
@@ -1956,7 +2033,7 @@
},
"info": {
"title": "Alisten",
"version": "ae9a632d648f3840"
"version": "cbfa923a9cbe9f0a"
},
"openapi": "3.1.0",
"paths": {
@@ -3811,6 +3888,93 @@
"summary": "把若干首歌移出歌单,一次 1 到 1000 首;不在歌单里的忽略"
}
},
"/api/me/playlists/{id}/share": {
"delete": {
"operationId": "unshareMyPlaylist",
"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": "停止分享:分享链接随即失效;没有分享时也算成功"
},
"post": {
"operationId": "shareMyPlaylist",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShareMyPlaylistRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShareMyPlaylistResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "开启分享或重新生成分享链接:每次都生成新的 shareId,之前的链接随即失效;“我喜欢”也能分享"
}
},
"/api/members": {
"post": {
"operationId": "createMember",
@@ -4608,6 +4772,98 @@
"summary": "退出登录:发出请求的成员 token 随即失效"
}
},
"/api/shared-playlists/{shareId}": {
"get": {
"operationId": "getSharedPlaylist",
"parameters": [
{
"in": "path",
"name": "shareId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedPlaylist"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [],
"summary": "读取分享的歌单,不需要身份;不返回所有者的成员 ID 和用户名;停止分享、重新生成链接或删除歌单之后返回 NOT_FOUND"
}
},
"/api/shared-playlists/{shareId}/copies": {
"post": {
"operationId": "copySharedPlaylist",
"parameters": [
{
"in": "path",
"name": "shareId",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CopySharedPlaylistRequest"
}
}
},
"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 个”的限制;分享已失效时返回 NOT_FOUND"
}
},
"/api/site": {
"get": {
"operationId": "getSite",
+32 -1
View File
@@ -4,7 +4,7 @@ package protocol
import "encoding/json"
const Version = "ae9a632d648f3840"
const Version = "cbfa923a9cbe9f0a"
type APIKey struct {
Key string `json:"key"`
@@ -181,6 +181,10 @@ type CopyMyPlaylistItemsResponse struct {
Added int64 `json:"added"`
}
type CopySharedPlaylistRequest struct {
Name string `json:"name"`
}
type CreateMemberRequest struct{}
type CreateMemberResponse struct {
@@ -437,6 +441,7 @@ type MyPlaylist struct {
Favorites bool `json:"favorites"`
ID string `json:"id"`
Name string `json:"name"`
ShareID *string `json:"shareId"`
TrackCount int64 `json:"trackCount"`
UpdatedAtMs int64 `json:"updatedAtMs"`
}
@@ -742,6 +747,28 @@ type SettingsUpdatedEvent struct {
PlayMode PlayMode `json:"playMode"`
}
type ShareMyPlaylistRequest struct{}
type ShareMyPlaylistResponse struct {
ShareID string `json:"shareId"`
}
type SharedPlaylist struct {
Favorites bool `json:"favorites"`
Items []MyPlaylistItem `json:"items"`
Name string `json:"name"`
OwnerAvatarHash string `json:"ownerAvatarHash"`
OwnerName string `json:"ownerName"`
}
func (v SharedPlaylist) MarshalJSON() ([]byte, error) {
type plain SharedPlaylist
if v.Items == nil {
v.Items = []MyPlaylistItem{}
}
return json.Marshal(plain(v))
}
type Source string
const (
@@ -895,6 +922,10 @@ var (
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}
RouteShareMyPlaylist = Route{Pattern: "POST /api/me/playlists/{id}/share", Auth: AuthMember, Status: 200}
RouteUnshareMyPlaylist = Route{Pattern: "DELETE /api/me/playlists/{id}/share", Auth: AuthMember, Status: 204}
RouteGetSharedPlaylist = Route{Pattern: "GET /api/shared-playlists/{shareId}", Auth: AuthNone, Status: 200}
RouteCopySharedPlaylist = Route{Pattern: "POST /api/shared-playlists/{shareId}/copies", Auth: AuthMember, Status: 201}
)
var RoomRoutes = []RoomRoute{