feat(02-02): add ScanWarning type and reclassify scan errors as warnings
- Add ScanWarning struct with FilePath/Phase/Err to ScanMetrics - Add mutex-protected addWarning method for concurrent use - Reclassify walk, extraction, commit, orphan, variant, FTS failures as warnings - Update commitBatch to return only fatal tx.Commit errors - Update cachedLinkArtist to check errors via database.IsUniqueViolation - Update handleConfigUpdate to capture and log scan warning count
This commit is contained in:
+61
-16
@@ -317,15 +317,13 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
errMu.Lock()
|
metrics.addWarning(
|
||||||
scanErr = errors.Join(
|
"", "walk",
|
||||||
scanErr,
|
|
||||||
fmt.Errorf(
|
fmt.Errorf(
|
||||||
"problem walking library directory: %w",
|
"problem walking library directory: %w",
|
||||||
walkErr,
|
walkErr,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
errMu.Unlock()
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -352,6 +350,10 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
"hash", work.hashStr,
|
"hash", work.hashStr,
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(
|
||||||
|
"", "variant", err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -433,9 +435,10 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
errMu.Lock()
|
metrics.addWarning(
|
||||||
scanErr = errors.Join(scanErr, err)
|
work.absolutePath,
|
||||||
errMu.Unlock()
|
"extraction", err,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -491,6 +494,8 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(path, "orphan", err)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +508,8 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
"id", audioFile.ID,
|
"id", audioFile.ID,
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(path, "orphan", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
removed.Add(1)
|
removed.Add(1)
|
||||||
@@ -520,6 +527,8 @@ func (l *Library) Scan() (*ScanMetrics, error) {
|
|||||||
"could not generate missing sized variants",
|
"could not generate missing sized variants",
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning("", "variant", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.PostScanVariants = time.Since(variantStart)
|
metrics.PostScanVariants = time.Since(variantStart)
|
||||||
@@ -663,8 +672,6 @@ func (l *Library) commitBatch(
|
|||||||
|
|
||||||
txq := l.db.Queries.WithTx(tx)
|
txq := l.db.Queries.WithTx(tx)
|
||||||
|
|
||||||
var batchErr error
|
|
||||||
|
|
||||||
for i := range batch {
|
for i := range batch {
|
||||||
result := &batch[i]
|
result := &batch[i]
|
||||||
|
|
||||||
@@ -695,7 +702,9 @@ func (l *Library) commitBatch(
|
|||||||
"err", saveErr,
|
"err", saveErr,
|
||||||
)
|
)
|
||||||
|
|
||||||
batchErr = errors.Join(batchErr, saveErr)
|
metrics.addWarning(
|
||||||
|
result.absolutePath, "commit", saveErr,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +715,7 @@ func (l *Library) commitBatch(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return batchErr
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveAudioFile writes audio file metadata to the database (new files).
|
// saveAudioFile writes audio file metadata to the database (new files).
|
||||||
@@ -795,6 +804,8 @@ func (l *Library) saveAudioFile(
|
|||||||
"path", result.absolutePath,
|
"path", result.absolutePath,
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(result.absolutePath, "commit", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Debug(
|
l.logger.Debug(
|
||||||
@@ -873,6 +884,8 @@ func (l *Library) updateAudioFileMetadata(
|
|||||||
"id", result.existingFileID,
|
"id", result.existingFileID,
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(result.absolutePath, "commit", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := tx.ExecContext(
|
if _, err := tx.ExecContext(
|
||||||
@@ -890,6 +903,8 @@ func (l *Library) updateAudioFileMetadata(
|
|||||||
"path", result.absolutePath,
|
"path", result.absolutePath,
|
||||||
"err", err,
|
"err", err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(result.absolutePath, "commit", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.logger.Debug(
|
l.logger.Debug(
|
||||||
@@ -938,11 +953,11 @@ func (l *Library) processMetadata(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.cachedLinkArtist(q, cache, artistName, artistCredit.ID)
|
l.cachedLinkArtist(q, cache, metrics, artistName, artistCredit.ID)
|
||||||
|
|
||||||
// 3. Get or create artist credit for album artist.
|
// 3. Get or create artist credit for album artist.
|
||||||
albumArtistCreditID := l.resolveAlbumArtistCredit(
|
albumArtistCreditID := l.resolveAlbumArtistCredit(
|
||||||
q, cache, tags, artistCredit.ID,
|
q, cache, metrics, tags, artistCredit.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Get or create release group (album).
|
// 4. Get or create release group (album).
|
||||||
@@ -1071,9 +1086,12 @@ func (l *Library) cachedUpsertArtistCredit(
|
|||||||
|
|
||||||
// cachedLinkArtist upserts the artist record and creates the
|
// cachedLinkArtist upserts the artist record and creates the
|
||||||
// artist-credit-artist link, skipping work already done.
|
// artist-credit-artist link, skipping work already done.
|
||||||
|
// UNIQUE constraint violations are silently ignored (link already
|
||||||
|
// exists in the database). Other errors are recorded as scan warnings.
|
||||||
func (l *Library) cachedLinkArtist(
|
func (l *Library) cachedLinkArtist(
|
||||||
q *sqlcgen.Queries,
|
q *sqlcgen.Queries,
|
||||||
cache *entityCache,
|
cache *entityCache,
|
||||||
|
metrics *ScanMetrics,
|
||||||
name string,
|
name string,
|
||||||
creditID int64,
|
creditID int64,
|
||||||
) {
|
) {
|
||||||
@@ -1098,13 +1116,34 @@ func (l *Library) cachedLinkArtist(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = q.CreateArtistCreditArtist(
|
_, err := q.CreateArtistCreditArtist(
|
||||||
l.ctx,
|
l.ctx,
|
||||||
sqlcgen.CreateArtistCreditArtistParams{
|
sqlcgen.CreateArtistCreditArtistParams{
|
||||||
ArtistID: artist.ID,
|
ArtistID: artist.ID,
|
||||||
CreditID: creditID,
|
CreditID: creditID,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
if !database.IsUniqueViolation(err) {
|
||||||
|
l.logger.Warn(
|
||||||
|
"could not link artist to credit",
|
||||||
|
"artist", name,
|
||||||
|
"creditID", creditID,
|
||||||
|
"err", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
metrics.addWarning(
|
||||||
|
name, "commit",
|
||||||
|
fmt.Errorf(
|
||||||
|
"artist-credit link failed for %q: %w",
|
||||||
|
name, err,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNIQUE violation: link already exists in DB, not an error.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cache.linkedCredits[linkKey] = struct{}{}
|
cache.linkedCredits[linkKey] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -1176,6 +1215,7 @@ func (l *Library) linkRecordingGenres(
|
|||||||
func (l *Library) resolveAlbumArtistCredit(
|
func (l *Library) resolveAlbumArtistCredit(
|
||||||
q *sqlcgen.Queries,
|
q *sqlcgen.Queries,
|
||||||
cache *entityCache,
|
cache *entityCache,
|
||||||
|
metrics *ScanMetrics,
|
||||||
tags *metadata.TrackMetadata,
|
tags *metadata.TrackMetadata,
|
||||||
trackArtistCreditID int64,
|
trackArtistCreditID int64,
|
||||||
) sql.NullInt64 {
|
) sql.NullInt64 {
|
||||||
@@ -1197,7 +1237,7 @@ func (l *Library) resolveAlbumArtistCredit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
l.cachedLinkArtist(
|
l.cachedLinkArtist(
|
||||||
q, cache, tags.AlbumArtist, albumArtistCredit.ID,
|
q, cache, metrics, tags.AlbumArtist, albumArtistCredit.ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
return sql.NullInt64{
|
return sql.NullInt64{
|
||||||
@@ -1322,7 +1362,7 @@ func (l *Library) handleConfigUpdate(updatedConfigValues Config) error {
|
|||||||
|
|
||||||
l.conf.DirectoryPath = updatedConfigValues.DirectoryPath
|
l.conf.DirectoryPath = updatedConfigValues.DirectoryPath
|
||||||
|
|
||||||
if _, err := l.Scan(); err != nil {
|
if scanMetrics, err := l.Scan(); err != nil {
|
||||||
updateErr = errors.Join(
|
updateErr = errors.Join(
|
||||||
updateErr,
|
updateErr,
|
||||||
fmt.Errorf(
|
fmt.Errorf(
|
||||||
@@ -1330,6 +1370,11 @@ func (l *Library) handleConfigUpdate(updatedConfigValues Config) error {
|
|||||||
err,
|
err,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
} else if len(scanMetrics.Warnings) > 0 {
|
||||||
|
l.logger.Warn(
|
||||||
|
"library scan completed with warnings",
|
||||||
|
"warningCount", len(scanMetrics.Warnings),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ type ScanMetrics struct {
|
|||||||
Updated int64 `json:"updated"`
|
Updated int64 `json:"updated"`
|
||||||
Skipped int64 `json:"skipped"`
|
Skipped int64 `json:"skipped"`
|
||||||
Removed int64 `json:"removed"`
|
Removed int64 `json:"removed"`
|
||||||
|
|
||||||
|
// Non-fatal issues encountered during scanning.
|
||||||
|
Warnings []ScanWarning `json:"warnings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanWarning represents a non-fatal issue encountered during scanning.
|
||||||
|
type ScanWarning struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
Err error `json:"err"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScanMetrics() *ScanMetrics {
|
func newScanMetrics() *ScanMetrics {
|
||||||
@@ -80,6 +90,18 @@ func (m *ScanMetrics) addCoverArtSave(d time.Duration) {
|
|||||||
m.CoverArtSave += d
|
m.CoverArtSave += d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addWarning records a non-fatal scan issue. Safe for concurrent use.
|
||||||
|
func (m *ScanMetrics) addWarning(filePath, phase string, err error) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.Warnings = append(m.Warnings, ScanWarning{
|
||||||
|
FilePath: filePath,
|
||||||
|
Phase: phase,
|
||||||
|
Err: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// addThumbnailTier records the time spent generating a single
|
// addThumbnailTier records the time spent generating a single
|
||||||
// thumbnail tier. Safe for concurrent use from the thumbnail
|
// thumbnail tier. Safe for concurrent use from the thumbnail
|
||||||
// worker pool.
|
// worker pool.
|
||||||
|
|||||||
Reference in New Issue
Block a user