Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add directory watch functionality and enhance audio file proces… #401

Merged
merged 6 commits into from
Jan 19, 2025

Conversation

tphakala
Copy link
Owner

…sing

  • Introduced a new flag for the directory command to enable watching for new files.
  • Implemented a directory scanning mechanism that processes .wav and .flac files, including checks for file readiness and processing locks.
  • Enhanced error handling and logging for file analysis, ensuring better feedback during processing.
  • Updated configuration to include a watch option in the input settings, allowing for dynamic file monitoring.

These changes improve the usability of the directory command by enabling real-time file processing and enhancing the overall robustness of audio file handling.

…sing

- Introduced a new flag for the directory command to enable watching for new files.
- Implemented a directory scanning mechanism that processes .wav and .flac files, including checks for file readiness and processing locks.
- Enhanced error handling and logging for file analysis, ensuring better feedback during processing.
- Updated configuration to include a watch option in the input settings, allowing for dynamic file monitoring.

These changes improve the usability of the directory command by enabling real-time file processing and enhancing the overall robustness of audio file handling.
Copy link
Contributor

coderabbitai bot commented Jan 19, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This pull request introduces enhancements to the command functions across multiple files, focusing on context management for graceful shutdowns and improved error handling. A new --watch flag is added for monitoring directory changes. The DirectoryAnalysis and FileAnalysis functions are updated to accept context parameters, enabling cancellation of operations. Additional helper functions are introduced for file processing, and file extension checks are made case-insensitive. The changes collectively enhance the functionality and robustness of the application.

Changes

File Change Summary
cmd/directory/directory.go Enhanced Command function with context management, added --watch (-w) flag for directory monitoring, refined error handling.
internal/analysis/directory.go Introduced multiple new functions for file processing, state management, validation, and continuous directory scanning; updated DirectoryAnalysis to accept context.
internal/conf/config.go Added Watch boolean field to InputConfig struct.
internal/analysis/file.go, internal/myaudio/readfile.go Updated file extension handling to be case-insensitive in FileAnalysis and ReadAudioFileBuffered.
cmd/file/file.go Integrated context management and signal handling for graceful shutdowns in the command function.
main.go Added specific error handling for user-initiated cancellations with analysis.ErrAnalysisCanceled.
internal/analysis/errors.go Introduced new error variable ErrAnalysisCanceled for handling user cancellation.

Possibly related PRs

Poem

🐰 Hop, hop, watch the files flow!
A rabbit's eye on directories so
New files dance, processed with care
Continuous scanning everywhere
Code evolves, magic unfolds! 🎉


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57d555e and 3060a6b.

📒 Files selected for processing (2)
  • cmd/directory/directory.go (3 hunks)
  • cmd/file/file.go (2 hunks)

Finishing Touches

  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
internal/analysis/directory.go (2)

206-227: Optimize file size stability check to reduce processing time

The isFileSizeStable function currently checks the file size three times with a 2-second sleep in between, totaling at least 4 seconds per file. This could slow down the processing if there are many files. Consider reducing the number of checks or the sleep duration, or implementing a more efficient mechanism to detect if a file is still being written to.


284-290: Simplify file extension handling with strings.TrimSuffix

The code for removing the .wav and .flac extensions from the output path can be simplified using strings.TrimSuffix. This improves code readability and reduces potential errors.

Apply this diff to simplify extension handling:

 		outputPath := filepath.Join(settings.Output.File.Path, filepath.Base(path))
 		ext := strings.ToLower(filepath.Ext(outputPath))
