Skip to content

Commit d57d0d1

Browse files
committed
Update icons; Use separate VM for music section
1 parent b355fa4 commit d57d0d1

File tree

49 files changed

+1298
-150
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1298
-150
lines changed

src/Bible.Alarm.Shared/Models/Schedule/AlarmSchedule.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ int GetPublicationSortPriority(string code)
404404
throw new InvalidOperationException($"No tracks found in section {randomSection.SectionCode} for melody music publication {sample.Music.PublicationCode}");
405405
}
406406

407+
// Set the section code for the selected section
408+
sample.Music.SectionCode = randomSection.SectionCode;
409+
407410
// Select a random track from the selected section
408411
var randomTrack = randomSection.Tracks[Random.Shared.Next(randomSection.Tracks.Count)];
409412
sample.Music.TrackNumber = randomTrack.Number;

src/Bible.Alarm.Shared/Services/Media/BiblePublicationSectionService.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,76 @@ public async Task<SortedDictionary<int, BiblePublicationSection>> GetSectionsByP
107107
}
108108
}
109109

110+
public async Task<SortedDictionary<int, BiblePublicationSection>> GetMusicSectionsByPublicationAsync(string publicationCode, CancellationToken cancellationToken = default)
111+
{
112+
try
113+
{
114+
using var scope = scopeFactory.CreateScope();
115+
var dbContext = scope.ServiceProvider.GetRequiredService<MediaDbContext>();
116+
117+
// Load sections for music publications (Category=Music, LanguageId=null)
118+
var sections = await dbContext.BiblePublications
119+
.AsNoTracking()
120+
.Include(x => x.Category)
121+
.Include(x => x.Sections)
122+
.ThenInclude(s => s.UrlParams)
123+
.Where(x => x.Category.CategoryName == "Music"
124+
&& x.LanguageId == null
125+
&& x.PublicationCode == publicationCode)
126+
.SelectMany(x => x.Sections)
127+
.ToListAsync(cancellationToken);
128+
129+
// Filter sections that have numeric SectionCode and order by it
130+
// For music sections like "iam-1", "iam-2", we need to handle both numeric and non-numeric codes
131+
var sectionsByNumber = new Dictionary<int, BiblePublicationSection>();
132+
foreach (var section in sections)
133+
{
134+
// Try to parse SectionCode as int (for numeric codes)
135+
if (int.TryParse(section.SectionCode, out var sectionNumber))
136+
{
137+
if (!sectionsByNumber.ContainsKey(sectionNumber))
138+
{
139+
sectionsByNumber[sectionNumber] = section;
140+
}
141+
}
142+
else
143+
{
144+
// For non-numeric codes like "iam-1", extract the number part
145+
// e.g., "iam-1" -> 1, "iam-2" -> 2
146+
var parts = section.SectionCode.Split('-');
147+
if (parts.Length > 1 && int.TryParse(parts[parts.Length - 1], out var extractedNumber))
148+
{
149+
if (!sectionsByNumber.ContainsKey(extractedNumber))
150+
{
151+
sectionsByNumber[extractedNumber] = section;
152+
}
153+
}
154+
else
155+
{
156+
// If we can't extract a number, use 0 as a fallback (will be sorted last)
157+
if (!sectionsByNumber.ContainsKey(0))
158+
{
159+
sectionsByNumber[0] = section;
160+
}
161+
}
162+
}
163+
}
164+
165+
// Sort by the extracted number
166+
var sortedSections = sectionsByNumber
167+
.OrderBy(kvp => kvp.Key)
168+
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
169+
170+
return new SortedDictionary<int, BiblePublicationSection>(sortedSections);
171+
}
172+
catch (Exception ex)
173+
{
174+
logger.Error(ex, "Error getting Music sections by publication. PublicationCode={PublicationCode}",
175+
publicationCode);
176+
throw;
177+
}
178+
}
179+
110180
public void Dispose()
111181
{
112182
if (isDisposed)

src/Bible.Alarm.Shared/Services/Media/BiblePublicationService.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,25 @@ public sealed class BiblePublicationService(IServiceScopeFactory scopeFactory, I
7474
}
7575
}
7676

