feat: expose playlist imports over HTTP
This commit is contained in:
1 parent
2bf241ae3c
commit
8966954489
11 files changed
+866
-6
No files matched your search
@@ -36,7 +36,9 @@ const (
|
||||
maxBodyBytes = 64 << 10
|
||||
// 歌单的批量接口一次最多 1000 首,链接曲目的 ID 最长 2048 字节,每首再留 256 字节给来源、字段名和 JSON 的标点。
|
||||
maxBatchBodyBytes = 1000 * (2048 + 256)
|
||||
requestDeadline = 3 * time.Minute
|
||||
// 导入文件一次最多 1000 首,每首除了曲目还带标题、歌手、专辑和两个地址。nginx 中 /api/me/playlists/imports 的上限要与它保持一致。
|
||||
maxImportBodyBytes = 1 << 20
|
||||
requestDeadline = 3 * time.Minute
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
@@ -120,6 +122,9 @@ func New(s Services, metrics prometheus.Registerer, tracer trace.TracerProvider,
|
||||
srv.handle(mux, protocol.RouteListMyTrackPlaylists, srv.listMyTrackPlaylists)
|
||||
srv.handleLimit(mux, protocol.RouteCopyMyPlaylistItems, maxBatchBodyBytes, srv.copyMyPlaylistItems)
|
||||
srv.handleLimit(mux, protocol.RouteMoveMyPlaylistItems, maxBatchBodyBytes, srv.moveMyPlaylistItems)
|
||||
srv.handleLimit(mux, protocol.RouteImportMyPlaylist, maxImportBodyBytes, srv.importMyPlaylist)
|
||||
srv.handle(mux, protocol.RouteResolveMyPlaylistLink, srv.resolveMyPlaylistLink)
|
||||
srv.handle(mux, protocol.RouteImportMyPlatformPlaylist, srv.importMyPlatformPlaylist)
|
||||
srv.handle(mux, protocol.RouteListRooms, srv.listRooms)
|
||||
srv.handle(mux, protocol.RouteGetRoom, srv.getRoom)
|
||||
srv.handle(mux, protocol.RouteCreateRoom, srv.createRoom)
|
||||
@@ -326,6 +331,9 @@ var knownErrors = []struct {
|
||||
{playlist.ErrPlaylistFull, protocol.ErrorCodeConflict},
|
||||
{playlist.ErrTrackNotReady, protocol.ErrorCodeConflict},
|
||||
{playlist.ErrTooFast, protocol.ErrorCodeRateLimited},
|
||||
{playlist.ErrInvalidPlaylistLink, protocol.ErrorCodeInvalidArgument},
|
||||
{playlist.ErrUnsupportedPlaylist, protocol.ErrorCodeInvalidArgument},
|
||||
{playlist.ErrShortLinkFailed, protocol.ErrorCodeProviderError},
|
||||
{playlistorder.ErrBeforeMoved, protocol.ErrorCodeInvalidArgument},
|
||||
{playlistorder.ErrBeforeMissing, protocol.ErrorCodeConflict},
|
||||
{realtime.ErrRequesterNotAllowed, protocol.ErrorCodeInvalidArgument},
|
||||
|
||||
@@ -2,7 +2,9 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/playlist"
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
"github.com/dihemoe/alisten/internal/domain/track"
|
||||
"github.com/dihemoe/alisten/protocol"
|
||||
@@ -144,3 +146,51 @@ func (s *server) moveMyPlaylistItems(ctx context.Context, c call) (any, error) {
|
||||
}
|
||||
return nil, s.Playlists.Move(ctx, c.member.ID, c.r.PathValue("id"), tracksOf(req.Tracks), before)
|
||||
}
|
||||
|
||||
func (s *server) importMyPlaylist(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeImportMyPlaylistRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracks := make([]playlist.ImportedTrack, len(req.Tracks))
|
||||
for i, t := range req.Tracks {
|
||||
tracks[i] = playlist.ImportedTrack{
|
||||
Track: trackOf(t.Track),
|
||||
Title: t.Title,
|
||||
Artist: deref(t.Artist),
|
||||
Album: deref(t.Album),
|
||||
CoverURL: deref(t.CoverURL),
|
||||
PageURL: deref(t.PageURL),
|
||||
Duration: time.Duration(t.DurationMs) * time.Millisecond,
|
||||
}
|
||||
}
|
||||
p, n, err := s.Playlists.ImportFile(ctx, c.member.ID, req.Name, tracks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.ImportMyPlaylistResponse{Playlist: s.playlistDTO(p), Imported: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *server) resolveMyPlaylistLink(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeResolveMyPlaylistLinkRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
source, id, err := s.Playlists.ResolveLink(ctx, c.member.ID, req.Link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.ResolveMyPlaylistLinkResponse{Source: protocol.Source(source), PlaylistID: id}, nil
|
||||
}
|
||||
|
||||
func (s *server) importMyPlatformPlaylist(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeImportMyPlatformPlaylistRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, n, err := s.Playlists.ImportPlatform(ctx, c.member.ID, req.Name, track.Source(req.Source), req.PlaylistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.ImportMyPlatformPlaylistResponse{Playlist: s.playlistDTO(p), Imported: int64(n)}, nil
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/dihemoe/alisten/internal/adapter/out/postgres"
|
||||
"github.com/dihemoe/alisten/internal/adapter/out/queue"
|
||||
"github.com/dihemoe/alisten/internal/adapter/out/redis"
|
||||
"github.com/dihemoe/alisten/internal/adapter/out/shortlink"
|
||||
"github.com/dihemoe/alisten/internal/adapter/out/storage"
|
||||
"github.com/dihemoe/alisten/internal/app/admin"
|
||||
"github.com/dihemoe/alisten/internal/app/catalog"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"github.com/dihemoe/alisten/internal/app/prepare"
|
||||
"github.com/dihemoe/alisten/internal/app/realtime"
|
||||
"github.com/dihemoe/alisten/internal/app/site"
|
||||
playlistlink "github.com/dihemoe/alisten/internal/domain/playlist"
|
||||
"github.com/dihemoe/alisten/internal/domain/track"
|
||||
)
|
||||
|
||||
@@ -80,7 +82,7 @@ func newPublic(cfg serveConfig, pool *pgxpool.Pool, rdb *goredis.Client, instanc
|
||||
services := api.Services{
|
||||
Identity: identity.New(store, store, store, gateway),
|
||||
Accounts: identity.NewAccounts(store, store, redis.NewAccountQuota(rdb), gateway, base.With("component", "accounts")),
|
||||
Playlists: playlist.New(store, store, store, music, redis.NewAccountQuota(rdb), nil),
|
||||
Playlists: playlist.New(store, store, store, music, redis.NewAccountQuota(rdb), shortlink.New(playlistlink.AllowedRedirectHost, playlistlink.Recognized)),
|
||||
Lobby: rooms,
|
||||
Admin: admin.New(cfg.adminToken, rooms, store, store, store, src.neteaseLogin, src.qqLogin, src.bilibiliLogin, store, gateway),
|
||||
Gateway: gateway,
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
歌单导入:只有已注册的账号能导入。导入文件时,曲目表里已有的歌用服务端的元数据,没有的用文件里的元数据(不是平台自己的 https 地址的封面和页面链接被丢弃,链接曲目没有封面);重复的歌只留第一次出现的位置,标题为空的跳过,来源中的第一首排在歌单最前。链接可以识别出平台歌单,目前只能导入网易云的歌单。
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$alice", "member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/imports
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"name": "游客", "tracks": []}
|
||||
-- response --
|
||||
403
|
||||
{"error": {"code": "FORBIDDEN", "message": "注册账号之后才能使用歌单"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-links
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"link": "https://music.163.com/#/playlist?id=3778678"}
|
||||
-- response --
|
||||
403
|
||||
{"error": {"code": "FORBIDDEN", "message": "注册账号之后才能使用歌单"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/platform-imports
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"name": "游客", "source": "netease", "playlistId": "3778678"}
|
||||
-- response --
|
||||
403
|
||||
{"error": {"code": "FORBIDDEN", "message": "注册账号之后才能使用歌单"}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"username": "alice", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"account": {"username": "alice"}}
|
||||
|
||||
-- request --
|
||||
POST /api/rooms
|
||||
|
||||
{"name": "临时", "description": ""}
|
||||
-- response --
|
||||
201
|
||||
Location: /api/rooms/$room
|
||||
{"id": "$room"}
|
||||
|
||||
-- request --
|
||||
POST /api/rooms/$room/tickets
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"ticket": "$ticket"}
|
||||
|
||||
-- stream alice --
|
||||
Authorization: Bearer $ticket
|
||||
-- event alice --
|
||||
event: snapshot
|
||||
data: {"room": {"term": 1, "seq": 0, "room": {"id": "$room", "name": "临时", "description": "", "kind": "ephemeral"},
|
||||
"playMode": "sequential", "nowPlaying": null, "pending": null, "items": [],
|
||||
"members": {"sessions": [], "onlineCount": 0}}}
|
||||
|
||||
-- request --
|
||||
POST /api/room/queue
|
||||
Authorization: Bearer $ticket
|
||||
|
||||
{"track": {"source": "netease", "id": "186016"}}
|
||||
-- response --
|
||||
201
|
||||
Location: /api/room/queue/1
|
||||
{"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"}}
|
||||
-- event alice --
|
||||
event: room.activity
|
||||
id: 1
|
||||
data: {"kind": "event", "type": "room.activity", "term": 1, "seq": 1, "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": "点了《晴天》"}}
|
||||
-- event alice --
|
||||
event: queue.updated
|
||||
id: 2
|
||||
data: {"kind": "event", "type": "queue.updated", "term": 1, "seq": 2, "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"}}}
|
||||
-- event alice --
|
||||
event: queue.updated
|
||||
id: 3
|
||||
data: {"kind": "event", "type": "queue.updated", "term": 1, "seq": 3, "data": {"pending": null, "items": []}}
|
||||
-- event alice --
|
||||
event: playback.updated
|
||||
id: 4
|
||||
data: {"kind": "event", "type": "playback.updated", "term": 1, "seq": 4, "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 --
|
||||
POST /api/me/playlists/imports
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"name": " 文件里的歌单 ", "tracks": [
|
||||
{"track": {"source": "netease", "id": "186016"}, "title": "改过的标题", "artist": "改过的歌手", "durationMs": 1000, "coverUrl": "https://evil.example/c.jpg"},
|
||||
{"track": {"source": "netease", "id": "999"}, "title": "文件里的歌", "artist": "歌手", "album": "专辑", "durationMs": 61000, "coverUrl": "http://p1.music.126.net/a.jpg", "pageUrl": "https://evil.example/song"},
|
||||
{"track": {"source": "link", "id": "https://example.com/song.mp3"}, "title": "链接曲目", "durationMs": 5000, "coverUrl": "https://p1.music.126.net/b.jpg"},
|
||||
{"track": {"source": "netease", "id": "186016"}, "title": "重复", "durationMs": 1},
|
||||
{"track": {"source": "netease", "id": "1000"}, "title": " ", "durationMs": 1}
|
||||
]}
|
||||
-- response --
|
||||
201
|
||||
{"playlist": {"id": "$imported", "name": "文件里的歌单", "favorites": false, "trackCount": 3, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg"}, "imported": 3}
|
||||
|
||||
-- request --
|
||||
GET /api/me/playlists/$imported/items
|
||||
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}},
|
||||
{"track": {"source": "netease", "id": "999"}, "meta": {"title": "文件里的歌", "artist": "歌手", "album": "专辑", "coverUrl": "", "pageUrl": "https://music.163.com/#/song?id=999", "durationMs": 61000}},
|
||||
{"track": {"source": "link", "id": "https://example.com/song.mp3"}, "meta": {"title": "链接曲目", "artist": "", "album": "", "coverUrl": "", "pageUrl": "https://example.com/song.mp3", "durationMs": 5000}}
|
||||
]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/imports
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"name": " ", "tracks": []}
|
||||
-- response --
|
||||
201
|
||||
{"playlist": {"id": "$empty", "name": "导入的歌单", "favorites": false, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null}, "imported": 0}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-links
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"link": "https://music.163.com/#/playlist?id=3778678"}
|
||||
-- response --
|
||||
200
|
||||
{"source": "netease", "playlistId": "3778678"}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-links
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"link": "https://example.com/playlist?id=1"}
|
||||
-- response --
|
||||
400
|
||||
{"error": {"code": "INVALID_ARGUMENT", "message": "不是网易云或 QQ 音乐的歌单链接"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/platform-imports
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"name": "周杰伦精选", "source": "netease", "playlistId": "3778678"}
|
||||
-- response --
|
||||
201
|
||||
{"playlist": {"id": "$platform", "name": "周杰伦精选", "favorites": false, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg"}, "imported": 2}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/platform-imports
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"name": "QQ", "source": "qq", "playlistId": "1"}
|
||||
-- response --
|
||||
400
|
||||
{"error": {"code": "INVALID_ARGUMENT", "message": "暂时只能导入网易云的歌单"}}
|
||||
@@ -114,6 +114,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, "")
|
||||
}
|
||||
@@ -578,6 +590,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")
|
||||
@@ -859,6 +992,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)
|
||||
|
||||
@@ -194,6 +194,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,
|
||||
@@ -1002,6 +1016,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
@@ -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": {
|
||||
@@ -1290,6 +1406,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": {
|
||||
@@ -1741,7 +1887,7 @@
|
||||
},
|
||||
"info": {
|
||||
"title": "Alisten",
|
||||
"version": "5b9cd2cb39780e46"
|
||||
"version": "42bbfea8c8da3f68"
|
||||
},
|
||||
"openapi": "3.1.0",
|
||||
"paths": {
|
||||
@@ -2878,6 +3024,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",
|
||||
@@ -2995,6 +3184,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
@@ -4,7 +4,7 @@ package protocol
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const Version = "5b9cd2cb39780e46"
|
||||
const Version = "42bbfea8c8da3f68"
|
||||
|
||||
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"`
|
||||
}
|
||||
@@ -556,6 +595,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"`
|
||||
@@ -782,6 +830,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}
|
||||
|
||||
@@ -19,6 +19,14 @@ location ^~ /api/ {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://$serve;
|
||||
}
|
||||
|
||||
# 取值必须不小于 serve 对导入文件接口的请求体上限 maxImportBodyBytes(1 MiB),原因同上。
|
||||
location = /api/me/playlists/imports {
|
||||
client_max_body_size 1m;
|
||||
client_body_buffer_size 1m;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://$serve;
|
||||
}
|
||||
}
|
||||
location ^~ /media/ {
|
||||
proxy_pass http://$serve;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
import type { APIKey, Account, AdminAccount, ErrorInfo, Member, MyPlaylist, MyPlaylistItem, QQLoginType, QRLogin, QRLoginResult, RoomListing, SourceAccount, StoredMedia, Track } from '../realtime/protocol.gen'
|
||||
import type { APIKey, Account, AdminAccount, ErrorInfo, ImportedTrack, Member, MyPlaylist, MyPlaylistItem, QQLoginType, QRLogin, QRLoginResult, RoomListing, Source, SourceAccount, StoredMedia, Track } from '../realtime/protocol.gen'
|
||||
|
||||
export interface AddMyPlaylistItemRequest {
|
||||
track: Track
|
||||
@@ -157,6 +157,27 @@ export interface GetSiteResponse {
|
||||
registrationOpen: boolean
|
||||
}
|
||||
|
||||
export interface ImportMyPlatformPlaylistRequest {
|
||||
name: string
|
||||
playlistId: string
|
||||
source: Source
|
||||
}
|
||||
|
||||
export interface ImportMyPlatformPlaylistResponse {
|
||||
imported: number
|
||||
playlist: MyPlaylist
|
||||
}
|
||||
|
||||
export interface ImportMyPlaylistRequest {
|
||||
name: string
|
||||
tracks: ImportedTrack[]
|
||||
}
|
||||
|
||||
export interface ImportMyPlaylistResponse {
|
||||
imported: number
|
||||
playlist: MyPlaylist
|
||||
}
|
||||
|
||||
export interface ListMyPlaylistItemsResponse {
|
||||
items: MyPlaylistItem[]
|
||||
}
|
||||
@@ -210,6 +231,15 @@ export interface RenameMyPlaylistRequest {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ResolveMyPlaylistLinkRequest {
|
||||
link: string
|
||||
}
|
||||
|
||||
export interface ResolveMyPlaylistLinkResponse {
|
||||
playlistId: string
|
||||
source: Source
|
||||
}
|
||||
|
||||
export interface UpdateMeRequest {
|
||||
email?: string
|
||||
name?: string
|
||||
@@ -253,6 +283,9 @@ export function httpClient(send: HttpSend) {
|
||||
copyMyPlaylistItems: (id: string, body: CopyMyPlaylistItemsRequest) => send<CopyMyPlaylistItemsResponse>({ method: 'POST', path: `/api/me/playlists/${encodeURIComponent(id)}/copies`, auth: 'member', body }),
|
||||
moveMyPlaylistItems: (id: string, body: MoveMyPlaylistItemsRequest) => send<void>({ method: 'POST', path: `/api/me/playlists/${encodeURIComponent(id)}/moves`, auth: 'member', body }),
|
||||
listMyTrackPlaylists: (body: ListMyTrackPlaylistsRequest) => send<ListMyTrackPlaylistsResponse>({ method: 'POST', path: '/api/me/playlist-memberships', auth: 'member', body }),
|
||||
importMyPlaylist: (body: ImportMyPlaylistRequest) => send<ImportMyPlaylistResponse>({ method: 'POST', path: '/api/me/playlists/imports', auth: 'member', body }),
|
||||
resolveMyPlaylistLink: (body: ResolveMyPlaylistLinkRequest) => send<ResolveMyPlaylistLinkResponse>({ method: 'POST', path: '/api/me/playlist-links', auth: 'member', body }),
|
||||
importMyPlatformPlaylist: (body: ImportMyPlatformPlaylistRequest) => send<ImportMyPlatformPlaylistResponse>({ method: 'POST', path: '/api/me/playlists/platform-imports', auth: 'member', body }),
|
||||
listRooms: () => send<ListRoomsResponse>({ method: 'GET', path: '/api/rooms', auth: 'none' }),
|
||||
getRoom: (ref: string) => send<RoomListing>({ method: 'GET', path: `/api/rooms/${encodeURIComponent(ref)}`, auth: 'none' }),
|
||||
createRoom: (body: CreateRoomRequest) => send<CreateRoomResponse>({ method: 'POST', path: '/api/rooms', auth: 'none', body }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
export const PROTOCOL_VERSION = '5b9cd2cb39780e46'
|
||||
export const PROTOCOL_VERSION = '42bbfea8c8da3f68'
|
||||
|
||||
export interface APIKey {
|
||||
key: string
|
||||
@@ -61,6 +61,16 @@ export interface HelloResponse {
|
||||
serverTimeMs: number
|
||||
}
|
||||
|
||||
export interface ImportedTrack {
|
||||
album?: string
|
||||
artist?: string
|
||||
coverUrl?: string
|
||||
durationMs: number
|
||||
pageUrl?: string
|
||||
title: string
|
||||
track: Track
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
avatarHash: string
|
||||
id: string
|
||||
|
||||
Reference in new issue
Block a user