├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── app-icon.png ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public ├── tauri.svg └── vite.svg ├── screenshots ├── 01.png ├── 02.png ├── 03.png ├── 04.png └── 05.png ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ ├── desktop.json │ └── migrated.json ├── gen │ └── schemas │ │ ├── acl-manifests.json │ │ ├── capabilities.json │ │ ├── desktop-schema.json │ │ └── linux-schema.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── db.rs │ ├── fs_track.rs │ ├── library.rs │ ├── lrclib.rs │ ├── lrclib │ │ ├── challenge_solver.rs │ │ ├── flag.rs │ │ ├── get.rs │ │ ├── get_by_id.rs │ │ ├── publish.rs │ │ ├── request_challenge.rs │ │ └── search.rs │ ├── lyrics.rs │ ├── main.rs │ ├── persistent_entities.rs │ ├── player.rs │ ├── state.rs │ └── utils.rs └── tauri.conf.json ├── src ├── App.vue ├── assets │ ├── buy-me-a-coffee.png │ └── lrclib.png ├── components │ ├── About.vue │ ├── ChooseDirectory.vue │ ├── CopyablePre.vue │ ├── Library.vue │ ├── NowPlaying.vue │ ├── SelectStrategy.vue │ ├── common │ │ ├── BaseModal.vue │ │ ├── CheckboxButton.vue │ │ └── RadioButton.vue │ ├── icons │ │ ├── EqualEnter.vue │ │ └── Equalizer.vue │ ├── library │ │ ├── AlbumList.vue │ │ ├── ArtistList.vue │ │ ├── Config.vue │ │ ├── DownloadViewer.vue │ │ ├── EditLyrics.vue │ │ ├── LibraryHeader.vue │ │ ├── MiniSearch.vue │ │ ├── MyLrclib.vue │ │ ├── SearchBar.vue │ │ ├── SearchLyrics.vue │ │ ├── TrackList.vue │ │ ├── album-list │ │ │ ├── AlbumItem.vue │ │ │ └── AlbumTrackList.vue │ │ ├── artist-list │ │ │ ├── ArtistItem.vue │ │ │ └── ArtistTrackList.vue │ │ ├── edit-lyrics │ │ │ ├── PublishLyrics.vue │ │ │ ├── PublishPlainText.vue │ │ │ └── Save.vue │ │ ├── my-lrclib │ │ │ ├── EditLyrics.vue │ │ │ ├── FlagLyrics.vue │ │ │ ├── PreviewLyrics.vue │ │ │ └── SearchResult.vue │ │ ├── search-lyrics │ │ │ └── Preview.vue │ │ └── track-list │ │ │ └── TrackItem.vue │ └── now-playing │ │ ├── LyricsViewer.vue │ │ ├── PlainLyricsViewer.vue │ │ ├── Seek.vue │ │ └── VolumeSlider.vue ├── composables │ ├── downloader.js │ ├── edit-lyrics.js │ ├── global-state.js │ ├── player.js │ ├── search-library.js │ └── search-lyrics.js ├── main.js ├── style.css └── utils │ ├── human-duration.js │ ├── lyrics-lint.js │ ├── lyrics.js │ └── plain-text-lint.js ├── tailwind.config.cjs └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tranxuanthang 2 | buy_me_a_coffee: thangtran 3 | custom: ["https://paypal.me/tranxuanthang98"] 4 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: 'publish' 2 | on: 3 | push: 4 | branches: 5 | - release 6 | 7 | jobs: 8 | publish-tauri: 9 | permissions: 10 | contents: write 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - platform: 'macos-latest' # for Arm based macs (M1 and above). 16 | args: '--target aarch64-apple-darwin' 17 | - platform: 'macos-latest' # for Intel based macs. 18 | args: '--target x86_64-apple-darwin' 19 | - platform: 'ubuntu-24.04' # for Tauri v1 you could replace this with ubuntu-20.04. 20 | args: '' 21 | - platform: 'windows-latest' 22 | args: '' 23 | 24 | runs-on: ${{ matrix.platform }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: setup node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: lts/* 31 | - name: install Rust stable 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 35 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 36 | - name: install dependencies (ubuntu only) 37 | if: matrix.platform == 'ubuntu-24.04' 38 | run: | 39 | sudo apt-get update 40 | sudo apt-get install -y libappindicator3-dev librsvg2-dev patchelf libasound2-dev \ 41 | libwebkit2gtk-4.1-0=2.44.0-2 \ 42 | libwebkit2gtk-4.1-dev=2.44.0-2 \ 43 | libjavascriptcoregtk-4.1-0=2.44.0-2 \ 44 | libjavascriptcoregtk-4.1-dev=2.44.0-2 \ 45 | gir1.2-javascriptcoregtk-4.1=2.44.0-2 \ 46 | gir1.2-webkit2-4.1=2.44.0-2 47 | - name: install frontend dependencies 48 | run: npm install # change this to npm or pnpm depending on which one you use 49 | - uses: tauri-apps/tauri-action@v0 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | tagName: __VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 54 | releaseName: '__VERSION__' 55 | releaseBody: 'See the assets to download this version and install.' 56 | releaseDraft: true 57 | prerelease: false 58 | args: ${{ matrix.args }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 tranxuanthang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LRCGET 2 | 3 | Utility for mass-downloading LRC synced lyrics for your offline music library. 4 | 5 | LRCGET will scan every files in your chosen directory for music files, then and try to download lyrics to a LRC files having the same name and save them to the same directory as your music files. 6 | 7 | LRCGET is the official client of [LRCLIB](https://lrclib.net) service. 8 | 9 | ## Download 10 | 11 | 🎉 Latest version: v0.9.0 12 | 13 | Visit the [release page](https://github.com/tranxuanthang/lrcget/releases) to download. 14 | 15 | OS Support: 16 | 17 | - [x] Windows 10 18 | - [x] Linux (Ubuntu and AppImage build) 19 | - [x] macOS 20 | 21 | ## Screenshots 22 | 23 | ![02.png](screenshots/02.png?2) 24 | 25 | ![03.png](screenshots/03.png?2) 26 | 27 | ![04.png](screenshots/04.png?2) 28 | 29 | ![05.png](screenshots/05.png?2) 30 | 31 | ## Donation 32 | 33 | Toss a coin to your developer? 34 | 35 | **GitHub Sponsors (Recommended - 100% of your support goes to the developer):** 36 | 37 | https://github.com/sponsors/tranxuanthang 38 | 39 | **Buy Me a Coffee:** 40 | 41 | https://www.buymeacoffee.com/thangtran 42 | 43 | **Paypal:** 44 | 45 | https://paypal.me/tranxuanthang98 46 | 47 | **Monero (XMR):** 48 | 49 | ``` 50 | 43ZN5qDdGQhPGthFnngD8rjCHYLsEFBcyJjDC1GPZzVxWSfT8R48QCLNGyy6Z9LvatF5j8kSgv23DgJpixJg8bnmMnKm3b7 51 | ``` 52 | 53 | **Litecoin (LTC):** 54 | 55 | ``` 56 | ltc1q7texq5qsp59gclqlwf6asrqmhm98gruvz94a48 57 | ``` 58 | 59 | ## Troubleshooting 60 | 61 | **Audio cannot be played in Linux (Ubuntu and other distros)** 62 | 63 | Try to install `pipewire-alsa` package. For example, in Ubuntu or Debian-based distros: 64 | 65 | ``` 66 | sudo apt install pipewire-alsa 67 | ``` 68 | 69 | **App won't open in Windows 10/11** 70 | 71 | If you are using Windows 10 LTSC, or have tried running some scripts to debloat Windows 10 (which will uninstall Microsoft Edge and its webview component), you might have issues as LRCGET depends on WebView2. Reinstalling Microsoft Edge might fix the problem (see issue https://github.com/tranxuanthang/lrcget/issues/45). 72 | 73 | **Scrollbar is invisible in Linux (KDE Plasma 5/6)** 74 | 75 | The exact cause is still unknown, but it can be fixed by going to System Settings > Appearance > Global Theme > Application Style > Configure GNOME/GTK Application Style... > Change to something other than breeze (Awaita or Default) > Apply (see comment https://github.com/tranxuanthang/lrcget/issues/44#issuecomment-1962998268) 76 | 77 | ## Contact 78 | 79 | If you prefer to contact by email: 80 | 81 | [hoangtudevops@protonmail.com](mailto:hoangtudevops@protonmail.com) 82 | 83 | ## Development 84 | 85 | LRCGET is made with [Tauri](https://tauri.app). 86 | 87 | To start developing the application, you need to do the [prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites) steps according to your operating system. 88 | 89 | For example, you need the following components to start the development in Windows: 90 | 91 | - Microsoft Visual Studio C++ Build Tools 92 | - Rust 1.81.0 or higher 93 | - NodeJS v16.18.0 or higher 94 | 95 | Start the development window with the following command: 96 | 97 | ``` shell 98 | cd lrcget 99 | npm install 100 | npm run tauri dev 101 | ``` 102 | 103 | ## Building 104 | 105 | Start the build process with the following command: 106 | 107 | ``` shell 108 | cd lrcget 109 | npm install 110 | npm run tauri build 111 | ``` 112 | 113 | Your built binaries are located at: 114 | 115 | ``` 116 | ./src-tauri/target/release/ 117 | ``` 118 | 119 | For more detailed instruction, follow the [building guide](https://tauri.app/v1/guides/building/) to build the application according to your OS platform. 120 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/app-icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LRCGET 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lrcget", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "tauri": "tauri" 10 | }, 11 | "dependencies": { 12 | "@tanstack/vue-virtual": "^3.1.2", 13 | "@tauri-apps/api": "^2.1.1", 14 | "@tauri-apps/plugin-dialog": "^2.2.0", 15 | "@tauri-apps/plugin-global-shortcut": "^2.2.0", 16 | "@tauri-apps/plugin-os": "^2.2.0", 17 | "@tauri-apps/plugin-shell": "^2.2.0", 18 | "codemirror": "^6.0.1", 19 | "floating-vue": "^5.2.2", 20 | "lodash": "^4.17.21", 21 | "lrc-kit": "github:tranxuanthang/lrc-kit", 22 | "mdue": "^0.1.4", 23 | "path-browserify": "^1.0.1", 24 | "semver": "^7.6.0", 25 | "vue": "^3.4", 26 | "vue-3-slider-component": "^1.0.1", 27 | "vue-codemirror": "^6.1.1", 28 | "vue-final-modal": "^4.5.5", 29 | "vue-toastification": "^2.0.0-rc.5" 30 | }, 31 | "devDependencies": { 32 | "@tauri-apps/cli": "^2.1.0", 33 | "@vitejs/plugin-vue": "^3.0.1", 34 | "autoprefixer": "^10.4.12", 35 | "postcss": "^8.4.18", 36 | "tailwindcss": "^3.2.1", 37 | "vite": "^3.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/screenshots/01.png -------------------------------------------------------------------------------- /screenshots/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/screenshots/02.png -------------------------------------------------------------------------------- /screenshots/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/screenshots/03.png -------------------------------------------------------------------------------- /screenshots/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/screenshots/04.png -------------------------------------------------------------------------------- /screenshots/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/screenshots/05.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lrcget" 3 | version = "0.9.3" 4 | description = "Utility for mass-downloading LRC synced lyrics for your offline music library." 5 | authors = ["tranxuanthang"] 6 | license = "MIT" 7 | repository = "https://github.com/tranxuanthang/lrcget" 8 | edition = "2021" 9 | rust-version = "1.81" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [build-dependencies] 14 | tauri-build = { version = "2", features = [] } 15 | 16 | [dependencies] 17 | serde_json = "1.0" 18 | serde = { version = "1.0", features = ["derive"] } 19 | tauri = { version = "2", features = [ "protocol-asset", "devtools"] } 20 | globwalk = "0.9.1" 21 | reqwest = { version = "0.12.7", features = ["json"] } 22 | lofty = "0.21.1" 23 | anyhow = "1.0.89" 24 | thiserror = "1.0" 25 | rusqlite = { version = "0.32.1", features = ["bundled"] } 26 | secular = { version="1.0.1", features= ["bmp", "normalization"] } 27 | collapse = "0.1.2" 28 | rayon = "1.10.0" 29 | indoc = "2" 30 | tokio = { version = "1.40", features = ["full"] } 31 | ring = "0.17.8" 32 | data-encoding = "2.4.0" 33 | kira = "0.9.5" 34 | symphonia = { version = "0.5.4", features = ["all"] } 35 | regex = "1.10.4" 36 | lrc = "0.1.8" 37 | tauri-plugin-os = "2" 38 | tauri-plugin-shell = "2" 39 | tauri-plugin-dialog = "2" 40 | 41 | [features] 42 | # by default Tauri runs in production mode 43 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 44 | default = [ "custom-protocol" ] 45 | # this feature is used used for production builds where `devPath` points to the filesystem 46 | # DO NOT remove this 47 | custom-protocol = [ "tauri/custom-protocol" ] 48 | 49 | [profile.dev.package.kira] 50 | opt-level = 3 51 | 52 | [profile.dev.package.cpal] 53 | opt-level = 3 54 | 55 | [profile.dev.package.symphonia] 56 | opt-level = 3 57 | 58 | [profile.dev.package.symphonia-bundle-mp3] 59 | opt-level = 3 60 | 61 | [profile.dev.package.symphonia-format-ogg] 62 | opt-level = 3 63 | 64 | [profile.dev.package.symphonia-codec-vorbis] 65 | opt-level = 3 66 | 67 | [profile.dev.package.symphonia-bundle-flac] 68 | opt-level = 3 69 | 70 | [profile.dev.package.symphonia-codec-pcm] 71 | opt-level = 3 72 | 73 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 74 | tauri-plugin-global-shortcut = "2" 75 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "global-shortcut:default", 13 | "shell:default", 14 | "dialog:default" 15 | ] 16 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/migrated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "migrated", 3 | "description": "permissions that were migrated from v1", 4 | "local": true, 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "core:window:allow-create", 11 | "core:window:allow-center", 12 | "core:window:allow-request-user-attention", 13 | "core:window:allow-set-resizable", 14 | "core:window:allow-set-maximizable", 15 | "core:window:allow-set-minimizable", 16 | "core:window:allow-set-closable", 17 | "core:window:allow-set-title", 18 | "core:window:allow-maximize", 19 | "core:window:allow-unmaximize", 20 | "core:window:allow-minimize", 21 | "core:window:allow-unminimize", 22 | "core:window:allow-show", 23 | "core:window:allow-hide", 24 | "core:window:allow-close", 25 | "core:window:allow-set-decorations", 26 | "core:window:allow-set-always-on-top", 27 | "core:window:allow-set-content-protected", 28 | "core:window:allow-set-size", 29 | "core:window:allow-set-min-size", 30 | "core:window:allow-set-max-size", 31 | "core:window:allow-set-position", 32 | "core:window:allow-set-fullscreen", 33 | "core:window:allow-set-focus", 34 | "core:window:allow-set-icon", 35 | "core:window:allow-set-skip-taskbar", 36 | "core:window:allow-set-cursor-grab", 37 | "core:window:allow-set-cursor-visible", 38 | "core:window:allow-set-cursor-icon", 39 | "core:window:allow-set-cursor-position", 40 | "core:window:allow-set-ignore-cursor-events", 41 | "core:window:allow-start-dragging", 42 | "core:webview:allow-print", 43 | "shell:allow-open", 44 | "dialog:allow-open", 45 | "dialog:allow-save", 46 | "dialog:allow-message", 47 | "dialog:allow-ask", 48 | "dialog:allow-confirm", 49 | "global-shortcut:allow-is-registered", 50 | "global-shortcut:allow-register", 51 | "global-shortcut:allow-register-all", 52 | "global-shortcut:allow-unregister", 53 | "global-shortcut:allow-unregister-all", 54 | "os:allow-platform", 55 | "os:allow-version", 56 | "os:allow-os-type", 57 | "os:allow-family", 58 | "os:allow-arch", 59 | "os:allow-exe-extension", 60 | "os:allow-locale", 61 | "os:allow-hostname", 62 | "os:default" 63 | ] 64 | } -------------------------------------------------------------------------------- /src-tauri/gen/schemas/capabilities.json: -------------------------------------------------------------------------------- 1 | {"desktop-capability":{"identifier":"desktop-capability","description":"","local":true,"windows":["main"],"permissions":["global-shortcut:default","shell:default","dialog:default"],"platforms":["macOS","windows","linux"]},"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-create","core:window:allow-center","core:window:allow-request-user-attention","core:window:allow-set-resizable","core:window:allow-set-maximizable","core:window:allow-set-minimizable","core:window:allow-set-closable","core:window:allow-set-title","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-minimize","core:window:allow-unminimize","core:window:allow-show","core:window:allow-hide","core:window:allow-close","core:window:allow-set-decorations","core:window:allow-set-always-on-top","core:window:allow-set-content-protected","core:window:allow-set-size","core:window:allow-set-min-size","core:window:allow-set-max-size","core:window:allow-set-position","core:window:allow-set-fullscreen","core:window:allow-set-focus","core:window:allow-set-icon","core:window:allow-set-skip-taskbar","core:window:allow-set-cursor-grab","core:window:allow-set-cursor-visible","core:window:allow-set-cursor-icon","core:window:allow-set-cursor-position","core:window:allow-set-ignore-cursor-events","core:window:allow-start-dragging","core:webview:allow-print","shell:allow-open","dialog:allow-open","dialog:allow-save","dialog:allow-message","dialog:allow-ask","dialog:allow-confirm","global-shortcut:allow-is-registered","global-shortcut:allow-register","global-shortcut:allow-register-all","global-shortcut:allow-unregister","global-shortcut:allow-unregister-all","os:allow-platform","os:allow-version","os:allow-os-type","os:allow-family","os:allow-arch","os:allow-exe-extension","os:allow-locale","os:allow-hostname","os:default"]}} -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/library.rs: -------------------------------------------------------------------------------- 1 | use crate::db; 2 | use crate::fs_track::{self, FsTrack}; 3 | use crate::persistent_entities::{PersistentAlbum, PersistentArtist, PersistentTrack}; 4 | use anyhow::Result; 5 | use rusqlite::Connection; 6 | use tauri::AppHandle; 7 | 8 | pub fn initialize_library(conn: &mut Connection, app_handle: AppHandle) -> Result<()> { 9 | let init = db::get_init(conn)?; 10 | if init { 11 | return Ok(()); 12 | } 13 | 14 | db::clean_library(conn)?; 15 | 16 | let directories = db::get_directories(conn)?; 17 | let result = fs_track::load_tracks_from_directories(&directories, conn, app_handle); 18 | 19 | match result { 20 | Ok(()) => { 21 | db::set_init(true, conn)?; 22 | Ok(()) 23 | } 24 | Err(err) => { 25 | let uninitialization = uninitialize_library(conn); 26 | if let Err(uninit_error) = uninitialization { 27 | println!( 28 | "Uninitialization library errored. Message: {}", 29 | uninit_error.to_string() 30 | ); 31 | } 32 | Err(err) 33 | } 34 | } 35 | } 36 | 37 | pub fn uninitialize_library(conn: &Connection) -> Result<()> { 38 | db::clean_library(conn)?; 39 | db::set_init(false, conn)?; 40 | Ok(()) 41 | } 42 | 43 | pub fn add_tracks(tracks: Vec, conn: &Connection) -> Result<()> { 44 | for track in tracks.iter() { 45 | db::add_track(&track, conn)?; 46 | } 47 | Ok(()) 48 | } 49 | 50 | pub fn get_tracks(conn: &Connection) -> Result> { 51 | db::get_tracks(conn) 52 | } 53 | 54 | pub fn get_track_ids(search_query: Option, without_plain_lyrics: bool, without_synced_lyrics: bool, conn: &Connection) -> Result> { 55 | match search_query { 56 | Some(search_query) => db::get_search_track_ids(&search_query, conn), 57 | None => db::get_track_ids(without_plain_lyrics, without_synced_lyrics, conn), 58 | } 59 | } 60 | 61 | pub fn get_track(id: i64, conn: &Connection) -> Result { 62 | db::get_track_by_id(id, conn) 63 | } 64 | 65 | pub fn get_albums(conn: &Connection) -> Result> { 66 | db::get_albums(conn) 67 | } 68 | 69 | pub fn get_album_ids(conn: &Connection) -> Result> { 70 | db::get_album_ids(conn) 71 | } 72 | 73 | pub fn get_album(id: i64, conn: &Connection) -> Result { 74 | db::get_album_by_id(id, conn) 75 | } 76 | 77 | pub fn get_artists(conn: &Connection) -> Result> { 78 | db::get_artists(conn) 79 | } 80 | 81 | pub fn get_artist_ids(conn: &Connection) -> Result> { 82 | db::get_artist_ids(conn) 83 | } 84 | 85 | pub fn get_artist(id: i64, conn: &Connection) -> Result { 86 | db::get_artist_by_id(id, conn) 87 | } 88 | 89 | pub fn get_album_tracks(album_id: i64, conn: &Connection) -> Result> { 90 | db::get_album_tracks(album_id, conn) 91 | } 92 | 93 | pub fn get_artist_tracks(artist_id: i64, conn: &Connection) -> Result> { 94 | db::get_artist_tracks(artist_id, conn) 95 | } 96 | 97 | pub fn get_album_track_ids(album_id: i64, without_plain_lyrics: bool, without_synced_lyrics: bool, conn: &Connection) -> Result> { 98 | db::get_album_track_ids(album_id, without_plain_lyrics, without_synced_lyrics, conn) 99 | } 100 | 101 | pub fn get_artist_track_ids(artist_id: i64, without_plain_lyrics: bool, without_synced_lyrics: bool, conn: &Connection) -> Result> { 102 | db::get_artist_track_ids(artist_id, without_plain_lyrics, without_synced_lyrics, conn) 103 | } 104 | 105 | pub fn get_init(conn: &Connection) -> Result { 106 | db::get_init(conn) 107 | } 108 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib.rs: -------------------------------------------------------------------------------- 1 | pub mod challenge_solver; 2 | pub mod flag; 3 | pub mod get; 4 | pub mod get_by_id; 5 | pub mod publish; 6 | pub mod request_challenge; 7 | pub mod search; 8 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/challenge_solver.rs: -------------------------------------------------------------------------------- 1 | use data_encoding::HEXUPPER; 2 | use ring::digest::{Context, SHA256}; 3 | 4 | fn verify_nonce(result: &Vec, target: &Vec) -> bool { 5 | if result.len() != target.len() { 6 | return false; 7 | } 8 | 9 | for i in 0..(result.len() - 1) { 10 | if result[i] > target[i] { 11 | return false; 12 | } else if result[i] < target[i] { 13 | break; 14 | } 15 | } 16 | 17 | return true; 18 | } 19 | 20 | pub fn solve_challenge(prefix: &str, target_hex: &str) -> String { 21 | let mut nonce = 0; 22 | let mut hashed; 23 | let target = HEXUPPER.decode(target_hex.as_bytes()).unwrap(); 24 | 25 | loop { 26 | let mut context = Context::new(&SHA256); 27 | let input = format!("{}{}", prefix, nonce); 28 | context.update(input.as_bytes()); 29 | hashed = context.finish().as_ref().to_vec(); 30 | 31 | let result = verify_nonce(&hashed, &target); 32 | if result { 33 | break; 34 | } else { 35 | nonce += 1; 36 | } 37 | } 38 | 39 | nonce.to_string() 40 | } 41 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/flag.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use reqwest; 5 | use serde::{Deserialize, Serialize}; 6 | use thiserror::Error; 7 | 8 | #[derive(Serialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct Request { 11 | track_id: i64, 12 | reason: String, 13 | } 14 | 15 | #[derive(Error, Deserialize, Debug)] 16 | #[serde(rename_all = "camelCase")] 17 | #[error("{error}: {message}")] 18 | pub struct ResponseError { 19 | status_code: Option, 20 | error: String, 21 | message: String, 22 | } 23 | 24 | pub async fn request( 25 | track_id: i64, 26 | reason: &str, 27 | publish_token: &str, 28 | lrclib_instance: &str, 29 | ) -> Result<()> { 30 | let data = Request { 31 | track_id, 32 | reason: reason.to_owned(), 33 | }; 34 | 35 | let version = env!("CARGO_PKG_VERSION"); 36 | let user_agent = format!( 37 | "LRCGET v{} (https://github.com/tranxuanthang/lrcget)", 38 | version 39 | ); 40 | let client = reqwest::Client::builder() 41 | .timeout(Duration::from_secs(10)) 42 | .user_agent(user_agent) 43 | .build()?; 44 | let api_endpoint = format!("{}/api/flag", lrclib_instance.trim_end_matches('/')); 45 | let url = reqwest::Url::parse(&api_endpoint)?; 46 | let res = client 47 | .post(url) 48 | .header("X-Publish-Token", publish_token) 49 | .json(&data) 50 | .send() 51 | .await?; 52 | 53 | match res.status() { 54 | reqwest::StatusCode::CREATED => Ok(()), 55 | 56 | reqwest::StatusCode::BAD_REQUEST 57 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 58 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 59 | let error = res.json::().await?; 60 | Err(error.into()) 61 | } 62 | 63 | _ => Err(ResponseError { 64 | status_code: None, 65 | error: "UnknownError".to_string(), 66 | message: "Unknown error happened".to_string(), 67 | } 68 | .into()), 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/get.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::utils::strip_timestamp; 4 | use anyhow::Result; 5 | use reqwest; 6 | use serde::{Deserialize, Serialize}; 7 | use thiserror::Error; 8 | 9 | #[derive(Deserialize, Serialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct RawResponse { 12 | pub plain_lyrics: Option, 13 | pub synced_lyrics: Option, 14 | instrumental: bool, 15 | lang: Option, 16 | isrc: Option, 17 | spotify_id: Option, 18 | name: Option, 19 | album_name: Option, 20 | artist_name: Option, 21 | release_date: Option, 22 | duration: Option, 23 | } 24 | 25 | #[derive(Serialize)] 26 | #[serde(tag = "type", content = "lyrics")] 27 | pub enum Response { 28 | SyncedLyrics(String, String), 29 | UnsyncedLyrics(String), 30 | IsInstrumental, 31 | None, 32 | } 33 | 34 | impl Response { 35 | pub fn from_raw_response(lrclib_response: RawResponse) -> Response { 36 | match lrclib_response.synced_lyrics { 37 | Some(synced_lyrics) => { 38 | let plain_lyrics = match lrclib_response.plain_lyrics { 39 | Some(plain_lyrics) => plain_lyrics, 40 | None => strip_timestamp(&synced_lyrics), 41 | }; 42 | Response::SyncedLyrics(synced_lyrics, plain_lyrics) 43 | } 44 | None => match lrclib_response.plain_lyrics { 45 | Some(unsynced_lyrics) => Response::UnsyncedLyrics(unsynced_lyrics), 46 | None => { 47 | if lrclib_response.instrumental { 48 | Response::IsInstrumental 49 | } else { 50 | Response::None 51 | } 52 | } 53 | }, 54 | } 55 | } 56 | } 57 | 58 | #[derive(Error, Deserialize, Debug)] 59 | #[serde(rename_all = "camelCase")] 60 | #[error("{error}: {message}")] 61 | pub struct ResponseError { 62 | status_code: Option, 63 | error: String, 64 | message: String, 65 | } 66 | 67 | async fn make_request( 68 | title: &str, 69 | album_name: &str, 70 | artist_name: &str, 71 | duration: f64, 72 | lrclib_instance: &str, 73 | ) -> Result { 74 | let params: Vec<(String, String)> = vec![ 75 | ("artist_name".to_owned(), artist_name.to_owned()), 76 | ("track_name".to_owned(), title.to_owned()), 77 | ("album_name".to_owned(), album_name.to_owned()), 78 | ("duration".to_owned(), duration.round().to_string()), 79 | ]; 80 | 81 | let version = env!("CARGO_PKG_VERSION"); 82 | let user_agent = format!( 83 | "LRCGET v{} (https://github.com/tranxuanthang/lrcget)", 84 | version 85 | ); 86 | let client = reqwest::Client::builder() 87 | .timeout(Duration::from_secs(10)) 88 | .user_agent(user_agent) 89 | .build()?; 90 | let api_endpoint = format!("{}/api/get", lrclib_instance.trim_end_matches('/')); 91 | let url = reqwest::Url::parse_with_params(&api_endpoint, ¶ms)?; 92 | Ok(client.get(url).send().await?) 93 | } 94 | 95 | pub async fn request_raw( 96 | title: &str, 97 | album_name: &str, 98 | artist_name: &str, 99 | duration: f64, 100 | lrclib_instance: &str, 101 | ) -> Result { 102 | let res = make_request(title, album_name, artist_name, duration, lrclib_instance).await?; 103 | 104 | match res.status() { 105 | reqwest::StatusCode::OK => { 106 | let lrclib_response = res.json::().await?; 107 | 108 | if lrclib_response.synced_lyrics.is_some() || lrclib_response.plain_lyrics.is_some() { 109 | Ok(lrclib_response) 110 | } else { 111 | Err(ResponseError { 112 | status_code: Some(404), 113 | error: "NotFound".to_string(), 114 | message: "There is no lyrics for this track".to_string(), 115 | } 116 | .into()) 117 | } 118 | } 119 | 120 | reqwest::StatusCode::NOT_FOUND => Err(ResponseError { 121 | status_code: Some(404), 122 | error: "NotFound".to_string(), 123 | message: "There is no lyrics for this track".to_string(), 124 | } 125 | .into()), 126 | 127 | reqwest::StatusCode::BAD_REQUEST 128 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 129 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 130 | let error = res.json::().await?; 131 | Err(error.into()) 132 | } 133 | 134 | _ => Err(ResponseError { 135 | status_code: None, 136 | error: "UnknownError".to_string(), 137 | message: "Unknown error happened".to_string(), 138 | } 139 | .into()), 140 | } 141 | } 142 | 143 | pub async fn request( 144 | title: &str, 145 | album_name: &str, 146 | artist_name: &str, 147 | duration: f64, 148 | lrclib_instance: &str, 149 | ) -> Result { 150 | let res = make_request(title, album_name, artist_name, duration, lrclib_instance).await?; 151 | 152 | match res.status() { 153 | reqwest::StatusCode::OK => { 154 | let lrclib_response = res.json::().await?; 155 | 156 | Ok(Response::from_raw_response(lrclib_response)) 157 | } 158 | 159 | reqwest::StatusCode::NOT_FOUND => Ok(Response::None), 160 | 161 | reqwest::StatusCode::BAD_REQUEST 162 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 163 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 164 | let error = res.json::().await?; 165 | Err(error.into()) 166 | } 167 | 168 | _ => Err(ResponseError { 169 | status_code: None, 170 | error: "UnknownError".to_string(), 171 | message: "Unknown error happened".to_string(), 172 | } 173 | .into()), 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/get_by_id.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::utils::strip_timestamp; 4 | use anyhow::Result; 5 | use reqwest; 6 | use serde::{Deserialize, Serialize}; 7 | use thiserror::Error; 8 | 9 | #[derive(Deserialize, Serialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct RawResponse { 12 | pub plain_lyrics: Option, 13 | pub synced_lyrics: Option, 14 | instrumental: bool, 15 | lang: Option, 16 | isrc: Option, 17 | spotify_id: Option, 18 | name: Option, 19 | album_name: Option, 20 | artist_name: Option, 21 | release_date: Option, 22 | duration: Option, 23 | } 24 | 25 | #[derive(Serialize)] 26 | #[serde(tag = "type", content = "lyrics")] 27 | pub enum Response { 28 | SyncedLyrics(String, String), 29 | UnsyncedLyrics(String), 30 | IsInstrumental, 31 | None, 32 | } 33 | 34 | impl Response { 35 | pub fn from_raw_response(lrclib_response: RawResponse) -> Response { 36 | match lrclib_response.synced_lyrics { 37 | Some(synced_lyrics) => { 38 | let plain_lyrics = match lrclib_response.plain_lyrics { 39 | Some(plain_lyrics) => plain_lyrics, 40 | None => strip_timestamp(&synced_lyrics), 41 | }; 42 | Response::SyncedLyrics(synced_lyrics, plain_lyrics) 43 | } 44 | None => match lrclib_response.plain_lyrics { 45 | Some(unsynced_lyrics) => Response::UnsyncedLyrics(unsynced_lyrics), 46 | None => { 47 | if lrclib_response.instrumental { 48 | Response::IsInstrumental 49 | } else { 50 | Response::None 51 | } 52 | } 53 | }, 54 | } 55 | } 56 | } 57 | 58 | #[derive(Error, Deserialize, Debug)] 59 | #[serde(rename_all = "camelCase")] 60 | #[error("{error}: {message}")] 61 | pub struct ResponseError { 62 | status_code: Option, 63 | error: String, 64 | message: String, 65 | } 66 | 67 | async fn make_request(id: i64, lrclib_instance: &str) -> Result { 68 | let version = env!("CARGO_PKG_VERSION"); 69 | let user_agent = format!( 70 | "LRCGET v{} (https://github.com/tranxuanthang/lrcget)", 71 | version 72 | ); 73 | let client = reqwest::Client::builder() 74 | .timeout(Duration::from_secs(10)) 75 | .user_agent(user_agent) 76 | .build()?; 77 | let api_endpoint = format!("{}/api/get/{}", lrclib_instance.trim_end_matches('/'), id); 78 | Ok(client.get(&api_endpoint).send().await?) 79 | } 80 | 81 | pub async fn request_raw(id: i64, lrclib_instance: &str) -> Result { 82 | let res = make_request(id, lrclib_instance).await?; 83 | 84 | match res.status() { 85 | reqwest::StatusCode::OK => { 86 | let lrclib_response = res.json::().await?; 87 | 88 | if lrclib_response.synced_lyrics.is_some() 89 | || lrclib_response.plain_lyrics.is_some() 90 | || lrclib_response.instrumental 91 | { 92 | Ok(lrclib_response) 93 | } else { 94 | Err(ResponseError { 95 | status_code: Some(404), 96 | error: "NotFound".to_string(), 97 | message: "There is no lyrics for this track".to_string(), 98 | } 99 | .into()) 100 | } 101 | } 102 | 103 | reqwest::StatusCode::NOT_FOUND => Err(ResponseError { 104 | status_code: Some(404), 105 | error: "NotFound".to_string(), 106 | message: "There is no lyrics for this track".to_string(), 107 | } 108 | .into()), 109 | 110 | reqwest::StatusCode::BAD_REQUEST 111 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 112 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 113 | let error = res.json::().await?; 114 | Err(error.into()) 115 | } 116 | 117 | _ => Err(ResponseError { 118 | status_code: None, 119 | error: "UnknownError".to_string(), 120 | message: "Unknown error happened".to_string(), 121 | } 122 | .into()), 123 | } 124 | } 125 | 126 | pub async fn request(id: i64, lrclib_instance: &str) -> Result { 127 | let res = make_request(id, lrclib_instance).await?; 128 | 129 | match res.status() { 130 | reqwest::StatusCode::OK => { 131 | let lrclib_response = res.json::().await?; 132 | 133 | Ok(Response::from_raw_response(lrclib_response)) 134 | } 135 | 136 | reqwest::StatusCode::NOT_FOUND => Ok(Response::None), 137 | 138 | reqwest::StatusCode::BAD_REQUEST 139 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 140 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 141 | let error = res.json::().await?; 142 | Err(error.into()) 143 | } 144 | 145 | _ => Err(ResponseError { 146 | status_code: None, 147 | error: "UnknownError".to_string(), 148 | message: "Unknown error happened".to_string(), 149 | } 150 | .into()), 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/publish.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use reqwest; 5 | use serde::{Deserialize, Serialize}; 6 | use thiserror::Error; 7 | 8 | #[derive(Serialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct Request { 11 | track_name: String, 12 | album_name: String, 13 | artist_name: String, 14 | duration: f64, 15 | plain_lyrics: String, 16 | synced_lyrics: String, 17 | } 18 | 19 | #[derive(Error, Deserialize, Debug)] 20 | #[serde(rename_all = "camelCase")] 21 | #[error("{error}: {message}")] 22 | pub struct ResponseError { 23 | status_code: Option, 24 | error: String, 25 | message: String, 26 | } 27 | 28 | pub async fn request( 29 | title: &str, 30 | album_name: &str, 31 | artist_name: &str, 32 | duration: f64, 33 | plain_lyrics: &str, 34 | synced_lyrics: &str, 35 | publish_token: &str, 36 | lrclib_instance: &str, 37 | ) -> Result<()> { 38 | let data = Request { 39 | artist_name: artist_name.to_owned(), 40 | track_name: title.to_owned(), 41 | album_name: album_name.to_owned(), 42 | duration: duration.round(), 43 | plain_lyrics: plain_lyrics.to_owned(), 44 | synced_lyrics: synced_lyrics.to_owned(), 45 | }; 46 | 47 | let version = env!("CARGO_PKG_VERSION"); 48 | let user_agent = format!( 49 | "LRCGET v{} (https://github.com/tranxuanthang/lrcget)", 50 | version 51 | ); 52 | let client = reqwest::Client::builder() 53 | .timeout(Duration::from_secs(10)) 54 | .user_agent(user_agent) 55 | .build()?; 56 | let api_endpoint = format!("{}/api/publish", lrclib_instance.trim_end_matches('/')); 57 | let url = reqwest::Url::parse(&api_endpoint)?; 58 | let res = client 59 | .post(url) 60 | .header("X-Publish-Token", publish_token) 61 | .json(&data) 62 | .send() 63 | .await?; 64 | 65 | match res.status() { 66 | reqwest::StatusCode::CREATED => Ok(()), 67 | 68 | reqwest::StatusCode::BAD_REQUEST 69 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 70 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 71 | let error = res.json::().await?; 72 | Err(error.into()) 73 | } 74 | 75 | _ => Err(ResponseError { 76 | status_code: None, 77 | error: "UnknownError".to_string(), 78 | message: "Unknown error happened".to_string(), 79 | } 80 | .into()), 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/request_challenge.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use reqwest; 5 | use serde::Deserialize; 6 | use thiserror::Error; 7 | 8 | #[derive(Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct Response { 11 | pub prefix: String, 12 | pub target: String, 13 | } 14 | 15 | #[derive(Error, Deserialize, Debug)] 16 | #[serde(rename_all = "camelCase")] 17 | #[error("{error}: {message}")] 18 | pub struct ResponseError { 19 | status_code: Option, 20 | error: String, 21 | message: String, 22 | } 23 | 24 | pub async fn request(lrclib_instance: &str) -> Result { 25 | let version = env!("CARGO_PKG_VERSION"); 26 | let user_agent = format!( 27 | "LRCGET v{} (https://github.com/tranxuanthang/lrcget)", 28 | version 29 | ); 30 | let client = reqwest::Client::builder() 31 | .timeout(Duration::from_secs(10)) 32 | .user_agent(user_agent) 33 | .build()?; 34 | let api_endpoint = format!( 35 | "{}/api/request-challenge", 36 | lrclib_instance.trim_end_matches('/') 37 | ); 38 | let url = reqwest::Url::parse(&api_endpoint)?; 39 | let res = client.post(url).send().await?; 40 | 41 | match res.status() { 42 | reqwest::StatusCode::OK => { 43 | let response = res.json::().await?; 44 | Ok(response) 45 | } 46 | 47 | reqwest::StatusCode::BAD_REQUEST 48 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 49 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 50 | let error = res.json::().await?; 51 | Err(error.into()) 52 | } 53 | 54 | _ => Err(ResponseError { 55 | status_code: None, 56 | error: "UnknownError".to_string(), 57 | message: "Unknown error happened".to_string(), 58 | } 59 | .into()), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/src/lrclib/search.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use reqwest; 5 | use serde::{Deserialize, Serialize}; 6 | use thiserror::Error; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | #[serde(rename_all = "camelCase")] 10 | struct SearchItem { 11 | id: i64, 12 | name: Option, 13 | artist_name: Option, 14 | album_name: Option, 15 | duration: Option, 16 | instrumental: bool, 17 | plain_lyrics: Option, 18 | synced_lyrics: Option, 19 | } 20 | 21 | #[derive(Deserialize, Serialize)] 22 | pub struct Response(Vec); 23 | 24 | #[derive(Error, Deserialize, Debug)] 25 | #[serde(rename_all = "camelCase")] 26 | #[error("{error}: {message}")] 27 | pub struct ResponseError { 28 | status_code: Option, 29 | error: String, 30 | message: String, 31 | } 32 | 33 | pub async fn request( 34 | title: &str, 35 | album_name: &str, 36 | artist_name: &str, 37 | q: &str, 38 | lrclib_instance: &str, 39 | ) -> Result { 40 | let params: Vec<(String, String)> = vec![ 41 | ("track_name".to_owned(), title.to_owned()), 42 | ("artist_name".to_owned(), artist_name.to_owned()), 43 | ("album_name".to_owned(), album_name.to_owned()), 44 | ("q".to_owned(), q.to_owned()), 45 | ]; 46 | 47 | let version = env!("CARGO_PKG_VERSION"); 48 | let user_agent = format!( 49 | "LRCGET v{} (https://github.com/tranxuanthang/lrcget)", 50 | version 51 | ); 52 | let client = reqwest::Client::builder() 53 | .timeout(Duration::from_secs(10)) 54 | .user_agent(user_agent) 55 | .build()?; 56 | let api_endpoint = format!("{}/api/search", lrclib_instance.trim_end_matches('/')); 57 | let url = reqwest::Url::parse_with_params(&api_endpoint, ¶ms)?; 58 | let res = client.get(url).send().await?; 59 | 60 | match res.status() { 61 | reqwest::StatusCode::OK => { 62 | let lrclib_response = res.json::().await?; 63 | Ok(lrclib_response) 64 | } 65 | 66 | reqwest::StatusCode::BAD_REQUEST 67 | | reqwest::StatusCode::SERVICE_UNAVAILABLE 68 | | reqwest::StatusCode::INTERNAL_SERVER_ERROR => { 69 | let error = res.json::().await?; 70 | Err(error.into()) 71 | } 72 | 73 | _ => Err(ResponseError { 74 | status_code: None, 75 | error: "UnknownError".to_string(), 76 | message: "Unknown error happened".to_string(), 77 | } 78 | .into()), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src-tauri/src/persistent_entities.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | pub struct PersistentTrack { 5 | pub id: i64, 6 | pub file_path: String, 7 | pub file_name: String, 8 | pub title: String, 9 | pub album_name: String, 10 | pub album_artist_name: Option, 11 | pub album_id: i64, 12 | pub artist_name: String, 13 | pub artist_id: i64, 14 | pub image_path: Option, 15 | pub track_number: Option, 16 | pub txt_lyrics: Option, 17 | pub lrc_lyrics: Option, 18 | pub duration: f64, 19 | pub instrumental: bool, 20 | } 21 | 22 | #[derive(Serialize)] 23 | pub struct PersistentAlbum { 24 | pub id: i64, 25 | pub name: String, 26 | pub image_path: Option, 27 | pub artist_name: String, 28 | pub album_artist_name: Option, 29 | pub tracks_count: i64, 30 | } 31 | 32 | #[derive(Serialize)] 33 | pub struct PersistentArtist { 34 | pub id: i64, 35 | pub name: String, 36 | // pub albums_count: i64, 37 | pub tracks_count: i64, 38 | } 39 | 40 | #[derive(Serialize)] 41 | pub struct PersistentConfig { 42 | pub skip_tracks_with_synced_lyrics: bool, 43 | pub skip_tracks_with_plain_lyrics: bool, 44 | pub try_embed_lyrics: bool, 45 | pub theme_mode: String, 46 | pub lrclib_instance: String, 47 | } 48 | -------------------------------------------------------------------------------- /src-tauri/src/player.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use kira::{ 3 | manager::{backend::DefaultBackend, AudioManager, AudioManagerSettings}, 4 | sound::{ 5 | streaming::{StreamingSoundData, StreamingSoundHandle}, 6 | FromFileError, PlaybackState, 7 | }, 8 | tween::Tween, 9 | }; 10 | 11 | use crate::persistent_entities::PersistentTrack; 12 | use serde::Serialize; 13 | 14 | #[derive(Serialize)] 15 | #[serde(rename_all = "snake_case")] 16 | pub enum PlayerStatus { 17 | Playing, 18 | Paused, 19 | Stopped, 20 | } 21 | 22 | #[derive(Serialize)] 23 | pub struct Player { 24 | #[serde(skip)] 25 | manager: AudioManager, 26 | #[serde(skip)] 27 | sound_handle: Option>, 28 | #[serde(skip)] 29 | pub track: Option, 30 | pub status: PlayerStatus, 31 | pub progress: f64, 32 | pub duration: f64, 33 | pub volume: f64, 34 | } 35 | 36 | impl Player { 37 | pub fn new() -> Result { 38 | let manager = AudioManager::::new(AudioManagerSettings::default())?; 39 | 40 | Ok(Player { 41 | manager, 42 | sound_handle: None, 43 | track: None, 44 | status: PlayerStatus::Stopped, 45 | progress: 0.0, 46 | duration: 0.0, 47 | volume: 1.0, 48 | }) 49 | } 50 | 51 | pub fn renew_state(&mut self) { 52 | if let Some(ref mut sound_handle) = self.sound_handle { 53 | match sound_handle.state() { 54 | PlaybackState::Playing => self.status = PlayerStatus::Playing, 55 | PlaybackState::Pausing => self.status = PlayerStatus::Playing, 56 | PlaybackState::Stopping => self.status = PlayerStatus::Playing, 57 | PlaybackState::Paused => self.status = PlayerStatus::Paused, 58 | PlaybackState::Stopped => self.status = PlayerStatus::Stopped, 59 | } 60 | } else { 61 | self.status = PlayerStatus::Stopped 62 | } 63 | 64 | match self.sound_handle { 65 | Some(ref mut sound_handle) => { 66 | self.progress = sound_handle.position(); 67 | } 68 | None => {} 69 | } 70 | } 71 | 72 | pub fn play(&mut self, track: PersistentTrack) -> Result<()> { 73 | let _ = self.stop(); 74 | self.track = Some(track); 75 | 76 | if let Some(ref mut track) = self.track { 77 | let sound_data = StreamingSoundData::from_file(&track.file_path)?; 78 | 79 | self.duration = sound_data.duration().as_secs_f64(); 80 | self.sound_handle = Some(self.manager.play(sound_data)?); 81 | self.sound_handle 82 | .as_mut() 83 | .unwrap() 84 | .set_volume(self.volume, Tween::default()); 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | pub fn resume(&mut self) { 91 | if let Some(ref mut sound_handle) = self.sound_handle { 92 | sound_handle.resume(Tween::default()); 93 | } 94 | } 95 | 96 | pub fn pause(&mut self) { 97 | if let Some(ref mut sound_handle) = self.sound_handle { 98 | sound_handle.pause(Tween::default()); 99 | } 100 | } 101 | 102 | pub fn seek(&mut self, position: f64) { 103 | if let Some(ref mut sound_handle) = self.sound_handle { 104 | match sound_handle.state() { 105 | PlaybackState::Playing => sound_handle.seek_to(position), 106 | _ => { 107 | sound_handle.seek_to(position); 108 | sound_handle.resume(Tween::default()); 109 | } 110 | } 111 | } 112 | } 113 | 114 | pub fn stop(&mut self) { 115 | if let Some(ref mut sound_handle) = self.sound_handle { 116 | sound_handle.stop(Tween::default()); 117 | self.sound_handle = None; 118 | self.track = None; 119 | self.duration = 0.0; 120 | self.progress = 0.0; 121 | self.status = PlayerStatus::Stopped; 122 | } 123 | } 124 | 125 | pub fn set_volume(&mut self, volume: f64) { 126 | if let Some(ref mut sound_handle) = self.sound_handle { 127 | sound_handle.set_volume(volume, Tween::default()); 128 | } 129 | self.volume = volume; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src-tauri/src/state.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::Connection; 2 | use tauri::{AppHandle, Manager, State}; 3 | 4 | use crate::player::Player; 5 | 6 | pub struct AppState { 7 | pub db: std::sync::Mutex>, 8 | pub player: std::sync::Mutex>, 9 | } 10 | 11 | pub trait ServiceAccess { 12 | fn db(&self, operation: F) -> TResult 13 | where 14 | F: FnOnce(&Connection) -> TResult; 15 | 16 | fn db_mut(&self, operation: F) -> TResult 17 | where 18 | F: FnOnce(&mut Connection) -> TResult; 19 | } 20 | 21 | impl ServiceAccess for AppHandle { 22 | fn db(&self, operation: F) -> TResult 23 | where 24 | F: FnOnce(&Connection) -> TResult, 25 | { 26 | let app_state: State = self.state(); 27 | let db_connection_guard = app_state.db.lock().unwrap(); 28 | let db = db_connection_guard.as_ref().unwrap(); 29 | 30 | operation(db) 31 | } 32 | 33 | fn db_mut(&self, operation: F) -> TResult 34 | where 35 | F: FnOnce(&mut Connection) -> TResult, 36 | { 37 | let app_state: State = self.state(); 38 | let mut db_connection_guard = app_state.db.lock().unwrap(); 39 | let db = db_connection_guard.as_mut().unwrap(); 40 | 41 | operation(db) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src-tauri/src/utils.rs: -------------------------------------------------------------------------------- 1 | use collapse::collapse; 2 | use regex::Regex; 3 | use secular::lower_lay_string; 4 | 5 | pub fn prepare_input(input: &str) -> String { 6 | let mut prepared_input = lower_lay_string(&input); 7 | 8 | let re = Regex::new(r#"[`~!@#$%^&*()_|+\-=?;:",.<>\{\}\[\]\\\/]"#).unwrap(); 9 | prepared_input = re.replace_all(&prepared_input, " ").to_string(); 10 | 11 | let re = Regex::new(r#"['’]"#).unwrap(); 12 | prepared_input = re.replace_all(&prepared_input, "").to_string(); 13 | 14 | prepared_input = prepared_input.to_lowercase(); 15 | prepared_input = collapse(&prepared_input); 16 | 17 | prepared_input 18 | } 19 | 20 | pub fn strip_timestamp(synced_lyrics: &str) -> String { 21 | let re = Regex::new(r"^\[(.*)\] *").unwrap(); 22 | let plain_lyrics = re.replace_all(synced_lyrics, ""); 23 | plain_lyrics.to_string() 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "npm run dev", 4 | "beforeBuildCommand": "npm run build", 5 | "frontendDist": "../dist", 6 | "devUrl": "http://localhost:1420" 7 | }, 8 | "bundle": { 9 | "active": true, 10 | "category": "Music", 11 | "copyright": "", 12 | "windows": { 13 | "certificateThumbprint": null, 14 | "digestAlgorithm": "sha256", 15 | "timestampUrl": "" 16 | }, 17 | "externalBin": [], 18 | "icon": [ 19 | "icons/32x32.png", 20 | "icons/128x128.png", 21 | "icons/128x128@2x.png", 22 | "icons/icon.icns", 23 | "icons/icon.ico" 24 | ], 25 | "linux": { 26 | "deb": { 27 | "depends": [] 28 | }, 29 | "appimage": { 30 | "bundleMediaFramework": false 31 | } 32 | }, 33 | "longDescription": "", 34 | "macOS": { 35 | "entitlements": null, 36 | "exceptionDomain": "", 37 | "frameworks": [], 38 | "providerShortName": null, 39 | "signingIdentity": "-" 40 | }, 41 | "resources": [], 42 | "shortDescription": "Utility for mass-downloading LRC synced lyrics for your offline music library.", 43 | "targets": "all" 44 | }, 45 | "productName": "LRCGET", 46 | "mainBinaryName": "LRCGET", 47 | "identifier": "net.lrclib.lrcget", 48 | "plugins": {}, 49 | "app": { 50 | "withGlobalTauri": false, 51 | "windows": [ 52 | { 53 | "fullscreen": false, 54 | "minWidth": 1024, 55 | "minHeight": 768, 56 | "width": 1024, 57 | "height": 768, 58 | "resizable": true, 59 | "title": "LRCGET", 60 | "decorations": true, 61 | "transparent": false, 62 | "useHttpsScheme": true 63 | } 64 | ], 65 | "security": { 66 | "assetProtocol": { 67 | "scope": [ 68 | "**" 69 | ], 70 | "enable": true 71 | }, 72 | "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost; media-src *; connect-src ipc: http://ipc.localhost 'self' asset: *; style-src 'unsafe-inline' 'self'" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 86 | 87 | 89 | -------------------------------------------------------------------------------- /src/assets/buy-me-a-coffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src/assets/buy-me-a-coffee.png -------------------------------------------------------------------------------- /src/assets/lrclib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tranxuanthang/lrcget/d95c22eb2b4f91c294b7cfab003c895062546658/src/assets/lrclib.png -------------------------------------------------------------------------------- /src/components/About.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 144 | -------------------------------------------------------------------------------- /src/components/ChooseDirectory.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 93 | -------------------------------------------------------------------------------- /src/components/CopyablePre.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 48 | -------------------------------------------------------------------------------- /src/components/Library.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 155 | -------------------------------------------------------------------------------- /src/components/NowPlaying.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 146 | -------------------------------------------------------------------------------- /src/components/SelectStrategy.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 54 | -------------------------------------------------------------------------------- /src/components/common/BaseModal.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 65 | -------------------------------------------------------------------------------- /src/components/common/CheckboxButton.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /src/components/common/RadioButton.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 60 | -------------------------------------------------------------------------------- /src/components/icons/EqualEnter.vue: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /src/components/icons/Equalizer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 71 | -------------------------------------------------------------------------------- /src/components/library/AlbumList.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 84 | -------------------------------------------------------------------------------- /src/components/library/ArtistList.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 84 | -------------------------------------------------------------------------------- /src/components/library/Config.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 188 | -------------------------------------------------------------------------------- /src/components/library/DownloadViewer.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 91 | -------------------------------------------------------------------------------- /src/components/library/LibraryHeader.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 121 | 122 | 135 | -------------------------------------------------------------------------------- /src/components/library/MiniSearch.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /src/components/library/MyLrclib.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 60 | -------------------------------------------------------------------------------- /src/components/library/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 66 | -------------------------------------------------------------------------------- /src/components/library/SearchLyrics.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 180 | -------------------------------------------------------------------------------- /src/components/library/TrackList.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 108 | -------------------------------------------------------------------------------- /src/components/library/album-list/AlbumItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 50 | -------------------------------------------------------------------------------- /src/components/library/album-list/AlbumTrackList.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 127 | -------------------------------------------------------------------------------- /src/components/library/artist-list/ArtistItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 48 | -------------------------------------------------------------------------------- /src/components/library/artist-list/ArtistTrackList.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 125 | -------------------------------------------------------------------------------- /src/components/library/edit-lyrics/PublishLyrics.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 165 | -------------------------------------------------------------------------------- /src/components/library/edit-lyrics/PublishPlainText.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 165 | -------------------------------------------------------------------------------- /src/components/library/edit-lyrics/Save.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /src/components/library/my-lrclib/FlagLyrics.vue: -------------------------------------------------------------------------------- 1 |