-		if ext == ".wav" {
-			outputPath = outputPath[:len(outputPath)-4]
-		} else if ext == ".flac" {
-			outputPath = outputPath[:len(outputPath)-5]
+		outputPath = strings.TrimSuffix(outputPath, ext)
internal/myaudio/readfile.go (1)

64-64: Refactor to avoid redundancy in extension handling

The code for obtaining the lowercase file extension is repeated. Consider creating a helper function to avoid redundancy and enhance maintainability.

Apply this diff to refactor the extension handling:

+func getLowercaseExt(path string) string {
+	return strings.ToLower(filepath.Ext(path))
+}
...
 	file, err := os.Open(settings.Input.Path)
 	if err != nil {
 		return err
 	}
 	defer file.Close()

-	ext := strings.ToLower(filepath.Ext(settings.Input.Path))
+	ext := getLowercaseExt(settings.Input.Path)
internal/analysis/file.go (1)

68-72: LGTM! Consider enhancing the error message.

The case-insensitive extension check is a good approach. However, the error message could be more user-friendly by listing the supported formats.

-		return fmt.Errorf("\033[31m❌ Invalid audio file %s: unsupported audio format: %s\033[0m", filepath.Base(filePath), filepath.Ext(filePath))
+		return fmt.Errorf("\033[31m❌ Invalid audio file %s: unsupported audio format: %s (supported formats: .wav, .flac)\033[0m", filepath.Base(filePath), filepath.Ext(filePath))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfbaf96 and 32c6eb2.

📒 Files selected for processing (5)
  • cmd/directory/directory.go (1 hunks)
  • internal/analysis/directory.go (2 hunks)
  • internal/analysis/file.go (2 hunks)
  • internal/conf/config.go (1 hunks)
  • internal/myaudio/readfile.go (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (8)
internal/analysis/directory.go (3)

396-399: Ensure proper cleanup and cancellation upon receiving signals

In the signal handling goroutine, after calling cleanupProcessingFiles(), you invoke cancel(). Verify that all resources are properly released and that there are no race conditions or deadlocks caused by the context cancellation.


99-104: 🛠️ Refactor suggestion

Reconsider the use of os.OpenFile for file locking detection

The isFileLocked function currently determines if a file is locked by attempting to open it with read-write permissions. However, failure to open a file might not necessarily indicate that the file is locked; it could also be due to permission issues or the file being deleted. Consider a more reliable method to check if the file is being written to, such as checking if the file size is stable or using platform-specific file locking mechanisms.


369-370: ⚠️ Potential issue

Return after logging error when creating output directory

In the error handling at line 370, you log that you failed to create the output directory but continue execution. This could lead to further errors down the line. Consider returning the error after logging to prevent execution when the output directory cannot be created.

Apply this diff to return the error:

   	log.Printf("Failed to create output directory: %v", err)
+	return err

Likely invalid or redundant comment.

cmd/directory/directory.go (1)

39-39: Verify there are no flag conflicts with the -w shorthand

The shorthand -w for the --watch flag might conflict with other flags in your application or dependencies. Ensure that -w is not already used elsewhere to prevent unexpected behavior.

internal/myaudio/readfile.go (1)

44-44: Standardize file extension handling for case-insensitive comparison

Converting file extensions to lowercase using strings.ToLower ensures consistent handling of files regardless of the case of the extension. This change improves robustness in processing audio files.

internal/analysis/file.go (2)

Line range hint 74-89: LGTM! Thorough audio validation.

The validation logic is comprehensive, checking file accessibility, audio format, and sample data. Good use of defer for file closing and clear error messages.


Line range hint 41-47: LGTM! Excellent error handling.

The error handling is robust, with proper error wrapping and user-friendly feedback. Good practice to save partial results before exiting.

internal/conf/config.go (1)

198-198: LGTM! Well-documented configuration field.

The addition of the Watch field with the yaml:"-" tag is appropriate for runtime configuration. The field's purpose is clearly documented.

Comment on lines 146 to 147
expectedSize := int64(wavHeader[0]) | int64(wavHeader[1])<<8 | int64(wavHeader[2])<<16 | int64(wavHeader[3])<<24
expectedSize += 8 // Add 8 bytes for RIFF header
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential integer overflow when calculating expectedSize

The calculation of expectedSize could result in an integer overflow if the WAV file is larger than 2GB. Ensure that the variables used can accommodate large file sizes without overflowing by using uint64 where appropriate.

Comment on lines 232 to 263
valid, reason, err := verifyAudioFile(path)
if err != nil {
log.Printf("Error verifying audio file %s: %v", filepath.Base(path), err)
return false, nil
}
if !valid {
log.Printf("File %s is not ready: %s", filepath.Base(path), reason)
return false, nil
}

// Check if file is locked
if isFileLocked(path) {
log.Printf("File %s is locked, skipping...", filepath.Base(path))
return false, nil
}

// Check modification time
info, err := os.Stat(path)
if err != nil {
return false, err
}
if time.Since(info.ModTime()) < 30*time.Second {
log.Printf("File %s was modified too recently, waiting...", filepath.Base(path))
return false, nil
}

// Check file size stability
stable, err := isFileSizeStable(path)
if err != nil || !stable {
return false, err
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle errors consistently in isFileReadyForProcessing

In isFileReadyForProcessing, when verifyAudioFile returns an error, the function logs the error but does not return it, instead returning false, nil. Similarly, when isFileSizeStable returns false, the function returns false, err, where err could be nil. This inconsistency could lead to suppressed errors. Ensure that all errors are appropriately returned and handled.

Apply this diff to return and handle errors correctly:

 			return false, nil
-		}
+		}
+		return false, err
 		}
 		if !valid {
 			log.Printf("File %s is not ready: %s", filepath.Base(path), reason)
 			return false, nil
 		}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 145 to 159
