diff --git a/MY_LRCLIB_REWORK_PLAN.md b/MY_LRCLIB_REWORK_PLAN.md index 5496b6e..f91410a 100644 --- a/MY_LRCLIB_REWORK_PLAN.md +++ b/MY_LRCLIB_REWORK_PLAN.md @@ -396,9 +396,11 @@ Users can pick: - [x] Phase 4: LRCLIB lyricsfile integration - [x] Migration 12: Add `lrclib_instance` and `lrclib_id` columns to `lyricsfiles` table - [x] Add `prepare_lrclib_lyricsfile` command to fetch/create standalone lyricsfiles + - [x] Add `refresh_lrclib_lyricsfile` command to force re-download from LRCLIB - [x] Update `save_lyrics` command to support standalone lyricsfiles - [x] Update `AssociateTrackModal.vue` - remove "Edit without audio" option - [x] Update `SearchResult.vue` - use `prepare_lrclib_lyricsfile` command + - [x] Create `LyricsfileConflictModal.vue` - ask user to redownload or continue when lyrics already exist - [x] Update `EditLyricsV2.vue` - accept `lyricsfileId` and `initialLyricsfile` props - [x] Update `useEditLyricsV2Document.js` - support initialization from external lyricsfile - [ ] Phase 5: Remove legacy `EditLyrics.vue` (after testing) diff --git a/src-tauri/ARCHITECTURE.md b/src-tauri/ARCHITECTURE.md index 001281c..06e9b64 100644 --- a/src-tauri/ARCHITECTURE.md +++ b/src-tauri/ARCHITECTURE.md @@ -201,7 +201,8 @@ Implements `From` for seamless conversion from database entitie | `retrieve_lyrics/by_id()` | Get raw LRCLIB response | | `search_lyrics()` | Search LRCLIB database | | `apply_lyrics()` | Save a selected LRCLIB result into database-backed lyrics storage | -| `prepare_lrclib_lyricsfile(lrclib_id)` | Get or create lyricsfile from LRCLIB. Checks local cache first, fetches from API if needed. Saves to `lyricsfiles` table with `lrclib_instance` + `lrclib_id`. Returns `lyricsfile_id` + content. | +| `prepare_lrclib_lyricsfile(lrclib_id)` | Get or create lyricsfile from LRCLIB. Checks local cache first, fetches from API if needed. Saves to `lyricsfiles` table with `lrclib_instance` + `lrclib_id`. Returns `lyricsfile_id` + content + `exists_in_db` flag. | +| `refresh_lrclib_lyricsfile(lrclib_id)` | Force re-download lyrics from LRCLIB API. Updates existing record in `lyricsfiles` table. Returns refreshed `lyricsfile_id` + content. | | `save_lyrics(track_id?, lyricsfile_id?, plain?, synced?, lyricsfile?)` | Save lyrics edits. For library tracks: provide `track_id`. For standalone LRCLIB lyrics: provide `lyricsfile_id`. Prefers `lyricsfile` format. | | `publish_lyrics(title, album, artist, duration, plain?, synced?, lyricsfile?)` | Upload to LRCLIB (with PoW; accepts Lyricsfile-only payloads) | | `export_lyrics(track_id, formats, lyricsfile?)` | Manual export to `.txt`, `.lrc`, or embedded tags | diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5204969..958b9f7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -796,6 +796,7 @@ struct PrepareLyricsfileResult { pub plain_lyrics: String, pub synced_lyrics: String, pub is_instrumental: bool, + pub exists_in_db: bool, } #[tauri::command] @@ -826,6 +827,7 @@ async fn prepare_lrclib_lyricsfile( plain_lyrics: parsed.plain_lyrics.unwrap_or_default(), synced_lyrics: parsed.synced_lyrics.unwrap_or_default(), is_instrumental: parsed.is_instrumental, + exists_in_db: true, }); } @@ -881,6 +883,75 @@ async fn prepare_lrclib_lyricsfile( plain_lyrics: parsed.plain_lyrics.unwrap_or_default(), synced_lyrics: parsed.synced_lyrics.unwrap_or_default(), is_instrumental: parsed.is_instrumental, + exists_in_db: false, + }) +} + +#[tauri::command] +async fn refresh_lrclib_lyricsfile( + lrclib_id: i64, + app_handle: AppHandle, +) -> Result { + // Get config for LRCLIB instance URL + let config = app_handle + .db(|db: &Connection| db::get_config(db)) + .map_err(|err| err.to_string())?; + + let lrclib_instance = config.lrclib_instance; + + // Fetch fresh data from LRCLIB API (always re-download) + let lrclib_response = lrclib::get_by_id::request_raw(lrclib_id, &lrclib_instance) + .await + .map_err(|err| err.to_string())?; + + // Extract metadata from LRCLIB response + let title = lrclib_response.name.unwrap_or_default(); + let album_name = lrclib_response.album_name.unwrap_or_default(); + let artist_name = lrclib_response.artist_name.unwrap_or_default(); + let duration = lrclib_response.duration.unwrap_or(0.0); + + // Build or use existing lyricsfile content + let lyricsfile_content = if let Some(lyricsfile) = lrclib_response.lyricsfile { + // LRCLIB provided a lyricsfile, use it directly + lyricsfile + } else { + // Need to build lyricsfile from plain/synced lyrics + let metadata = + lyricsfile::LyricsfileTrackMetadata::new(&title, &album_name, &artist_name, duration); + + let plain = lrclib_response.plain_lyrics.as_deref(); + let synced = lrclib_response.synced_lyrics.as_deref(); + + lyricsfile::build_lyricsfile(&metadata, plain, synced) + .ok_or("Failed to build lyricsfile from LRCLIB response")? + }; + + // Parse for return values + let parsed = lyricsfile::parse_lyricsfile(&lyricsfile_content).map_err(|e| e.to_string())?; + + // Save to database (will update existing if lrclib_instance + lrclib_id match) + let lyricsfile_id = app_handle + .db(|db: &Connection| { + db::upsert_lyricsfile_for_lrclib( + &lrclib_instance, + lrclib_id, + &title, + &album_name, + &artist_name, + duration, + &lyricsfile_content, + db, + ) + }) + .map_err(|err| err.to_string())?; + + Ok(PrepareLyricsfileResult { + lyricsfile_id, + lyricsfile: lyricsfile_content, + plain_lyrics: parsed.plain_lyrics.unwrap_or_default(), + synced_lyrics: parsed.synced_lyrics.unwrap_or_default(), + is_instrumental: parsed.is_instrumental, + exists_in_db: true, // After refresh, it definitely exists }) } @@ -1552,6 +1623,7 @@ async fn main() { get_audio_metadata, prepare_search_query, prepare_lrclib_lyricsfile, + refresh_lrclib_lyricsfile, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/ARCHITECTURE.md b/src/ARCHITECTURE.md index fe837f1..a6bf68a 100644 --- a/src/ARCHITECTURE.md +++ b/src/ARCHITECTURE.md @@ -79,7 +79,7 @@ Module-level ref composables (singletons by design): - _V2 Instrumental Support_: Tracks can be marked as instrumental via `PlainLyricsEmptyState.vue` or `SyncedLyricsEmptyState.vue`. When marked, the plain/synced tab switcher is disabled and a centered popup appears with an "Unmark as instrumental" button. The instrumental state is stored in the `lyricsfile` metadata and managed by `useEditLyricsV2Document.js`. - **Export (Mass)**: `LibraryHeader.vue` has an export button (with dropdown) that emits `exportAllLyrics` → `Library.vue` opens `ExportViewer.vue` modal → `useExporter()` composable manages queue → invokes `export_track_lyrics` command per track. Exports to `.txt`, `.lrc`, and/or embedded metadata. - **My LRCLIB**: User workflows (preview, edit, publish, flag) in `my-lrclib/` -- **Track Association (My LRCLIB Edit Flow)**: When editing lyrics from My LRCLIB search results, the flow is: (1) Call `prepare_lrclib_lyricsfile(lrclibId)` to fetch/create the lyricsfile in the database (stored with `lrclib_instance` and `lrclib_id`), (2) Show `AssociateTrackModal.vue` for selecting either a library track or a file from the computer, (3) Open `EditLyricsV2.vue` with the selected audio source and the standalone lyricsfile. The `get_audio_metadata` backend command extracts metadata from selected files using the existing scanner logic. Supports playback for both library tracks and arbitrary file-based tracks. +- **Track Association (My LRCLIB Edit Flow)**: When editing lyrics from My LRCLIB search results, the flow is: (1) Call `prepare_lrclib_lyricsfile(lrclibId)` to fetch/create the lyricsfile in the database (stored with `lrclib_instance` and `lrclib_id`). If lyrics already exist locally, show `LyricsfileConflictModal.vue` to ask if user wants to redownload from LRCLIB or continue with local version. (2) Show `AssociateTrackModal.vue` for selecting either a library track or a file from the computer. (3) Open `EditLyricsV2.vue` with the selected audio source and the standalone lyricsfile. The `get_audio_metadata` backend command extracts metadata from selected files using the existing scanner logic. Supports playback for both library tracks and arbitrary file-based tracks. Utils: `src/utils/` (parsing, linting), Composables: `composables/edit-lyrics/`, `composables/edit-lyrics-v2/`, `composables/export.js` diff --git a/src/components/library/EditLyricsV2.vue b/src/components/library/EditLyricsV2.vue index 953ac25..bf6d167 100644 --- a/src/components/library/EditLyricsV2.vue +++ b/src/components/library/EditLyricsV2.vue @@ -139,18 +139,32 @@ import { useGlobalState } from '@/composables/global-state.js' import { usePlayer } from '@/composables/player.js' const props = defineProps({ - track: { + // Audio source for playback (library track or file-based track) + audioSource: { type: Object, default: null, }, + // Source type: 'track' for normal library editing, 'lrclib' for LRCLIB flow + source: { + type: String, + default: 'track', + validator: (value) => ['track', 'lrclib'].includes(value), + }, + // For LRCLIB flow: standalone lyricsfile ID lyricsfileId: { type: Number, default: null, }, - initialLyricsfile: { + // For LRCLIB flow: initial lyricsfile content + lyricsfileContent: { type: String, default: null, }, + // For LRCLIB flow: lyricsfile metadata (title, artist, album, duration_ms) + lyricsfileMetadata: { + type: Object, + default: null, + }, }) const emit = defineEmits(['close']) diff --git a/src/components/library/my-lrclib/LyricsfileConflictModal.vue b/src/components/library/my-lrclib/LyricsfileConflictModal.vue new file mode 100644 index 0000000..895e4a1 --- /dev/null +++ b/src/components/library/my-lrclib/LyricsfileConflictModal.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/components/library/my-lrclib/SearchResult.vue b/src/components/library/my-lrclib/SearchResult.vue index c921a83..ccf8b27 100644 --- a/src/components/library/my-lrclib/SearchResult.vue +++ b/src/components/library/my-lrclib/SearchResult.vue @@ -116,6 +116,7 @@ import EditLyrics from './EditLyrics.vue' import PreviewLyrics from './PreviewLyrics.vue' import FlagLyrics from './FlagLyrics.vue' import AssociateTrackModal from './AssociateTrackModal.vue' +import LyricsfileConflictModal from './LyricsfileConflictModal.vue' import EditLyricsV2 from '../EditLyricsV2.vue' import { useModal } from 'vue-final-modal' @@ -225,6 +226,36 @@ const { open: openAssociateTrackModal, close: closeAssociateTrackModal } = useMo }, }) +// Conflict modal state +const conflictModalLrclibId = ref(null) +const conflictModalExistingResult = ref(null) + +const { open: openConflictModal, close: closeConflictModal } = useModal({ + component: LyricsfileConflictModal, + attrs: { + lrclibId: conflictModalLrclibId, + existingResult: conflictModalExistingResult, + onClose() { + closeConflictModal() + }, + onClosed() { + conflictModalLrclibId.value = null + conflictModalExistingResult.value = null + }, + onRedownload(result) { + // User chose to redownload - proceed with refreshed data + proceedToAssociateTrackModal(conflictModalLrclibTrack.value, result) + }, + onContinue(result) { + // User chose to continue with existing - proceed with existing data + proceedToAssociateTrackModal(conflictModalLrclibTrack.value, result) + }, + }, +}) + +// Store the track temporarily while showing conflict modal +const conflictModalLrclibTrack = ref(null) + onMounted(async () => { const config = await invoke('get_config') showLineCount.value = config.show_line_count @@ -259,17 +290,30 @@ const setShowingTrack = async track => { } } +const proceedToAssociateTrackModal = (track, result) => { + // Set the data and open associate modal + associateModalLrclibTrack.value = track + associateModalLyricsfileId.value = result.lyricsfileId + associateModalInitialLyricsfile.value = result.lyricsfile + openAssociateTrackModal() +} + const setEditingTrack = async track => { isOpeningTrack.value = true try { // Prepare lyricsfile from LRCLIB (fetches or gets from cache) const result = await invoke('prepare_lrclib_lyricsfile', { lrclibId: track.id }) - // Show association modal - associateModalLrclibTrack.value = track - associateModalLyricsfileId.value = result.lyricsfileId - associateModalInitialLyricsfile.value = result.lyricsfile - openAssociateTrackModal() + if (result.existsInDb) { + // Lyrics already exist - show conflict modal + conflictModalLrclibTrack.value = track + conflictModalLrclibId.value = track.id + conflictModalExistingResult.value = result + openConflictModal() + } else { + // New lyrics - proceed directly to associate modal + proceedToAssociateTrackModal(track, result) + } } catch (error) { toast.error('An error occurred while opening the lyrics. Please try again.') console.error(error)