Skip to content

Commit c069dab

Browse files
committed
clean up some diagnostics
1 parent 45aee4f commit c069dab

24 files changed

Lines changed: 184 additions & 115 deletions

Sockseek.Cli/Program.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ internal static async Task<CliExitCode> MainCore(string[] args, CliOutputControl
155155

156156
LogCliSessionStart(remoteSettings);
157157

158-
var cts = new CancellationTokenSource();
158+
using var cts = new CancellationTokenSource();
159159

160160
if (remoteSettings.IsEnabled)
161161
{
@@ -419,7 +419,7 @@ await backend.SubmitExtractJobAsync(
419419
{
420420
SockseekLog.Trace("Main: Entered finally block. Disposing clientManager...");
421421
engine.Cancel();
422-
cts.Cancel();
422+
await cts.CancelAsync();
423423
cliReporter?.Stop();
424424
clientManager.Dispose();
425425
Printing.SetBuffering(false);
@@ -673,7 +673,7 @@ private static async Task<CliExitCode> RunRemoteAsync(
673673
}
674674
finally
675675
{
676-
cts.Cancel();
676+
await cts.CancelAsync();
677677
cliReporter?.Stop();
678678
}
679679
}
@@ -1256,7 +1256,7 @@ internal static void EnsureDaemonEndpointAvailable(DaemonSettings daemonSettings
12561256

12571257
try
12581258
{
1259-
var listener = new System.Net.Sockets.TcpListener(ipAddress, daemonSettings.ListenPort);
1259+
using var listener = new System.Net.Sockets.TcpListener(ipAddress, daemonSettings.ListenPort);
12601260
listener.Start();
12611261
listener.Stop();
12621262
}

Sockseek.Cli/Services/InteractiveCliCoordinator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
namespace Sockseek.Cli;
1111

12+
// TODO [LIFETIME]: Make the interactive coordinator disposable as part of CLI run-scope ownership.
13+
// It owns a promptSemaphore today, while callers also start background RunUntilCompleteAsync work.
14+
// Dispose should be coordinated with that background task and the tests that construct coordinators directly.
1215
internal sealed class InteractiveCliCoordinator
1316
{
1417
private readonly ICliBackend backend;

Sockseek.Core/DownloadEngine.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ namespace Sockseek.Core;
2424
// 1. Break this class into isolated services (e.g., IJobPipeline, IQueueOrchestrator).
2525
// 2. Inject dependencies (ISearcher, IDownloader) via constructor injection.
2626
// This will drastically improve maintainability and make the orchestration logic actually unit-testable.
27+
//
28+
// TODO [ARCHITECTURE]: Define explicit engine/service lifetime ownership.
29+
// DownloadEngine currently owns app-wide cancellation, a Searcher with disposable throttling
30+
// semaphores, and background stale-download work. Refactor this into an async-disposable
31+
// run scope so completion, cancellation, and server-host shutdown all dispose the same graph.
2732
public class DownloadEngine
2833
{
2934
private Searcher? searcher = null;
@@ -607,7 +612,7 @@ public async Task RunAsync(CancellationToken ct)
607612
Events.RaiseEngineCompleted(Queue);
608613

609614
SockseekLog.Jobs.Debug("Exiting RunAsync");
610-
appCts.Cancel();
615+
await appCts.CancelAsync();
611616
}
612617

613618

@@ -2680,7 +2685,7 @@ async Task<JobOutcome> SearchAndDownloadSong(SongJob song, Job job, DownloadSett
26802685
if (fastDownload?.Status == FileDownloadStatus.Completed && fastDownload.Result != null)
26812686
{
26822687
// Fast download won — cancel the search.
2683-
searchCts.Cancel();
2688+
await searchCts.CancelAsync();
26842689
try { await searchTask; } catch (OperationCanceledException) { }
26852690

26862691
var result = fastDownload.Result;
@@ -3442,7 +3447,7 @@ async Task<JobOutcome> DownloadEmbeddedSong(
34423447
if (cancelGroupOnFail && ShouldCancelGroupOnEmbeddedOutcome(outcome))
34433448
{
34443449
CommitOutcome(song, outcome);
3445-
groupCts.Cancel();
3450+
await groupCts.CancelAsync();
34463451
throw new OperationCanceledException();
34473452
}
34483453

@@ -3457,7 +3462,7 @@ async Task<JobOutcome> DownloadEmbeddedSong(
34573462

34583463
if (cancelGroupOnFail && ShouldCancelGroupOnEmbeddedOutcome(finalOutcome))
34593464
{
3460-
groupCts.Cancel();
3465+
await groupCts.CancelAsync();
34613466
throw new OperationCanceledException();
34623467
}
34633468

@@ -3472,7 +3477,7 @@ async Task<JobOutcome> DownloadEmbeddedSong(
34723477
}
34733478
catch (OperationCanceledException) when (!groupCts.IsCancellationRequested && cancelGroupOnFail)
34743479
{
3475-
groupCts.Cancel();
3480+
await groupCts.CancelAsync();
34763481
throw;
34773482
}
34783483
finally

Sockseek.Core/Extractors/Csv.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Sockseek.Core.Jobs;
33
using System.Text.RegularExpressions;
44
using Sockseek.Core.Settings;
5+
using System.Globalization;
56

67
namespace Sockseek.Core.Extractors;
78
public partial class CsvExtractor : IExtractor, IInputMatcher
@@ -248,10 +249,10 @@ static double ParseTrackLength(string duration, string format)
248249
{
249250
switch (formatParts[i])
250251
{
251-
case "h": totalSeconds += double.Parse(durationParts[i]) * 3600; break;
252-
case "m": totalSeconds += double.Parse(durationParts[i]) * 60; break;
253-
case "s": totalSeconds += double.Parse(durationParts[i]); break;
254-
case "ms": totalSeconds += double.Parse(durationParts[i]) / Math.Pow(10, durationParts[i].Length); break;
252+
case "h": totalSeconds += double.Parse(durationParts[i], CultureInfo.InvariantCulture) * 3600; break;
253+
case "m": totalSeconds += double.Parse(durationParts[i], CultureInfo.InvariantCulture) * 60; break;
254+
case "s": totalSeconds += double.Parse(durationParts[i], CultureInfo.InvariantCulture); break;
255+
case "ms": totalSeconds += double.Parse(durationParts[i], CultureInfo.InvariantCulture) / Math.Pow(10, durationParts[i].Length); break;
255256
}
256257
}
257258
return totalSeconds;

Sockseek.Core/Extractors/List.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static bool InputMatches(string input)
2525
return !input.IsInternetUrl();
2626
}
2727

28-
public Task<Job> GetTracks(string input, ExtractionSettings extraction, ExtractorContext? context = null)
28+
public async Task<Job> GetTracks(string input, ExtractionSettings extraction, ExtractorContext? context = null)
2929
{
3030
var maxTracks = extraction.MaxTracks;
3131
var offset = extraction.Offset;
@@ -36,7 +36,7 @@ public Task<Job> GetTracks(string input, ExtractionSettings extraction, Extracto
3636
if (!File.Exists(listFilePath))
3737
throw new FileNotFoundException($"List file '{listFilePath}' not found");
3838

39-
var lines = File.ReadAllLines(listFilePath);
39+
var lines = await File.ReadAllLinesAsync(listFilePath);
4040

4141
var result = new JobList { ItemName = Path.GetFileNameWithoutExtension(listFilePath), EnablesIndexByDefault = true };
4242

@@ -103,7 +103,7 @@ public Task<Job> GetTracks(string input, ExtractionSettings extraction, Extracto
103103
added++;
104104
}
105105

106-
return Task.FromResult<Job>(result);
106+
return result;
107107
}
108108

109109
static List<string> ParseLine(string input)

Sockseek.Core/Extractors/MusicBrainz.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public partial class MusicBrainzExtractor : IExtractor, IInputMatcher
1414

1515
public static bool InputMatches(string input)
1616
{
17-
return input.IsInternetUrl() && input.ToLower().Contains("musicbrainz.org");
17+
return input.IsInternetUrl()
18+
&& input.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase);
1819
}
1920

2021
public async Task<Job> GetTracks(string input, ExtractionSettings extraction, ExtractorContext? context = null)
@@ -24,7 +25,7 @@ public async Task<Job> GetTracks(string input, ExtractionSettings extraction, Ex
2425
var offset = extraction.Offset;
2526
var reverse = extraction.Reverse;
2627

27-
var musicBrainzClient = new MusicBrainzClient(context.Log);
28+
using var musicBrainzClient = new MusicBrainzClient(context.Log);
2829

2930
int max = reverse ? int.MaxValue : maxTracks;
3031
int off = reverse ? 0 : offset;
@@ -68,7 +69,7 @@ public async Task<Job> GetTracks(string input, ExtractionSettings extraction, Ex
6869
}
6970
}
7071

71-
public class MusicBrainzClient
72+
public class MusicBrainzClient : IDisposable
7273
{
7374
private readonly HttpClient _httpClient;
7475
private readonly IJobLog _log;
@@ -205,4 +206,10 @@ public async Task<JobList> GetCollectionReleases(string mbid, int max, int offse
205206
_log.Info($"Found {queue.Jobs.Count} releases in collection '{collectionName}'");
206207
return queue;
207208
}
209+
210+
public void Dispose()
211+
{
212+
_httpClient.Dispose();
213+
GC.SuppressFinalize(this);
214+
}
208215
}

Sockseek.Core/Extractors/Spotify.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public async Task<Job> GetTracks(string input, ExtractionSettings extraction, Ex
3737
if (string.IsNullOrEmpty(_spotify.ClientId) || string.IsNullOrEmpty(_spotify.ClientSecret))
3838
throw new Exception("Spotify client ID and secret are required. Create a Spotify developer app and pass --spotify-id and --spotify-secret.");
3939

40-
spotifyClient = new Spotify(_spotify.ClientId ?? "", _spotify.ClientSecret ?? "", _spotify.Token ?? "", _spotify.Refresh ?? "", context.Log);
40+
using var spotifySession = new Spotify(_spotify.ClientId ?? "", _spotify.ClientSecret ?? "", _spotify.Token ?? "", _spotify.Refresh ?? "", context.Log);
41+
spotifyClient = spotifySession;
4142
await spotifyClient.Authorize(needLogin, extraction.RemoveTracksFromSource);
4243

4344
Job result;
@@ -185,7 +186,7 @@ private static string Describe(APIException exception)
185186
}
186187

187188

188-
public class Spotify
189+
public class Spotify : IDisposable
189190
{
190191
private EmbedIOAuthServer? _server;
191192
private readonly string _clientId;
@@ -528,7 +529,7 @@ public async Task RemoveTrackFromPlaylist(string playlistId, string trackUri)
528529
return (p.Name ?? "", p.Id ?? playlistId, songs);
529530
}
530531

531-
private string GetPlaylistIdFromUrl(string url)
532+
private static string GetPlaylistIdFromUrl(string url)
532533
{
533534
var uri = new Uri(url);
534535
var segments = uri.Segments;
@@ -566,10 +567,16 @@ public async Task<AlbumJob> GetAlbumJob(string url, ExtractionSettings extractio
566567
return albumJob;
567568
}
568569

569-
private string GetAlbumIdFromUrl(string url)
570+
private static string GetAlbumIdFromUrl(string url)
570571
{
571572
var uri = new Uri(url);
572573
var segments = uri.Segments;
573574
return segments[^1].TrimEnd('/');
574575
}
576+
577+
public void Dispose()
578+
{
579+
_server?.Dispose();
580+
GC.SuppressFinalize(this);
581+
}
575582
}

Sockseek.Core/Extractors/String.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Sockseek.Core.Models;
22
using Sockseek.Core.Jobs;
33
using Sockseek.Core.Settings;
4+
using System.Globalization;
45

56
namespace Sockseek.Core.Extractors;
67
public class StringExtractor : IExtractor, IInputMatcher
@@ -94,7 +95,7 @@ void setProperty(string key, string value)
9495
{
9596
case "title": _title = value; break;
9697
case "artist": _artist = value; break;
97-
case "length": _length = int.Parse(value); break;
98+
case "length": _length = int.Parse(value, CultureInfo.InvariantCulture); break;
9899
case "album": _album = value; break;
99100
case "artist-maybe-wrong":
100101
if (value == "true") _artistMaybeWrong = true;
@@ -106,12 +107,12 @@ void setProperty(string key, string value)
106107
_maxCount = -1;
107108
}
108109
else if (value.Last() == '-')
109-
_maxCount = int.Parse(value[..^1]);
110+
_maxCount = int.Parse(value[..^1], CultureInfo.InvariantCulture);
110111
else if (value.Last() == '+')
111-
_minCount = int.Parse(value[..^1]);
112+
_minCount = int.Parse(value[..^1], CultureInfo.InvariantCulture);
112113
else
113114
{
114-
_minCount = int.Parse(value);
115+
_minCount = int.Parse(value, CultureInfo.InvariantCulture);
115116
_maxCount = _minCount;
116117
}
117118
break;

Sockseek.Core/Extractors/YouTube.cs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task<Job> GetTracks(string input, ExtractionSettings extraction, Ex
4343
if (_yt.GetDeleted)
4444
{
4545
context.Log.Info("Getting deleted videos..");
46-
var archive = new YouTube.YouTubeArchiveRetriever(context.Log);
46+
using var archive = new YouTube.YouTubeArchiveRetriever(context.Log);
4747
deleted = await archive.RetrieveDeleted(input, printFailed: _yt.DeletedOnly);
4848
}
4949
if (!_yt.DeletedOnly)
@@ -115,7 +115,7 @@ public static string ApiKey
115115

116116
var playlistRequest = service.Playlists.List("snippet");
117117
playlistRequest.Id = playlistId;
118-
var playlistResponse = playlistRequest.Execute();
118+
var playlistResponse = await playlistRequest.ExecuteAsync();
119119
if (playlistResponse.Items.Count == 0)
120120
throw new InvalidOperationException($"Could not retrieve YouTube playlist '{playlistId}'.");
121121

@@ -131,7 +131,7 @@ public static string ApiKey
131131

132132
while (playlistItemsRequest != null && count < max + offset)
133133
{
134-
var playlistItemsResponse = playlistItemsRequest.Execute();
134+
var playlistItemsResponse = await playlistItemsRequest.ExecuteAsync();
135135
foreach (var playlistItem in playlistItemsResponse.Items)
136136
{
137137
if (count >= offset)
@@ -149,7 +149,7 @@ public static string ApiKey
149149

150150
var videoRequest = service.Videos.List("contentDetails,snippet");
151151
videoRequest.Id = playlistItem.Snippet.ResourceId.VideoId;
152-
var videoResponse = videoRequest.Execute();
152+
var videoResponse = await videoRequest.ExecuteAsync();
153153

154154
title = playlistItem.Snippet.Title;
155155
if (videoResponse.Items.Count == 0) continue;
@@ -295,7 +295,7 @@ public static async Task<SongJob> ParseTrackInfo(string title, string uploader,
295295
var service = RequireService();
296296
var videoRequest = service.Videos.List("contentDetails,snippet");
297297
videoRequest.Id = id;
298-
var videoResponse = videoRequest.Execute();
298+
var videoResponse = await videoRequest.ExecuteAsync();
299299
o.title = videoResponse.Items[0].Snippet.Title;
300300
o.uploader = videoResponse.Items[0].Snippet.ChannelTitle;
301301
o.length = (int)XmlConvert.ToTimeSpan(videoResponse.Items[0].ContentDetails.Duration).TotalSeconds;
@@ -333,7 +333,7 @@ public static void StopService()
333333
public static async Task<Dictionary<string, SongJob>> GetDictYtExplode(string url, int max = int.MaxValue, int offset = 0, IJobLog? log = null)
334334
{
335335
log ??= ExtractorContext.None.Log;
336-
var youtube = new YoutubeClient();
336+
using var youtube = new YoutubeClient();
337337
var playlist = await youtube.Playlists.GetAsync(url);
338338
var songs = new Dictionary<string, SongJob>();
339339
int count = 0;
@@ -360,15 +360,15 @@ public static async Task<Dictionary<string, SongJob>> GetDictYtExplode(string ur
360360

361361
public static async Task<string> GetPlaylistTitle(string url)
362362
{
363-
var youtube = new YoutubeClient();
363+
using var youtube = new YoutubeClient();
364364
var playlist = await youtube.Playlists.GetAsync(url);
365365
return playlist.Title;
366366
}
367367

368368
public static async Task<(string, List<SongJob>)> GetSongsYtExplode(string url, int max = int.MaxValue, int offset = 0, IJobLog? log = null)
369369
{
370370
log ??= ExtractorContext.None.Log;
371-
var youtube = new YoutubeClient();
371+
using var youtube = new YoutubeClient();
372372
var playlist = await youtube.Playlists.GetAsync(url);
373373
var playlistTitle = playlist.Title;
374374
var songs = new List<SongJob>();
@@ -410,7 +410,7 @@ public static async Task<string> UrlToId(string url)
410410
[GeneratedRegex(@"document\.title\s*=\s*""(.+?) - YouTube"";")]
411411
private static partial Regex DocumentTitleRegex();
412412

413-
public class YouTubeArchiveRetriever
413+
public class YouTubeArchiveRetriever : IDisposable
414414
{
415415
private readonly HttpClient _client;
416416
private readonly IJobLog _log;
@@ -424,7 +424,7 @@ public YouTubeArchiveRetriever(IJobLog? log = null)
424424

425425
public async Task<List<SongJob>> RetrieveDeleted(string url, bool printFailed = true)
426426
{
427-
var deletedVideoUrls = new BlockingCollection<string>();
427+
using var deletedVideoUrls = new BlockingCollection<string>();
428428

429429
int totalCount = 0;
430430
int archivedCount = 0;
@@ -435,7 +435,7 @@ public async Task<List<SongJob>> RetrieveDeleted(string url, bool printFailed =
435435
int workerCount = 4;
436436
var workers = new List<Task>();
437437

438-
var process = new Process
438+
using var process = new Process
439439
{
440440
StartInfo = new ProcessStartInfo
441441
{
@@ -498,7 +498,7 @@ public async Task<List<SongJob>> RetrieveDeleted(string url, bool printFailed =
498498
}
499499

500500
await Task.WhenAll(workers);
501-
process.WaitForExit();
501+
await process.WaitForExitAsync();
502502
deletedVideoUrls.CompleteAdding();
503503
_log.Info($"Deleted metadata total/archived/retrieved: {totalCount}/{archivedCount}/{songs.Count}");
504504

@@ -521,6 +521,12 @@ public async Task<List<SongJob>> RetrieveDeleted(string url, bool printFailed =
521521
return songs.ToList();
522522
}
523523

524+
public void Dispose()
525+
{
526+
_client.Dispose();
527+
GC.SuppressFinalize(this);
528+
}
529+
524530
private async Task<List<string>?> GetOldestArchiveUrls(string url, int limit)
525531
{
526532
var url2 = $"http://web.archive.org/cdx/search/cdx?url={url}&fl=timestamp,original&filter=statuscode:200&sort=timestamp:asc&limit={limit}";

Sockseek.Core/Jobs/SearchJob.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public sealed record AggregateAlbumProjection(AlbumQuery Query);
1717
public class SearchJob : Job
1818
{
1919
private readonly Lock _projectionCacheLock = new();
20-
private readonly Dictionary<string, (int Revision, bool IsComplete, object Value)> _projectionCache = [];
2120
private readonly Dictionary<string, object> _incrementalProjectionStates = [];
2221

2322
public string QueryText { get; }

0 commit comments

Comments
 (0)