Skip to content

Commit a21bfed

Browse files
committed
Add song metadata update functionality with gRPC support and UI enhancements
1 parent 2846e8a commit a21bfed

10 files changed

Lines changed: 186 additions & 20 deletions

File tree

.github/copilot-instructions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
If there's a blazor webassembly project and a blazor server project, don't try to run the webassembly project directly. Instead, run the server project which will handle the webassembly project as well.
22

3-
There should always be a blank line of whitespace between each method. New html elements should be on a new line.
3+
There should always be a blank line of whitespace between each method. New html elements should be on a new line.
4+
5+
Always make sure methods start on a new line. Always make sure there is a blank line between methods.

HomeSpeaker.Server2/Data/OnDiskDataStore.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,23 @@ public OnDiskDataStore()
1111
songs = new();
1212
}
1313

14-
private List<Song> songs;
15-
16-
public void Add(Song song)
14+
private List<Song> songs; public void Add(Song song)
1715
{
1816
song.SongId = songs.Count;
1917
songs.Add(song);
2018
}
2119

20+
public void UpdateSong(int songId, string name, string artist, string album)
21+
{
22+
var song = songs.FirstOrDefault(s => s.SongId == songId);
23+
if (song != null)
24+
{
25+
song.Name = name;
26+
song.Artist = artist;
27+
song.Album = album;
28+
}
29+
}
30+
2231
public IEnumerable<Album> GetAlbums()
2332
{
2433
foreach (var album in from s in songs

HomeSpeaker.Server2/IDataStore.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace HomeSpeaker.Server2;
55
public interface IDataStore
66
{
77
void Add(Song song);
8+
void UpdateSong(int songId, string name, string artist, string album);
89
IEnumerable<Artist> GetArtists();
910
IEnumerable<Album> GetAlbums();
1011
IEnumerable<Song> GetSongs();

HomeSpeaker.Server2/ITagParser.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace HomeSpeaker.Server
66
public interface ITagParser
77
{
88
Song CreateSong(string fullPath);
9+
void UpdateSongTags(string fullPath, string name, string artist, string album);
910
}
1011

1112
public class DefaultTagParser : ITagParser
@@ -27,6 +28,7 @@ public Song CreateSong(string fullPath)
2728
{
2829
title = fileName.Replace(".mp3", string.Empty);
2930
}
31+
3032
return new Song
3133
{
3234
Album = tag.Album.Value?.Replace("\0", string.Empty),
@@ -35,5 +37,41 @@ public Song CreateSong(string fullPath)
3537
Path = fullPath
3638
};
3739
}
40+
41+
public void UpdateSongTags(string fullPath, string name, string artist, string album)
42+
{
43+
try
44+
{
45+
logger.LogInformation("Updating MP3 tags for file: {fullPath}", fullPath);
46+
47+
using var mp3 = new Mp3(fullPath, Mp3Permissions.ReadWrite);
48+
49+
// Get or create a tag
50+
var tag = mp3.GetTag(Id3TagFamily.Version2X) ?? mp3.GetTag(Id3TagFamily.Version1X);
51+
52+
if (tag != null)
53+
{
54+
// Update the tag values
55+
tag.Title.Value = name;
56+
tag.Album.Value = album;
57+
tag.Artists.Value.Clear();
58+
tag.Artists.Value.Add(artist);
59+
60+
// Write the changes back to the file
61+
mp3.WriteTag(tag, WriteConflictAction.Replace);
62+
63+
logger.LogInformation("Successfully updated MP3 tags for file: {fullPath}", fullPath);
64+
}
65+
else
66+
{
67+
logger.LogWarning("No existing tags found and unable to create new tags for file: {fullPath}", fullPath);
68+
}
69+
}
70+
catch (Exception ex)
71+
{
72+
logger.LogError(ex, "Error updating MP3 tags for file: {fullPath}", fullPath);
73+
throw;
74+
}
75+
}
3876
}
3977
}

HomeSpeaker.Server2/Mp3Library.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,27 @@ internal void DeleteSong(int songId)
7878
IsDirty = true;
7979
}
8080

81+
internal void UpdateSong(int songId, string name, string artist, string album)
82+
{
83+
lock (lockObject)
84+
{
85+
logger.LogInformation("Updating song# {songId} with name: {name}, artist: {artist}, album: {album}", songId, name, artist, album);
86+
87+
// Find the song to get its file path
88+
var song = Songs.Where(s => s.SongId == songId).FirstOrDefault();
89+
if (song == null)
90+
{
91+
logger.LogWarning("Song with ID {songId} not found", songId);
92+
return;
93+
}
94+
95+
// Update the MP3 file tags
96+
tagParser.UpdateSongTags(song.Path, name, artist, album);
97+
98+
// Update the in-memory data store
99+
dataStore.UpdateSong(songId, name, artist, album);
100+
101+
logger.LogInformation("Successfully updated song# {songId} both in file and in memory", songId);
102+
}
103+
}
81104
}

