diff --git a/backend/gen/radio/v1/channel.pb.go b/backend/gen/radio/v1/channel.pb.go index c63302b..8bf416a 100644 --- a/backend/gen/radio/v1/channel.pb.go +++ b/backend/gen/radio/v1/channel.pb.go @@ -248,6 +248,50 @@ func (x *DeleteChannelRequest) GetId() string { return "" } +type ChannelListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Channels []*Channel `protobuf:"bytes,1,rep,name=channels,proto3" json:"channels,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelListResponse) Reset() { + *x = ChannelListResponse{} + mi := &file_radio_v1_channel_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelListResponse) ProtoMessage() {} + +func (x *ChannelListResponse) ProtoReflect() protoreflect.Message { + mi := &file_radio_v1_channel_proto_msgTypes[4] + 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 ChannelListResponse.ProtoReflect.Descriptor instead. +func (*ChannelListResponse) Descriptor() ([]byte, []int) { + return file_radio_v1_channel_proto_rawDescGZIP(), []int{4} +} + +func (x *ChannelListResponse) GetChannels() []*Channel { + if x != nil { + return x.Channels + } + return nil +} + type ChannelAddSongRequest struct { state protoimpl.MessageState `protogen:"open.v1"` SongId string `protobuf:"bytes,1,opt,name=song_id,json=songId,proto3" json:"song_id,omitempty"` @@ -258,7 +302,7 @@ type ChannelAddSongRequest struct { func (x *ChannelAddSongRequest) Reset() { *x = ChannelAddSongRequest{} - mi := &file_radio_v1_channel_proto_msgTypes[4] + mi := &file_radio_v1_channel_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -270,7 +314,7 @@ func (x *ChannelAddSongRequest) String() string { func (*ChannelAddSongRequest) ProtoMessage() {} func (x *ChannelAddSongRequest) ProtoReflect() protoreflect.Message { - mi := &file_radio_v1_channel_proto_msgTypes[4] + mi := &file_radio_v1_channel_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -283,7 +327,7 @@ func (x *ChannelAddSongRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChannelAddSongRequest.ProtoReflect.Descriptor instead. func (*ChannelAddSongRequest) Descriptor() ([]byte, []int) { - return file_radio_v1_channel_proto_rawDescGZIP(), []int{4} + return file_radio_v1_channel_proto_rawDescGZIP(), []int{5} } func (x *ChannelAddSongRequest) GetSongId() string { @@ -309,7 +353,7 @@ type ChannelGetSongsRequest struct { func (x *ChannelGetSongsRequest) Reset() { *x = ChannelGetSongsRequest{} - mi := &file_radio_v1_channel_proto_msgTypes[5] + mi := &file_radio_v1_channel_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -321,7 +365,7 @@ func (x *ChannelGetSongsRequest) String() string { func (*ChannelGetSongsRequest) ProtoMessage() {} func (x *ChannelGetSongsRequest) ProtoReflect() protoreflect.Message { - mi := &file_radio_v1_channel_proto_msgTypes[5] + mi := &file_radio_v1_channel_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -334,7 +378,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{5} + return file_radio_v1_channel_proto_rawDescGZIP(), []int{6} } func (x *ChannelGetSongsRequest) GetChannelId() string { @@ -353,7 +397,7 @@ type ChannelGetSongsResponse struct { func (x *ChannelGetSongsResponse) Reset() { *x = ChannelGetSongsResponse{} - 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 +409,7 @@ func (x *ChannelGetSongsResponse) String() string { func (*ChannelGetSongsResponse) ProtoMessage() {} func (x *ChannelGetSongsResponse) 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 +422,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{6} + return file_radio_v1_channel_proto_rawDescGZIP(), []int{7} } func (x *ChannelGetSongsResponse) GetSongs() []*Song { @@ -408,7 +452,9 @@ const file_radio_v1_channel_proto_rawDesc = "" + "\x15CreateChannelResponse\x12+\n" + "\achannel\x18\x01 \x01(\v2\x11.radio.v1.ChannelR\achannel\"0\n" + "\x14DeleteChannelRequest\x12\x18\n" + - "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"c\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"D\n" + + "\x13ChannelListResponse\x12-\n" + + "\bchannels\x18\x01 \x03(\v2\x11.radio.v1.ChannelR\bchannels\"c\n" + "\x15ChannelAddSongRequest\x12!\n" + "\asong_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06songId\x12'\n" + "\n" + @@ -417,11 +463,13 @@ const file_radio_v1_channel_proto_rawDesc = "" + "\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\xf6\x01\n" + + "\x05songs\x18\x01 \x03(\v2\x0e.radio.v1.SongR\x05songs2\x94\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\x12I\n" + - "\x0eChannelAddSong\x12\x1f.radio.v1.ChannelAddSongRequest\x1a\x16.google.protobuf.EmptyB\x85\x01\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" + + "\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" var ( @@ -436,35 +484,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, 7) +var file_radio_v1_channel_proto_msgTypes = make([]protoimpl.MessageInfo, 8) 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 - (*ChannelAddSongRequest)(nil), // 4: radio.v1.ChannelAddSongRequest - (*ChannelGetSongsRequest)(nil), // 5: radio.v1.ChannelGetSongsRequest - (*ChannelGetSongsResponse)(nil), // 6: radio.v1.ChannelGetSongsResponse - (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp - (*Song)(nil), // 8: radio.v1.Song - (*emptypb.Empty)(nil), // 9: google.protobuf.Empty + (*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 } var file_radio_v1_channel_proto_depIdxs = []int32{ - 7, // 0: radio.v1.Channel.created_at:type_name -> google.protobuf.Timestamp - 7, // 1: radio.v1.Channel.modified_at:type_name -> google.protobuf.Timestamp - 0, // 2: radio.v1.CreateChannelResponse.channel:type_name -> radio.v1.Channel - 8, // 3: radio.v1.ChannelGetSongsResponse.songs:type_name -> radio.v1.Song - 1, // 4: radio.v1.ChannelService.CreateChannel:input_type -> radio.v1.CreateChannelRequest - 3, // 5: radio.v1.ChannelService.DeleteChannel:input_type -> radio.v1.DeleteChannelRequest - 4, // 6: radio.v1.ChannelService.ChannelAddSong:input_type -> radio.v1.ChannelAddSongRequest - 2, // 7: radio.v1.ChannelService.CreateChannel:output_type -> radio.v1.CreateChannelResponse - 9, // 8: radio.v1.ChannelService.DeleteChannel:output_type -> google.protobuf.Empty - 9, // 9: radio.v1.ChannelService.ChannelAddSong:output_type -> google.protobuf.Empty - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 8, // 0: radio.v1.Channel.created_at:type_name -> google.protobuf.Timestamp + 8, // 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 + 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 + 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 + 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 } func init() { file_radio_v1_channel_proto_init() } @@ -479,7 +533,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: 7, + NumMessages: 8, 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 39219b2..6e5e548 100644 --- a/backend/gen/radio/v1/radiov1connect/channel.connect.go +++ b/backend/gen/radio/v1/radiov1connect/channel.connect.go @@ -40,16 +40,24 @@ const ( // ChannelServiceDeleteChannelProcedure is the fully-qualified name of the ChannelService's // DeleteChannel RPC. ChannelServiceDeleteChannelProcedure = "/radio.v1.ChannelService/DeleteChannel" + // ChannelServiceChannelListProcedure is the fully-qualified name of the ChannelService's + // ChannelList RPC. + ChannelServiceChannelListProcedure = "/radio.v1.ChannelService/ChannelList" // ChannelServiceChannelAddSongProcedure is the fully-qualified name of the ChannelService's // ChannelAddSong RPC. ChannelServiceChannelAddSongProcedure = "/radio.v1.ChannelService/ChannelAddSong" + // ChannelServiceChannelGetSongsProcedure is the fully-qualified name of the ChannelService's + // ChannelGetSongs RPC. + ChannelServiceChannelGetSongsProcedure = "/radio.v1.ChannelService/ChannelGetSongs" ) // ChannelServiceClient is a client for the radio.v1.ChannelService service. type ChannelServiceClient interface { CreateChannel(context.Context, *v1.CreateChannelRequest) (*v1.CreateChannelResponse, error) DeleteChannel(context.Context, *v1.DeleteChannelRequest) (*emptypb.Empty, error) + ChannelList(context.Context, *emptypb.Empty) (*v1.ChannelListResponse, error) ChannelAddSong(context.Context, *v1.ChannelAddSongRequest) (*emptypb.Empty, error) + ChannelGetSongs(context.Context, *v1.ChannelGetSongsRequest) (*v1.ChannelGetSongsResponse, error) } // NewChannelServiceClient constructs a client for the radio.v1.ChannelService service. By default, @@ -75,20 +83,34 @@ func NewChannelServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(channelServiceMethods.ByName("DeleteChannel")), connect.WithClientOptions(opts...), ), + channelList: connect.NewClient[emptypb.Empty, v1.ChannelListResponse]( + httpClient, + baseURL+ChannelServiceChannelListProcedure, + connect.WithSchema(channelServiceMethods.ByName("ChannelList")), + connect.WithClientOptions(opts...), + ), channelAddSong: connect.NewClient[v1.ChannelAddSongRequest, emptypb.Empty]( httpClient, baseURL+ChannelServiceChannelAddSongProcedure, connect.WithSchema(channelServiceMethods.ByName("ChannelAddSong")), connect.WithClientOptions(opts...), ), + channelGetSongs: connect.NewClient[v1.ChannelGetSongsRequest, v1.ChannelGetSongsResponse]( + httpClient, + baseURL+ChannelServiceChannelGetSongsProcedure, + connect.WithSchema(channelServiceMethods.ByName("ChannelGetSongs")), + connect.WithClientOptions(opts...), + ), } } // channelServiceClient implements ChannelServiceClient. type channelServiceClient struct { - createChannel *connect.Client[v1.CreateChannelRequest, v1.CreateChannelResponse] - deleteChannel *connect.Client[v1.DeleteChannelRequest, emptypb.Empty] - channelAddSong *connect.Client[v1.ChannelAddSongRequest, emptypb.Empty] + 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 calls radio.v1.ChannelService.CreateChannel. @@ -109,6 +131,15 @@ func (c *channelServiceClient) DeleteChannel(ctx context.Context, req *v1.Delete return nil, err } +// ChannelList calls radio.v1.ChannelService.ChannelList. +func (c *channelServiceClient) ChannelList(ctx context.Context, req *emptypb.Empty) (*v1.ChannelListResponse, error) { + response, err := c.channelList.CallUnary(ctx, connect.NewRequest(req)) + if response != nil { + return response.Msg, err + } + return nil, err +} + // ChannelAddSong calls radio.v1.ChannelService.ChannelAddSong. func (c *channelServiceClient) ChannelAddSong(ctx context.Context, req *v1.ChannelAddSongRequest) (*emptypb.Empty, error) { response, err := c.channelAddSong.CallUnary(ctx, connect.NewRequest(req)) @@ -118,11 +149,22 @@ func (c *channelServiceClient) ChannelAddSong(ctx context.Context, req *v1.Chann 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)) + if response != nil { + return response.Msg, err + } + return nil, err +} + // ChannelServiceHandler is an implementation of the radio.v1.ChannelService service. type ChannelServiceHandler interface { CreateChannel(context.Context, *v1.CreateChannelRequest) (*v1.CreateChannelResponse, error) DeleteChannel(context.Context, *v1.DeleteChannelRequest) (*emptypb.Empty, error) + ChannelList(context.Context, *emptypb.Empty) (*v1.ChannelListResponse, error) ChannelAddSong(context.Context, *v1.ChannelAddSongRequest) (*emptypb.Empty, error) + ChannelGetSongs(context.Context, *v1.ChannelGetSongsRequest) (*v1.ChannelGetSongsResponse, error) } // NewChannelServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -144,20 +186,36 @@ func NewChannelServiceHandler(svc ChannelServiceHandler, opts ...connect.Handler connect.WithSchema(channelServiceMethods.ByName("DeleteChannel")), connect.WithHandlerOptions(opts...), ) + channelServiceChannelListHandler := connect.NewUnaryHandlerSimple( + ChannelServiceChannelListProcedure, + svc.ChannelList, + connect.WithSchema(channelServiceMethods.ByName("ChannelList")), + connect.WithHandlerOptions(opts...), + ) channelServiceChannelAddSongHandler := connect.NewUnaryHandlerSimple( ChannelServiceChannelAddSongProcedure, svc.ChannelAddSong, connect.WithSchema(channelServiceMethods.ByName("ChannelAddSong")), connect.WithHandlerOptions(opts...), ) + channelServiceChannelGetSongsHandler := connect.NewUnaryHandlerSimple( + ChannelServiceChannelGetSongsProcedure, + svc.ChannelGetSongs, + connect.WithSchema(channelServiceMethods.ByName("ChannelGetSongs")), + connect.WithHandlerOptions(opts...), + ) return "/radio.v1.ChannelService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case ChannelServiceCreateChannelProcedure: channelServiceCreateChannelHandler.ServeHTTP(w, r) case ChannelServiceDeleteChannelProcedure: channelServiceDeleteChannelHandler.ServeHTTP(w, r) + case ChannelServiceChannelListProcedure: + channelServiceChannelListHandler.ServeHTTP(w, r) case ChannelServiceChannelAddSongProcedure: channelServiceChannelAddSongHandler.ServeHTTP(w, r) + case ChannelServiceChannelGetSongsProcedure: + channelServiceChannelGetSongsHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -175,6 +233,14 @@ func (UnimplementedChannelServiceHandler) DeleteChannel(context.Context, *v1.Del return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.ChannelService.DeleteChannel is not implemented")) } +func (UnimplementedChannelServiceHandler) ChannelList(context.Context, *emptypb.Empty) (*v1.ChannelListResponse, error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.ChannelService.ChannelList is not implemented")) +} + func (UnimplementedChannelServiceHandler) ChannelAddSong(context.Context, *v1.ChannelAddSongRequest) (*emptypb.Empty, error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.ChannelService.ChannelAddSong 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/gen/radio/v1/radiov1connect/user.connect.go b/backend/gen/radio/v1/radiov1connect/user.connect.go new file mode 100644 index 0000000..e830c24 --- /dev/null +++ b/backend/gen/radio/v1/radiov1connect/user.connect.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: radio/v1/user.proto + +package radiov1connect + +import ( + connect "connectrpc.com/connect" + context "context" + v1 "echsen.club/radio/gen/radio/v1" + errors "errors" + emptypb "google.golang.org/protobuf/types/known/emptypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // UserServiceName is the fully-qualified name of the UserService service. + UserServiceName = "radio.v1.UserService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // UserServiceGetCurrentUserProcedure is the fully-qualified name of the UserService's + // GetCurrentUser RPC. + UserServiceGetCurrentUserProcedure = "/radio.v1.UserService/GetCurrentUser" +) + +// UserServiceClient is a client for the radio.v1.UserService service. +type UserServiceClient interface { + GetCurrentUser(context.Context, *emptypb.Empty) (*v1.GetCurrentUserResponse, error) +} + +// NewUserServiceClient constructs a client for the radio.v1.UserService service. By default, it +// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) UserServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + userServiceMethods := v1.File_radio_v1_user_proto.Services().ByName("UserService").Methods() + return &userServiceClient{ + getCurrentUser: connect.NewClient[emptypb.Empty, v1.GetCurrentUserResponse]( + httpClient, + baseURL+UserServiceGetCurrentUserProcedure, + connect.WithSchema(userServiceMethods.ByName("GetCurrentUser")), + connect.WithClientOptions(opts...), + ), + } +} + +// userServiceClient implements UserServiceClient. +type userServiceClient struct { + getCurrentUser *connect.Client[emptypb.Empty, v1.GetCurrentUserResponse] +} + +// GetCurrentUser calls radio.v1.UserService.GetCurrentUser. +func (c *userServiceClient) GetCurrentUser(ctx context.Context, req *emptypb.Empty) (*v1.GetCurrentUserResponse, error) { + response, err := c.getCurrentUser.CallUnary(ctx, connect.NewRequest(req)) + if response != nil { + return response.Msg, err + } + return nil, err +} + +// UserServiceHandler is an implementation of the radio.v1.UserService service. +type UserServiceHandler interface { + GetCurrentUser(context.Context, *emptypb.Empty) (*v1.GetCurrentUserResponse, error) +} + +// NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path +// on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + userServiceMethods := v1.File_radio_v1_user_proto.Services().ByName("UserService").Methods() + userServiceGetCurrentUserHandler := connect.NewUnaryHandlerSimple( + UserServiceGetCurrentUserProcedure, + svc.GetCurrentUser, + connect.WithSchema(userServiceMethods.ByName("GetCurrentUser")), + connect.WithHandlerOptions(opts...), + ) + return "/radio.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case UserServiceGetCurrentUserProcedure: + userServiceGetCurrentUserHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedUserServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedUserServiceHandler struct{} + +func (UnimplementedUserServiceHandler) GetCurrentUser(context.Context, *emptypb.Empty) (*v1.GetCurrentUserResponse, error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("radio.v1.UserService.GetCurrentUser is not implemented")) +} diff --git a/backend/gen/radio/v1/user.pb.go b/backend/gen/radio/v1/user.pb.go new file mode 100644 index 0000000..6aa36b0 --- /dev/null +++ b/backend/gen/radio/v1/user.pb.go @@ -0,0 +1,196 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc (unknown) +// source: radio/v1/user.proto + +package radiov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + DisplayName string `protobuf:"bytes,3,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_radio_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_radio_v1_user_proto_msgTypes[0] + 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 User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_radio_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +type GetCurrentUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentUserResponse) Reset() { + *x = GetCurrentUserResponse{} + mi := &file_radio_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentUserResponse) ProtoMessage() {} + +func (x *GetCurrentUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_radio_v1_user_proto_msgTypes[1] + 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 GetCurrentUserResponse.ProtoReflect.Descriptor instead. +func (*GetCurrentUserResponse) Descriptor() ([]byte, []int) { + return file_radio_v1_user_proto_rawDescGZIP(), []int{1} +} + +func (x *GetCurrentUserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +var File_radio_v1_user_proto protoreflect.FileDescriptor + +const file_radio_v1_user_proto_rawDesc = "" + + "\n" + + "\x13radio/v1/user.proto\x12\bradio.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1bgoogle/protobuf/empty.proto\"O\n" + + "\x04User\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12!\n" + + "\fdisplay_name\x18\x03 \x01(\tR\vdisplayName\"<\n" + + "\x16GetCurrentUserResponse\x12\"\n" + + "\x04user\x18\x01 \x01(\v2\x0e.radio.v1.UserR\x04user2Y\n" + + "\vUserService\x12J\n" + + "\x0eGetCurrentUser\x12\x16.google.protobuf.Empty\x1a .radio.v1.GetCurrentUserResponseB\x82\x01\n" + + "\fcom.radio.v1B\tUserProtoP\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" + +var ( + file_radio_v1_user_proto_rawDescOnce sync.Once + file_radio_v1_user_proto_rawDescData []byte +) + +func file_radio_v1_user_proto_rawDescGZIP() []byte { + file_radio_v1_user_proto_rawDescOnce.Do(func() { + file_radio_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_radio_v1_user_proto_rawDesc), len(file_radio_v1_user_proto_rawDesc))) + }) + return file_radio_v1_user_proto_rawDescData +} + +var file_radio_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_radio_v1_user_proto_goTypes = []any{ + (*User)(nil), // 0: radio.v1.User + (*GetCurrentUserResponse)(nil), // 1: radio.v1.GetCurrentUserResponse + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty +} +var file_radio_v1_user_proto_depIdxs = []int32{ + 0, // 0: radio.v1.GetCurrentUserResponse.user:type_name -> radio.v1.User + 2, // 1: radio.v1.UserService.GetCurrentUser:input_type -> google.protobuf.Empty + 1, // 2: radio.v1.UserService.GetCurrentUser:output_type -> radio.v1.GetCurrentUserResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_radio_v1_user_proto_init() } +func file_radio_v1_user_proto_init() { + if File_radio_v1_user_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_radio_v1_user_proto_rawDesc), len(file_radio_v1_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_radio_v1_user_proto_goTypes, + DependencyIndexes: file_radio_v1_user_proto_depIdxs, + MessageInfos: file_radio_v1_user_proto_msgTypes, + }.Build() + File_radio_v1_user_proto = out.File + file_radio_v1_user_proto_goTypes = nil + file_radio_v1_user_proto_depIdxs = nil +} diff --git a/backend/internal/channel/create.go b/backend/internal/channel/create.go index bca1ed1..11d7fe2 100644 --- a/backend/internal/channel/create.go +++ b/backend/internal/channel/create.go @@ -23,7 +23,7 @@ func (handler *ChannelHandler) CreateChannel( return nil, err } - c, err := handler.store.CreateChannel(ctx, user.ID, req.Frequency, "insane Description") + c, err := handler.store.CreateChannel(ctx, user.ID, req.Frequency, req.Description) if err != nil { if errors.Is(err, s.ErrFrequencyExists) { return nil, connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("Frequency already exists in database")) diff --git a/backend/internal/channel/handler.go b/backend/internal/channel/handler.go index 2b36902..2f82604 100644 --- a/backend/internal/channel/handler.go +++ b/backend/internal/channel/handler.go @@ -1,7 +1,15 @@ package channel import ( + "context" + "errors" + "log/slog" + + "connectrpc.com/connect" + radiov1 "echsen.club/radio/gen/radio/v1" s "echsen.club/radio/internal/store" + "echsen.club/radio/internal/user" + "google.golang.org/protobuf/types/known/emptypb" ) type ChannelHandler struct { @@ -13,3 +21,26 @@ func NewHandler(store *s.Store) *ChannelHandler { store: store, } } + +func (handler *ChannelHandler) ChannelList( + ctx context.Context, + empty *emptypb.Empty, +) (*radiov1.ChannelListResponse, error) { + _, err := user.RequireUser(ctx) + if err != nil { + return nil, err + } + + channels, err := handler.store.ChannelList(ctx) + if err != nil { + slog.Error("couldnt list channels") + return nil, connect.NewError(connect.CodeInternal, errors.New("couldnt list channels")) + } + + pbChannels := make([]*radiov1.Channel, len(channels)) + for i, s := range channels { + pbChannels[i] = s.ToProto() + } + + return &radiov1.ChannelListResponse{Channels: pbChannels}, nil +} diff --git a/backend/internal/channel/song.go b/backend/internal/channel/song.go index 948098e..01ca5d2 100644 --- a/backend/internal/channel/song.go +++ b/backend/internal/channel/song.go @@ -2,7 +2,9 @@ package channel import ( "context" + "errors" "fmt" + "log/slog" "connectrpc.com/connect" radiov1 "echsen.club/radio/gen/radio/v1" @@ -19,10 +21,14 @@ func (handler *ChannelHandler) ChannelGetSongs(ctx context.Context, req *radiov1 channel_id, err := uuid.Parse(req.ChannelId) if err != nil { - + return nil, err } songs, err := handler.store.GetSongsFromChannel(ctx, channel_id) + if err != nil { + slog.Error("couldnt get songs from channel") + return nil, connect.NewError(connect.CodeInternal, errors.New("couldnt list songs")) + } pbSongs := make([]*radiov1.Song, len(songs)) for i, s := range songs { pbSongs[i] = s.ToProto() @@ -49,14 +55,17 @@ func (handler *ChannelHandler) ChannelAddSong(ctx context.Context, req *radiov1. 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 { - return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("unautherized")) + slog.Info("insert channel permission denied") + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("unauthorized")) } err = handler.store.AddSongToChannel(ctx, songId, channelId) + slog.Info("added song to channel") return nil, nil } diff --git a/backend/internal/models/channel.go b/backend/internal/models/channel.go index 6118531..a3665c2 100644 --- a/backend/internal/models/channel.go +++ b/backend/internal/models/channel.go @@ -3,7 +3,9 @@ package models import ( "time" + radiov1 "echsen.club/radio/gen/radio/v1" "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" ) type Channel struct { @@ -14,3 +16,14 @@ type Channel struct { CreatedAt time.Time `db:"created_at"` ModifiedAt time.Time `db:"modified_at"` } + +func (c Channel) ToProto() *radiov1.Channel { + return &radiov1.Channel{ + Id: c.ID.String(), + OwnerId: c.ID.String(), + Frequency: c.Frequency, + Description: c.Description, + CreatedAt: timestamppb.New(c.CreatedAt), + ModifiedAt: timestamppb.New(c.ModifiedAt), + } +} diff --git a/backend/internal/models/song.go b/backend/internal/models/song.go index a3b5f9b..a3bde1a 100644 --- a/backend/internal/models/song.go +++ b/backend/internal/models/song.go @@ -19,8 +19,11 @@ type Song struct { func (s Song) ToProto() *radiov1.Song { return &radiov1.Song{ - Id: s.ID.String(), - Name: s.Name, - CreatedAt: timestamppb.New(s.CreatedAt), + Id: s.ID.String(), + OwnerId: s.OwnerID.String(), + Filename: s.Filename, + Name: s.Name, + CreatedAt: timestamppb.New(s.CreatedAt), + ModifiedAt: timestamppb.New(s.ModifiedAt), } } diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index c488943..1da0121 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -3,6 +3,7 @@ package models import ( "time" + radiov1 "echsen.club/radio/gen/radio/v1" "github.com/google/uuid" ) @@ -13,3 +14,11 @@ type User struct { DisplayName string `db:"display_name"` CreatedAt time.Time `db:"created_at"` } + +func (u User) ToProto() *radiov1.User { + return &radiov1.User{ + Id: u.ID.String(), + Email: u.Email, + DisplayName: u.DisplayName, + } +} diff --git a/backend/internal/oauth/oauth.go b/backend/internal/oauth/oauth.go index d9d428a..ac6a6f1 100644 --- a/backend/internal/oauth/oauth.go +++ b/backend/internal/oauth/oauth.go @@ -107,8 +107,6 @@ func (s *Oauth) LoginHandler(w http.ResponseWriter, r *http.Request) { setCallbackCookie(w, r, "state", state) setCallbackCookie(w, r, "nonce", nonce) - - http.Redirect(w, r, s.Oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound) } type UserClaims struct { @@ -167,6 +165,8 @@ func (s *Oauth) CallbackHandler(onSuccess func(w http.ResponseWriter, r *http.Re Email: claims.Email, Name: claims.Name, }) + + http.Redirect(w, r, "http://localhost:5173", http.StatusTemporaryRedirect) } } diff --git a/backend/internal/song/create.go b/backend/internal/song/create.go index 7dab6b1..cec522a 100644 --- a/backend/internal/song/create.go +++ b/backend/internal/song/create.go @@ -56,6 +56,7 @@ func (handler *SongHandler) UploadSong(ctx context.Context, stream *connect.Clie } if err := stream.Err(); err != nil { + slog.Error("stream error", "error", err) return nil, connect.NewError(connect.CodeUnknown, err) } @@ -69,6 +70,7 @@ func (handler *SongHandler) UploadSong(ctx context.Context, stream *connect.Clie Size: totalBytes, } + slog.Info("song upload succeded") return res, nil } diff --git a/backend/internal/state/middleware.go b/backend/internal/state/middleware.go index aae02c6..93805af 100644 --- a/backend/internal/state/middleware.go +++ b/backend/internal/state/middleware.go @@ -83,5 +83,5 @@ func (state *State) handleAuthSuccess(w http.ResponseWriter, r *http.Request, cl return } - http.Redirect(w, r, "/", http.StatusFound) + http.Redirect(w, r, "http://localhost:5173", http.StatusFound) } diff --git a/backend/internal/state/state.go b/backend/internal/state/state.go index 1e3d785..c9c8596 100644 --- a/backend/internal/state/state.go +++ b/backend/internal/state/state.go @@ -12,6 +12,7 @@ import ( "echsen.club/radio/internal/oauth" "echsen.club/radio/internal/song" "echsen.club/radio/internal/store" + "echsen.club/radio/internal/user" "github.com/jmoiron/sqlx" ) @@ -23,6 +24,7 @@ type State struct { store *store.Store channelHandler *channel.ChannelHandler songHandler *song.SongHandler + userHandler *user.UserHandler } func New(db *sqlx.DB, config *config.Config) *State { @@ -36,6 +38,7 @@ func New(db *sqlx.DB, config *config.Config) *State { channelHandler := channel.NewHandler(store) songHandler := song.NewHandler(store, config) + userHandler := user.NewHandler(store, config) state := &State{ db: db, @@ -45,6 +48,7 @@ func New(db *sqlx.DB, config *config.Config) *State { store: store, channelHandler: channelHandler, songHandler: songHandler, + userHandler: userHandler, } state.RegisterHandlers() @@ -69,4 +73,6 @@ func (state *State) RegisterHandlers() { state.mux.Handle(radiov1connect.NewChannelServiceHandler(state.channelHandler, connect.WithInterceptors(state.validateInterceptor))) state.mux.Handle(radiov1connect.NewSongServiceHandler(state.songHandler, connect.WithInterceptors(state.validateInterceptor))) + state.mux.Handle(radiov1connect.NewUserServiceHandler(state.userHandler, connect.WithInterceptors(state.validateInterceptor))) + } diff --git a/backend/internal/store/channel.go b/backend/internal/store/channel.go index a347ae1..67b660b 100644 --- a/backend/internal/store/channel.go +++ b/backend/internal/store/channel.go @@ -71,21 +71,30 @@ func (s *Store) GetChannel(ctx context.Context, id uuid.UUID) (*m.Channel, error return &c, nil } +func (s *Store) ChannelList(ctx context.Context) ([]m.Channel, error) { + var channels []m.Channel + query := ` + SELECT * FROM channels + ` + err := s.db.SelectContext(ctx, &channels, query) + + return channels, err +} + func (s *Store) GetSongsFromChannel(ctx context.Context, id uuid.UUID) ([]m.Song, error) { var songs []m.Song query := ` - SELECT * + SELECT s.* FROM songs s JOIN channel_songs cs ON s.id = cs.song_id WHERE cs.channel_id = $1 ` - err := s.db.GetContext(ctx, &songs, query, id.String()) + err := s.db.SelectContext(ctx, &songs, query, id.String()) return songs, err } func (store *Store) AddSongToChannel(ctx context.Context, song_id uuid.UUID, channel_id uuid.UUID) error { - // TODO: query := ` INSERT INTO channel_songs (channel_id, song_id) VALUES ($1, $2) @@ -93,26 +102,13 @@ func (store *Store) AddSongToChannel(ctx context.Context, song_id uuid.UUID, cha _, err := store.db.ExecContext(ctx, query, channel_id.String(), song_id.String()) if err != nil { + slog.Error("couldnt add song to channel", "error", err) return fmt.Errorf("couldnt add song to channel") } return nil } -func (store *Store) DeleteSong(ctx context.Context, song_id uuid.UUID) error { - query := ` - DELETE FROM songs WHERE id=$1; - ` - - _, err := store.db.ExecContext(ctx, query, song_id) - if err != nil { - slog.Error("couldnt delete song from database") - return fmt.Errorf("couldnt delete song") - } - - return nil -} - // func (s *Store) ChannelAssignFrequency(id uuid.UUID, frequency string) error { // query := ` // ` diff --git a/backend/internal/store/song.go b/backend/internal/store/song.go index 87f757c..8a82ea7 100644 --- a/backend/internal/store/song.go +++ b/backend/internal/store/song.go @@ -20,10 +20,10 @@ func (store *Store) CreateSong(ctx context.Context, owner_id uuid.UUID, filename query := ` INSERT INTO songs (owner_id, filename, name) VALUES ($1, $2, $3) - RETURNING id, owner_id, filename, name, created_at, modified_at + RETURNING *; ` - err := store.db.QueryRowContext(ctx, query, owner_id, filename).Scan(&song) + err := store.db.QueryRowxContext(ctx, query, owner_id, filename, filename).StructScan(&song) if err != nil { var pqErr *pq.Error @@ -33,7 +33,7 @@ func (store *Store) CreateSong(ctx context.Context, owner_id uuid.UUID, filename return nil, ErrSongExists } } - slog.Error("Error inserting song into database") + slog.Error("Error inserting song into database", "error", err) return nil, fmt.Errorf("failed to insert song into database") } slog.Info("Insert song into database") @@ -41,6 +41,20 @@ func (store *Store) CreateSong(ctx context.Context, owner_id uuid.UUID, filename return &song, nil } +func (store *Store) DeleteSong(ctx context.Context, song_id uuid.UUID) error { + query := ` + DELETE FROM songs WHERE id=$1; + ` + + _, err := store.db.ExecContext(ctx, query, song_id) + if err != nil { + slog.Error("couldnt delete song from database") + return fmt.Errorf("couldnt delete song") + } + + return nil +} + var ErrSongDoesntExist = errors.New("song doesnt exist") func (store *Store) GetSong(ctx context.Context, song_id uuid.UUID) (*m.Song, error) { diff --git a/backend/internal/user/handler.go b/backend/internal/user/handler.go new file mode 100644 index 0000000..20f4104 --- /dev/null +++ b/backend/internal/user/handler.go @@ -0,0 +1,27 @@ +package user + +import ( + "context" + + radiov1 "echsen.club/radio/gen/radio/v1" + "echsen.club/radio/internal/config" + s "echsen.club/radio/internal/store" + "google.golang.org/protobuf/types/known/emptypb" +) + +type UserHandler struct { + store *s.Store + config *config.Config +} + +func NewHandler(store *s.Store, config *config.Config) *UserHandler { + return &UserHandler{store, config} +} + +func (handler *UserHandler) GetCurrentUser(ctx context.Context, _ *emptypb.Empty) (*radiov1.GetCurrentUserResponse, error) { + user, err := RequireUser(ctx) + if err != nil { + return nil, err + } + return &radiov1.GetCurrentUserResponse{User: user.ToProto()}, nil +} diff --git a/buf.gen.yaml b/buf.gen.yaml index 4f1c542..9b579a9 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -8,11 +8,13 @@ plugins: opt: - paths=source_relative - simple + - local: protoc-gen-es + out: frontend/src/gen + opt: + - target=ts managed: enabled: true override: - file_option: go_package_prefix value: echsen.club/radio/gen - disable: - - file_option: go_package - module: buf.build/bufbuild/protovalidate + path: radio diff --git a/flake.nix b/flake.nix index 19310ba..8a8d14c 100644 --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,7 @@ buf protoc-gen-go protoc-gen-connect-go + protoc-gen-es air just goose diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fead2e9..aaceea9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,6 +7,11 @@ "": { "name": "radio", "version": "0.0.1", + "dependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@connectrpc/connect": "^2.1.1", + "@connectrpc/connect-web": "^2.1.1" + }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.49.1", @@ -17,6 +22,31 @@ "vite": "^7.2.6" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.10.2.tgz", + "integrity": "sha512-uFsRXwIGyu+r6AMdz+XijIIZJYpoWeYzILt5yZ2d3mCjQrWUTVpVD9WL/jZAbvp+Ed04rOhrsk7FiTcEDseB5A==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@connectrpc/connect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.1.tgz", + "integrity": "sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-2.1.1.tgz", + "integrity": "sha512-J8317Q2MaFRCT1jzVR1o06bZhDIBmU0UAzWx6xOIXzOq8+k71/+k7MUF7AwcBUX+34WIvbm5syRgC5HXQA8fOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0", + "@connectrpc/connect": "2.1.1" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index bfea1e4..218839b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,5 +19,10 @@ "svelte-check": "^4.3.4", "typescript": "^5.9.3", "vite": "^7.2.6" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.10.2", + "@connectrpc/connect": "^2.1.1", + "@connectrpc/connect-web": "^2.1.1" } } diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..b65c0ad --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,45 @@ +:root { + --term-bg: #0d1117; + --term-text: #00ff41; /* Classic Terminal Green */ + --term-dim: #008f11; + --term-highlight: #1a222c; + --term-border: #00ff41; + --font-mono: 'Courier New', Courier, monospace; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background-color: var(--term-bg); + color: var(--term-text); + font-family: var(--font-mono); + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: none; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; +} +::-webkit-scrollbar-track { + background: var(--term-bg); +} +::-webkit-scrollbar-thumb { + background: var(--term-dim); +} +::-webkit-scrollbar-thumb:hover { + background: var(--term-text); +} diff --git a/frontend/src/gen/buf/validate/validate_pb.ts b/frontend/src/gen/buf/validate/validate_pb.ts new file mode 100644 index 0000000..c5910c3 --- /dev/null +++ b/frontend/src/gen/buf/validate/validate_pb.ts @@ -0,0 +1,5 @@ +import { fileDesc } from "@bufbuild/protobuf/codegenv2"; +import type { GenFile } from "@bufbuild/protobuf/codegenv2"; + +export const file_buf_validate_validate: GenFile = /*@__PURE__*/ + fileDesc("ChtidWYvdmFsaWRhdGUvdmFsaWRhdGUucHJvdG8=", []); diff --git a/frontend/src/gen/radio/v1/channel_pb.ts b/frontend/src/gen/radio/v1/channel_pb.ts new file mode 100644 index 0000000..3de3a06 --- /dev/null +++ b/frontend/src/gen/radio/v1/channel_pb.ts @@ -0,0 +1,237 @@ +// @generated by protoc-gen-es v2.10.1 with parameter "target=ts" +// @generated from file radio/v1/channel.proto (package radio.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../buf/validate/validate_pb"; +import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Song } from "./song_pb"; +import { file_radio_v1_song } from "./song_pb"; +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]); + +/** + * @generated from message radio.v1.Channel + */ +export type Channel = Message<"radio.v1.Channel"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string owner_id = 2; + */ + ownerId: string; + + /** + * @generated from field: string frequency = 3; + */ + frequency: string; + + /** + * @generated from field: string description = 4; + */ + description: string; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 5; + */ + createdAt?: Timestamp; + + /** + * @generated from field: google.protobuf.Timestamp modified_at = 6; + */ + modifiedAt?: Timestamp; +}; + +/** + * Describes the message radio.v1.Channel. + * Use `create(ChannelSchema)` to create a new message. + */ +export const ChannelSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 0); + +/** + * @generated from message radio.v1.CreateChannelRequest + */ +export type CreateChannelRequest = Message<"radio.v1.CreateChannelRequest"> & { + /** + * @generated from field: string frequency = 1; + */ + frequency: string; + + /** + * @generated from field: string description = 2; + */ + description: string; +}; + +/** + * Describes the message radio.v1.CreateChannelRequest. + * Use `create(CreateChannelRequestSchema)` to create a new message. + */ +export const CreateChannelRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 1); + +/** + * @generated from message radio.v1.CreateChannelResponse + */ +export type CreateChannelResponse = Message<"radio.v1.CreateChannelResponse"> & { + /** + * @generated from field: radio.v1.Channel channel = 1; + */ + channel?: Channel; +}; + +/** + * Describes the message radio.v1.CreateChannelResponse. + * Use `create(CreateChannelResponseSchema)` to create a new message. + */ +export const CreateChannelResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 2); + +/** + * @generated from message radio.v1.DeleteChannelRequest + */ +export type DeleteChannelRequest = Message<"radio.v1.DeleteChannelRequest"> & { + /** + * @generated from field: string id = 1; + */ + id: string; +}; + +/** + * Describes the message radio.v1.DeleteChannelRequest. + * Use `create(DeleteChannelRequestSchema)` to create a new message. + */ +export const DeleteChannelRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 3); + +/** + * @generated from message radio.v1.ChannelListResponse + */ +export type ChannelListResponse = Message<"radio.v1.ChannelListResponse"> & { + /** + * @generated from field: repeated radio.v1.Channel channels = 1; + */ + channels: Channel[]; +}; + +/** + * Describes the message radio.v1.ChannelListResponse. + * Use `create(ChannelListResponseSchema)` to create a new message. + */ +export const ChannelListResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 4); + +/** + * @generated from message radio.v1.ChannelAddSongRequest + */ +export type ChannelAddSongRequest = Message<"radio.v1.ChannelAddSongRequest"> & { + /** + * @generated from field: string song_id = 1; + */ + songId: string; + + /** + * @generated from field: string channel_id = 2; + */ + channelId: string; +}; + +/** + * Describes the message radio.v1.ChannelAddSongRequest. + * Use `create(ChannelAddSongRequestSchema)` to create a new message. + */ +export const ChannelAddSongRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 5); + +/** + * @generated from message radio.v1.ChannelGetSongsRequest + */ +export type ChannelGetSongsRequest = Message<"radio.v1.ChannelGetSongsRequest"> & { + /** + * @generated from field: string channel_id = 1; + */ + channelId: string; +}; + +/** + * Describes the message radio.v1.ChannelGetSongsRequest. + * Use `create(ChannelGetSongsRequestSchema)` to create a new message. + */ +export const ChannelGetSongsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 6); + +/** + * @generated from message radio.v1.ChannelGetSongsResponse + */ +export type ChannelGetSongsResponse = Message<"radio.v1.ChannelGetSongsResponse"> & { + /** + * @generated from field: repeated radio.v1.Song songs = 1; + */ + songs: Song[]; +}; + +/** + * Describes the message radio.v1.ChannelGetSongsResponse. + * Use `create(ChannelGetSongsResponseSchema)` to create a new message. + */ +export const ChannelGetSongsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_channel, 7); + +/** + * @generated from service radio.v1.ChannelService + */ +export const ChannelService: GenService<{ + /** + * @generated from rpc radio.v1.ChannelService.CreateChannel + */ + createChannel: { + methodKind: "unary"; + input: typeof CreateChannelRequestSchema; + output: typeof CreateChannelResponseSchema; + }, + /** + * @generated from rpc radio.v1.ChannelService.DeleteChannel + */ + deleteChannel: { + methodKind: "unary"; + input: typeof DeleteChannelRequestSchema; + output: typeof EmptySchema; + }, + /** + * @generated from rpc radio.v1.ChannelService.ChannelList + */ + channelList: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof ChannelListResponseSchema; + }, + /** + * @generated from rpc radio.v1.ChannelService.ChannelAddSong + */ + channelAddSong: { + methodKind: "unary"; + input: typeof ChannelAddSongRequestSchema; + output: typeof EmptySchema; + }, + /** + * @generated from rpc radio.v1.ChannelService.ChannelGetSongs + */ + channelGetSongs: { + methodKind: "unary"; + input: typeof ChannelGetSongsRequestSchema; + output: typeof ChannelGetSongsResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_radio_v1_channel, 0); + diff --git a/frontend/src/gen/radio/v1/song_pb.ts b/frontend/src/gen/radio/v1/song_pb.ts new file mode 100644 index 0000000..89b1171 --- /dev/null +++ b/frontend/src/gen/radio/v1/song_pb.ts @@ -0,0 +1,150 @@ +// @generated by protoc-gen-es v2.10.1 with parameter "target=ts" +// @generated from file radio/v1/song.proto (package radio.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../buf/validate/validate_pb"; +import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file radio/v1/song.proto. + */ +export const file_radio_v1_song: GenFile = /*@__PURE__*/ + fileDesc("ChNyYWRpby92MS9zb25nLnByb3RvEghyYWRpby52MSK5AQoEU29uZxIUCgJpZBgBIAEoCUIIukgFcgOwAQESGgoIb3duZXJfaWQYAiABKAlCCLpIBXIDsAEBEhAKCGZpbGVuYW1lGAMgASgJEgwKBG5hbWUYBCABKAkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASLwoLbW9kaWZpZWRfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIkAKEVVwbG9hZFNvbmdSZXF1ZXN0EhIKCGZpbGVuYW1lGAEgASgJSAASDwoFY2h1bmsYAiABKAxIAEIGCgRkYXRhIj0KElVwbG9hZFNvbmdSZXNwb25zZRIZCgdzb25nX2lkGAEgASgJQgi6SAVyA7ABARIMCgRzaXplGAIgASgDIi4KEURlbGV0ZVNvbmdSZXF1ZXN0EhkKB3NvbmdfaWQYASABKAlCCLpIBXIDsAEBMpsBCgtTb25nU2VydmljZRJJCgpVcGxvYWRTb25nEhsucmFkaW8udjEuVXBsb2FkU29uZ1JlcXVlc3QaHC5yYWRpby52MS5VcGxvYWRTb25nUmVzcG9uc2UoARJBCgpEZWxldGVTb25nEhsucmFkaW8udjEuRGVsZXRlU29uZ1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHlCggEKDGNvbS5yYWRpby52MUIJU29uZ1Byb3RvUAFaJmVjaHNlbi5jbHViL3JhZGlvL2dlbi9yYWRpby92MTtyYWRpb3YxogIDUlhYqgIIUmFkaW8uVjHKAghSYWRpb1xWMeICFFJhZGlvXFYxXEdQQk1ldGFkYXRh6gIJUmFkaW86OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_empty, file_google_protobuf_timestamp]); + +/** + * @generated from message radio.v1.Song + */ +export type Song = Message<"radio.v1.Song"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string owner_id = 2; + */ + ownerId: string; + + /** + * @generated from field: string filename = 3; + */ + filename: string; + + /** + * @generated from field: string name = 4; + */ + name: string; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 5; + */ + createdAt?: Timestamp; + + /** + * @generated from field: google.protobuf.Timestamp modified_at = 6; + */ + modifiedAt?: Timestamp; +}; + +/** + * Describes the message radio.v1.Song. + * Use `create(SongSchema)` to create a new message. + */ +export const SongSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_song, 0); + +/** + * @generated from message radio.v1.UploadSongRequest + */ +export type UploadSongRequest = Message<"radio.v1.UploadSongRequest"> & { + /** + * @generated from oneof radio.v1.UploadSongRequest.data + */ + data: { + /** + * @generated from field: string filename = 1; + */ + value: string; + case: "filename"; + } | { + /** + * @generated from field: bytes chunk = 2; + */ + value: Uint8Array; + case: "chunk"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message radio.v1.UploadSongRequest. + * Use `create(UploadSongRequestSchema)` to create a new message. + */ +export const UploadSongRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_song, 1); + +/** + * @generated from message radio.v1.UploadSongResponse + */ +export type UploadSongResponse = Message<"radio.v1.UploadSongResponse"> & { + /** + * @generated from field: string song_id = 1; + */ + songId: string; + + /** + * @generated from field: int64 size = 2; + */ + size: bigint; +}; + +/** + * Describes the message radio.v1.UploadSongResponse. + * Use `create(UploadSongResponseSchema)` to create a new message. + */ +export const UploadSongResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_song, 2); + +/** + * @generated from message radio.v1.DeleteSongRequest + */ +export type DeleteSongRequest = Message<"radio.v1.DeleteSongRequest"> & { + /** + * @generated from field: string song_id = 1; + */ + songId: string; +}; + +/** + * Describes the message radio.v1.DeleteSongRequest. + * Use `create(DeleteSongRequestSchema)` to create a new message. + */ +export const DeleteSongRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_song, 3); + +/** + * @generated from service radio.v1.SongService + */ +export const SongService: GenService<{ + /** + * @generated from rpc radio.v1.SongService.UploadSong + */ + uploadSong: { + methodKind: "client_streaming"; + input: typeof UploadSongRequestSchema; + output: typeof UploadSongResponseSchema; + }, + /** + * @generated from rpc radio.v1.SongService.DeleteSong + */ + deleteSong: { + methodKind: "unary"; + input: typeof DeleteSongRequestSchema; + output: typeof EmptySchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_radio_v1_song, 0); + diff --git a/frontend/src/gen/radio/v1/user_pb.ts b/frontend/src/gen/radio/v1/user_pb.ts new file mode 100644 index 0000000..2b0b151 --- /dev/null +++ b/frontend/src/gen/radio/v1/user_pb.ts @@ -0,0 +1,76 @@ +// @generated by protoc-gen-es v2.10.1 with parameter "target=ts" +// @generated from file radio/v1/user.proto (package radio.v1, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import { file_buf_validate_validate } from "../../buf/validate/validate_pb"; +import type { EmptySchema } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file radio/v1/user.proto. + */ +export const file_radio_v1_user: GenFile = /*@__PURE__*/ + fileDesc("ChNyYWRpby92MS91c2VyLnByb3RvEghyYWRpby52MSI3CgRVc2VyEgoKAmlkGAEgASgJEg0KBWVtYWlsGAIgASgJEhQKDGRpc3BsYXlfbmFtZRgDIAEoCSI2ChZHZXRDdXJyZW50VXNlclJlc3BvbnNlEhwKBHVzZXIYASABKAsyDi5yYWRpby52MS5Vc2VyMlkKC1VzZXJTZXJ2aWNlEkoKDkdldEN1cnJlbnRVc2VyEhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5GiAucmFkaW8udjEuR2V0Q3VycmVudFVzZXJSZXNwb25zZUKCAQoMY29tLnJhZGlvLnYxQglVc2VyUHJvdG9QAVomZWNoc2VuLmNsdWIvcmFkaW8vZ2VuL3JhZGlvL3YxO3JhZGlvdjGiAgNSWFiqAghSYWRpby5WMcoCCFJhZGlvXFYx4gIUUmFkaW9cVjFcR1BCTWV0YWRhdGHqAglSYWRpbzo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_protobuf_empty]); + +/** + * @generated from message radio.v1.User + */ +export type User = Message<"radio.v1.User"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * @generated from field: string email = 2; + */ + email: string; + + /** + * @generated from field: string display_name = 3; + */ + displayName: string; +}; + +/** + * Describes the message radio.v1.User. + * Use `create(UserSchema)` to create a new message. + */ +export const UserSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_user, 0); + +/** + * @generated from message radio.v1.GetCurrentUserResponse + */ +export type GetCurrentUserResponse = Message<"radio.v1.GetCurrentUserResponse"> & { + /** + * @generated from field: radio.v1.User user = 1; + */ + user?: User; +}; + +/** + * Describes the message radio.v1.GetCurrentUserResponse. + * Use `create(GetCurrentUserResponseSchema)` to create a new message. + */ +export const GetCurrentUserResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_radio_v1_user, 1); + +/** + * @generated from service radio.v1.UserService + */ +export const UserService: GenService<{ + /** + * @generated from rpc radio.v1.UserService.GetCurrentUser + */ + getCurrentUser: { + methodKind: "unary"; + input: typeof EmptySchema; + output: typeof GetCurrentUserResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_radio_v1_user, 0); + diff --git a/frontend/src/lib/components/CreateChannelForm.svelte b/frontend/src/lib/components/CreateChannelForm.svelte new file mode 100644 index 0000000..dc5b348 --- /dev/null +++ b/frontend/src/lib/components/CreateChannelForm.svelte @@ -0,0 +1,194 @@ + + +
+ {#if !isOpen} + + {:else} +
+
+ NEW CHANNEL CONFIGURATION + +
+ +
+ + +
+ +
+ + +
+ + {#if error} +
! ERROR: {error}
+ {/if} + +
+ +
+
+ {/if} +
+ + diff --git a/frontend/src/lib/components/PlayerWidget.svelte b/frontend/src/lib/components/PlayerWidget.svelte new file mode 100644 index 0000000..4cb2e6b --- /dev/null +++ b/frontend/src/lib/components/PlayerWidget.svelte @@ -0,0 +1,90 @@ + + +
+
+ STATUS: + {player.isPlaying ? 'PLAYING' : 'IDLE'} +
+ + {#if player.currentTrack} +
+
+ CH: + {player.currentTrack.channel} +
+
+ TRACK: + {player.currentTrack.title} +
+
+ {:else} +
+ -- No Signal -- +
+ {/if} + +
+ +
VOL: {Math.round(player.volume * 100)}%
+
+
+ + diff --git a/frontend/src/lib/components/TerminalList.svelte b/frontend/src/lib/components/TerminalList.svelte new file mode 100644 index 0000000..d8a7195 --- /dev/null +++ b/frontend/src/lib/components/TerminalList.svelte @@ -0,0 +1,95 @@ + + +
    + {#each items as item} +
  • + +
  • + {/each} +
+ + diff --git a/frontend/src/lib/components/UploadButton.svelte b/frontend/src/lib/components/UploadButton.svelte new file mode 100644 index 0000000..27ceb21 --- /dev/null +++ b/frontend/src/lib/components/UploadButton.svelte @@ -0,0 +1,185 @@ + + +
+ + + {#if error} +
+ ! ERROR: {error} +
+ {/if} +
+ + diff --git a/frontend/src/lib/rpc.ts b/frontend/src/lib/rpc.ts new file mode 100644 index 0000000..13c3d05 --- /dev/null +++ b/frontend/src/lib/rpc.ts @@ -0,0 +1,44 @@ +import { createClient, type Transport } from "@connectrpc/connect"; +import { createConnectTransport } from "@connectrpc/connect-web"; +import { ChannelService } from "../gen/radio/v1/channel_pb"; +import { SongService } from "../gen/radio/v1/song_pb"; +import { UserService } from "../gen/radio/v1/user_pb"; + +export type FetchFn = typeof fetch; + +export function getTransport(fetchFn: FetchFn = fetch): Transport { + return createConnectTransport({ + baseUrl: "", + fetch: (input, init) => { + const options: any = { + ...init, + credentials: "include" + }; + + // Always set duplex: 'half' if there's a body, + // as ConnectRPC might send a stream. + if (options.body) { + options.duplex = 'half'; + } + + return fetchFn(input, options); + }, + }); +} + +export const channelClient = createClient(ChannelService, getTransport()); +export const songClient = createClient(SongService, getTransport()); +export const userClient = createClient(UserService, getTransport()); + +export function getChannelClient(fetchFn: FetchFn) { + return createClient(ChannelService, getTransport(fetchFn)); +} + +export function getSongClient(fetchFn: FetchFn) { + return createClient(SongService, getTransport(fetchFn)); +} + +export function getUserClient(fetchFn: FetchFn) { + return createClient(UserService, getTransport(fetchFn)); +} + diff --git a/frontend/src/lib/state/player.svelte.ts b/frontend/src/lib/state/player.svelte.ts new file mode 100644 index 0000000..ad08fec --- /dev/null +++ b/frontend/src/lib/state/player.svelte.ts @@ -0,0 +1,24 @@ +export interface Track { + id: string; + title: string; + channel: string; +} + +export class PlayerState { + isPlaying = $state(false); + currentTrack = $state(null); + volume = $state(0.5); + + constructor() {} + + play(track: Track) { + this.currentTrack = track; + this.isPlaying = true; + } + + togglePlay() { + this.isPlaying = !this.isPlaying; + } +} + +export const player = new PlayerState(); diff --git a/frontend/src/lib/state/user.svelte.ts b/frontend/src/lib/state/user.svelte.ts new file mode 100644 index 0000000..4160520 --- /dev/null +++ b/frontend/src/lib/state/user.svelte.ts @@ -0,0 +1,19 @@ +export class UserState { + id = $state(null); + email = $state(null); + displayName = $state(null); + + setUser(user: { id: string; email: string; displayName: string }) { + this.id = user.id; + this.email = user.email; + this.displayName = user.displayName; + } + + clearUser() { + this.id = null; + this.email = null; + this.displayName = null; + } +} + +export const userState = new UserState(); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 9cebde5..d672a5b 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,11 +1,60 @@ - - - +
+ + +
+ {@render children()} +
-{@render children()} +
+ READY. + _ +
+
+ + diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts new file mode 100644 index 0000000..be9a4f7 --- /dev/null +++ b/frontend/src/routes/+layout.ts @@ -0,0 +1,24 @@ +import { getUserClient } from '$lib/rpc'; +import { userState } from '$lib/state/user.svelte'; +import type { LayoutLoad } from './$types'; + +export const ssr = false; + +export const load: LayoutLoad = (async (event) => { + const client = getUserClient(event.fetch); + try { + const response = await client.getCurrentUser({}); + if (response.user) { + userState.setUser({ + id: response.user.id, + email: response.user.email, + displayName: response.user.displayName + }); + } + return { user: response.user }; + } catch (err) { + console.error("Failed to fetch current user:", err); + userState.clearUser(); + return { user: null }; + } +}) satisfies LayoutLoad; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index cc88df0..be8f721 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,2 +1,90 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + + + +{#if data.error} +
+ ERROR: {data.error} +
+ Check backend logs or connection. +
+{:else if items.length === 0} +
+ -- NO CHANNELS FOUND -- +
+{:else} + +{/if} + + diff --git a/frontend/src/routes/+page.ts b/frontend/src/routes/+page.ts new file mode 100644 index 0000000..1c10e31 --- /dev/null +++ b/frontend/src/routes/+page.ts @@ -0,0 +1,35 @@ +import { getChannelClient } from '$lib/rpc'; +import type { PageLoad } from './$types'; +import { ConnectError, Code } from "@connectrpc/connect"; + +export const ssr = false; + +export const load: PageLoad = (async (event) => { + const client = getChannelClient(event.fetch); + try { + const response = await client.channelList({}); + return { + channels: response.channels, + error: null + }; + } catch (err) { + console.error("Failed to fetch channels:", err); + let errorMessage = "Unknown Error"; + if (err instanceof ConnectError) { + errorMessage = err.message; + if (err.code === Code.Unauthenticated) { + if (typeof window !== 'undefined') { + window.location.href = "http://localhost:8080/auth/login"; + } + return { channels: [], error: "Redirecting to login..." }; + } + } else if (err instanceof Error) { + errorMessage = err.message; + } + + return { + channels: [], + error: errorMessage + }; + } +}) satisfies PageLoad; diff --git a/frontend/src/routes/[channelId]/+page.svelte b/frontend/src/routes/[channelId]/+page.svelte new file mode 100644 index 0000000..5637309 --- /dev/null +++ b/frontend/src/routes/[channelId]/+page.svelte @@ -0,0 +1,117 @@ + + + + +{#if data.error} +
+ ERROR: {data.error} +
+ Check backend logs or connection. +
+{:else if items.length === 0} +
+ -- NO SONGS FOUND -- +
+{:else} + +{/if} + + diff --git a/frontend/src/routes/[channelId]/+page.ts b/frontend/src/routes/[channelId]/+page.ts new file mode 100644 index 0000000..c1c0c03 --- /dev/null +++ b/frontend/src/routes/[channelId]/+page.ts @@ -0,0 +1,46 @@ +import { getChannelClient } from '$lib/rpc'; +import type { PageLoad } from './$types'; +import { ConnectError, Code } from "@connectrpc/connect"; + +export const ssr = false; + +export const load: PageLoad = (async (event) => { + const client = getChannelClient(event.fetch); + const channelId = event.params.channelId; + try { + // We fetch both songs and the channel list to find the owner of this channel + const [songsRes, channelsRes] = await Promise.all([ + client.channelGetSongs({ channelId }), + client.channelList({}) + ]); + + const channel = channelsRes.channels.find(c => c.id === channelId); + + return { + songs: songsRes.songs, + channel, + channelId, + error: null + }; + } catch (err) { + console.error("Failed to fetch songs or channel:", err); + let errorMessage = "Unknown Error"; + if (err instanceof ConnectError) { + errorMessage = err.message; + if (err.code === Code.Unauthenticated) { + if (typeof window !== 'undefined') { + window.location.href = "http://localhost:8080/auth/login"; + } + return { songs: [], channelId, error: "Redirecting to login..." }; + } + } else if (err instanceof Error) { + errorMessage = err.message; + } + + return { + songs: [], + channelId, + error: errorMessage + }; + } +}) satisfies PageLoad; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bbf8c7d..0bd3c60 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,5 +2,19 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], + server: { + proxy: { + // Proxy ConnectRPC requests + '/radio.v1': { + target: 'http://localhost:8080', + changeOrigin: true + }, + // Proxy Auth requests (if needed for redirects or checking status) + '/auth': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + } }); diff --git a/proto/radio/v1/channel.proto b/proto/radio/v1/channel.proto index dfe8752..df17a6d 100644 --- a/proto/radio/v1/channel.proto +++ b/proto/radio/v1/channel.proto @@ -30,6 +30,10 @@ message DeleteChannelRequest { string id = 1 [(buf.validate.field).string.uuid = true]; } +message ChannelListResponse { + repeated Channel channels = 1; +} + message ChannelAddSongRequest { string song_id = 1 [(buf.validate.field).string.uuid = true]; string channel_id = 2 [(buf.validate.field).string.uuid = true]; @@ -46,5 +50,7 @@ message ChannelGetSongsResponse { service ChannelService { rpc CreateChannel(CreateChannelRequest) returns (CreateChannelResponse); rpc DeleteChannel(DeleteChannelRequest) returns (google.protobuf.Empty); + rpc ChannelList(google.protobuf.Empty) returns (ChannelListResponse); rpc ChannelAddSong(ChannelAddSongRequest) returns (google.protobuf.Empty); + rpc ChannelGetSongs(ChannelGetSongsRequest) returns (ChannelGetSongsResponse); } diff --git a/proto/radio/v1/user.proto b/proto/radio/v1/user.proto new file mode 100644 index 0000000..81c2746 --- /dev/null +++ b/proto/radio/v1/user.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package radio.v1; + +option go_package = "backend/gen/radio/v1;radiov1"; + +import "buf/validate/validate.proto"; +import "google/protobuf/empty.proto"; + +message User { + string id = 1; + string email = 2; + string display_name = 3; +} + +message GetCurrentUserResponse { + User user = 1; +} + +service UserService { + rpc GetCurrentUser(google.protobuf.Empty) returns (GetCurrentUserResponse); +}