feat: expose playlist imports over HTTP

This commit is contained in:
DiheChen committed 2026-11-11 00:01:47 +08:00
1 parent 9256f64e24
commit 649916ab9d
11 files changed
+866 -6

No files matched your search

+154
View File
@@ -115,6 +115,18 @@ func DecodeListMyTrackPlaylistsRequest(body []byte) (ListMyTrackPlaylistsRequest
return decodeListMyTrackPlaylistsRequest(body, "")
}
func DecodeImportMyPlaylistRequest(body []byte) (ImportMyPlaylistRequest, error) {
return decodeImportMyPlaylistRequest(body, "")
}
func DecodeResolveMyPlaylistLinkRequest(body []byte) (ResolveMyPlaylistLinkRequest, error) {
return decodeResolveMyPlaylistLinkRequest(body, "")
}
func DecodeImportMyPlatformPlaylistRequest(body []byte) (ImportMyPlatformPlaylistRequest, error) {
return decodeImportMyPlatformPlaylistRequest(body, "")
}
func DecodeCreateRoomRequest(body []byte) (CreateRoomRequest, error) {
return decodeCreateRoomRequest(body, "")
}
@@ -579,6 +591,127 @@ func decodeHelloRequest(raw json.RawMessage, path string) (HelloRequest, error)
return v, nil
}
func decodeImportMyPlatformPlaylistRequest(raw json.RawMessage, path string) (ImportMyPlatformPlaylistRequest, error) {
var v ImportMyPlatformPlaylistRequest
m, err := decodeObject(raw, path, "name", "playlistId", "source")
if err != nil {
return v, err
}
if err = requireFields(m, path, "name", "playlistId", "source"); 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
}
if v.PlaylistID, err = decodeString(m["playlistId"], field(path, "playlistId")); err != nil {
return v, err
}
if err = checkMinLength(v.PlaylistID, field(path, "playlistId"), 1); err != nil {
return v, err
}
if err = checkMaxLength(v.PlaylistID, field(path, "playlistId"), 100); err != nil {
return v, err
}
if v.Source, err = decodeSource(m["source"], field(path, "source")); err != nil {
return v, err
}
return v, nil
}
func decodeImportMyPlaylistRequest(raw json.RawMessage, path string) (ImportMyPlaylistRequest, error) {
var v ImportMyPlaylistRequest
m, err := decodeObject(raw, path, "name", "tracks")
if err != nil {
return v, err
}
if err = requireFields(m, path, "name", "tracks"); 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"), 500); err != nil {
return v, err
}
if v.Tracks, err = decodeArray(decodeImportedTrack, 1000)(m["tracks"], field(path, "tracks")); err != nil {
return v, err
}
return v, nil
}
func decodeImportedTrack(raw json.RawMessage, path string) (ImportedTrack, error) {
var v ImportedTrack
m, err := decodeObject(raw, path, "album", "artist", "coverUrl", "durationMs", "pageUrl", "title", "track")
if err != nil {
return v, err
}
if err = requireFields(m, path, "durationMs", "title", "track"); err != nil {
return v, err
}
if r, ok := m["album"]; ok {
x, err := decodeString(r, field(path, "album"))
if err != nil {
return v, err
}
if err = checkMaxLength(x, field(path, "album"), 500); err != nil {
return v, err
}
v.Album = &x
}
if r, ok := m["artist"]; ok {
x, err := decodeString(r, field(path, "artist"))
if err != nil {
return v, err
}
if err = checkMaxLength(x, field(path, "artist"), 500); err != nil {
return v, err
}
v.Artist = &x
}
if r, ok := m["coverUrl"]; ok {
x, err := decodeString(r, field(path, "coverUrl"))
if err != nil {
return v, err
}
if err = checkMaxLength(x, field(path, "coverUrl"), 2048); err != nil {
return v, err
}
v.CoverURL = &x
}
if v.DurationMs, err = decodeInteger(m["durationMs"], field(path, "durationMs")); err != nil {
return v, err
}
if err = checkMinimum(v.DurationMs, field(path, "durationMs"), 0); err != nil {
return v, err
}
if err = checkMaximum(v.DurationMs, field(path, "durationMs"), 86400000); err != nil {
return v, err
}
if r, ok := m["pageUrl"]; ok {
x, err := decodeString(r, field(path, "pageUrl"))
if err != nil {
return v, err
}
if err = checkMaxLength(x, field(path, "pageUrl"), 2048); err != nil {
return v, err
}
v.PageURL = &x
}
if v.Title, err = decodeString(m["title"], field(path, "title")); err != nil {
return v, err
}
if err = checkMaxLength(v.Title, field(path, "title"), 500); err != nil {
return v, err
}
if v.Track, err = decodeTrack(m["track"], field(path, "track")); err != nil {
return v, err
}
return v, nil
}
func decodeListMyTrackPlaylistsRequest(raw json.RawMessage, path string) (ListMyTrackPlaylistsRequest, error) {
var v ListMyTrackPlaylistsRequest
m, err := decodeObject(raw, path, "track")
@@ -870,6 +1003,27 @@ func decodeRenameMyPlaylistRequest(raw json.RawMessage, path string) (RenameMyPl
return v, nil
}
func decodeResolveMyPlaylistLinkRequest(raw json.RawMessage, path string) (ResolveMyPlaylistLinkRequest, error) {
var v ResolveMyPlaylistLinkRequest
m, err := decodeObject(raw, path, "link")
if err != nil {
return v, err
}
if err = requireFields(m, path, "link"); err != nil {
return v, err
}
if v.Link, err = decodeString(m["link"], field(path, "link")); err != nil {
return v, err
}
if err = checkMinLength(v.Link, field(path, "link"), 1); err != nil {
return v, err
}
if err = checkMaxLength(v.Link, field(path, "link"), 2048); err != nil {
return v, err
}
return v, nil
}
func decodeRoomSyncRequest(raw json.RawMessage, path string) (RoomSyncRequest, error) {
var v RoomSyncRequest
_, err := decodeObject(raw, path)
+91
View File
@@ -199,6 +199,20 @@
},
"required": ["track", "meta"]
},
"ImportedTrack": {
"type": "object",
"additionalProperties": false,
"properties": {
"track": {"$ref": "#/$defs/Track"},
"title": {"type": "string", "maxLength": 500},
"artist": {"type": "string", "maxLength": 500},
"album": {"type": "string", "maxLength": 500},
"durationMs": {"type": "integer", "minimum": 0, "maximum": 86400000},
"coverUrl": {"type": "string", "maxLength": 2048},
"pageUrl": {"type": "string", "maxLength": 2048}
},
"required": ["track", "title", "durationMs"]
},
"Members": {
"type": "object",
"additionalProperties": false,
@@ -1009,6 +1023,83 @@
"required": ["playlistIds"]
}
},
{
"name": "importMyPlaylist",
"description": "把导出的歌单文件里的歌导入成一个新歌单,歌单名不合法时用“导入的歌单”;只收前 1000 首,重复的只留第一次出现的位置;曲目表里已有的歌用服务端的元数据,没有的用文件里的,标题为空的跳过;新歌单受“最多 50 个”的限制;返回实际导入的数量",
"method": "POST",
"path": "/api/me/playlists/imports",
"auth": "member",
"status": 201,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {"type": "string", "maxLength": 500},
"tracks": {"type": "array", "items": {"$ref": "#/$defs/ImportedTrack"}, "maxItems": 1000}
},
"required": ["name", "tracks"]
},
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"playlist": {"$ref": "#/$defs/MyPlaylist"},
"imported": {"type": "integer"}
},
"required": ["playlist", "imported"]
}
},
{
"name": "resolveMyPlaylistLink",
"description": "识别平台歌单的链接,短链接会跟随跳转;目前只支持网易云",
"method": "POST",
"path": "/api/me/playlist-links",
"auth": "member",
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"link": {"type": "string", "minLength": 1, "maxLength": 2048}
},
"required": ["link"]
},
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"source": {"$ref": "#/$defs/Source"},
"playlistId": {"type": "string"}
},
"required": ["source", "playlistId"]
}
},
{
"name": "importMyPlatformPlaylist",
"description": "把平台歌单导入成一个新歌单,规则同 importMyPlaylist,元数据取自平台;目前只支持网易云",
"method": "POST",
"path": "/api/me/playlists/platform-imports",
"auth": "member",
"status": 201,
"request": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {"$ref": "#/$defs/PlaylistName"},
"source": {"$ref": "#/$defs/Source"},
"playlistId": {"type": "string", "minLength": 1, "maxLength": 100}
},
"required": ["name", "source", "playlistId"]
},
"response": {
"type": "object",
"additionalProperties": false,
"properties": {
"playlist": {"$ref": "#/$defs/MyPlaylist"},
"imported": {"type": "integer"}
},
"required": ["playlist", "imported"]
}
},
{
"name": "listRooms",
"description": "房间列表",
+276 -1
View File
@@ -713,6 +713,122 @@
],
"type": "object"
},
"ImportMyPlatformPlaylistRequest": {
"additionalProperties": false,
"properties": {
"name": {
"description": "歌单名:去掉首尾空白后 1 到 50 个字符",
"maxLength": 200,
"type": "string"
},
"playlistId": {
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"source": {
"$ref": "#/components/schemas/Source"
}
},
"required": [
"name",
"source",
"playlistId"
],
"type": "object"
},
"ImportMyPlatformPlaylistResponse": {
"additionalProperties": false,
"properties": {
"imported": {
"type": "integer"
},
"playlist": {
"$ref": "#/components/schemas/MyPlaylist"
}
},
"required": [
"playlist",
"imported"
],
"type": "object"
},
"ImportMyPlaylistRequest": {
"additionalProperties": false,
"properties": {
"name": {
"maxLength": 500,
"type": "string"
},
"tracks": {
"items": {
"$ref": "#/components/schemas/ImportedTrack"
},
"maxItems": 1000,
"type": "array"
}
},
"required": [
"name",
"tracks"
],
"type": "object"
},
"ImportMyPlaylistResponse": {
"additionalProperties": false,
"properties": {
"imported": {
"type": "integer"
},
"playlist": {
"$ref": "#/components/schemas/MyPlaylist"
}
},
"required": [
"playlist",
"imported"
],
"type": "object"
},
"ImportedTrack": {
"additionalProperties": false,
"properties": {
"album": {
"maxLength": 500,
"type": "string"
},
"artist": {
"maxLength": 500,
"type": "string"
},
"coverUrl": {
"maxLength": 2048,
"type": "string"
},
"durationMs": {
"maximum": 86400000,
"minimum": 0,
"type": "integer"
},
"pageUrl": {
"maxLength": 2048,
"type": "string"
},
"title": {
"maxLength": 500,
"type": "string"
},
"track": {
"$ref": "#/components/schemas/Track"
}
},
"required": [
"track",
"title",
"durationMs"
],
"type": "object"
},
"ListMyPlaylistItemsResponse": {
"additionalProperties": false,
"properties": {
@@ -1301,6 +1417,36 @@
],
"type": "object"
},
"ResolveMyPlaylistLinkRequest": {
"additionalProperties": false,
"properties": {
"link": {
"maxLength": 2048,
"minLength": 1,
"type": "string"
}
},
"required": [
"link"
],
"type": "object"
},
"ResolveMyPlaylistLinkResponse": {
"additionalProperties": false,
"properties": {
"playlistId": {
"type": "string"
},
"source": {
"$ref": "#/components/schemas/Source"
}
},
"required": [
"source",
"playlistId"
],
"type": "object"
},
"RoomInfo": {
"additionalProperties": false,
"properties": {
@@ -1752,7 +1898,7 @@
},
"info": {
"title": "Alisten",
"version": "cdc97c61b16da452"
"version": "ed36759d403f9894"
},
"openapi": "3.1.0",
"paths": {
@@ -2889,6 +3035,49 @@
"summary": "修改昵称和头像;email 用来计算头像哈希,已注册的账号同时保存它,去掉首尾空白后应形如 name@example.com,空字符串表示清除"
}
},
"/api/me/playlist-links": {
"post": {
"operationId": "resolveMyPlaylistLink",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResolveMyPlaylistLinkRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResolveMyPlaylistLinkResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "识别平台歌单的链接,短链接会跟随跳转;目前只支持网易云"
}
},
"/api/me/playlist-memberships": {
"post": {
"operationId": "listMyTrackPlaylists",
@@ -3006,6 +3195,92 @@
"summary": "新建歌单:最多 50 个(含“我喜欢”),可以重名"
}
},
"/api/me/playlists/imports": {
"post": {
"operationId": "importMyPlaylist",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportMyPlaylistRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportMyPlaylistResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "把导出的歌单文件里的歌导入成一个新歌单,歌单名不合法时用“导入的歌单”;只收前 1000 首,重复的只留第一次出现的位置;曲目表里已有的歌用服务端的元数据,没有的用文件里的,标题为空的跳过;新歌单受“最多 50 个”的限制;返回实际导入的数量"
}
},
"/api/me/playlists/platform-imports": {
"post": {
"operationId": "importMyPlatformPlaylist",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportMyPlatformPlaylistRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportMyPlatformPlaylistResponse"
}
}
},
"description": "成功"
},
"default": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "失败"
}
},
"security": [
{
"memberToken": []
}
],
"summary": "把平台歌单导入成一个新歌单,规则同 importMyPlaylist,元数据取自平台;目前只支持网易云"
}
},
"/api/me/playlists/{id}": {
"delete": {
"operationId": "deleteMyPlaylist",
+52 -1
View File
@@ -4,7 +4,7 @@ package protocol
import "encoding/json"
const Version = "cdc97c61b16da452"
const Version = "ed36759d403f9894"
type APIKey struct {
Key string `json:"key"`
@@ -274,6 +274,45 @@ type HelloResponse struct {
ServerTimeMs int64 `json:"serverTimeMs"`
}
type ImportMyPlatformPlaylistRequest struct {
Name string `json:"name"`
PlaylistID string `json:"playlistId"`
Source Source `json:"source"`
}
type ImportMyPlatformPlaylistResponse struct {
Imported int64 `json:"imported"`
Playlist MyPlaylist `json:"playlist"`
}
type ImportMyPlaylistRequest struct {
Name string `json:"name"`
Tracks []ImportedTrack `json:"tracks"`
}
func (v ImportMyPlaylistRequest) MarshalJSON() ([]byte, error) {
type plain ImportMyPlaylistRequest
if v.Tracks == nil {
v.Tracks = []ImportedTrack{}
}
return json.Marshal(plain(v))
}
type ImportMyPlaylistResponse struct {
Imported int64 `json:"imported"`
Playlist MyPlaylist `json:"playlist"`
}
type ImportedTrack struct {
Album *string `json:"album,omitempty"`
Artist *string `json:"artist,omitempty"`
CoverURL *string `json:"coverUrl,omitempty"`
DurationMs int64 `json:"durationMs"`
PageURL *string `json:"pageUrl,omitempty"`
Title string `json:"title"`
Track Track `json:"track"`
}
type ListMyPlaylistItemsResponse struct {
Items []MyPlaylistItem `json:"items"`
}
@@ -558,6 +597,15 @@ type RenameMyPlaylistRequest struct {
Name string `json:"name"`
}
type ResolveMyPlaylistLinkRequest struct {
Link string `json:"link"`
}
type ResolveMyPlaylistLinkResponse struct {
PlaylistID string `json:"playlistId"`
Source Source `json:"source"`
}
type RoomActivityEvent struct {
Entry *Entry `json:"entry,omitempty"`
Kind ActivityKind `json:"kind"`
@@ -784,6 +832,9 @@ var (
RouteCopyMyPlaylistItems = Route{Pattern: "POST /api/me/playlists/{id}/copies", Auth: AuthMember, Status: 200}
RouteMoveMyPlaylistItems = Route{Pattern: "POST /api/me/playlists/{id}/moves", Auth: AuthMember, Status: 204}
RouteListMyTrackPlaylists = Route{Pattern: "POST /api/me/playlist-memberships", Auth: AuthMember, Status: 200}
RouteImportMyPlaylist = Route{Pattern: "POST /api/me/playlists/imports", Auth: AuthMember, Status: 201}
RouteResolveMyPlaylistLink = Route{Pattern: "POST /api/me/playlist-links", Auth: AuthMember, Status: 200}
RouteImportMyPlatformPlaylist = Route{Pattern: "POST /api/me/playlists/platform-imports", Auth: AuthMember, Status: 201}
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}