HomeSpeaker.Server2/Services/HomeSpeakerService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ public override Task<DeleteSongReply> DeleteSong(DeleteSongRequest request, Serv
121121
return Task.FromResult(new DeleteSongReply());
122122
}
123123

124+
public override Task<UpdateSongReply> UpdateSong(UpdateSongRequest request, ServerCallContext context)
125+
{
126+
library.UpdateSong(request.SongId, request.Name, request.Artist, request.Album);
127+
return Task.FromResult(new UpdateSongReply());
128+
}
129+
124130
public override async Task<SearchVideoReply> SearchViedo(SearchVideoRequest request, ServerCallContext context)
125131
{
126132
var videos = await youtubeService.SearchAsync(request.SearchTerm);

HomeSpeaker.Shared/homespeaker.proto

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ service HomeSpeaker {
2020
rpc ShuffleQueue(ShuffleQueueRequest) returns (ShuffleQueueReply);
2121
rpc SendEvent(google.protobuf.Empty) returns (stream StreamServerEvent);
2222
rpc SearchViedo(SearchVideoRequest) returns (SearchVideoReply);
23-
rpc CacheVideo(CacheVideoRequest) returns (stream CacheVideoReply);
24-
rpc DeleteSong(DeleteSongRequest) returns (DeleteSongReply);
23+
rpc CacheVideo(CacheVideoRequest) returns (stream CacheVideoReply); rpc DeleteSong(DeleteSongRequest) returns (DeleteSongReply);
24+
rpc UpdateSong(UpdateSongRequest) returns (UpdateSongReply);
2525
rpc AddSongToPlaylist(AddSongToPlaylistRequest) returns (AddSongToPlaylistReply);
2626
rpc RemoveSongFromPlaylist(RemoveSongFromPlaylistRequest) returns (RemoveSongFromPlaylistReply); rpc GetPlaylists(GetPlaylistsRequest) returns (GetPlaylistsReply); rpc PlayPlaylist(PlayPlaylistRequest) returns (PlayPlaylistReply);
2727
rpc RenamePlaylist(RenamePlaylistRequest) returns (RenamePlaylistReply);
@@ -136,6 +136,16 @@ message DeleteSongRequest{
136136
int32 SongId=1;
137137
}
138138
message DeleteSongReply{}
139+
140+
message UpdateSongRequest{
141+
int32 SongId=1;
142+
string Name=2;
143+
string Artist=3;
144+
string Album=4;
145+
}
146+
147+
message UpdateSongReply{}
148+
139149
message RenamePlaylistRequest{
140150
string OldName=1;
141151
string NewName=2;

HomeSpeaker.WebAssembly/Components/Music/Library/Song.razor

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,48 @@ else
2626
<button @onclick=toggleDetailsView class="btn btn-outline"><span class=@toggleIcon></span></button>
2727
<PlayButtonWithDropdown Song="SongViewModel" />
2828
<PlusButtonWithMenu Song="SongViewModel" />
29-
</div>
30-
@if (showDetails)
29+
</div> @if (showDetails)
3130
{
3231
<div class="d-flex justify-content-between mb-2 pt-2 ps-2 g-0">
3332
<div class="">
3433
<button class="btn btn-outline-danger" @onclick=delete><span class="oi oi-delete me-1"></span>Delete</button>
3534
</div>
3635
<div class="">
37-
<button class="btn btn-outline-success" @onclick=togglePlaylistModalVisibility><span class="oi oi-list me-1"></span>Playlists</button>
38-
</div>
39-
<div class="">
40-
<button class="btn btn-outline-info" disabled><span class="oi oi-pencil me-1"></span>Edit</button>
36+
<button class="btn btn-outline-info" @onclick=toggleEditModalVisibility><span class="oi oi-pencil me-1"></span>Edit</button>
4137
</div>
4238
</div>
4339
}
4440
</div>
4541
}
4642

47-
@if (!isPlaylistModalHidden)
43+
@if (!isEditModalHidden)
4844
{
49-
<FluentDialog Hidden=@isPlaylistModalHidden>
50-
<AddToPlaylistModal Song=SongViewModel Closed=@togglePlaylistModalVisibility />
45+
<FluentDialog Hidden=@isEditModalHidden>
46+
<div class="modal-dialog">
47+
<div class="modal-content">
48+
<div class="modal-header">
49+
<h5 class="modal-title">Edit Song</h5>
50+
</div>
51+
<div class="modal-body">
52+
<div class="mb-3">
53+
<label for="songName" class="form-label">Title</label>
54+
<input type="text" class="form-control" id="songName" @bind="editedName" />
55+
</div>
56+
<div class="mb-3">
57+
<label for="songArtist" class="form-label">Artist</label>
58+
<input type="text" class="form-control" id="songArtist" @bind="editedArtist" />
59+
</div>
60+
<div class="mb-3">
61+
<label for="songAlbum" class="form-label">Album</label>
62+
<input type="text" class="form-control" id="songAlbum" @bind="editedAlbum" />
63+
</div>
64+
</div>
65+
<div class="modal-footer">
66+
<button type="button" class="btn btn-secondary" @onclick=cancelEdit>Cancel</button>
67+
<button type="button" class="btn btn-primary" @onclick=saveEdit>Save</button>
68+
</div>
69+
</div>
70+
</div>
5171
</FluentDialog>
5272
}
5373

@@ -65,8 +85,16 @@ else
6585
[Parameter]
6686
public EventCallback<int> OnDeleted { get; set; }
6787

88+
[Parameter]
89+
public EventCallback OnUpdated { get; set; }
90+
6891
private bool showDetails { get; set; }
69-
private bool isPlaylistModalHidden = true; private void toggleDetailsView() => showDetails = !showDetails;
92+
private bool isEditModalHidden = true;
93+
private string editedName = string.Empty;
94+
private string editedArtist = string.Empty;
95+
private string editedAlbum = string.Empty;
96+
97+
private void toggleDetailsView() => showDetails = !showDetails;
7098
private string toggleIcon => showDetails ? "oi oi-chevron-top" : "oi oi-chevron-bottom";
7199

72100
private bool isDeleteConfirmationOpen = false;
@@ -82,5 +110,41 @@ else
82110
}
83111
}
84112