// Since we've already read the first 4 bytes, we need to use the first 4 bytes of wavHeader
expectedSize := int64(wavHeader[0]) | int64(wavHeader[1])<<8 | int64(wavHeader[2])<<16 | int64(wavHeader[3])<<24
expectedSize += 8 // Add 8 bytes for RIFF header

// If file is smaller than header indicates, it's definitely incomplete
if actualSize < expectedSize {
return false, fmt.Sprintf("incomplete WAV file: expected %d bytes, got %d", expectedSize, actualSize), nil
}

// Allow for small differences (up to 1KB) due to block sizes
sizeDiff := actualSize - expectedSize
if sizeDiff > 1024 {
return false, fmt.Sprintf("WAV file size mismatch: expected %d bytes, got %d (difference too large: %d bytes)",
expectedSize, actualSize, sizeDiff), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider endianness when reading WAV header fields

The calculation of expectedSize in verifyAudioFile reads bytes directly to form an integer. This assumes little-endian byte order. However, WAV files are typically stored in little-endian format, but it's safer to explicitly specify endianness. Use binary.Read with the appropriate endianness to correctly parse the header fields.

Apply this diff to use binary.Read for correct parsing:

+import "encoding/binary"
...
-			expectedSize := int64(wavHeader[0]) | int64(wavHeader[1])<<8 | int64(wavHeader[2])<<16 | int64(wavHeader[3])<<24
+			var chunkSize uint32
+			err = binary.Read(bytes.NewReader(wavHeader[0:4]), binary.LittleEndian, &chunkSize)
+			if err != nil {
+				return false, "", fmt.Errorf("failed to read chunk size: %v", err)
+			}
+			expectedSize := int64(chunkSize) + 8 // Add 8 bytes for RIFF header

Committable suggestion skipped: line range outside the PR's diff.

… error handling

- Added context cancellation support to audio file analysis, allowing for graceful shutdowns during processing.
- Implemented signal handling for user-initiated cancellations, ensuring clean exits without errors.
- Enhanced error handling in file and directory analysis, providing clearer feedback on issues encountered during processing.
- Updated the WAV and FLAC file validation logic to improve robustness and accuracy in file readiness checks.

These changes improve the user experience by allowing for more responsive and controlled audio processing, especially in long-running tasks.
@tphakala
Copy link
Owner Author

@coderabbitai full review

Copy link
Contributor

coderabbitai bot commented Jan 19, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
internal/analysis/directory.go (3)

91-92: Handle errors when removing stale lock files

The error returned by os.Remove(outputPathProcessing) is not being checked. Ignoring errors from file operations can lead to unintended behavior if the file cannot be removed.

Apply this diff to handle the error:

    if isLockFileStale(outputPathProcessing) {
        log.Printf("Removing stale lock file: %s", outputPathProcessing)
-       os.Remove(outputPathProcessing)
+       if err := os.Remove(outputPathProcessing); err != nil {
+           log.Printf("Error removing stale lock file %s: %v", outputPathProcessing, err)
+       }
        return false
    }

338-344: Simplify file extension handling using strings.TrimSuffix

Currently, the code manually strips the file extension based on its length, which can be error-prone and less readable. Using strings.TrimSuffix simplifies the code and makes it more maintainable.

Apply this diff to simplify the extension handling:

     outputPath := filepath.Join(settings.Output.File.Path, filepath.Base(path))
     ext := strings.ToLower(filepath.Ext(outputPath))
-    if ext == ".wav" {
-        outputPath = outputPath[:len(outputPath)-4]
-    } else if ext == ".flac" {
-        outputPath = outputPath[:len(outputPath)-5]
-    }
+    outputPath = strings.TrimSuffix(outputPath, ext)

412-414: Continue directory traversal on access errors

Returning an error in the filepath.WalkDir function will terminate the directory traversal. To ensure all accessible files are processed, log the error and continue with the traversal when an access error occurs.

Apply this diff to handle errors gracefully:

     if err != nil {
-        return fmt.Errorf("error accessing path %s: %w", path, err)
+        log.Printf("Error accessing path %s: %v", path, err)
+        return nil
     }
internal/analysis/file.go (3)

36-41: Consider removing redundant context check before writeResults.

While the context check before processing is crucial, the second check before writeResults might be unnecessary as:

  1. Writing results is typically a quick operation
  2. If cancellation occurs during writing, it's better to complete the write than leave partial results
-	// Check for context cancellation before writing results
-	select {
-	case <-ctx.Done():
-		return ctx.Err()
-	default:
-	}
-
 	return writeResults(settings, notes)

Also applies to: 56-61


83-89: Optimize file handling to avoid double open.

The file is being opened twice:

  1. First to check validity
  2. Again in GetAudioInfo
    Consider removing the explicit file open and rely on GetAudioInfo for validation.
 	// Check file extension (case-insensitive)
 	ext := strings.ToLower(filepath.Ext(filePath))
 	if ext != ".wav" && ext != ".flac" {
 		return fmt.Errorf("\033[31m❌ Invalid audio file %s: unsupported audio format: %s\033[0m", filepath.Base(filePath), filepath.Ext(filePath))
 	}

-	// Open the file to check if it's a valid audio file
-	file, err := os.Open(filePath)
-	if err != nil {
-		return fmt.Errorf("\033[31m❌ Error opening file %s: %w\033[0m", filepath.Base(filePath), err)
-	}
-	defer file.Close()

301-302: Enhance error context for cancellation.

Consider wrapping the context error with additional information about the processing state.

 		case <-ctx.Done():
-			return ctx.Err()
+			return fmt.Errorf("analysis cancelled at chunk position %v: %w", filePosition, ctx.Err())
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32c6eb2 and 04ff386.

📒 Files selected for processing (4)
  • cmd/file/file.go (2 hunks)
  • internal/analysis/directory.go (2 hunks)
  • internal/analysis/file.go (10 hunks)
  • main.go (1 hunks)
🔇 Additional comments (3)
internal/analysis/file.go (3)

4-4: LGTM! Good addition of context support.

The addition of context handling will enable proper cancellation of long-running analysis tasks.

Also applies to: 8-8, 20-20


111-111: LGTM! Well-implemented context handling in worker goroutine.

The context check is properly placed within the worker loop to enable quick cancellation response.

Also applies to: 162-168


205-207: LGTM! Good cleanup handling in progress monitor.

The progress monitoring goroutine properly handles cancellation and ensures cleanup.

Comment on lines 527 to 528
sleepTime := 30 + rand.Intn(15)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Seed the random number generator for true randomness

The rand.Intn(15) function uses the default seed, resulting in the same sequence each time the application runs. To ensure that the sleep intervals vary between runs, seed the random number generator.

Apply this diff to seed the random number generator at the start of DirectoryAnalysis:

+    // Seed the random number generator
+    rand.Seed(time.Now().UnixNano())

Place this code before the watch loop begins.

Committable suggestion skipped: line range outside the PR's diff.

main.go Outdated
Comment on lines 71 to 74
if err.Error() == "error processing audio: context canceled" {
// Clean exit for user-initiated cancellation
os.Exit(0)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid comparing error messages as strings

Comparing error messages as strings is fragile and can break if the error message changes. Instead, use errors.Is to compare errors, which is more reliable and idiomatic in Go.

Apply this diff to use errors.Is for error comparison:

+   "errors"
    // Other imports...

    if err := rootCmd.Execute(); err != nil {
-       if err.Error() == "error processing audio: context canceled" {
+       if errors.Is(err, context.Canceled) {
            // Clean exit for user-initiated cancellation
            os.Exit(0)
        }

Committable suggestion skipped: line range outside the PR's diff.

cmd/file/file.go Outdated
Comment on lines 44 to 47
if err == context.Canceled {
// Return nil for user-initiated cancellation
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use errors.Is to compare errors with context.Canceled

Directly comparing errors using == may not always work as expected due to error wrapping. Use errors.Is to reliably check for context.Canceled.

Apply this diff to improve error comparison:

+   "errors"
    // Other imports...

    err := analysis.FileAnalysis(settings, ctx)
-   if err == context.Canceled {
+   if errors.Is(err, context.Canceled) {
        // Return nil for user-initiated cancellation
        return nil
    }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 258 to 261
case <-ctx.Done():
processingError = ctx.Err()
close(doneChan)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding mutex for processingError access.

There's a potential race condition between setting processingError and closing doneChan. Consider using a mutex to synchronize access to processingError.

 	var processingError error
+	var processingErrorMutex sync.Mutex
 	go func() {
 		if settings.Debug {
 			fmt.Println("DEBUG: Result collector started")
 		}
 		for i := 1; i <= totalChunks; i++ {
 			select {
 			case <-ctx.Done():
+				processingErrorMutex.Lock()
 				processingError = ctx.Err()
+				processingErrorMutex.Unlock()
 				close(doneChan)
 				return

Committable suggestion skipped: line range outside the PR's diff.

- Added a new error type, ErrAnalysisCanceled, to handle user-initiated cancellations during audio analysis.
- Updated error handling in main.go and directory.go to utilize the new error type for clearer cancellation feedback.
- Enhanced synchronization in file.go to ensure safe access to processing errors across goroutines.

These changes enhance the robustness of audio processing by providing clearer error reporting and supporting graceful cancellation of analysis tasks.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
cmd/file/file.go (1)

44-47: 🛠️ Refactor suggestion

Use errors.Is for error comparison with context.Canceled

Directly comparing errors using == may not reliably detect if an error is context.Canceled, especially if the error has been wrapped. Use errors.Is to check for context cancellation.

Apply this diff to improve error handling:

+   "errors"
    // Other imports...

    err := analysis.FileAnalysis(settings, ctx)
-   if err == context.Canceled {
+   if errors.Is(err, context.Canceled) {
        // Return nil for user-initiated cancellation
        return nil
    }
main.go (1)

71-74: 🛠️ Refactor suggestion

Use errors.Is for robust error comparison.

Comparing error messages as strings is fragile. Use errors.Is with proper error types for more reliable error handling.

Apply this diff to improve error handling:

+ "context"
+ "errors"
  // Other imports...

  if err := rootCmd.Execute(); err != nil {
-   if err.Error() == "error processing audio: context canceled" {
+   if errors.Is(err, context.Canceled) {
      // Clean exit for user-initiated cancellation
      os.Exit(0)
    }
🧹 Nitpick comments (5)
cmd/file/file.go (1)

25-39: Ensure signal handling cleans up resources properly

The current signal handling setup is good for graceful shutdowns. However, consider handling additional signals like os.Kill and os.Quit to cover more termination scenarios. Also, ensure that any other resources or goroutines are properly cleaned up upon cancellation.

internal/analysis/file.go (1)

56-62: Avoid redundant context cancellation checks

The context cancellation check before writing results may be unnecessary since context cancellation would already have been handled during processing. Ensure that the extra check is needed, or remove it to simplify the code.

internal/analysis/directory.go (2)

101-127: Improve file lock detection on non-Windows systems

On non-Windows systems, syscall.O_NONBLOCK may not have the desired effect when checking file locks. Consider using flock or other file locking mechanisms to more accurately detect if a file is in use.


484-512: Ensure clean shutdown and resource cleanup

While the signal handling and context cancellation are implemented, consider handling all possible signals that could terminate the program. Additionally, make sure that all goroutines are properly terminated to prevent resource leaks.

internal/myaudio/readfile.go (1)

Line range hint 44-52: Extract duplicate file extension handling logic.

The file extension handling logic is duplicated between GetAudioInfo and ReadAudioFileBuffered. Consider extracting it into a helper function.

+func getSupportedAudioFormat(filePath string) (string, error) {
+    ext := strings.ToLower(filepath.Ext(filePath))
+    switch ext {
+    case ".wav", ".flac":
+        return ext, nil
+    default:
+        return "", fmt.Errorf("unsupported audio format: %s", ext)
+    }
+}

 func GetAudioInfo(filePath string) (AudioInfo, error) {
     file, err := os.Open(filePath)
     if err != nil {
         return AudioInfo{}, err
     }
     defer file.Close()
 
-    ext := strings.ToLower(filepath.Ext(filePath))
-    switch ext {
-    case ".wav":
+    ext, err := getSupportedAudioFormat(filePath)
+    if err != nil {
+        return AudioInfo{}, err
+    }
+    switch ext {
+    case ".wav":
         return readWAVInfo(file)
-    case ".flac":
+    case ".flac":
         return readFLACInfo(file)
-    default:
-        return AudioInfo{}, fmt.Errorf("unsupported audio format: %s", ext)
     }
+    return AudioInfo{}, fmt.Errorf("unhandled audio format: %s", ext)
 }

Also applies to: 64-72

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfbaf96 and 04ff386.

📒 Files selected for processing (7)
  • cmd/directory/directory.go (1 hunks)
  • cmd/file/file.go (2 hunks)
  • internal/analysis/directory.go (2 hunks)
  • internal/analysis/file.go (10 hunks)
  • internal/conf/config.go (1 hunks)
  • internal/myaudio/readfile.go (3 hunks)
  • main.go (1 hunks)
🔇 Additional comments (6)
internal/analysis/file.go (1)

84-89: Enhance audio file validation beyond file extensions

Relying solely on file extensions to validate audio files may not be sufficient, as files could have incorrect extensions. Consider inspecting the file content or using a library to verify that the file is indeed a valid WAV or FLAC file.

Apply this diff to improve file validation:

 func validateAudioFile(filePath string) error {
     fileInfo, err := os.Stat(filePath)
     if err != nil {
         return fmt.Errorf("\033[31m❌ Error accessing file %s: %w\033[0m", filepath.Base(filePath), err)
     }

-    // Check file extension (case-insensitive)
-    ext := strings.ToLower(filepath.Ext(filePath))
-    if ext != ".wav" && ext != ".flac" {
-        return fmt.Errorf("\033[31m❌ Invalid audio file %s: unsupported audio format: %s\033[0m", filepath.Base(filePath), filepath.Ext(filePath))
-    }

     // Open the file to check if it's a valid audio file
     file, err := os.Open(filePath)
     if err != nil {
         return fmt.Errorf("\033[31m❌ Error opening file %s: %w\033[0m", filepath.Base(filePath), err)
     }

This change removes the extension check and relies on attempting to read the file, which provides a more reliable validation.

internal/analysis/directory.go (2)

527-528: Seed the random number generator for true randomness

The use of rand.Intn(15) without seeding the random number generator results in the same sequence of sleep intervals each time the application runs. Seed the random number generator to ensure variability in sleep durations.

Apply this diff to seed the random number generator:

+    // Seed the random number generator
+    rand.Seed(time.Now().UnixNano())

     // Watch mode - continuously scan with random intervals
     log.Printf("Starting directory watch on %s (Press Ctrl+C to stop)", watchDir)

273-309: 🛠️ Refactor suggestion

Handle errors consistently in file readiness checks

In the isFileReadyForProcessing function, error handling could be improved for consistency:

  • When verifyAudioFile returns an error, consider returning the error instead of suppressing it with a log message.
  • When isFileSizeStable returns false with a nil error, ensure that this is the intended behavior.

Apply this diff to improve error handling:

     // Check if file is ready for processing
     isFileReadyForProcessing := func(path string) (bool, error) {
         // First verify the audio file headers and size
         valid, reason, err := verifyAudioFile(path)
         if err != nil {
-            return false, fmt.Errorf("error verifying audio file %s: %w", filepath.Base(path), err)
+            log.Printf("Error verifying audio file %s: %v", filepath.Base(path), err)
+            return false, nil
         }
         if !valid {
             log.Printf("File %s is not ready: %s", filepath.Base(path), reason)
             return false, nil
         }

         // Check file size stability
         stable, err := isFileSizeStable(path)
         if err != nil {
-            return false, fmt.Errorf("error checking file size stability for %s: %w", filepath.Base(path), err)
+            log.Printf("Error checking file size stability for %s: %v", filepath.Base(path), err)
+            return false, nil
         }

This adjustment ensures that errors are logged and processing can continue with other files.

Likely invalid or redundant comment.

cmd/directory/directory.go (1)

39-39: LGTM! Clean implementation of the watch flag.

The watch flag is properly integrated into the directory command using the standard Go flag patterns.

internal/conf/config.go (1)

198-198: LGTM! Consistent configuration field addition.

The Watch field is properly added with the yaml:"-" tag, consistent with other runtime-only configuration fields in the struct.

internal/myaudio/readfile.go (1)

44-44: LGTM! Improved file extension handling.

Case-insensitive file extension comparison makes the code more robust and user-friendly.

Also applies to: 64-64

…ocessing

- Removed unnecessary log statements in favor of direct fmt.Printf for signal handling during graceful shutdowns.
- Enhanced error handling in audio analysis to prioritize cancellation errors and ensure proper reporting of partial results.
- Improved synchronization in goroutines to prevent race conditions when sending errors and results, ensuring robust processing flow.

These changes contribute to cleaner code and improved responsiveness during audio analysis, enhancing overall user experience.
… handling

- Added context cancellation support to the directory analysis process, allowing for graceful shutdowns during file processing.
- Implemented signal handling for SIGINT and SIGTERM to manage user-initiated cancellations effectively.
- Updated the DirectoryAnalysis function to include a context parameter, ensuring that analysis can be interrupted cleanly.
- Enhanced error handling to provide clearer feedback on cancellation and processing issues.
- Introduced a new watchDirectory function to continuously monitor for new audio files, improving real-time processing capabilities.

These changes improve the responsiveness and robustness of the audio analysis workflow, enhancing user experience during long-running tasks.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
internal/analysis/file.go (1)

169-178: Consider using a buffered error channel.

The error channel might block if multiple workers encounter errors simultaneously. Consider using a buffered channel to prevent potential goroutine leaks.

-	errorChan := make(chan error, 1)
+	errorChan := make(chan error, numWorkers)
internal/analysis/directory.go (2)

19-84: Consider parameterizing lock file timeouts.

The lock file staleness (5 minutes) and cleanup (60 minutes) timeouts are hardcoded. Consider making these configurable via settings.

+// Default timeout values
+const (
+    DefaultLockStalenessTimeout = 5 * time.Minute
+    DefaultLockCleanupTimeout  = 60 * time.Minute
+)

 // isLockFileStale checks if a lock file is older than 5 minutes
 func isLockFileStale(path string) bool {
     info, err := os.Stat(path)
     if err != nil {
         return false
     }
-    return time.Since(info.ModTime()) > 5*time.Minute
+    return time.Since(info.ModTime()) > settings.LockStalenessTimeout
 }

326-356: Add jitter to watch interval.

The random interval (30 + rand.Intn(15)) might not provide enough jitter to prevent thundering herd problems in large deployments.

-			sleepTime := 30 + rand.Intn(15)
+			// Base interval of 30 seconds with ±50% jitter
+			baseInterval := 30
+			jitter := rand.Float64()*float64(baseInterval) - float64(baseInterval)/2
+			sleepTime := baseInterval + int(jitter)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04ff386 and 57d555e.

📒 Files selected for processing (6)
  • cmd/directory/directory.go (3 hunks)
  • cmd/file/file.go (2 hunks)
  • internal/analysis/directory.go (1 hunks)
  • internal/analysis/errors.go (1 hunks)
  • internal/analysis/file.go (11 hunks)
  • main.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • main.go
  • cmd/file/file.go
🔇 Additional comments (12)
internal/analysis/errors.go (1)

1-6: LGTM! Well-documented error variable.

The error variable is properly defined and documented, providing clear context for analysis cancellation scenarios.

cmd/directory/directory.go (4)

24-38: LGTM! Robust signal handling implementation.

The implementation properly handles SIGTERM and SIGINT signals for graceful shutdown, with clear user feedback.


47-54: LGTM! Proper error handling for cancellation.

The error handling correctly distinguishes between cancellation and other errors, ensuring appropriate exit behavior.


58-61: LGTM! Command configuration enhancement.

Disabling automatic usage messages on errors improves the user experience by preventing redundant output.


74-74: LGTM! Watch flag implementation.

The watch flag is properly integrated with the existing flag system and aligns with the PR objectives.

internal/analysis/file.go (3)

75-79: LGTM! Case-insensitive file extension validation.

The implementation properly handles both .wav and .flac extensions in a case-insensitive manner.


277-281: LGTM! Thread-safe error handling in result collector.

The implementation properly uses mutex to ensure thread-safe access to the processing error.


359-361: LGTM! Proper error handling for cancellation.

The implementation correctly distinguishes between cancellation and other errors, returning appropriate error types.

Also applies to: 372-374

internal/analysis/directory.go (4)

86-178: LGTM! Comprehensive file readiness checks.

The implementation includes thorough checks for file locks, modification time, and size stability, with proper error handling and logging.


180-262: LGTM! Robust file processing implementation.

The implementation properly handles file locking, processing state, and cleanup, with appropriate error handling for cancellation.


264-324: LGTM! Efficient directory scanning.

The implementation properly handles recursive scanning and provides good progress feedback.


358-395: LGTM! Well-structured main analysis function.

The implementation properly initializes resources, handles initial scanning, and manages watch mode appropriately.

- Replaced direct comparison of error types with errors.Is for better error handling in both directory.go and file.go.
- This change enhances the clarity and robustness of cancellation handling during analysis processes, ensuring that user-initiated cancellations are managed more effectively.
@tphakala tphakala merged commit 7603992 into main Jan 19, 2025
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant