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": "暂时只能导入网易云的歌单"}}
|
||||
Reference in new issue
Block a user