85-
void togglePlaylistModalVisibility() => isPlaylistModalHidden = !isPlaylistModalHidden;
113+
void toggleEditModalVisibility()
114+
{
115+
if (isEditModalHidden)
116+
{
117+
// Opening modal - populate with current values
118+
editedName = SongViewModel.Name;
119+
editedArtist = SongViewModel.Artist;
120+
editedAlbum = SongViewModel.Album;
121+
}
122+
isEditModalHidden = !isEditModalHidden;
123+
}
124+
125+
void cancelEdit()
126+
{
127+
isEditModalHidden = true;
128+
}
129+
130+
async Task saveEdit()
131+
{
132+
try
133+
{
134+
await svc.UpdateSongAsync(SongViewModel.SongId, editedName, editedArtist, editedAlbum);
135+
136+
// Update the local model
137+
SongViewModel.Name = editedName;
138+
SongViewModel.Artist = editedArtist;
139+
SongViewModel.Album = editedAlbum;
140+
141+
isEditModalHidden = true;
142+
await OnUpdated.InvokeAsync();
143+
}
144+
catch (Exception ex)
145+
{
146+
logger.LogError(ex, "Error updating song");
147+
// You could show an error message here
148+
}
149+
}
86150
}

HomeSpeaker.WebAssembly/Models/SongViewModel.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
public class SongViewModel
44
{
55
public int SongId { get; set; }
6-
public required string Name { get; init; }
6+
public required string Name { get; set; }
77
private string? path;
88
public string? Path
99
{
@@ -17,8 +17,8 @@ public string? Path
1717
Folder = System.IO.Path.GetDirectoryName(path);
1818
}
1919
}
20-
public required string Album { get; init; }
21-
public required string Artist { get; init; }
20+
public required string Album { get; set; }
21+
public required string Artist { get; set; }
2222
public string? Folder { get; private set; }
2323
}
2424

HomeSpeaker.WebAssembly/Services/HomeSpeakerService.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,19 @@ await client.ReorderPlaylistSongsAsync(new ReorderPlaylistSongsRequest
143143
logger.LogInformation("Successfully called ReorderPlaylistSongs gRPC method for playlist: {playlistName}", playlistName);
144144
}
145145

146+
public async Task UpdateSongAsync(int songId, string name, string artist, string album)
147+
{
148+
logger.LogInformation("Calling UpdateSong gRPC method for song: {songId}", songId);
149+
await client.UpdateSongAsync(new UpdateSongRequest
150+
{
151+
SongId = songId,
152+
Name = name,
153+
Artist = artist,
154+
Album = album
155+
});
156+
logger.LogInformation("Successfully called UpdateSong gRPC method for song: {songId}", songId);
157+
}
158+
146159
readonly char[] separators = new[] { '/', '\\' };
147160

148161
public async Task<IEnumerable<string>> GetFolders()

0 commit comments

Comments
 (0)