feat: expose playlist sharing over HTTP
This commit is contained in:
1 parent
eed25578c7
commit
8b941764b1
13 files changed
+743
-19
No files matched your search
@@ -165,6 +165,10 @@ func New(s Services, metrics prometheus.Registerer, tracer trace.TracerProvider,
|
||||
srv.handle(mux, protocol.RouteClearMyListens, srv.clearMyListens)
|
||||
srv.handle(mux, protocol.RouteEnableListenRecording, srv.enableListenRecording)
|
||||
srv.handle(mux, protocol.RouteDisableListenRecording, srv.disableListenRecording)
|
||||
srv.handle(mux, protocol.RouteShareMyPlaylist, srv.shareMyPlaylist)
|
||||
srv.handle(mux, protocol.RouteUnshareMyPlaylist, srv.unshareMyPlaylist)
|
||||
srv.handle(mux, protocol.RouteGetSharedPlaylist, srv.getSharedPlaylist)
|
||||
srv.handle(mux, protocol.RouteCopySharedPlaylist, srv.copySharedPlaylist)
|
||||
mux.HandleFunc("GET /ws", srv.websocket)
|
||||
mux.HandleFunc("GET /api/room/events", srv.roomEvents)
|
||||
mux.HandleFunc("GET /media/{source}/{id}", srv.redirectMedia)
|
||||
@@ -352,6 +356,7 @@ var knownErrors = []struct {
|
||||
{catalog.ErrRateLimited, protocol.ErrorCodeRateLimited},
|
||||
{errClientOutdated, protocol.ErrorCodeClientOutdated},
|
||||
{errPanicked, protocol.ErrorCodeInternal},
|
||||
{port.ErrShareNotFound, protocol.ErrorCodeNotFound},
|
||||
}
|
||||
|
||||
func errorInfo(err error) (info protocol.ErrorInfo, expected bool) {
|
||||
|
||||
@@ -24,6 +24,7 @@ func (s *server) playlistDTO(p port.PlaylistRecord) protocol.MyPlaylist {
|
||||
CreatedAtMs: p.CreatedAt.UnixMilli(),
|
||||
UpdatedAtMs: p.UpdatedAt.UnixMilli(),
|
||||
CoverURL: cover,
|
||||
ShareID: optional(p.ShareID),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,3 +195,48 @@ func (s *server) importMyPlatformPlaylist(ctx context.Context, c call) (any, err
|
||||
}
|
||||
return protocol.ImportMyPlatformPlaylistResponse{Playlist: s.playlistDTO(p), Imported: int64(n)}, nil
|
||||
}
|
||||
|
||||
func (s *server) shareMyPlaylist(ctx context.Context, c call) (any, error) {
|
||||
if _, err := protocol.DecodeShareMyPlaylistRequest(c.body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shareID, err := s.Playlists.Share(ctx, c.member.ID, c.r.PathValue("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.ShareMyPlaylistResponse{ShareID: shareID}, nil
|
||||
}
|
||||
|
||||
func (s *server) unshareMyPlaylist(ctx context.Context, c call) (any, error) {
|
||||
return nil, s.Playlists.Unshare(ctx, c.member.ID, c.r.PathValue("id"))
|
||||
}
|
||||
|
||||
func (s *server) getSharedPlaylist(ctx context.Context, c call) (any, error) {
|
||||
p, err := s.Playlists.Shared(ctx, c.r.PathValue("shareId"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]protocol.MyPlaylistItem, len(p.Items))
|
||||
for i, item := range p.Items {
|
||||
items[i] = s.playlistItemDTO(item)
|
||||
}
|
||||
return protocol.SharedPlaylist{
|
||||
Name: p.Name,
|
||||
Favorites: p.Favorites,
|
||||
OwnerName: p.OwnerName,
|
||||
OwnerAvatarHash: p.OwnerAvatarHash,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) copySharedPlaylist(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeCopySharedPlaylistRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := s.Playlists.CopyShared(ctx, c.member.ID, c.r.PathValue("shareId"), req.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.playlistDTO(p), nil
|
||||
}
|
||||
+3
-3
@@ -119,7 +119,7 @@ Authorization: Bearer $alice
|
||||
]}
|
||||
-- response --
|
||||
201
|
||||
{"playlist": {"id": "$imported", "name": "文件里的歌单", "favorites": false, "trackCount": 3, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg"}, "imported": 3}
|
||||
{"playlist": {"id": "$imported", "name": "文件里的歌单", "favorites": false, "trackCount": 3, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg", "shareId": null}, "imported": 3}
|
||||
|
||||
-- request --
|
||||
GET /api/me/playlists/$imported/items
|
||||
@@ -139,7 +139,7 @@ Authorization: Bearer $alice
|
||||
{"name": " ", "tracks": []}
|
||||
-- response --
|
||||
201
|
||||
{"playlist": {"id": "$empty", "name": "导入的歌单", "favorites": false, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null}, "imported": 0}
|
||||
{"playlist": {"id": "$empty", "name": "导入的歌单", "favorites": false, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null, "shareId": null}, "imported": 0}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-links
|
||||
@@ -166,7 +166,7 @@ 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}
|
||||
{"playlist": {"id": "$platform", "name": "周杰伦精选", "favorites": false, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg", "shareId": null}, "imported": 2}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/platform-imports
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
歌单分享:已注册的账号可以分享自己的任何歌单,包括“我喜欢”;每次开启都生成新的链接,旧链接随即失效;不需要身份就能读取分享,响应里没有所有者的成员 ID 和用户名;已注册的成员可以把分享的歌单复制成自己的新歌单;停止分享、删除歌单之后分享失效。
|
||||
|
||||
-- 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 --
|
||||
PATCH /api/me
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"email": "alice@example.com"}
|
||||
-- response --
|
||||
200
|
||||
{"member": {"id": "$aliceId", "name": "$aliceName", "avatarHash": "c160f8cc69a4f0bf2b0362752353d060"}}
|
||||
|
||||
-- request --
|
||||
GET /api/me/playlists
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null, "shareId": null}]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/items
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"track": {"source": "netease", "id": "186016"}}
|
||||
-- response --
|
||||
201
|
||||
{"item": {"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}}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/items
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"track": {"source": "netease", "id": "185811"}}
|
||||
-- response --
|
||||
201
|
||||
{"item": {"track": {"source": "netease", "id": "185811"}, "meta": {"title": "七里香", "artist": "周杰伦", "album": "七里香", "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "pageUrl": "https://music.163.com/#/song?id=185811", "durationMs": 299000}}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/share
|
||||
|
||||
{}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "成员 token 无效"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{}
|
||||
-- response --
|
||||
200
|
||||
{"shareId": "$share"}
|
||||
|
||||
-- request --
|
||||
GET /api/me/playlists
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "shareId": "$share"}]}
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/$share
|
||||
-- response --
|
||||
200
|
||||
{"name": "我喜欢", "favorites": true, "ownerName": "$aliceName", "ownerAvatarHash": "c160f8cc69a4f0bf2b0362752353d060", "items": [
|
||||
{"track": {"source": "netease", "id": "185811"}, "meta": {"title": "七里香", "artist": "周杰伦", "album": "七里香", "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "pageUrl": "https://music.163.com/#/song?id=185811", "durationMs": 299000}},
|
||||
{"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}}
|
||||
]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{}
|
||||
-- response --
|
||||
200
|
||||
{"shareId": "$newShare"}
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/$share
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "分享已失效"}}
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/$newShare
|
||||
-- response --
|
||||
200
|
||||
{"name": "我喜欢", "favorites": true, "ownerName": "$aliceName", "ownerAvatarHash": "c160f8cc69a4f0bf2b0362752353d060", "items": [
|
||||
{"track": {"source": "netease", "id": "185811"}, "meta": {"title": "七里香", "artist": "周杰伦", "album": "七里香", "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "pageUrl": "https://music.163.com/#/song?id=185811", "durationMs": 299000}},
|
||||
{"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}}
|
||||
]}
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/not-a-share
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "分享已失效"}}
|
||||
|
||||
-- request --
|
||||
POST /api/members
|
||||
|
||||
{}
|
||||
-- response --
|
||||
201
|
||||
{"memberToken": "$bob", "member": {"id": "$bobId", "name": "$bobName", "avatarHash": ""}}
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/$newShare
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
200
|
||||
{"name": "我喜欢", "favorites": true, "ownerName": "$aliceName", "ownerAvatarHash": "c160f8cc69a4f0bf2b0362752353d060", "items": [
|
||||
{"track": {"source": "netease", "id": "185811"}, "meta": {"title": "七里香", "artist": "周杰伦", "album": "七里香", "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "pageUrl": "https://music.163.com/#/song?id=185811", "durationMs": 299000}},
|
||||
{"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}}
|
||||
]}
|
||||
|
||||
-- request --
|
||||
POST /api/shared-playlists/$newShare/copies
|
||||
|
||||
{"name": "抄来的"}
|
||||
-- response --
|
||||
401
|
||||
{"error": {"code": "UNAUTHORIZED", "message": "成员 token 无效"}}
|
||||
|
||||
-- request --
|
||||
POST /api/shared-playlists/$newShare/copies
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"name": "抄来的"}
|
||||
-- response --
|
||||
403
|
||||
{"error": {"code": "FORBIDDEN", "message": "注册账号之后才能使用歌单"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{}
|
||||
-- response --
|
||||
403
|
||||
{"error": {"code": "FORBIDDEN", "message": "注册账号之后才能使用歌单"}}
|
||||
|
||||
-- request --
|
||||
POST /api/account
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"username": "bob", "password": "correct horse"}
|
||||
-- response --
|
||||
201
|
||||
{"account": {"username": "bob"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{}
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "歌单不存在"}}
|
||||
|
||||
-- request --
|
||||
DELETE /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "歌单不存在"}}
|
||||
|
||||
-- request --
|
||||
POST /api/shared-playlists/$newShare/copies
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"name": " "}
|
||||
-- response --
|
||||
400
|
||||
{"error": {"code": "INVALID_ARGUMENT", "message": "name:去掉首尾空白后应有 1 到 50 个字符,其中至少一个看得见,不能包含控制字符"}}
|
||||
|
||||
-- request --
|
||||
POST /api/shared-playlists/$newShare/copies
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"name": " 抄来的 "}
|
||||
-- response --
|
||||
201
|
||||
{"id": "$copy", "name": "抄来的", "favorites": false, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "shareId": null}
|
||||
|
||||
-- request --
|
||||
GET /api/me/playlists/$copy/items
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
200
|
||||
{"items": [
|
||||
{"track": {"source": "netease", "id": "185811"}, "meta": {"title": "七里香", "artist": "周杰伦", "album": "七里香", "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "pageUrl": "https://music.163.com/#/song?id=185811", "durationMs": 299000}},
|
||||
{"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}}
|
||||
]}
|
||||
|
||||
-- request --
|
||||
POST /api/shared-playlists/not-a-share/copies
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"name": "抄来的"}
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "分享已失效"}}
|
||||
|
||||
-- request --
|
||||
DELETE /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
DELETE /api/me/playlists/$favorites/share
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/$newShare
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "分享已失效"}}
|
||||
|
||||
-- request --
|
||||
POST /api/shared-playlists/$newShare/copies
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{"name": "抄来的"}
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "分享已失效"}}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$copy/share
|
||||
Authorization: Bearer $bob
|
||||
|
||||
{}
|
||||
-- response --
|
||||
200
|
||||
{"shareId": "$copyShare"}
|
||||
|
||||
-- request --
|
||||
DELETE /api/me/playlists/$copy
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
204
|
||||
|
||||
-- request --
|
||||
GET /api/shared-playlists/$copyShare
|
||||
-- response --
|
||||
404
|
||||
{"error": {"code": "NOT_FOUND", "message": "分享已失效"}}
|
||||
+5
-5
@@ -38,7 +38,7 @@ GET /api/me/playlists
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null}]}
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null, "shareId": null}]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/items
|
||||
@@ -93,7 +93,7 @@ Authorization: Bearer $alice
|
||||
{"name": " 通勤 "}
|
||||
-- response --
|
||||
201
|
||||
{"id": "$commute", "name": "通勤", "favorites": false, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null}
|
||||
{"id": "$commute", "name": "通勤", "favorites": false, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null, "shareId": null}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists
|
||||
@@ -224,7 +224,7 @@ GET /api/me/playlists
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/qilixiang.jpg"}, {"id": "$commute", "name": "上班路上", "favorites": false, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg"}]}
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "shareId": null}, {"id": "$commute", "name": "上班路上", "favorites": false, "trackCount": 2, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/example.jpg", "shareId": null}]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-memberships
|
||||
@@ -289,7 +289,7 @@ GET /api/me/playlists
|
||||
Authorization: Bearer $bob
|
||||
-- response --
|
||||
200
|
||||
{"items": [{"id": "$bobFavorites", "name": "我喜欢", "favorites": true, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null}]}
|
||||
{"items": [{"id": "$bobFavorites", "name": "我喜欢", "favorites": true, "trackCount": 0, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": null, "shareId": null}]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$bobFavorites/copies
|
||||
@@ -377,4 +377,4 @@ GET /api/me/playlists
|
||||
Authorization: Bearer $alice
|
||||
-- response --
|
||||
200
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 1, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/qilixiang.jpg"}]}
|
||||
{"items": [{"id": "$favorites", "name": "我喜欢", "favorites": true, "trackCount": 1, "createdAtMs": 1, "updatedAtMs": 1, "coverUrl": "https://p1.music.126.net/qilixiang.jpg", "shareId": null}]}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
import type { APIKey, Account, AdminAccount, ErrorInfo, ImportedTrack, Member, MyListen, 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, SharedPlaylist, Source, SourceAccount, StoredMedia, Track } from '../realtime/protocol.gen'
|
||||
|
||||
export interface AddMyPlaylistItemRequest {
|
||||
track: Track
|
||||
@@ -118,6 +118,10 @@ export interface CopyMyPlaylistItemsResponse {
|
||||
added: number
|
||||
}
|
||||
|
||||
export interface CopySharedPlaylistRequest {
|
||||
name: string
|
||||
}
|
||||
|
||||
export type CreateMemberRequest = Record<string, never>
|
||||
|
||||
export interface CreateMemberResponse {
|
||||
@@ -253,6 +257,12 @@ export interface ResolveMyPlaylistLinkResponse {
|
||||
source: Source
|
||||
}
|
||||
|
||||
export type ShareMyPlaylistRequest = Record<string, never>
|
||||
|
||||
export interface ShareMyPlaylistResponse {
|
||||
shareId: string
|
||||
}
|
||||
|
||||
export interface UpdateMeRequest {
|
||||
email?: string
|
||||
name?: string
|
||||
@@ -334,5 +344,9 @@ export function httpClient(send: HttpSend) {
|
||||
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' }),
|
||||
shareMyPlaylist: (id: string) => send<ShareMyPlaylistResponse>({ method: 'POST', path: `/api/me/playlists/${encodeURIComponent(id)}/share`, auth: 'member', body: {} }),
|
||||
unshareMyPlaylist: (id: string) => send<void>({ method: 'DELETE', path: `/api/me/playlists/${encodeURIComponent(id)}/share`, auth: 'member' }),
|
||||
getSharedPlaylist: (shareId: string) => send<SharedPlaylist>({ method: 'GET', path: `/api/shared-playlists/${encodeURIComponent(shareId)}`, auth: 'none' }),
|
||||
copySharedPlaylist: (shareId: string, body: CopySharedPlaylistRequest) => send<MyPlaylist>({ method: 'POST', path: `/api/shared-playlists/${encodeURIComponent(shareId)}/copies`, auth: 'member', body }),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protocolgen from protocol/idl.json; DO NOT EDIT.
|
||||
|
||||
export const PROTOCOL_VERSION = 'ae9a632d648f3840'
|
||||
export const PROTOCOL_VERSION = 'cbfa923a9cbe9f0a'
|
||||
|
||||
export interface APIKey {
|
||||
key: string
|
||||
@@ -94,6 +94,7 @@ export interface MyPlaylist {
|
||||
favorites: boolean
|
||||
id: string
|
||||
name: string
|
||||
shareId: string | null
|
||||
trackCount: number
|
||||
updatedAtMs: number
|
||||
}
|
||||
@@ -284,6 +285,14 @@ export interface SettingsUpdatedEvent {
|
||||
playMode: PlayMode
|
||||
}
|
||||
|
||||
export interface SharedPlaylist {
|
||||
favorites: boolean
|
||||
items: MyPlaylistItem[]
|
||||
name: string
|
||||
ownerAvatarHash: string
|
||||
ownerName: string
|
||||
}
|
||||
|
||||
export type Source = 'netease' | 'qq' | 'bilibili' | 'link'
|
||||
|
||||
export interface SourceAccount {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { sortPlaylists } from './playlist-sort'
|
||||
|
||||
function playlist(id: string, name: string, trackCount: number, createdAtMs: number, updatedAtMs: number, favorites = false): MyPlaylist {
|
||||
return { id, name, favorites, trackCount, createdAtMs, updatedAtMs, coverUrl: null }
|
||||
return { id, name, favorites, trackCount, createdAtMs, updatedAtMs, coverUrl: null, shareId: null }
|
||||
}
|
||||
|
||||
const favorites = playlist('p0', '我喜欢', 3, 50, 50, true)
|
||||
|
||||
@@ -25,15 +25,15 @@ class FakeStorage {
|
||||
}
|
||||
}
|
||||
|
||||
const favorites = { id: 'p1', name: '我喜欢', favorites: true, trackCount: 1, createdAtMs: 1, updatedAtMs: 1, coverUrl: null }
|
||||
const favorites = { id: 'p1', name: '我喜欢', favorites: true, trackCount: 1, createdAtMs: 1, updatedAtMs: 1, coverUrl: null, shareId: null }
|
||||
const sunny = { source: 'netease', id: '186016' } as const
|
||||
const rain = { source: 'netease', id: '185811' } as const
|
||||
const commute = { id: 'p2', name: '通勤', favorites: false, trackCount: 0, createdAtMs: 1, updatedAtMs: 1, coverUrl: null }
|
||||
const commute = { id: 'p2', name: '通勤', favorites: false, trackCount: 0, createdAtMs: 1, updatedAtMs: 1, coverUrl: null, shareId: null }
|
||||
const meta = { title: '晴天', artist: '周杰伦', album: '叶惠美', coverUrl: '', pageUrl: '', durationMs: 0 }
|
||||
const notReady = '这首歌还在准备,准备好之后再保存'
|
||||
const full = '一个歌单最多 1000 首歌'
|
||||
const changed = '歌单已经变了,刷新之后再试'
|
||||
const imported = { id: 'p3', name: '导入的歌单', favorites: false, trackCount: 2, createdAtMs: 2, updatedAtMs: 2, coverUrl: null }
|
||||
const imported = { id: 'p3', name: '导入的歌单', favorites: false, trackCount: 2, createdAtMs: 2, updatedAtMs: 2, coverUrl: null, shareId: null }
|
||||
const fileTrack = { track: sunny, title: '晴天', durationMs: 269000 }
|
||||
|
||||
let requests: string[]
|
||||
|
||||
Reference in new issue
Block a user