feat: tell which of my playlists contain a song

This commit is contained in:
DiheChen committed 2026-11-09 01:47:45 +08:00
1 parent 5c34450ccf
commit 24abca6dc7
17 files changed
+270 -3

No files matched your search

+1
View File
@@ -119,6 +119,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.handle(mux, protocol.RouteRemoveMyPlaylistItems, srv.removeMyPlaylistItems)
srv.handle(mux, protocol.RouteListMyTrackPlaylists, srv.listMyTrackPlaylists)
srv.handle(mux, protocol.RouteCopyMyPlaylistItems, srv.copyMyPlaylistItems)
srv.handle(mux, protocol.RouteMoveMyPlaylistItems, srv.moveMyPlaylistItems)
srv.handle(mux, protocol.RouteListRooms, srv.listRooms)
+12
View File
@@ -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
}
@@ -210,6 +210,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)
@@ -438,6 +438,30 @@ func TestConcurrentCrossCopiesDoNotDeadlock(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")
+7
View File
@@ -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
+3
View File
@@ -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)
}
+2
View File
@@ -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。
+12
View File
@@ -659,6 +659,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()
+18
View File
@@ -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