From 1ec71f186177173c5e2d29b8fd6baef59eab326f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:09:54 +0000 Subject: [PATCH 1/8] Initial plan From 9c6095d119e8868310905dcc4ea48049c68811bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:12:43 +0000 Subject: [PATCH 2/8] Add YouTube integration design options document Co-authored-by: mickeyr <154081+mickeyr@users.noreply.github.com> --- docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md | 201 +++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md diff --git a/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md b/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md new file mode 100644 index 0000000..3ba8dd1 --- /dev/null +++ b/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md @@ -0,0 +1,201 @@ +# YouTube Integration Design Options + +## Overview +This document presents two competing design approaches for integrating YouTube search functionality into the Music Library tab, eliminating the need for a separate YouTube tab. + +## Current State +- **YouTube Tab**: Separate tab with search box, results grid, and "Add to Library" buttons +- **Music Library Tab**: Shows Artists → Albums → Songs navigation with search +- **Issue**: YouTube tab only provides search functionality, not playable content like other tabs + +--- + +## Design Option 1: Action Bar with Toolbar Buttons + +### Concept +Add a toolbar/action bar to the Music Library tab with distinct action buttons that expose all library management functions, including YouTube search. + +### Layout +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Music Library Tab │ +├─────────────────────────────────────────────────────────────────────┤ +│ Actions: [➕ Import Folder] [📁 Add Files] [🔍 Search YouTube] │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ [Search: "Filter library..." ] │ +│ │ +│ ▼ The Beatles (4 albums) │ +│ ▼ Abbey Road (1969) [🎵] (17 tracks) │ +│ 1. Come Together │ +│ 2. Something │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### YouTube Search Flow +1. User clicks **"🔍 Search YouTube"** button in action bar +2. Opens a **modal dialog** or **popup window** with YouTube search interface +3. Dialog contains: + - Search input field + - Max results spinner + - Search/Clear buttons + - Results DataGrid + - "Add Selected to Library" / "Add All to Library" buttons +4. User performs search, reviews results, adds songs +5. Dialog can stay open or close after adding songs +6. Status messages appear in main window status bar +7. Library refreshes automatically when songs are added + +### Advantages +✅ **Clear action-oriented interface** - All library management actions in one place +✅ **Familiar pattern** - Similar to "Import Folder" and "Add Files" actions +✅ **Non-intrusive** - Doesn't clutter the main library view +✅ **Flexible** - Dialog can be resized, moved, kept open while browsing library +✅ **Easy to discover** - Prominent button makes functionality obvious +✅ **Maintains separation** - YouTube search is clearly a different action from browsing + +### Disadvantages +❌ **Extra click required** - User must open dialog to search YouTube +❌ **Context switch** - Switching between dialog and main window +❌ **Dialog management** - Need to handle dialog lifecycle, positioning + +### Implementation Complexity +- **Low to Medium** +- Create new `YouTubeSearchWindow.axaml` (similar to existing dialogs) +- Add action buttons to `MusicLibraryView.axaml` header +- Wire up button click to open dialog +- Handle events from dialog (SongsAdded) +- Minimal changes to existing ViewModels + +--- + +## Design Option 2: Integrated Search Mode + +### Concept +Integrate YouTube search directly into the Music Library view with a mode toggle that switches between "Library" and "YouTube" search contexts. + +### Layout +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Music Library Tab │ +├─────────────────────────────────────────────────────────────────────┤ +│ Search Mode: (•) Library ( ) YouTube │ +│ │ +│ [Search: "Type to search library or YouTube..." ] [🔍] │ +│ │ +│ ▼ The Beatles (4 albums) │ +│ ▼ Abbey Road (1969) [🎵] (17 tracks) │ +│ 1. Come Together │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + +When YouTube mode selected: +┌─────────────────────────────────────────────────────────────────────┐ +│ Music Library Tab │ +├─────────────────────────────────────────────────────────────────────┤ +│ Search Mode: ( ) Library (•) YouTube Max Results: [20] │ +│ │ +│ [Search: "Search YouTube for songs..." ] [🔍] │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ Title │ Artist │ Duration │ Year │ YT ID │ │ +│ │ Come Together │ The Beatles │ 4:20 │ 1969 │ abc123 │ │ +│ │ Something │ The Beatles │ 3:03 │ 1969 │ def456 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ +│ [Add Selected to Library] [Add All to Library] [Clear Results] │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### YouTube Search Flow +1. User toggles search mode from "Library" to "YouTube" +2. Library view switches to YouTube search interface +3. Search box placeholder changes to indicate YouTube search +4. User types query and presses Enter or clicks search button +5. Results appear in DataGrid format (same as current YouTube tab) +6. User can add songs to library with buttons at bottom +7. Toggle back to "Library" mode to browse local music + +### Advantages +✅ **Seamless integration** - Everything in one view, no context switching +✅ **Consistent search experience** - Same search box used for both modes +✅ **Quick access** - No need to open dialogs or switch tabs +✅ **Progressive disclosure** - YouTube controls only appear when mode is selected +✅ **Unified workflow** - Search YouTube → Add to Library → Browse Library + +### Disadvantages +❌ **Mode confusion** - Users might not realize they're searching YouTube vs library +❌ **View switching** - Library view gets replaced with YouTube results +❌ **Complex state management** - Need to track and restore view state when switching modes +❌ **Cluttered header** - More controls at top of view +❌ **Lost context** - Can't see library while searching YouTube + +### Implementation Complexity +- **Medium to High** +- Add mode toggle (RadioButton group) to `MusicLibraryView.axaml` +- Add conditional visibility for library views vs YouTube results view +- Integrate YouTube search controls and results into `MusicLibraryView.axaml` +- Significant changes to `MusicLibraryViewModel` to handle dual modes +- State management for switching between modes +- More complex XAML with multiple conditional visibility bindings + +--- + +## Comparison Matrix + +| Aspect | Option 1: Action Bar | Option 2: Integrated Search | +|--------|---------------------|----------------------------| +| **Discovery** | ⭐⭐⭐⭐ Prominent button | ⭐⭐⭐ Mode toggle visible | +| **Ease of Use** | ⭐⭐⭐⭐ Clear workflow | ⭐⭐⭐⭐ Quick switching | +| **Clarity** | ⭐⭐⭐⭐⭐ Very clear separation | ⭐⭐⭐ Potential confusion | +| **Flexibility** | ⭐⭐⭐⭐⭐ Dialog can stay open | ⭐⭐⭐ Must switch modes | +| **Context Preservation** | ⭐⭐⭐⭐⭐ Library always visible | ⭐⭐ Library replaced | +| **Implementation** | ⭐⭐⭐⭐ Low complexity | ⭐⭐⭐ Medium complexity | +| **Consistency** | ⭐⭐⭐⭐ Matches File menu actions | ⭐⭐⭐⭐ Unified search experience | +| **User Flow** | ⭐⭐⭐ Extra click needed | ⭐⭐⭐⭐⭐ Direct access | + +--- + +## Recommendation Considerations + +### Choose Option 1 (Action Bar) if: +- You want **maximum clarity** about what action is being performed +- You prefer **separation of concerns** (browsing vs searching) +- You want users to **see library while searching YouTube** +- You value **simpler implementation** and maintenance +- You want consistency with other "add to library" actions (Import Folder, Add Files) + +### Choose Option 2 (Integrated Search) if: +- You want **single-view workflow** without context switching +- You prefer **unified search experience** across library and YouTube +- You're comfortable with **mode-based UI** paradigms +- You want **fastest access** to YouTube search (no dialog) +- You value **consistency** of having search in the same location + +--- + +## Personal Recommendation + +**I recommend Option 1: Action Bar with Toolbar Buttons** + +### Rationale: +1. **Clarity**: Users clearly understand they're performing an action (searching YouTube) vs filtering existing content +2. **Context preservation**: Library remains visible while YouTube dialog is open +3. **Consistency**: Matches the existing pattern of File → Import Folder and File → Add Files +4. **Simpler implementation**: Lower risk, easier to maintain +5. **Flexibility**: Dialog can be sized, positioned, and kept open as needed +6. **Discovery**: Prominent button makes YouTube functionality easy to find +7. **UX alignment**: The issue states YouTube functionality should be "integrated" but also "expose the current actions" - an action button does exactly this + +The dialog approach better matches the user's mental model: "I want to search YouTube and add songs to my library" is an **action**, not a **view mode**. This keeps the Music Library tab focused on browsing/playing local music while making YouTube search readily accessible as a library-building action. + +--- + +## Next Steps +1. Review both design options +2. Select preferred approach (or request modifications) +3. Create detailed implementation plan +4. Implement chosen design +5. Test with real usage scenarios +6. Update documentation From cbdd17437abe76769e62fe039d35d64e1d633f97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:17:38 +0000 Subject: [PATCH 3/8] Add hybrid design option (expandable panel approach) Co-authored-by: mickeyr <154081+mickeyr@users.noreply.github.com> --- docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md | 199 ++++++++++++++++++++- 1 file changed, 197 insertions(+), 2 deletions(-) diff --git a/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md b/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md index 3ba8dd1..6697ef3 100644 --- a/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md +++ b/docs/YOUTUBE_INTEGRATION_DESIGN_OPTIONS.md @@ -192,9 +192,204 @@ The dialog approach better matches the user's mental model: "I want to search Yo --- +--- + +## Design Option 3: Hybrid Approach - Expandable Search Panel + +### Concept +Combine the best of both worlds: use an action button to trigger YouTube search, but display results in an expandable panel within the Music Library view instead of a separate dialog. This keeps YouTube functionality integrated while preserving context. + +### Layout (Default State) +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Music Library Tab │ +├─────────────────────────────────────────────────────────────────────┤ +│ Actions: [➕ Import Folder] [📁 Add Files] [🔍 Search YouTube] │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ [Search: "Filter library..." ] │ +│ │ +│ ▼ The Beatles (4 albums) │ +│ ▼ Abbey Road (1969) [🎵] (17 tracks) │ +│ 1. Come Together │ +│ 2. Something │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Layout (YouTube Panel Expanded) +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Music Library Tab │ +├─────────────────────────────────────────────────────────────────────┤ +│ Actions: [➕ Import Folder] [📁 Add Files] [🔍 Search YouTube] │ +├─────────────────────────────────────────────────────────────────────┤ +│ ▼ YouTube Search ──────────────────────────────────────────── [✕] │ +│ │ [Search: "artist or song name..."] Max: [20] [Search] [Clear] │ +│ │ │ +│ │ ┌───────────────────────────────────────────────────────────────┐│ +│ │ │ Title │ Artist │ Duration │ Year │ YT ID ││ +│ │ │ Come Together │ The Beatles │ 4:20 │ 1969 │ abc123 ││ +│ │ │ Something │ The Beatles │ 3:03 │ 1969 │ def456 ││ +│ │ └───────────────────────────────────────────────────────────────┘│ +│ │ │ +│ │ [Add Selected] [Add All] Results: 2 found │ +│ └───────────────────────────────────────────────────────────────────┘ +├─────────────────────────────────────────────────────────────────────┤ +│ [Search: "Filter library..." ] │ +│ │ +│ ▼ The Beatles (4 albums) │ +│ ▼ Abbey Road (1969) [🎵] (17 tracks) │ +│ 1. Come Together │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### YouTube Search Flow +1. User clicks **"🔍 Search YouTube"** button in action bar +2. **Expandable panel** slides down between action bar and library view +3. Panel contains compact YouTube search interface: + - Single-line search controls (search box, max results, buttons) + - Collapsible DataGrid for results (starts collapsed until search is performed) + - Action buttons at bottom of results +4. User performs search, results appear in panel's grid +5. **Library view remains visible below** (scrolls down to accommodate panel) +6. User can: + - Add songs to library from results + - Collapse panel with [✕] button to return to full library view + - Keep panel open while browsing library (resize/scroll) +7. Status messages appear in main window status bar +8. Panel auto-refreshes library when songs are added + +### Visual Behavior +- **Panel Animation**: Smooth expand/collapse animation (200ms) +- **Height Management**: + - Collapsed: 0px (hidden) + - Expanded (no results): ~80px (just search controls) + - Expanded (with results): ~250-300px (search controls + grid) +- **Scroll Behavior**: Library view scrolls underneath panel naturally +- **Persistence**: Panel state (expanded/collapsed) persists during session +- **Keyboard**: ESC key collapses panel + +### Advantages +✅ **Best of both worlds** - Action-oriented trigger + integrated display +✅ **Context preservation** - Library always visible, just shifts down +✅ **No modal dialogs** - Everything in one cohesive view +✅ **Quick access** - Panel toggles instantly, no dialog lifecycle +✅ **Clear action** - Button makes functionality discoverable +✅ **Flexible** - Panel can be collapsed when not needed +✅ **Progressive disclosure** - Panel only appears when explicitly requested +✅ **Unified space** - All library management in one view +✅ **Better workflow** - See library while searching, add songs without losing context + +### Disadvantages +❌ **Vertical space** - Panel reduces library view height when expanded +❌ **Animation complexity** - Need smooth expand/collapse transitions +❌ **State management** - Track panel open/closed state +❌ **Layout shifts** - Library content moves when panel expands + +### Implementation Complexity +- **Medium** +- Add expandable panel control to `MusicLibraryView.axaml` (Grid with row definitions) +- Bind panel visibility to `IsYouTubePanelExpanded` property +- Integrate YouTube search UI (simpler than Option 1's dialog, less complex than Option 2's mode switching) +- Add collapse/expand animations (optional but nice) +- `MusicLibraryViewModel` gains YouTube panel state properties +- Moderate XAML changes with conditional row height bindings + +### Comparison to Other Options + +| Aspect | Option 1 (Dialog) | Option 2 (Mode) | Option 3 (Hybrid) | +|--------|------------------|-----------------|-------------------| +| **Context** | Separate window | View replaced | Library visible below | +| **Access** | Extra click | Toggle mode | Single click | +| **Clarity** | ⭐⭐⭐⭐⭐ Very clear | ⭐⭐⭐ Modes confusing | ⭐⭐⭐⭐ Clear action | +| **Integration** | ⭐⭐⭐ Dialog separate | ⭐⭐⭐⭐⭐ Fully integrated | ⭐⭐⭐⭐⭐ Integrated panel | +| **Workflow** | ⭐⭐⭐ Switch windows | ⭐⭐⭐ Switch modes | ⭐⭐⭐⭐⭐ Simultaneous view | +| **Complexity** | ⭐⭐⭐⭐ Low | ⭐⭐⭐ High | ⭐⭐⭐⭐ Medium | +| **Space usage** | ⭐⭐⭐⭐ No impact | ⭐⭐⭐ Replaces view | ⭐⭐⭐⭐ Shares space | + +### Why Hybrid Works Better + +1. **Combines strengths**: Action-oriented (like Option 1) + integrated display (like Option 2) +2. **Eliminates weaknesses**: No modal management, no mode confusion, no context loss +3. **Natural workflow**: "I want to search YouTube [click button] → [panel expands] → search → add → [collapse or keep open]" +4. **Modern pattern**: Expandable panels are common in modern UIs (search filters, tool panels, etc.) +5. **Flexible usage**: Can leave panel open for multiple searches or collapse when done +6. **Single view**: Everything accessible without tab/window switching +7. **Progressive**: Only shows YouTube UI when user explicitly requests it + +### User Scenarios + +**Scenario 1: Quick YouTube Addition** +1. Click "Search YouTube" → panel expands +2. Type "Beatles Abbey Road", press Enter +3. Review results, click "Add Selected" +4. Click [✕] to collapse panel +5. Library refreshes, new songs appear + +**Scenario 2: Building a Playlist** +1. Click "Search YouTube" → panel stays open +2. Search "Pink Floyd", add songs +3. Scroll library below to see what's already there +4. Search "Led Zeppelin", add more +5. Browse library while keeping panel open +6. Close panel when done + +**Scenario 3: Quick Check** +1. Panel already open from previous session +2. Type search query, check if song exists on YouTube +3. Don't add anything, just exploring +4. Keep panel open or collapse as needed + +--- + +## Updated Recommendation + +Given the three options, here's the updated ranking: + +### 🥇 **Recommended: Option 3 - Hybrid Expandable Panel** + +**Why it wins:** +- Perfect balance of clarity, integration, and flexibility +- Preserves library context while showing YouTube results +- More modern and intuitive than modal dialogs +- Simpler state management than full mode switching +- Single-view workflow without context loss +- Matches user mental model: "expand tools when needed, collapse when done" + +### 🥈 **Second: Option 1 - Action Bar with Dialog** + +**Still good for:** +- Teams preferring traditional dialog patterns +- When modal focus is desired +- Simpler implementation if panel animation is too complex + +### 🥉 **Third: Option 2 - Integrated Search Mode** + +**Use only if:** +- You strongly prefer unified search paradigm +- Losing library context while searching is acceptable +- Mode-based UIs are your design philosophy + +--- + +## Implementation Recommendation + +**Start with Option 3 (Hybrid)** because: +1. Best user experience +2. Reasonable implementation complexity +3. Modern, flexible design +4. Can fall back to Option 1 if panel approach proves problematic +5. Aligns perfectly with the issue requirement: "integrated" yet "exposing actions" + +If during implementation the expandable panel proves too complex or has performance issues, Option 1 (Dialog) is a solid fallback that's simpler to implement. + +--- + ## Next Steps -1. Review both design options -2. Select preferred approach (or request modifications) +1. Review all three design options +2. Select preferred approach (Option 1, 2, or 3) 3. Create detailed implementation plan 4. Implement chosen design 5. Test with real usage scenarios From c143da13254020779a62632f669b9929824ce398 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:26:34 +0000 Subject: [PATCH 4/8] Implement hybrid expandable YouTube panel in Music Library tab Co-authored-by: mickeyr <154081+mickeyr@users.noreply.github.com> --- .../ViewModels/MainWindowViewModel.cs | 5 +- .../ViewModels/MusicLibraryViewModel.cs | 325 +++++++++++++++++- src/Muine.App/Views/MainWindow.axaml | 9 +- src/Muine.App/Views/MainWindow.axaml.cs | 18 + src/Muine.App/Views/MusicLibraryView.axaml | 121 ++++++- src/Muine.App/Views/MusicLibraryView.axaml.cs | 21 ++ 6 files changed, 487 insertions(+), 12 deletions(-) diff --git a/src/Muine.App/ViewModels/MainWindowViewModel.cs b/src/Muine.App/ViewModels/MainWindowViewModel.cs index 8ace7dd..8f873be 100644 --- a/src/Muine.App/ViewModels/MainWindowViewModel.cs +++ b/src/Muine.App/ViewModels/MainWindowViewModel.cs @@ -160,11 +160,14 @@ public MainWindowViewModel() _mprisService.PreviousRequested += (s, e) => _ = PlayPreviousCommand.ExecuteAsync(null); // Initialize view models - MusicLibraryViewModel = new MusicLibraryViewModel(_databaseService); + MusicLibraryViewModel = new MusicLibraryViewModel(_databaseService, _youtubeService, _taggingQueue, managedLibraryService, _metadataService); PlaylistViewModel = new PlaylistViewModel(); RadioViewModel = new RadioViewModel(_radioStationService, _radioMetadataService, _radioBrowserService); YouTubeSearchViewModel = new YouTubeSearchViewModel(_youtubeService, _databaseService, _taggingQueue, managedLibraryService, _metadataService); + // Subscribe to library events + MusicLibraryViewModel.SongsAddedToLibrary += OnYouTubeSongsAddedToLibrary; + // Subscribe to YouTube events YouTubeSearchViewModel.SongsAddedToLibrary += OnYouTubeSongsAddedToLibrary; YouTubeSearchViewModel.YouTubeSongNeedsMetadataReview += OnYouTubeSongNeedsMetadataReview; diff --git a/src/Muine.App/ViewModels/MusicLibraryViewModel.cs b/src/Muine.App/ViewModels/MusicLibraryViewModel.cs index 6dcbe9f..234e587 100644 --- a/src/Muine.App/ViewModels/MusicLibraryViewModel.cs +++ b/src/Muine.App/ViewModels/MusicLibraryViewModel.cs @@ -1,18 +1,24 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Muine.Core.Models; using Muine.Core.Services; +using static Muine.Core.Services.LoggingService; namespace Muine.App.ViewModels; public partial class MusicLibraryViewModel : ViewModelBase { private readonly MusicDatabaseService _databaseService; + private readonly YouTubeService? _youtubeService; + private readonly BackgroundTaggingQueue? _taggingQueue; + private readonly ManagedLibraryService? _managedLibraryService; + private readonly MetadataService? _metadataService; private List _allAlbums = new(); [ObservableProperty] @@ -57,13 +63,47 @@ public partial class MusicLibraryViewModel : ViewModelBase [ObservableProperty] private string _currentFilteredAlbum = string.Empty; + // YouTube Panel Properties + [ObservableProperty] + private bool _isYouTubePanelExpanded = false; + + [ObservableProperty] + private string _youtubeSearchQuery = string.Empty; + + [ObservableProperty] + private ObservableCollection _youtubeSearchResults = new(); + + [ObservableProperty] + private Song? _selectedYouTubeSong; + + [ObservableProperty] + private bool _isYouTubeSearching; + + [ObservableProperty] + private string _youtubeStatusMessage = string.Empty; + + [ObservableProperty] + private int _maxYouTubeResults = 20; + + // Event fired when songs are added to the library from YouTube + public event EventHandler? SongsAddedToLibrary; + public int TotalSongs => AllSongs.Count; public int ArtistCount => Artists.Count; public int AlbumCount => _allAlbums.Count; - public MusicLibraryViewModel(MusicDatabaseService databaseService) + public MusicLibraryViewModel( + MusicDatabaseService databaseService, + YouTubeService? youtubeService = null, + BackgroundTaggingQueue? taggingQueue = null, + ManagedLibraryService? managedLibraryService = null, + MetadataService? metadataService = null) { _databaseService = databaseService; + _youtubeService = youtubeService; + _taggingQueue = taggingQueue; + _managedLibraryService = managedLibraryService; + _metadataService = metadataService; } public async Task LoadLibraryAsync() @@ -255,4 +295,287 @@ partial void OnSearchQueryChanged(string value) } } } + + [RelayCommand] + private void ToggleYouTubePanel() + { + IsYouTubePanelExpanded = !IsYouTubePanelExpanded; + if (!IsYouTubePanelExpanded) + { + // Clear search when closing panel + YoutubeSearchQuery = string.Empty; + YoutubeSearchResults.Clear(); + YoutubeStatusMessage = string.Empty; + } + } + + [RelayCommand] + private async Task SearchYouTubeAsync() + { + if (_youtubeService == null) + { + YoutubeStatusMessage = "YouTube service is not available"; + return; + } + + if (string.IsNullOrWhiteSpace(YoutubeSearchQuery)) + { + YoutubeStatusMessage = "Please enter a search query"; + return; + } + + IsYouTubeSearching = true; + YoutubeStatusMessage = $"Searching YouTube for '{YoutubeSearchQuery}'..."; + + try + { + YoutubeSearchResults.Clear(); + var results = await _youtubeService.SearchAsync(YoutubeSearchQuery, MaxYouTubeResults); + + foreach (var song in results) + { + YoutubeSearchResults.Add(song); + } + + YoutubeStatusMessage = results.Count > 0 + ? $"Found {results.Count} results" + : "No results found"; + } + catch (Exception ex) + { + YoutubeStatusMessage = $"Error searching: {ex.Message}"; + LoggingService.Error($"YouTube search failed", ex, "MusicLibraryViewModel"); + } + finally + { + IsYouTubeSearching = false; + } + } + + [RelayCommand(CanExecute = nameof(CanAddYouTubeSongToLibrary))] + private async Task AddYouTubeSongToLibraryAsync() + { + if (SelectedYouTubeSong == null || _youtubeService == null) + return; + + IsYouTubeSearching = true; + YoutubeStatusMessage = $"Downloading '{SelectedYouTubeSong.Title}'..."; + + try + { + // Download YouTube audio to temp directory + var tempPath = await _youtubeService.DownloadToTempAsync(SelectedYouTubeSong.YouTubeId!); + + if (tempPath == null || !File.Exists(tempPath)) + { + YoutubeStatusMessage = $"Failed to download '{SelectedYouTubeSong.Title}'"; + return; + } + + YoutubeStatusMessage = $"Processing '{SelectedYouTubeSong.Title}'..."; + + // If managed library service is available, import to managed library + if (_managedLibraryService != null && _metadataService != null) + { + // Read metadata from downloaded file + var song = _metadataService.ReadSongMetadata(tempPath); + if (song != null) + { + // Preserve YouTube info + song.SourceType = SongSourceType.YouTube; + song.YouTubeId = SelectedYouTubeSong.YouTubeId; + song.YouTubeUrl = SelectedYouTubeSong.YouTubeUrl; + + // Import to managed library (moves file from temp to library) + var importResult = await _managedLibraryService.ImportFileAsync(tempPath, copyInsteadOfMove: false); + + if (importResult.Success && importResult.ImportedSong != null) + { + YoutubeStatusMessage = $"Added '{SelectedYouTubeSong.Title}' to library"; + + // Queue for metadata enhancement (year, cover art, etc.) + if (importResult.NeedsMetadataEnhancement) + { + _taggingQueue?.EnqueueSong(importResult.ImportedSong, downloadCoverArt: true); + LoggingService.Info($"Queued YouTube song for metadata enhancement: {importResult.ImportedSong.DisplayName}", "MusicLibraryViewModel"); + } + + // Notify that song was added and refresh library + SongsAddedToLibrary?.Invoke(this, EventArgs.Empty); + await LoadLibraryAsync(); + } + else + { + YoutubeStatusMessage = $"Error importing: {importResult.ErrorMessage}"; + // Clean up temp file if import failed + if (File.Exists(tempPath)) + { + try { File.Delete(tempPath); } catch { } + } + } + } + else + { + YoutubeStatusMessage = "Failed to read metadata from downloaded file"; + // Clean up temp file + if (File.Exists(tempPath)) + { + try { File.Delete(tempPath); } catch { } + } + } + } + else + { + // Fallback: old behavior - save directly to database + SelectedYouTubeSong.Filename = tempPath; + await _databaseService.SaveSongAsync(SelectedYouTubeSong); + + // Queue for metadata enhancement + _taggingQueue?.EnqueueSong(SelectedYouTubeSong, downloadCoverArt: true); + LoggingService.Info($"Queued YouTube song for metadata enhancement: {SelectedYouTubeSong.DisplayName}", "MusicLibraryViewModel"); + + YoutubeStatusMessage = $"Added '{SelectedYouTubeSong.Title}' to library"; + + // Notify that library has been updated + SongsAddedToLibrary?.Invoke(this, EventArgs.Empty); + await LoadLibraryAsync(); + } + } + catch (Exception ex) + { + YoutubeStatusMessage = $"Error adding to library: {ex.Message}"; + LoggingService.Error($"Failed to add YouTube song to library: {SelectedYouTubeSong?.Title}", ex, "MusicLibraryViewModel"); + } + finally + { + IsYouTubeSearching = false; + } + } + + [RelayCommand(CanExecute = nameof(CanAddAllYouTubeSongsToLibrary))] + private async Task AddAllYouTubeSongsToLibraryAsync() + { + if (YoutubeSearchResults.Count == 0 || _youtubeService == null) + return; + + IsYouTubeSearching = true; + var count = 0; + var total = YoutubeSearchResults.Count; + + try + { + foreach (var song in YoutubeSearchResults.ToList()) + { + try + { + count++; + YoutubeStatusMessage = $"Downloading {count}/{total}: {song.Title}..."; + + // Download YouTube audio to temp directory + var tempPath = await _youtubeService.DownloadToTempAsync(song.YouTubeId!); + + if (tempPath == null || !File.Exists(tempPath)) + { + LoggingService.Warning($"Failed to download: {song.Title}", "MusicLibraryViewModel"); + continue; + } + + // If managed library service is available, import to managed library + if (_managedLibraryService != null && _metadataService != null) + { + // Read metadata from downloaded file + var downloadedSong = _metadataService.ReadSongMetadata(tempPath); + if (downloadedSong != null) + { + // Preserve YouTube info + downloadedSong.SourceType = SongSourceType.YouTube; + downloadedSong.YouTubeId = song.YouTubeId; + downloadedSong.YouTubeUrl = song.YouTubeUrl; + + // Import to managed library + var importResult = await _managedLibraryService.ImportFileAsync(tempPath, copyInsteadOfMove: false); + + if (importResult.Success && importResult.ImportedSong != null) + { + // Queue for metadata enhancement if needed + if (importResult.NeedsMetadataEnhancement) + { + _taggingQueue?.EnqueueSong(importResult.ImportedSong, downloadCoverArt: true); + } + } + else + { + LoggingService.Warning($"Failed to import: {song.Title} - {importResult.ErrorMessage}", "MusicLibraryViewModel"); + if (File.Exists(tempPath)) + { + try { File.Delete(tempPath); } catch { } + } + } + } + else + { + LoggingService.Warning($"Failed to read metadata: {song.Title}", "MusicLibraryViewModel"); + if (File.Exists(tempPath)) + { + try { File.Delete(tempPath); } catch { } + } + } + } + else + { + // Fallback: old behavior + song.Filename = tempPath; + await _databaseService.SaveSongAsync(song); + _taggingQueue?.EnqueueSong(song, downloadCoverArt: true); + } + } + catch (Exception ex) + { + LoggingService.Error($"Failed to add YouTube song: {song.Title}", ex, "MusicLibraryViewModel"); + } + } + + YoutubeStatusMessage = $"Added {count} songs to library"; + + // Notify that library has been updated + SongsAddedToLibrary?.Invoke(this, EventArgs.Empty); + await LoadLibraryAsync(); + } + catch (Exception ex) + { + YoutubeStatusMessage = $"Error adding to library: {ex.Message}"; + LoggingService.Error($"Failed to add YouTube songs to library", ex, "MusicLibraryViewModel"); + } + finally + { + IsYouTubeSearching = false; + } + } + + [RelayCommand] + private void ClearYouTubeResults() + { + YoutubeSearchResults.Clear(); + SelectedYouTubeSong = null; + YoutubeStatusMessage = "Results cleared"; + } + + private bool CanAddYouTubeSongToLibrary() => SelectedYouTubeSong != null && !IsYouTubeSearching; + private bool CanAddAllYouTubeSongsToLibrary() => YoutubeSearchResults.Count > 0 && !IsYouTubeSearching; + + partial void OnSelectedYouTubeSongChanged(Song? value) + { + AddYouTubeSongToLibraryCommand.NotifyCanExecuteChanged(); + } + + partial void OnYoutubeSearchResultsChanged(ObservableCollection value) + { + AddAllYouTubeSongsToLibraryCommand.NotifyCanExecuteChanged(); + } + + partial void OnIsYouTubeSearchingChanged(bool value) + { + AddYouTubeSongToLibraryCommand.NotifyCanExecuteChanged(); + AddAllYouTubeSongsToLibraryCommand.NotifyCanExecuteChanged(); + } } diff --git a/src/Muine.App/Views/MainWindow.axaml b/src/Muine.App/Views/MainWindow.axaml index c40a351..533ec4d 100644 --- a/src/Muine.App/Views/MainWindow.axaml +++ b/src/Muine.App/Views/MainWindow.axaml @@ -46,7 +46,8 @@ - - - - - - diff --git a/src/Muine.App/Views/MainWindow.axaml.cs b/src/Muine.App/Views/MainWindow.axaml.cs index 7303eac..fe93454 100644 --- a/src/Muine.App/Views/MainWindow.axaml.cs +++ b/src/Muine.App/Views/MainWindow.axaml.cs @@ -49,6 +49,14 @@ private void OnWindowLoaded(object? sender, RoutedEventArgs e) _sliderThumb.DragCompleted += OnThumbDragCompleted; } } + + // Wire up MusicLibraryView events + var musicLibraryView = this.FindControl("MusicLibraryView"); + if (musicLibraryView != null) + { + musicLibraryView.ImportFolderRequested += OnLibraryImportFolderRequested; + musicLibraryView.AddFilesRequested += OnLibraryAddFilesRequested; + } } private Thumb? FindThumbInSlider(Slider slider) @@ -238,6 +246,16 @@ private async void OnEditMetadataRequested(object? sender, Song song) } } + private void OnLibraryImportFolderRequested(object? sender, EventArgs e) + { + OnImportMusicFolderClick(sender, new RoutedEventArgs()); + } + + private void OnLibraryAddFilesRequested(object? sender, EventArgs e) + { + OnAddMusicFilesClick(sender, new RoutedEventArgs()); + } + private async void OnPlaylistSongDoubleClick(object? sender, Song song) { if (DataContext is MainWindowViewModel viewModel) diff --git a/src/Muine.App/Views/MusicLibraryView.axaml b/src/Muine.App/Views/MusicLibraryView.axaml index 978905e..c5e5437 100644 --- a/src/Muine.App/Views/MusicLibraryView.axaml +++ b/src/Muine.App/Views/MusicLibraryView.axaml @@ -10,11 +10,126 @@ + - + + + + + +