diff --git a/backend/gen/radio/v1/channel.pb.go b/backend/gen/radio/v1/channel.pb.go index 8bf416a..5556c12 100644 --- a/backend/gen/radio/v1/channel.pb.go +++ b/backend/gen/radio/v1/channel.pb.go @@ -344,6 +344,58 @@ func (x *ChannelAddSongRequest) GetChannelId() string { return "" } +type ChannelRemoveSongRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SongId string `protobuf:"bytes,1,opt,name=song_id,json=songId,proto3" json:"song_id,omitempty"` + ChannelId string `protobuf:"bytes,2,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelRemoveSongRequest) Reset() { + *x = ChannelRemoveSongRequest{} + mi := &file_radio_v1_channel_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelRemoveSongRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelRemoveSongRequest) ProtoMessage() {} + +func (x *ChannelRemoveSongRequest) ProtoReflect() protoreflect.Message { + mi := &file_radio_v1_channel_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelRemoveSongRequest.ProtoReflect.Descriptor instead. +func (*ChannelRemoveSongRequest) Descriptor() ([]byte, []int) { + return file_radio_v1_channel_proto_rawDescGZIP(), []int{6} +} + +func (x *ChannelRemoveSongRequest) GetSongId() string { + if x != nil { + return x.SongId + } + return "" +} + +func (x *ChannelRemoveSongRequest) GetChannelId() string { + if x != nil { + return x.ChannelId + } + return "" +} + type ChannelGetSongsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` @@ -353,7 +405,7 @@ type ChannelGetSongsRequest struct { func (x *ChannelGetSongsRequest) Reset() { *x = ChannelGetSongsRequest{} - mi := &file_radio_v1_channel_proto_msgTypes[6] + mi := &file_radio_v1_channel_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -365,7 +417,7 @@ func (x *ChannelGetSongsRequest) String() string { func (*ChannelGetSongsRequest) ProtoMessage() {} func (x *ChannelGetSongsRequest) ProtoReflect() protoreflect.Message { - mi := &file_radio_v1_channel_proto_msgTypes[6] + mi := &file_radio_v1_channel_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -378,7 +430,7 @@ func (x *ChannelGetSongsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelGetSongsRequest.ProtoReflect.Descriptor instead. func (*ChannelGetSongsRequest) Descriptor() ([]byte, []int) { - return file_radio_v1_channel_proto_rawDescGZIP(), []int{6} + return file_radio_v1_channel_proto_rawDescGZIP(), []int{7} } func (x *ChannelGetSongsRequest) GetChannelId() string { @@ -397,7 +449,7 @@ type ChannelGetSongsResponse struct { func (x *ChannelGetSongsResponse) Reset() { *x = ChannelGetSongsResponse{} - mi := &file_radio_v1_channel_proto_msgTypes[7] + mi := &file_radio_v1_channel_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -409,7 +461,7 @@ func (x *ChannelGetSongsResponse) String() string { func (*ChannelGetSongsResponse) ProtoMessage() {} func (x *ChannelGetSongsResponse) ProtoReflect() protoreflect.Message { - mi := &file_radio_v1_channel_proto_msgTypes[7] + mi := &file_radio_v1_channel_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -422,7 +474,7 @@ func (x *ChannelGetSongsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelGetSongsResponse.ProtoReflect.Descriptor instead. func (*ChannelGetSongsResponse) Descriptor() ([]byte, []int) { - return file_radio_v1_channel_proto_rawDescGZIP(), []int{7} + return file_radio_v1_channel_proto_rawDescGZIP(), []int{8} } func (x *ChannelGetSongsResponse) GetSongs() []*Song { @@ -458,17 +510,22 @@ const file_radio_v1_channel_proto_rawDesc = "" + "\x15ChannelAddSongRequest\x12!\n" + "\asong_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06songId\x12'\n" + "\n" + + "channel_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tchannelId\"f\n" + + "\x18ChannelRemoveSongRequest\x12!\n" + + "\asong_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06songId\x12'\n" + + "\n" + "channel_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tchannelId\"A\n" + "\x16ChannelGetSongsRequest\x12'\n" + "\n" + "channel_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\tchannelId\"?\n" + "\x17ChannelGetSongsResponse\x12$\n" + - "\x05songs\x18\x01 \x03(\v2\x0e.radio.v1.SongR\x05songs2\x94\x03\n" + + "\x05songs\x18\x01 \x03(\v2\x0e.radio.v1.SongR\x05songs2\xe5\x03\n" + "\x0eChannelService\x12P\n" + "\rCreateChannel\x12\x1e.radio.v1.CreateChannelRequest\x1a\x1f.radio.v1.CreateChannelResponse\x12G\n" + "\rDeleteChannel\x12\x1e.radio.v1.DeleteChannelRequest\x1a\x16.google.protobuf.Empty\x12D\n" + "\vChannelList\x12\x16.google.protobuf.Empty\x1a\x1d.radio.v1.ChannelListResponse\x12I\n" + - "\x0eChannelAddSong\x12\x1f.radio.v1.ChannelAddSongRequest\x1a\x16.google.protobuf.Empty\x12V\n" + + "\x0eChannelAddSong\x12\x1f.radio.v1.ChannelAddSongRequest\x1a\x16.google.protobuf.Empty\x12O\n" + + "\x11ChannelRemoveSong\x12\".radio.v1.ChannelRemoveSongRequest\x1a\x16.google.protobuf.Empty\x12V\n" + "\x0fChannelGetSongs\x12 .radio.v1.ChannelGetSongsRequest\x1a!.radio.v1.ChannelGetSongsResponseB\x85\x01\n" + "\fcom.radio.v1B\fChannelProtoP\x01Z&echsen.club/radio/gen/radio/v1;radiov1\xa2\x02\x03RXX\xaa\x02\bRadio.V1\xca\x02\bRadio\\V1\xe2\x02\x14Radio\\V1\\GPBMetadata\xea\x02\tRadio::V1b\x06proto3" @@ -484,38 +541,41 @@ func file_radio_v1_channel_proto_rawDescGZIP() []byte { return file_radio_v1_channel_proto_rawDescData } -var file_radio_v1_channel_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_radio_v1_channel_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_radio_v1_channel_proto_goTypes = []any{ - (*Channel)(nil), // 0: radio.v1.Channel - (*CreateChannelRequest)(nil), // 1: radio.v1.CreateChannelRequest - (*CreateChannelResponse)(nil), // 2: radio.v1.CreateChannelResponse - (*DeleteChannelRequest)(nil), // 3: radio.v1.DeleteChannelRequest - (*ChannelListResponse)(nil), // 4: radio.v1.ChannelListResponse - (*ChannelAddSongRequest)(nil), // 5: radio.v1.ChannelAddSongRequest - (*ChannelGetSongsRequest)(nil), // 6: radio.v1.ChannelGetSongsRequest - (*ChannelGetSongsResponse)(nil), // 7: radio.v1.ChannelGetSongsResponse - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp - (*Song)(nil), // 9: radio.v1.Song - (*emptypb.Empty)(nil), // 10: google.protobuf.Empty + (*Channel)(nil), // 0: radio.v1.Channel + (*CreateChannelRequest)(nil), // 1: radio.v1.CreateChannelRequest + (*CreateChannelResponse)(nil), // 2: radio.v1.CreateChannelResponse + (*DeleteChannelRequest)(nil), // 3: radio.v1.DeleteChannelRequest + (*ChannelListResponse)(nil), // 4: radio.v1.ChannelListResponse + (*ChannelAddSongRequest)(nil), // 5: radio.v1.ChannelAddSongRequest + (*ChannelRemoveSongRequest)(nil), // 6: radio.v1.ChannelRemoveSongRequest + (*ChannelGetSongsRequest)(nil), // 7: radio.v1.ChannelGetSongsRequest + (*ChannelGetSongsResponse)(nil), // 8: radio.v1.ChannelGetSongsResponse + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp + (*Song)(nil), // 10: radio.v1.Song + (*emptypb.Empty)(nil), // 11: google.protobuf.Empty } var file_radio_v1_channel_proto_depIdxs = []int32{ - 8, // 0: radio.v1.Channel.created_at:type_name -> google.protobuf.Timestamp - 8, // 1: radio.v1.Channel.modified_at:type_name -> google.protobuf.Timestamp + 9, // 0: radio.v1.Channel.created_at:type_name -> google.protobuf.Timestamp + 9, // 1: radio.v1.Channel.modified_at:type_name -> google.protobuf.Timestamp 0, // 2: radio.v1.CreateChannelResponse.channel:type_name -> radio.v1.Channel 0, // 3: radio.v1.ChannelListResponse.channels:type_name -> radio.v1.Channel - 9, // 4: radio.v1.ChannelGetSongsResponse.songs:type_name -> radio.v1.Song + 10, // 4: radio.v1.ChannelGetSongsResponse.songs:type_name -> radio.v1.Song 1, // 5: radio.v1.ChannelService.CreateChannel:input_type -> radio.v1.CreateChannelRequest 3, // 6: radio.v1.ChannelService.DeleteChannel:input_type -> radio.v1.DeleteChannelRequest - 10, // 7: radio.v1.ChannelService.ChannelList:input_type -> google.protobuf.Empty + 11, // 7: radio.v1.ChannelService.ChannelList:input_type -> google.protobuf.Empty 5, // 8: radio.v1.ChannelService.ChannelAddSong:input_type -> radio.v1.ChannelAddSongRequest - 6, // 9: radio.v1.ChannelService.ChannelGetSongs:input_type -> radio.v1.ChannelGetSongsRequest - 2, // 10: radio.v1.ChannelService.CreateChannel:output_type -> radio.v1.CreateChannelResponse - 10, // 11: radio.v1.ChannelService.DeleteChannel:output_type -> google.protobuf.Empty - 4, // 12: radio.v1.ChannelService.ChannelList:output_type -> radio.v1.ChannelListResponse - 10, // 13: radio.v1.ChannelService.ChannelAddSong:output_type -> google.protobuf.Empty - 7, // 14: radio.v1.ChannelService.ChannelGetSongs:output_type -> radio.v1.ChannelGetSongsResponse - 10, // [10:15] is the sub-list for method output_type - 5, // [5:10] is the sub-list for method input_type + 6, // 9: radio.v1.ChannelService.ChannelRemoveSong:input_type -> radio.v1.ChannelRemoveSongRequest + 7, // 10: radio.v1.ChannelService.ChannelGetSongs:input_type -> radio.v1.ChannelGetSongsRequest + 2, // 11: radio.v1.ChannelService.CreateChannel:output_type -> radio.v1.CreateChannelResponse + 11, // 12: radio.v1.ChannelService.DeleteChannel:output_type -> google.protobuf.Empty + 4, // 13: radio.v1.ChannelService.ChannelList:output_type -> radio.v1.ChannelListResponse + 11, // 14: radio.v1.ChannelService.ChannelAddSong:output_type -> google.protobuf.Empty + 11, // 15: radio.v1.ChannelService.ChannelRemoveSong:output_type -> google.protobuf.Empty + 8, // 16: radio.v1.ChannelService.ChannelGetSongs:output_type -> radio.v1.ChannelGetSongsResponse + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type 5, // [5:5] is the sub-list for extension type_name 5, // [5:5] is the sub-list for extension extendee 0, // [0:5] is the sub-list for field type_name @@ -533,7 +593,7 @@ func file_radio_v1_channel_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_radio_v1_channel_proto_rawDesc), len(file_radio_v1_channel_proto_rawDesc)), NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/backend/gen/radio/v1/radiov1connect/channel.connect.go b/backend/gen/radio/v1/radiov1connect/channel.connect.go index 6e5e548..1ce6be5 100644 --- a/backend/gen/radio/v1/radiov1connect/channel.connect.go +++ b/backend/gen/radio/v1/radiov1connect/channel.connect.go @@ -46,6 +46,9 @@ const ( // ChannelServiceChannelAddSongProcedure is the fully-qualified name of the ChannelService's // ChannelAddSong RPC. ChannelServiceChannelAddSongProcedure = "/radio.v1.ChannelService/ChannelAddSong" + // ChannelServiceChannelRemoveSongProcedure is the fully-qualified name of the ChannelService's + // ChannelRemoveSong RPC. + ChannelServiceChannelRemoveSongProcedure = "/radio.v1.ChannelService/ChannelRemoveSong" // ChannelServiceChannelGetSongsProcedure is the fully-qualified name of the ChannelService's // ChannelGetSongs RPC. ChannelServiceChannelGetSongsProcedure = "/radio.v1.ChannelService/ChannelGetSongs" @@ -57,6 +60,7 @@ type ChannelServiceClient interface { DeleteChannel(context.Context, *v1.DeleteChannelRequest) (*emptypb.Empty, error) ChannelList(context.Context, *emptypb.Empty) (*v1.ChannelListResponse, error) ChannelAddSong(context.Context, *v1.ChannelAddSongRequest) (*emptypb.Empty, error) + ChannelRemoveSong(context.Context, *v1.ChannelRemoveSongRequest) (*emptypb.Empty, error) ChannelGetSongs(context.Context, *v1.ChannelGetSongsRequest) (*v1.ChannelGetSongsResponse, error) } @@ -95,6 +99,12 @@ func NewChannelServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(channelServiceMethods.ByName("ChannelAddSong")), connect.WithClientOptions(opts...), ), + channelRemoveSong: connect.NewClient[v1.ChannelRemoveSongRequest, emptypb.Empty]( + httpClient, + baseURL+ChannelServiceChannelRemoveSongProcedure, + connect.WithSchema(channelServiceMethods.ByName("ChannelRemoveSong")), + connect.WithClientOptions(opts...), + ), channelGetSongs: connect.NewClient[v1.ChannelGetSongsRequest, v1.ChannelGetSongsResponse]( httpClient, baseURL+ChannelServiceChannelGetSongsProcedure, @@ -106,11 +116,12 @@ func NewChannelServiceClient(httpClient connect.HTTPClient, baseURL string, opts // channelServiceClient implements ChannelServiceClient. type channelServiceClient struct { - createChannel *connect.Client[v1.CreateChannelRequest, v1.CreateChannelResponse] - deleteChannel *connect.Client[v1.DeleteChannelRequest, emptypb.Empty] - channelList *connect.Client[emptypb.Empty, v1.ChannelListResponse] - channelAddSong *connect.Client[v1.ChannelAddSongRequest, emptypb.Empty] - channelGetSongs *connect.Client[v1.ChannelGetSongsRequest, v1.ChannelGetSongsResponse] + createChannel *connect.Client[v1.CreateChannelRequest, v1.CreateChannelResponse] + deleteChannel *connect.Client[v1.DeleteChannelRequest, emptypb.Empty] + channelList *connect.Client[emptypb.Empty, v1.ChannelListResponse] + channelAddSong *connect.Client[v1.ChannelAddSongRequest, emptypb.Empty] + channelRemoveSong *connect.Client[v1.ChannelRemoveSongRequest, emptypb.Empty] + channelGetSongs *connect.Client[v1.ChannelGetSongsRequest, v1.ChannelGetSongsResponse] } // CreateChannel calls radio.v1.ChannelService.CreateChannel. @@ -149,6 +160,15 @@ func (c *channelServiceClient) ChannelAddSong(ctx context.Context, req *v1.Chann return nil, err } +// ChannelRemoveSong calls radio.v1.ChannelService.ChannelRemoveSong. +func (c *channelServiceClient) ChannelRemoveSong(ctx context.Context, req *v1.ChannelRemoveSongRequest) (*emptypb.Empty, error) { + response, err := c.channelRemoveSong.CallUnary(ctx, connect.NewRequest(req)) + if response != nil { + return response.Msg, err + } + return nil, err +} + // ChannelGetSongs calls radio.v1.ChannelService.ChannelGetSongs. func (c *channelServiceClient) ChannelGetSongs(ctx context.Context, req *v1.ChannelGetSongsRequest) (*v1.ChannelGetSongsResponse, error) { response, err := c.channelGetSongs.CallUnary(ctx, connect.NewRequest(req)) @@ -164,6 +184,7 @@ type ChannelServiceHandler interface { DeleteChannel(context.Context, *v1.DeleteChannelRequest) (*emptypb.Empty, error) ChannelList(context.Context, *emptypb.Empty) (*v1.ChannelListResponse, error) ChannelAddSong(context.Context, *v1.ChannelAddSongRequest) (*emptypb.Empty, error) + ChannelRemoveSong(context.Context, *v1.ChannelRemoveSongRequest) (*emptypb.Empty, error) ChannelGetSongs(context.Context, *v1.ChannelGetSongsRequest) (*v1.ChannelGetSongsResponse, error) } @@ -198,6 +219,12 @@ func NewChannelServiceHandler(svc ChannelServiceHandler, opts ...connect.Handler connect.WithSchema(channelServiceMethods.ByName("ChannelAddSong")), connect.WithHandlerOptions(opts...), ) + channelServiceChannelRemoveSongHandler := connect.NewUnaryHandlerSimple( + ChannelServiceChannelRemoveSongProcedure, + svc.ChannelRemoveSong, + connect.WithSchema(channelServiceMethods.ByName("ChannelRemoveSong")), + connect.WithHandlerOptions(opts...), + ) channelServiceChannelGetSongsHandler := connect.NewUnaryHandlerSimple( ChannelServiceChannelGetSongsProcedure, svc.ChannelGetSongs, @@ -214,6 +241,8 @@ func NewChannelServiceHandler(svc ChannelServiceHandler, opts ...connect.Handler channelServiceChannelListHandler.ServeHTTP(w, r) case ChannelServiceChannelAddSongProcedure: channelServiceChannelAddSongHandler.ServeHTTP(w, r) + case ChannelServiceChannelRemoveSongProcedure: + channelServiceChannelRemoveSongHandler.ServeHTTP(w, r) case ChannelServiceChannelGetSongsProcedure: channelServiceChannelGetSongsHandler.ServeHTTP(w, r) default: @@ -241,6 +270,10 @@ func (UnimplementedChannelServiceHandler) ChannelAddSong(context.Context, *v1.Ch return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.ChannelService.ChannelAddSong is not implemented")) } +func (UnimplementedChannelServiceHandler) ChannelRemoveSong(context.Context, *v1.ChannelRemoveSongRequest) (*emptypb.Empty, error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.ChannelService.ChannelRemoveSong is not implemented")) +} + func (UnimplementedChannelServiceHandler) ChannelGetSongs(context.Context, *v1.ChannelGetSongsRequest) (*v1.ChannelGetSongsResponse, error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.ChannelService.ChannelGetSongs is not implemented")) } diff --git a/backend/internal/channel/song.go b/backend/internal/channel/song.go index 01ca5d2..9d344a3 100644 --- a/backend/internal/channel/song.go +++ b/backend/internal/channel/song.go @@ -67,5 +67,41 @@ func (handler *ChannelHandler) ChannelAddSong(ctx context.Context, req *radiov1. err = handler.store.AddSongToChannel(ctx, songId, channelId) slog.Info("added song to channel") - return nil, nil + return nil, err +} + +func (handler *ChannelHandler) ChannelRemoveSong( + ctx context.Context, + req *radiov1.ChannelRemoveSongRequest, +) (*emptypb.Empty, error) { + user, err := user.RequireUser(ctx) + if err != nil { + return nil, err + } + + channelId, err := uuid.Parse(req.ChannelId) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("uuid not valid")) + } + + songId, err := uuid.Parse(req.SongId) + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("uuid not valid")) + } + + channel, err := handler.store.GetChannel(ctx, channelId) + if err != nil { + slog.Info("channel not found") + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("channel not found")) + } + + if user.ID != channel.OwnerID { + slog.Info("insert channel permission denied") + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("unauthorized")) + } + + err = handler.store.RemoveSongFromChannel(ctx, songId, channelId) + slog.Info("removing song from channel") + + return nil, err } diff --git a/backend/internal/store/channel.go b/backend/internal/store/channel.go index 67b660b..8002500 100644 --- a/backend/internal/store/channel.go +++ b/backend/internal/store/channel.go @@ -109,6 +109,20 @@ func (store *Store) AddSongToChannel(ctx context.Context, song_id uuid.UUID, cha return nil } +func (store *Store) RemoveSongFromChannel(ctx context.Context, song_id uuid.UUID, channel_id uuid.UUID) error { + query := ` + DELETE FROM channel_songs WHERE channel_id = $1 AND song_id = $2 + ` + + _, err := store.db.ExecContext(ctx, query, channel_id.String(), song_id.String()) + if err != nil { + slog.Error("couldnt remove song from channel", "error", err) + return fmt.Errorf("couldnt remove song from channel") + } + + return nil +} + // func (s *Store) ChannelAssignFrequency(id uuid.UUID, frequency string) error { // query := ` // ` diff --git a/frontend/src/gen/radio/v1/channel_pb.ts b/frontend/src/gen/radio/v1/channel_pb.ts index 3de3a06..f8d3f21 100644 --- a/frontend/src/gen/radio/v1/channel_pb.ts +++ b/frontend/src/gen/radio/v1/channel_pb.ts @@ -15,7 +15,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file radio/v1/channel.proto. */ export const file_radio_v1_channel: GenFile = /*@__PURE__*/ - fileDesc("ChZyYWRpby92MS9jaGFubmVsLnByb3RvEghyYWRpby52MSLEAQoHQ2hhbm5lbBIUCgJpZBgBIAEoCUIIukgFcgOwAQESGgoIb3duZXJfaWQYAiABKAlCCLpIBXIDsAEBEhEKCWZyZXF1ZW5jeRgDIAEoCRITCgtkZXNjcmlwdGlvbhgEIAEoCRIuCgpjcmVhdGVkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgttb2RpZmllZF9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiPgoUQ3JlYXRlQ2hhbm5lbFJlcXVlc3QSEQoJZnJlcXVlbmN5GAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjsKFUNyZWF0ZUNoYW5uZWxSZXNwb25zZRIiCgdjaGFubmVsGAEgASgLMhEucmFkaW8udjEuQ2hhbm5lbCIsChREZWxldGVDaGFubmVsUmVxdWVzdBIUCgJpZBgBIAEoCUIIukgFcgOwAQEiOgoTQ2hhbm5lbExpc3RSZXNwb25zZRIjCghjaGFubmVscxgBIAMoCzIRLnJhZGlvLnYxLkNoYW5uZWwiUAoVQ2hhbm5lbEFkZFNvbmdSZXF1ZXN0EhkKB3NvbmdfaWQYASABKAlCCLpIBXIDsAEBEhwKCmNoYW5uZWxfaWQYAiABKAlCCLpIBXIDsAEBIjYKFkNoYW5uZWxHZXRTb25nc1JlcXVlc3QSHAoKY2hhbm5lbF9pZBgBIAEoCUIIukgFcgOwAQEiOAoXQ2hhbm5lbEdldFNvbmdzUmVzcG9uc2USHQoFc29uZ3MYASADKAsyDi5yYWRpby52MS5Tb25nMpQDCg5DaGFubmVsU2VydmljZRJQCg1DcmVhdGVDaGFubmVsEh4ucmFkaW8udjEuQ3JlYXRlQ2hhbm5lbFJlcXVlc3QaHy5yYWRpby52MS5DcmVhdGVDaGFubmVsUmVzcG9uc2USRwoNRGVsZXRlQ2hhbm5lbBIeLnJhZGlvLnYxLkRlbGV0ZUNoYW5uZWxSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5EkQKC0NoYW5uZWxMaXN0EhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Gh0ucmFkaW8udjEuQ2hhbm5lbExpc3RSZXNwb25zZRJJCg5DaGFubmVsQWRkU29uZxIfLnJhZGlvLnYxLkNoYW5uZWxBZGRTb25nUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJWCg9DaGFubmVsR2V0U29uZ3MSIC5yYWRpby52MS5DaGFubmVsR2V0U29uZ3NSZXF1ZXN0GiEucmFkaW8udjEuQ2hhbm5lbEdldFNvbmdzUmVzcG9uc2VChQEKDGNvbS5yYWRpby52MUIMQ2hhbm5lbFByb3RvUAFaJmVjaHNlbi5jbHViL3JhZGlvL2dlbi9yYWRpby92MTtyYWRpb3YxogIDUlhYqgIIUmFkaW8uVjHKAghSYWRpb1xWMeICFFJhZGlvXFYxXEdQQk1ldGFkYXRh6gIJUmFkaW86OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_timestamp, file_radio_v1_song]); + fileDesc("ChZyYWRpby92MS9jaGFubmVsLnByb3RvEghyYWRpby52MSLEAQoHQ2hhbm5lbBIUCgJpZBgBIAEoCUIIukgFcgOwAQESGgoIb3duZXJfaWQYAiABKAlCCLpIBXIDsAEBEhEKCWZyZXF1ZW5jeRgDIAEoCRITCgtkZXNjcmlwdGlvbhgEIAEoCRIuCgpjcmVhdGVkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIvCgttb2RpZmllZF9hdBgGIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiPgoUQ3JlYXRlQ2hhbm5lbFJlcXVlc3QSEQoJZnJlcXVlbmN5GAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjsKFUNyZWF0ZUNoYW5uZWxSZXNwb25zZRIiCgdjaGFubmVsGAEgASgLMhEucmFkaW8udjEuQ2hhbm5lbCIsChREZWxldGVDaGFubmVsUmVxdWVzdBIUCgJpZBgBIAEoCUIIukgFcgOwAQEiOgoTQ2hhbm5lbExpc3RSZXNwb25zZRIjCghjaGFubmVscxgBIAMoCzIRLnJhZGlvLnYxLkNoYW5uZWwiUAoVQ2hhbm5lbEFkZFNvbmdSZXF1ZXN0EhkKB3NvbmdfaWQYASABKAlCCLpIBXIDsAEBEhwKCmNoYW5uZWxfaWQYAiABKAlCCLpIBXIDsAEBIlMKGENoYW5uZWxSZW1vdmVTb25nUmVxdWVzdBIZCgdzb25nX2lkGAEgASgJQgi6SAVyA7ABARIcCgpjaGFubmVsX2lkGAIgASgJQgi6SAVyA7ABASI2ChZDaGFubmVsR2V0U29uZ3NSZXF1ZXN0EhwKCmNoYW5uZWxfaWQYASABKAlCCLpIBXIDsAEBIjgKF0NoYW5uZWxHZXRTb25nc1Jlc3BvbnNlEh0KBXNvbmdzGAEgAygLMg4ucmFkaW8udjEuU29uZzLlAwoOQ2hhbm5lbFNlcnZpY2USUAoNQ3JlYXRlQ2hhbm5lbBIeLnJhZGlvLnYxLkNyZWF0ZUNoYW5uZWxSZXF1ZXN0Gh8ucmFkaW8udjEuQ3JlYXRlQ2hhbm5lbFJlc3BvbnNlEkcKDURlbGV0ZUNoYW5uZWwSHi5yYWRpby52MS5EZWxldGVDaGFubmVsUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRJECgtDaGFubmVsTGlzdBIWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eRodLnJhZGlvLnYxLkNoYW5uZWxMaXN0UmVzcG9uc2USSQoOQ2hhbm5lbEFkZFNvbmcSHy5yYWRpby52MS5DaGFubmVsQWRkU29uZ1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkSTwoRQ2hhbm5lbFJlbW92ZVNvbmcSIi5yYWRpby52MS5DaGFubmVsUmVtb3ZlU29uZ1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkSVgoPQ2hhbm5lbEdldFNvbmdzEiAucmFkaW8udjEuQ2hhbm5lbEdldFNvbmdzUmVxdWVzdBohLnJhZGlvLnYxLkNoYW5uZWxHZXRTb25nc1Jlc3BvbnNlQoUBCgxjb20ucmFkaW8udjFCDENoYW5uZWxQcm90b1ABWiZlY2hzZW4uY2x1Yi9yYWRpby9nZW4vcmFkaW8vdjE7cmFkaW92MaICA1JYWKoCCFJhZGlvLlYxygIIUmFkaW9cVjHiAhRSYWRpb1xWMVxHUEJNZXRhZGF0YeoCCVJhZGlvOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_timestamp, file_radio_v1_song]); /** * @generated from message radio.v1.Channel @@ -154,6 +154,28 @@ export type ChannelAddSongRequest = Message<"radio.v1.ChannelAddSongRequest"> & export const ChannelAddSongRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_radio_v1_channel, 5); +/** + * @generated from message radio.v1.ChannelRemoveSongRequest + */ +export type ChannelRemoveSongRequest = Message<"radio.v1.ChannelRemoveSongRequest"> & { + /** + * @generated from field: string song_id = 1; + */ + songId: string; + + /** + * @generated from field: string channel_id = 2; + */ + channelId: string; +}; + +/** + * Describes the message radio.v1.ChannelRemoveSongRequest. + * Use `create(ChannelRemoveSongRequestSchema)` to create a new message. + */ +export const ChannelRemoveSongRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 6); + /** * @generated from message radio.v1.ChannelGetSongsRequest */ @@ -169,7 +191,7 @@ export type ChannelGetSongsRequest = Message<"radio.v1.ChannelGetSongsRequest"> * Use `create(ChannelGetSongsRequestSchema)` to create a new message. */ export const ChannelGetSongsRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_radio_v1_channel, 6); + messageDesc(file_radio_v1_channel, 7); /** * @generated from message radio.v1.ChannelGetSongsResponse @@ -186,7 +208,7 @@ export type ChannelGetSongsResponse = Message<"radio.v1.ChannelGetSongsResponse" * Use `create(ChannelGetSongsResponseSchema)` to create a new message. */ export const ChannelGetSongsResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_radio_v1_channel, 7); + messageDesc(file_radio_v1_channel, 8); /** * @generated from service radio.v1.ChannelService @@ -224,6 +246,14 @@ export const ChannelService: GenService<{ input: typeof ChannelAddSongRequestSchema; output: typeof EmptySchema; }, + /** + * @generated from rpc radio.v1.ChannelService.ChannelRemoveSong + */ + channelRemoveSong: { + methodKind: "unary"; + input: typeof ChannelRemoveSongRequestSchema; + output: typeof EmptySchema; + }, /** * @generated from rpc radio.v1.ChannelService.ChannelGetSongs */ diff --git a/frontend/src/lib/components/TerminalList.svelte b/frontend/src/lib/components/TerminalList.svelte index abfbe59..806c797 100644 --- a/frontend/src/lib/components/TerminalList.svelte +++ b/frontend/src/lib/components/TerminalList.svelte @@ -6,10 +6,11 @@ meta?: string; } - let { items, onSelect, onDelete, activeId } = $props<{ + let { items, onSelect, onDelete, deleteLabel = 'DELETE', activeId } = $props<{ items: Item[]; onSelect: (id: string) => void; onDelete?: (id: string) => void; + deleteLabel?: string; activeId?: string; }>(); @@ -37,9 +38,9 @@ {/if} diff --git a/frontend/src/routes/[channelId]/+page.svelte b/frontend/src/routes/[channelId]/+page.svelte index a7d6c3d..f6218b9 100644 --- a/frontend/src/routes/[channelId]/+page.svelte +++ b/frontend/src/routes/[channelId]/+page.svelte @@ -4,7 +4,7 @@ import TerminalList from '$lib/components/TerminalList.svelte'; import AddSongSection from '$lib/components/AddSongSection.svelte'; import TerminalConfirm from '$lib/components/TerminalConfirm.svelte'; - import { channelClient, songClient } from '$lib/rpc'; + import { channelClient } from '$lib/rpc'; import { goto, invalidateAll } from '$app/navigation'; interface Song { @@ -64,10 +64,13 @@ async function executeDeleteSong() { if (!songToDeleteId) return; try { - await songClient.deleteSong({ songId: songToDeleteId }); + await channelClient.channelRemoveSong({ + channelId, + songId: songToDeleteId + }); await invalidateAll(); } catch (err) { - console.error('Failed to delete song:', err); + console.error('Failed to remove song:', err); } finally { songToDeleteId = null; } @@ -107,6 +110,7 @@ items={items} onSelect={handleSelect} onDelete={isOwner ? confirmDeleteSong : undefined} + deleteLabel="REMOVE" activeId={(player.currentTrack as any)?.id} /> {/if} @@ -121,7 +125,7 @@ (songToDeleteId = null)} /> diff --git a/proto/radio/v1/channel.proto b/proto/radio/v1/channel.proto index df17a6d..1131338 100644 --- a/proto/radio/v1/channel.proto +++ b/proto/radio/v1/channel.proto @@ -39,6 +39,11 @@ message ChannelAddSongRequest { string channel_id = 2 [(buf.validate.field).string.uuid = true]; } +message ChannelRemoveSongRequest { + string song_id = 1 [(buf.validate.field).string.uuid = true]; + string channel_id = 2 [(buf.validate.field).string.uuid = true]; +} + message ChannelGetSongsRequest { string channel_id = 1 [(buf.validate.field).string.uuid = true]; } @@ -52,5 +57,6 @@ service ChannelService { rpc DeleteChannel(DeleteChannelRequest) returns (google.protobuf.Empty); rpc ChannelList(google.protobuf.Empty) returns (ChannelListResponse); rpc ChannelAddSong(ChannelAddSongRequest) returns (google.protobuf.Empty); + rpc ChannelRemoveSong(ChannelRemoveSongRequest) returns (google.protobuf.Empty); rpc ChannelGetSongs(ChannelGetSongsRequest) returns (ChannelGetSongsResponse); }