├── .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 | 
24 |
25 | 
26 |
27 | 
28 |
29 | 
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 |
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