refactored full rescan to use hook/lifecycle pattern and added rescan package
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Refactoring Catalog
|
# Refactoring Catalog
|
||||||
|
|
||||||
Prioritized list of architectural improvements identified during a full codebase audit (Feb 2026). Items are grouped by priority — tackle P1 before adding major new features, P2 as convenient, P3 opportunistically.
|
Prioritized list of architectural improvements identified during a full codebase audit (Feb 2026). Items are grouped by priority — tackle P1 before adding major features, P2 as convenient, P3 opportunistically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,23 +50,15 @@ Prioritized list of architectural improvements identified during a full codebase
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. Move `FullRescan` orchestration from library to app
|
### ~~10. Move `FullRescan` orchestration from library to app~~ — solved
|
||||||
|
|
||||||
**Problem:** `library.Library` holds references to the queue (`queueClearer` interface) and playlist service (`playlistRestorer` interface), set via `SetQueue()` and `SetPlaylistRestorer()`. The `FullRescan` method in `rescan.go` orchestrates clearing the queue and restoring playlists — cross-cutting concerns that aren't really library responsibilities.
|
Replaced `queueClearer`/`playlistRestorer` interfaces and `SetQueue`/`SetPlaylistRestorer` setters with a single `RescanHooks` struct containing `PreClear`/`PostScan` function callbacks. The app wires `queue.Clear` and `playlist.RestoreAllPlaylists` as hooks, so the library no longer has any knowledge of or dependency on those packages.
|
||||||
|
|
||||||
**Why it matters:** The library package shouldn't know about queue clearing or playlist restoration. This creates a dependency web (`app` -> `library` -> `queue`, `app` -> `library` -> `playlist`).
|
|
||||||
|
|
||||||
**Approach:** Move the `FullRescan` orchestration to the `app` level. The app already has references to all three packages. The library would only expose `Scan()` and a `ClearAndRescan()` that handles only library concerns (clear DB, walk files, extract metadata). The app's `FullRescan` handler would call `queue.Clear()`, `library.ClearAndRescan()`, then `playlist.RestoreAll()`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 11. Fix double `LibraryScanStarted` event during FullRescan
|
### ~~11. Fix double `LibraryScanStarted` event during FullRescan~~ — solved
|
||||||
|
|
||||||
**Problem:** `rescan.go:22` emits `LibraryScanStarted`, then calls `Scan()` which emits `LibraryScanStarted` again at `library.go:191`. The frontend receives two `LibraryScanStarted` events for a single full rescan.
|
Removed the `LibraryScanStarted` emission from `FullRescan` (resolved as part of item #10). The event is now only emitted from `Scan()`, giving exactly one emission per rescan.
|
||||||
|
|
||||||
**Why it matters:** Frontend components may show duplicate "scanning" UI state transitions or start/reset loading indicators twice.
|
|
||||||
|
|
||||||
**Approach:** Remove the `LibraryScanStarted` emission from either `FullRescan` or `Scan`. Since `Scan` is also called independently, keep it in `Scan` and remove it from `FullRescan`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -148,13 +148,13 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
|||||||
yj.queue.SetPlayer(yj.player)
|
yj.queue.SetPlayer(yj.player)
|
||||||
yj.queue.RestoreState()
|
yj.queue.RestoreState()
|
||||||
|
|
||||||
// Give the library a reference to the queue so FullRescan can
|
// Wire cross-cutting rescan hooks so the library can
|
||||||
// clear the queue and stop playback before wiping data.
|
// orchestrate queue clearing and playlist restoration
|
||||||
yj.library.SetQueue(yj.queue)
|
// without depending on those packages directly.
|
||||||
|
yj.library.SetRescanHooks(library.RescanHooks{
|
||||||
// Give the library a reference to the playlist service so
|
PreClear: yj.queue.Clear,
|
||||||
// FullRescan can restore playlists from M3U8 files.
|
PostScan: yj.playlist.RestoreAllPlaylists,
|
||||||
yj.library.SetPlaylistRestorer(yj.playlist)
|
})
|
||||||
|
|
||||||
// Register playback finished handler to drive queue auto-advance.
|
// Register playback finished handler to drive queue auto-advance.
|
||||||
yj.player.SetPlaybackFinishedHandler(yj.queue.OnPlaybackFinished)
|
yj.player.SetPlaybackFinishedHandler(yj.queue.OnPlaybackFinished)
|
||||||
|
|||||||
+21
-27
@@ -60,39 +60,33 @@ func newEntityCache() *entityCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// queueClearer is a narrow interface for clearing the playback queue.
|
// RescanHooks holds optional callbacks that run before and after
|
||||||
type queueClearer interface {
|
// the library-clear-and-scan phase of a full rescan. The app
|
||||||
Clear()
|
// layer sets these to coordinate cross-cutting concerns (e.g.
|
||||||
}
|
// clearing the queue, restoring playlists) without the library
|
||||||
|
// needing to know about those packages.
|
||||||
// playlistRestorer is a narrow interface for restoring playlists
|
type RescanHooks struct {
|
||||||
// from M3U8 files after a library rescan.
|
// PreClear runs before library data is wiped
|
||||||
type playlistRestorer interface {
|
// (e.g. clear queue and stop playback).
|
||||||
RestoreAllPlaylists()
|
PreClear func()
|
||||||
|
// PostScan runs after the scan completes
|
||||||
|
// (e.g. restore playlists from M3U8 files).
|
||||||
|
PostScan func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Library manages scanning and querying the music collection.
|
// Library manages scanning and querying the music collection.
|
||||||
type Library struct {
|
type Library struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
conf *Config
|
conf *Config
|
||||||
db *database.DB
|
db *database.DB
|
||||||
queue queueClearer
|
rescanHooks RescanHooks
|
||||||
playlistRestorer playlistRestorer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetQueue provides the library with a reference to the queue so
|
// SetRescanHooks provides optional hooks for cross-cutting
|
||||||
// that destructive operations like FullRescan can clear the queue
|
// orchestration during FullRescan.
|
||||||
// and stop playback before wiping data.
|
func (l *Library) SetRescanHooks(h RescanHooks) {
|
||||||
func (l *Library) SetQueue(q queueClearer) {
|
l.rescanHooks = h
|
||||||
l.queue = q
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPlaylistRestorer provides the library with a reference to
|
|
||||||
// the playlist service so that FullRescan can restore playlists
|
|
||||||
// from M3U8 files after wiping data.
|
|
||||||
func (l *Library) SetPlaylistRestorer(p playlistRestorer) {
|
|
||||||
l.playlistRestorer = p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLibrary creates a new library with the given configuration.
|
// NewLibrary creates a new library with the given configuration.
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
||||||
|
|
||||||
"yellowjacket/backend/events"
|
|
||||||
"yellowjacket/backend/system"
|
"yellowjacket/backend/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,14 +16,13 @@ import (
|
|||||||
func (l *Library) FullRescan() (*ScanMetrics, error) {
|
func (l *Library) FullRescan() (*ScanMetrics, error) {
|
||||||
l.logger.Info("beginning full library rescan")
|
l.logger.Info("beginning full library rescan")
|
||||||
|
|
||||||
runtime.EventsEmit(l.ctx, events.LibraryScanStarted)
|
// Run the pre-clear hook (e.g. clear queue / stop playback)
|
||||||
|
// before wiping data so the player is not referencing
|
||||||
// Stop playback and clear the queue before wiping data so
|
// now-deleted tracks.
|
||||||
// the player is not referencing now-deleted tracks.
|
|
||||||
clearQueueStart := time.Now()
|
clearQueueStart := time.Now()
|
||||||
|
|
||||||
if l.queue != nil {
|
if l.rescanHooks.PreClear != nil {
|
||||||
l.queue.Clear()
|
l.rescanHooks.PreClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
clearQueueDur := time.Since(clearQueueStart)
|
clearQueueDur := time.Since(clearQueueStart)
|
||||||
@@ -68,10 +64,10 @@ func (l *Library) FullRescan() (*ScanMetrics, error) {
|
|||||||
clearDBDur + clearFilesDur
|
clearDBDur + clearFilesDur
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore playlists from M3U8 files now that the library
|
// Run the post-scan hook (e.g. restore playlists from M3U8
|
||||||
// has been rescanned and audio_files are populated again.
|
// files) now that audio_files are populated again.
|
||||||
if l.playlistRestorer != nil {
|
if l.rescanHooks.PostScan != nil {
|
||||||
l.playlistRestorer.RestoreAllPlaylists()
|
l.rescanHooks.PostScan()
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics, err
|
return metrics, err
|
||||||
|
|||||||
+1
-3
@@ -19,6 +19,4 @@ export function Scan():Promise<library.ScanMetrics>;
|
|||||||
|
|
||||||
export function SetContext(arg1:context.Context):Promise<void>;
|
export function SetContext(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
export function SetPlaylistRestorer(arg1:library.playlistRestorer):Promise<void>;
|
export function SetRescanHooks(arg1:library.RescanHooks):Promise<void>;
|
||||||
|
|
||||||
export function SetQueue(arg1:library.queueClearer):Promise<void>;
|
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ export function SetContext(arg1) {
|
|||||||
return window['go']['library']['Library']['SetContext'](arg1);
|
return window['go']['library']['Library']['SetContext'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SetPlaylistRestorer(arg1) {
|
export function SetRescanHooks(arg1) {
|
||||||
return window['go']['library']['Library']['SetPlaylistRestorer'](arg1);
|
return window['go']['library']['Library']['SetRescanHooks'](arg1);
|
||||||
}
|
|
||||||
|
|
||||||
export function SetQueue(arg1) {
|
|
||||||
return window['go']['library']['Library']['SetQueue'](arg1);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,18 @@ export namespace library {
|
|||||||
this.Name = source["Name"];
|
this.Name = source["Name"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class RescanHooks {
|
||||||
|
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new RescanHooks(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
export class ScanMetrics {
|
export class ScanMetrics {
|
||||||
total: number;
|
total: number;
|
||||||
loadExisting: number;
|
loadExisting: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user