77-
public async Task<Dictionary<string, BiblePublication>> GetByLanguageCodeAsync(string languageCode, CancellationToken cancellationToken = default)
77+
public async Task<Dictionary<string, BiblePublication>> GetByLanguageCodeAsync(string languageCode, string? categoryName = null, CancellationToken cancellationToken = default)
7878
{
7979
try
8080
{
8181
using var scope = scopeFactory.CreateScope();
8282
var dbContext = scope.ServiceProvider.GetRequiredService<MediaDbContext>();
8383

84-
var publicationsList = await dbContext.BiblePublications
84+
var query = dbContext.BiblePublications
8585
.AsNoTracking()
8686
.Include(x => x.Category)
87-
.Where(x => x.Language != null && x.Language.LanguageCode == languageCode)
88-
.ToListAsync(cancellationToken);
87+
.Where(x => x.Language != null && x.Language.LanguageCode == languageCode);
88+
89+
// Filter by category if provided
90+
if (!string.IsNullOrWhiteSpace(categoryName))
91+
{
92+
query = query.Where(x => x.Category != null && x.Category.CategoryName == categoryName);
93+
}
94+
95+
var publicationsList = await query.ToListAsync(cancellationToken);
8996

9097
logger.Debug("GetByLanguageCodeAsync: Found {PublicationCount} publications for language={LanguageCode}",
9198
publicationsList.Count, languageCode);
@@ -114,17 +121,25 @@ public async Task<Dictionary<string, BiblePublication>> GetByLanguageCodeAsync(s
114121
}
115122
}
116123

117-
public async Task<Dictionary<string, Language>> GetDistinctLanguagesAsync(CancellationToken cancellationToken = default)
124+
public async Task<Dictionary<string, Language>> GetDistinctLanguagesAsync(string? categoryName = null, CancellationToken cancellationToken = default)
118125
{
119126
try
120127
{
121128
using var scope = scopeFactory.CreateScope();
122129
var dbContext = scope.ServiceProvider.GetRequiredService<MediaDbContext>();
123130

124-
var biblePublicationsCount = await dbContext.BiblePublications.CountAsync(cancellationToken);
125-
var distinctLanguages = await dbContext.BiblePublications
131+
var query = dbContext.BiblePublications
126132
.AsNoTracking()
127-
.Where(x => x.Language != null)
133+
.Where(x => x.Language != null);
134+
135+
// Filter by category if provided
136+
if (!string.IsNullOrWhiteSpace(categoryName))
137+
{
138+
query = query.Where(x => x.Category != null && x.Category.CategoryName == categoryName);
139+
}
140+
141+
var biblePublicationsCount = await query.CountAsync(cancellationToken);
142+
var distinctLanguages = await query
128143
.Select(x => x.Language!)
129144
.Distinct()
130145
.ToListAsync(cancellationToken);

src/Bible.Alarm.Shared/Services/Media/Interfaces/IBiblePublicationSectionService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ public interface IBiblePublicationSectionService : IDisposable
2121
/// Gets all BiblePublicationSections for a given publication (by language code and publication code).
2222
/// </summary>
2323
Task<SortedDictionary<int, BiblePublicationSection>> GetSectionsByPublicationAsync(string languageCode, string publicationCode, CancellationToken cancellationToken = default);
24+
25+
/// <summary>
26+
/// Gets sections for music publications (Category=Music, LanguageId=null).
27+
/// Used for instrumental music like Kingdom Melodies.
28+
/// </summary>
29+
Task<SortedDictionary<int, BiblePublicationSection>> GetMusicSectionsByPublicationAsync(string publicationCode, CancellationToken cancellationToken = default);
2430

2531
/// <summary>
2632
/// Gets a BiblePublicationSection by language code, publication code, and section number.

src/Bible.Alarm.Shared/Services/Media/Interfaces/IBiblePublicationService.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ public interface IBiblePublicationService : IDisposable
2424
Task<BiblePublication?> GetByLanguageAndCodeWithTracksAsync(string languageCode, string publicationCode, CancellationToken cancellationToken = default);
2525

2626
/// <summary>
27-
/// Gets all BiblePublications for a given language code.
27+
/// Gets all BiblePublications for a given language code, optionally filtered by category.
2828
/// </summary>
29-
Task<Dictionary<string, BiblePublication>> GetByLanguageCodeAsync(string languageCode, CancellationToken cancellationToken = default);
29+
Task<Dictionary<string, BiblePublication>> GetByLanguageCodeAsync(string languageCode, string? categoryName = null, CancellationToken cancellationToken = default);
3030

3131
/// <summary>
32-
/// Gets all distinct Languages from BiblePublications.
32+
/// Gets all distinct Languages from BiblePublications, optionally filtered by category.
3333
/// </summary>
34-
Task<Dictionary<string, Language>> GetDistinctLanguagesAsync(CancellationToken cancellationToken = default);
34+
Task<Dictionary<string, Language>> GetDistinctLanguagesAsync(string? categoryName = null, CancellationToken cancellationToken = default);
3535
}
3636

src/Bible.Alarm.Shared/Services/Media/Interfaces/IMelodyMusicService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public interface IMelodyMusicService : IDisposable
2626
/// Gets all tracks for a MelodyMusic release by publication code, with Source included.
2727
/// </summary>
2828
Task<SortedDictionary<int, MusicTrack>> GetTracksByCodeAsync(string publicationCode, CancellationToken cancellationToken = default);
29+
30+
/// <summary>
31+
/// Gets tracks for a specific section in a music publication (e.g., "iam-1" section in Kingdom Melodies).
32+
/// </summary>
33+
Task<SortedDictionary<int, MusicTrack>> GetTracksBySectionCodeAsync(string publicationCode, string sectionCode, CancellationToken cancellationToken = default);
2934

3035
/// <summary>
3136
/// Updates the URL for a MelodyMusic track's audio source.

src/Bible.Alarm.Shared/Services/Media/MelodyMusicService.cs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public sealed class MelodyMusicService(IServiceScopeFactory scopeFactory, ILogge
7272
allTracks.AddRange(publication.Tracks);
7373
}
7474

75-
// Create a new publication object with all tracks combined
75+
// Create a new publication object with all tracks combined AND sections
7676
var publicationWithTracks = new BiblePublication
7777
{
7878
Id = publication.Id,
@@ -82,7 +82,8 @@ public sealed class MelodyMusicService(IServiceScopeFactory scopeFactory, ILogge
8282
CategoryId = publication.CategoryId,
8383
IsVideo = publication.IsVideo,
8484
Category = publication.Category,
85-
Tracks = allTracks
85+
Tracks = allTracks,
86+
Sections = publication.Sections // Include sections so GetSampleSchedule can select a section
8687
};
8788

8889
// MelodyMusic is a subclass of BiblePublication, so we can return the publication directly
@@ -202,6 +203,63 @@ public async Task<SortedDictionary<int, MusicTrack>> GetTracksByCodeAsync(string
202203
}
203204
}
204205

206+
public async Task<SortedDictionary<int, MusicTrack>> GetTracksBySectionCodeAsync(string publicationCode, string sectionCode, CancellationToken cancellationToken = default)
207+
{
208+
try
209+
{
210+
using var scope = scopeFactory.CreateScope();
211+
var dbContext = scope.ServiceProvider.GetRequiredService<MediaDbContext>();
212+
213+
// Get the publication with sections
214+
var publication = await dbContext.BiblePublications
215+
.AsNoTracking()
216+
.Include(x => x.Category)
217+
.Include(x => x.Sections)
218+
.ThenInclude(s => s.Tracks)
219+
.Where(x => x.Category.CategoryName == MusicCategoryName
220+
&& x.LanguageId == null
221+
&& x.PublicationCode == publicationCode)
222+
.FirstOrDefaultAsync(cancellationToken);
223+
224+
if (publication == null)
225+
{
226+
return new SortedDictionary<int, MusicTrack>();
227+
}
228+
229+
// Find the section by section code
230+
var section = publication.Sections?.FirstOrDefault(s =>
231+
s.SectionCode != null &&
232+
s.SectionCode.Equals(sectionCode, StringComparison.OrdinalIgnoreCase));
233+
234+
if (section == null || section.Tracks == null || section.Tracks.Count == 0)
235+
{
236+
return new SortedDictionary<int, MusicTrack>();
237+
}
238+
239+
// Map BiblePublicationTrack to MusicTrack
240+
var musicTracks = section.Tracks
241+
.OrderBy(t => t.Number)
242+
.Select(t => new MusicTrack
243+
{
244+
Number = t.Number,
245+
Title = t.Title,
246+
Url = string.Empty, // URLs are computed on-demand
247+
LookUpPath = string.Empty,
248+
DownloadCode = null,
249+
OriginalTrackNumber = null
250+
})
251+
.ToDictionary(x => x.Number, x => x);
252+
253+
return new SortedDictionary<int, MusicTrack>(musicTracks);
254+
}
255+
catch (Exception ex)
256+
{
257+
logger.Error(ex, "Error getting MelodyMusic tracks by section. PublicationCode={PublicationCode}, SectionCode={SectionCode}",
258+
publicationCode, sectionCode);
259+
throw;
260+
}
261+
}
262+
205263
public async Task UpdateTrackUrlAsync(string publicationCode, int trackNumber, string url, CancellationToken cancellationToken = default)
206264
{
207265
// URLs are now computed on-demand, no need to store them

src/Bible.Alarm/Common/Helpers/ServiceRegistrationHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ private static void RegisterViewModels(IServiceCollection services)
306306
services.AddTransient<CategorySelectionViewModel>();
307307
services.AddTransient<SectionSelectionViewModel>();
308308
services.AddTransient<ViewModels.BiblePublications.TrackSelectionViewModel>();
309+
services.AddTransient<ViewModels.Music.MusicSectionSelectionViewModel>();
309310
services.AddTransient<AlarmViewModel>();
310311
services.AddTransient<BiblePublicationSelectionContainerViewModel>();
311312
services.AddTransient<MusicSelectionContainerViewModel>();
@@ -344,6 +345,7 @@ private static void RegisterUiComponents(IServiceCollection services)
344345
services.AddTransient<BiblePublicationSelectionModal>();
345346
services.AddTransient<SectionSelectionModal>();
346347
services.AddTransient<Views.Music.TrackSelectionModal>();
348+
services.AddTransient<Views.Music.MusicSectionSelectionModal>();
347349
services.AddTransient<MusicSelectionModal>();
348350
services.AddTransient<SongPublicationSelectionModal>();
349351
services.AddTransient<Views.Bible.TrackSelectionModal>();

src/Bible.Alarm/Services/Database/ScheduleMigrationService.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
using Bible.Alarm.Services.Database.Interfaces;
22
using Bible.Alarm.Shared.Helpers;
3-
using Bible.Alarm.Shared.Services.Schedule.Interfaces;
43
using Serilog;
54

65
namespace Bible.Alarm.Services.Database;
76

87
public sealed class ScheduleMigrationService(
9-
ILogger logger,
10-
IAlarmScheduleService alarmScheduleService)
8+
ILogger logger)
119
: IScheduleMigrationService, IDisposable
1210
{
1311
private readonly CancellationTokenSource cancellationTokenSource = new();

src/Bible.Alarm/Services/Media/BiblePublicationNavigationService.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#nullable enable
12
using Bible.Alarm.Services.Media.Interfaces;
23
using Bible.Alarm.Shared.Models.Schedule;
34
using Serilog;
@@ -30,12 +31,6 @@ private async Task<int> ConvertSectionCodeToIntAsync(string? sectionCode, string
3031
// If parsing fails, SectionCode is not numeric (e.g., "gen" for Genesis)
3132
// For non-numeric section codes, return 0
3233
return 0;
33-
{
34-
logger.Warning(ex, "Error converting SectionCode {SectionCode} to int for {LanguageCode}/{PublicationCode}",
35-
sectionCode, languageCode, publicationCode);
36-
}
37-
38-
return 0;
3934
}
4035

4136
public async Task<bool> MoveToPreviousSectionAsync(BiblePublicationSchedule schedule)

0 commit comments

Comments
 (0)