feat: tell which of my playlists contain a song
This commit is contained in:
1 parent
b26019f459
commit
cd84727b6b
19 files changed
+282
-3
No files matched your search
@@ -117,6 +117,7 @@ func New(s Services, metrics prometheus.Registerer, tracer trace.TracerProvider,
|
||||
srv.handle(mux, protocol.RouteListMyPlaylistItems, srv.listMyPlaylistItems)
|
||||
srv.handle(mux, protocol.RouteAddMyPlaylistItem, srv.addMyPlaylistItem)
|
||||
srv.handleLimit(mux, protocol.RouteRemoveMyPlaylistItems, maxBatchBodyBytes, srv.removeMyPlaylistItems)
|
||||
srv.handle(mux, protocol.RouteListMyTrackPlaylists, srv.listMyTrackPlaylists)
|
||||
srv.handleLimit(mux, protocol.RouteCopyMyPlaylistItems, maxBatchBodyBytes, srv.copyMyPlaylistItems)
|
||||
srv.handleLimit(mux, protocol.RouteMoveMyPlaylistItems, maxBatchBodyBytes, srv.moveMyPlaylistItems)
|
||||
srv.handle(mux, protocol.RouteListRooms, srv.listRooms)
|
||||
|
||||
@@ -201,6 +201,10 @@ func TestPlaylistBatchBodyLimit(t *testing.T) {
|
||||
if got := send(t, server, http.MethodPost, "/api/me/playlists/"+favorites+"/removals", tooLarge, auth...); got != want {
|
||||
t.Errorf("超过批量上限的请求体 = %#v,期望 %#v", got, want)
|
||||
}
|
||||
want = failure(protocol.ErrorCodeInvalidArgument, "请求体不能超过 64 KiB")
|
||||
if got := send(t, server, http.MethodPost, "/api/me/playlist-memberships", `{"tracks":`+list+`}`, auth...); got != want {
|
||||
t.Errorf("其他接口超过 64 KiB 的请求体 = %#v,期望 %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedPathParametersAreNotFound(t *testing.T) {
|
||||
|
||||
@@ -108,6 +108,18 @@ func (s *server) removeMyPlaylistItems(ctx context.Context, c call) (any, error)
|
||||
return nil, s.Playlists.Remove(ctx, c.member.ID, c.r.PathValue("id"), tracksOf(req.Tracks))
|
||||
}
|
||||
|
||||
func (s *server) listMyTrackPlaylists(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeListMyTrackPlaylistsRequest(c.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err := s.Playlists.Containing(ctx, c.member.ID, trackOf(req.Track))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return protocol.ListMyTrackPlaylistsResponse{PlaylistIds: ids}, nil
|
||||
}
|
||||
|
||||
func (s *server) copyMyPlaylistItems(ctx context.Context, c call) (any, error) {
|
||||
req, err := protocol.DecodeCopyMyPlaylistItemsRequest(c.body)
|
||||
if err != nil {
|
||||
|
||||
@@ -452,3 +452,36 @@ func (q *Queries) TouchPlaylist(ctx context.Context, id string) error {
|
||||
_, err := q.db.Exec(ctx, touchPlaylist, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const trackPlaylists = `-- name: TrackPlaylists :many
|
||||
SELECT p.id FROM playlists p
|
||||
JOIN playlist_items i ON i.playlist_id = p.id
|
||||
WHERE p.owner_id = $1 AND i.source = $2 AND i.track_id = $3
|
||||
ORDER BY p.id
|
||||
`
|
||||
|
||||
type TrackPlaylistsParams struct {
|
||||
OwnerID string
|
||||
Source string
|
||||
TrackID string
|
||||
}
|
||||
|
||||
func (q *Queries) TrackPlaylists(ctx context.Context, arg TrackPlaylistsParams) ([]string, error) {
|
||||
rows, err := q.db.Query(ctx, trackPlaylists, arg.OwnerID, arg.Source, arg.TrackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -218,6 +218,17 @@ func (s *Store) RemovePlaylistItems(ctx context.Context, ownerID, id string, tra
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) TrackPlaylists(ctx context.Context, ownerID string, t track.Track) ([]string, error) {
|
||||
ids, err := s.q.TrackPlaylists(ctx, query.TrackPlaylistsParams{OwnerID: ownerID, Source: string(t.Source), TrackID: t.ID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询曲目 %s 所在的歌单:%w", t.ID, err)
|
||||
}
|
||||
if ids == nil {
|
||||
ids = []string{}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (s *Store) CopyPlaylistItems(ctx context.Context, ownerID, fromID, toID string, tracks []track.Track, limit int) (int, error) {
|
||||
var added int64
|
||||
err := s.owned(ctx, ownerID, fromID)
|
||||
|
||||
@@ -512,6 +512,30 @@ func TestConcurrentRemovalsAndDeletesDoNotDeadlock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackPlaylistsOnlyListsOwnPlaylists(t *testing.T) {
|
||||
store, _ := newStore(t)
|
||||
alice := registered(t, store, "alice")
|
||||
bob := registered(t, store, "bob")
|
||||
favorites := favoritesOf(t, store, alice.ID)
|
||||
commute := filled(t, store, alice.ID, "通勤", "1")
|
||||
filled(t, store, alice.ID, "空", "2")
|
||||
bobFavorites := favoritesOf(t, store, bob.ID)
|
||||
if err := store.AddPlaylistItem(t.Context(), alice.ID, favorites, songItem("1"), 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.AddPlaylistItem(t.Context(), bob.ID, bobFavorites, songItem("1"), 10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ids, err := store.TrackPlaylists(t.Context(), alice.ID, songItem("1").Track)
|
||||
if want := []string{favorites, commute}; err != nil || !slices.Equal(ids, want) {
|
||||
t.Errorf("所在的歌单 = %v, %v,期望只有本人的 %v", ids, err, want)
|
||||
}
|
||||
ids, err = store.TrackPlaylists(t.Context(), alice.ID, songItem("3").Track)
|
||||
if err != nil || ids == nil || len(ids) != 0 {
|
||||
t.Errorf("不在任何歌单里的歌 = %#v, %v,期望空切片", ids, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovePlaylistItems(t *testing.T) {
|
||||
store, _ := newStore(t)
|
||||
alice := registered(t, store, "alice")
|
||||
|
||||
@@ -124,6 +124,13 @@ func (s *Service) Remove(ctx context.Context, memberID, id string, tracks []trac
|
||||
return s.playlists.RemovePlaylistItems(ctx, memberID, id, tracks)
|
||||
}
|
||||
|
||||
func (s *Service) Containing(ctx context.Context, memberID string, t track.Track) ([]string, error) {
|
||||
if err := s.registered(ctx, memberID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.playlists.TrackPlaylists(ctx, memberID, t)
|
||||
}
|
||||
|
||||
func (s *Service) Copy(ctx context.Context, memberID, fromID, toID string, tracks []track.Track) (int, error) {
|
||||
if err := s.registered(ctx, memberID); err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -119,6 +119,9 @@ func TestGuestCannotUsePlaylists(t *testing.T) {
|
||||
if _, err := f.svc.Add(t.Context(), guest.ID, "any", sunny); !errors.Is(err, ErrNotRegistered) {
|
||||
t.Errorf("游客收藏 = %v,期望 ErrNotRegistered", err)
|
||||
}
|
||||
if _, err := f.svc.Containing(t.Context(), guest.ID, sunny); !errors.Is(err, ErrNotRegistered) {
|
||||
t.Errorf("游客查询所在的歌单 = %v,期望 ErrNotRegistered", err)
|
||||
}
|
||||
if err := f.svc.Remove(t.Context(), guest.ID, "any", []track.Track{sunny}); !errors.Is(err, ErrNotRegistered) {
|
||||
t.Errorf("游客批量移出 = %v,期望 ErrNotRegistered", err)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ type Playlists interface {
|
||||
AddPlaylistItem(ctx context.Context, ownerID, id string, item PlaylistItem, limit int) error
|
||||
// 不在歌单里的歌忽略。
|
||||
RemovePlaylistItems(ctx context.Context, ownerID, id string, tracks []track.Track) error
|
||||
// 只看这个成员自己的歌单;没有命中时返回空切片。
|
||||
TrackPlaylists(ctx context.Context, ownerID string, t track.Track) ([]string, error)
|
||||
// 把 fromID 歌单里的这些歌连同元数据复制到 toID 歌单,返回复制的数量。fromID 不属于 ownerID 时返回 ErrPlaylistNotFound。
|
||||
// 不在 fromID 里的和已在 toID 里的跳过;新复制的排在最前,tracks[0] 最靠前。
|
||||
// 复制之后超过 limit 首时整批拒绝,返回 ErrPlaylistLimit。
|
||||
|
||||
@@ -677,6 +677,18 @@ func (s *Store) RemovePlaylistItems(_ context.Context, ownerID, id string, track
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) TrackPlaylists(_ context.Context, ownerID string, t track.Track) ([]string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
ids := []string{}
|
||||
for _, p := range s.playlists {
|
||||
if p.owner == ownerID && slices.ContainsFunc(p.items, func(i port.PlaylistItem) bool { return i.Track == t }) {
|
||||
ids = append(ids, p.id)
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (s *Store) CopyPlaylistItems(_ context.Context, ownerID, fromID, toID string, tracks []track.Track, limit int) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -238,6 +239,13 @@ func TestPlaylistLinkTracks(t *testing.T) {
|
||||
t.Errorf("歌单里的曲目 = %+v,期望一首 ID %s、封面 %s", items.Items, link, coverURL)
|
||||
}
|
||||
|
||||
var memberships struct {
|
||||
PlaylistIDs []string `json:"playlistIds"`
|
||||
}
|
||||
call("POST /api/me/playlist-memberships"+auth+"\n\n"+`{"track": {"source": "link", "id": "https://Example.com/Song.mp3"}}`, http.StatusOK, &memberships)
|
||||
if !slices.Equal(memberships.PlaylistIDs, []string{favorites}) {
|
||||
t.Errorf("换一种大小写查所在的歌单 = %v,期望 [%s]", memberships.PlaylistIDs, favorites)
|
||||
}
|
||||
call("POST /api/me/playlists/"+favorites+"/removals"+auth+"\n\n"+`{"tracks": [{"source": "link", "id": "hTTps://example.COM/Song.mp3"}]}`, http.StatusNoContent, nil)
|
||||
call("GET /api/me/playlists/"+favorites+"/items"+auth, http.StatusOK, &items)
|
||||
if len(items.Items) != 0 {
|
||||
|
||||
@@ -226,6 +226,24 @@ Authorization: Bearer $alice
|
||||
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"}]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-memberships
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"track": {"source": "netease", "id": "186016"}}
|
||||
-- response --
|
||||
200
|
||||
{"playlistIds": ["$favorites", "$commute"]}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlist-memberships
|
||||
Authorization: Bearer $alice
|
||||
|
||||
{"track": {"source": "netease", "id": "999"}}
|
||||
-- response --
|
||||
200
|
||||
{"playlistIds": []}
|
||||
|
||||
-- request --
|
||||
POST /api/me/playlists/$favorites/removals
|
||||
Authorization: Bearer $alice
|
||||
|
||||
Reference in new issue
Block a user