├── .cargo
└── config.toml
├── .github
├── FUNDING.yml
└── workflows
│ └── build.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── assets
├── 128x128.png
├── 128x128@2x.png
├── 32x32.png
├── 64x64.bin
├── headerimage.bmp
├── icon.icns
├── icon.ico
├── installer-icon.ico
├── installer.nsi
├── mac-cross.toml
├── onetagger-logo-github.png
├── spotify_callback.html
└── welcomebanner.bmp
├── client
├── .gitignore
├── index.html
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── apps
│ │ │ ├── crossdj.png
│ │ │ ├── dex.png
│ │ │ ├── djaypro.png
│ │ │ ├── djuced.png
│ │ │ ├── engineprime.png
│ │ │ ├── mixxx.png
│ │ │ ├── rekordbox.png
│ │ │ ├── serato.png
│ │ │ ├── traktor.png
│ │ │ └── virtualdj.png
│ │ ├── bg.jpg
│ │ ├── favicon.png
│ │ ├── icon.png
│ │ ├── logo-full.png
│ │ ├── logo.svg
│ │ ├── placeholder.png
│ │ └── shazam_icon.svg
│ ├── components
│ │ ├── AddAlbumArt.vue
│ │ ├── AdvancedSettingsToggle.vue
│ │ ├── AutotaggerAdvanced.vue
│ │ ├── AutotaggerPlatformSpecific.vue
│ │ ├── AutotaggerPlatforms.vue
│ │ ├── AutotaggerProfile.vue
│ │ ├── AutotaggerTags.vue
│ │ ├── CliDialog.vue
│ │ ├── DJAppIcons.vue
│ │ ├── DevTools.vue
│ │ ├── ExitDialog.vue
│ │ ├── FolderBrowser.vue
│ │ ├── HelpButton.vue
│ │ ├── HelpRenamerExamples.vue
│ │ ├── Keybind.vue
│ │ ├── LogoText.vue
│ │ ├── ManualTag.vue
│ │ ├── PlatformsRepo.vue
│ │ ├── PlayerBar.vue
│ │ ├── PlaylistDropZone.vue
│ │ ├── QuickTagContextMenu.vue
│ │ ├── QuickTagFileBrowser.vue
│ │ ├── QuickTagGenreBar.vue
│ │ ├── QuickTagMoods.vue
│ │ ├── QuickTagRight.vue
│ │ ├── QuickTagTile.vue
│ │ ├── QuickTagTileThin.vue
│ │ ├── RenamerTokenName.vue
│ │ ├── Separators.vue
│ │ ├── Settings.vue
│ │ ├── SpotifyLogin.vue
│ │ ├── TagEditorAlbumArt.vue
│ │ ├── TagField.vue
│ │ ├── TagFields.vue
│ │ └── Waveform.vue
│ ├── main.ts
│ ├── scripts
│ │ ├── autotagger.ts
│ │ ├── manualtag.ts
│ │ ├── onetagger.ts
│ │ ├── player.ts
│ │ ├── quicktag.ts
│ │ ├── router.ts
│ │ ├── settings.ts
│ │ ├── tags.ts
│ │ └── utils.ts
│ ├── style
│ │ ├── app.scss
│ │ ├── fonts
│ │ │ ├── Dosis-Bold.ttf
│ │ │ ├── Dosis-ExtraBold.ttf
│ │ │ ├── Dosis-Light.ttf
│ │ │ ├── Dosis-Medium.ttf
│ │ │ ├── Dosis-Regular.ttf
│ │ │ ├── Dosis-SemiBold.ttf
│ │ │ ├── OFL.txt
│ │ │ └── wavefont.woff2
│ │ └── quasar.scss
│ ├── views
│ │ ├── AudioFeatures.vue
│ │ ├── Autotagger.vue
│ │ ├── AutotaggerStatus.vue
│ │ ├── Index.vue
│ │ ├── QuickTag.vue
│ │ ├── Renamer.vue
│ │ └── TagEditor.vue
│ └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
└── crates
├── onetagger-autotag
├── Cargo.toml
└── src
│ ├── audiofeatures.rs
│ ├── lib.rs
│ ├── platforms.rs
│ ├── repo.rs
│ └── shazam.rs
├── onetagger-cli
├── Cargo.toml
├── build.rs
└── src
│ └── main.rs
├── onetagger-platforms
├── Cargo.toml
├── assets
│ ├── bandcamp.png
│ ├── beatport.png
│ ├── beatsource.png
│ ├── bpmsupreme.png
│ ├── deezer.png
│ ├── discogs.png
│ ├── itunes.png
│ ├── junodownload.png
│ ├── musicbrainz.png
│ ├── musixmatch.png
│ ├── spotify.png
│ └── traxsource.png
└── src
│ ├── bandcamp.rs
│ ├── bandcamp_genres.rs
│ ├── beatport.rs
│ ├── beatsource.rs
│ ├── bpmsupreme.rs
│ ├── deezer.rs
│ ├── discogs.rs
│ ├── itunes.rs
│ ├── junodownload.rs
│ ├── lib.rs
│ ├── musicbrainz.rs
│ ├── musixmatch.rs
│ ├── spotify.rs
│ └── traxsource.rs
├── onetagger-player
├── Cargo.toml
└── src
│ ├── aiff.rs
│ ├── alac.rs
│ ├── flac.rs
│ ├── lib.rs
│ ├── mp3.rs
│ ├── mp4.rs
│ ├── ogg.rs
│ └── wav.rs
├── onetagger-playlist
├── Cargo.toml
└── src
│ └── lib.rs
├── onetagger-renamer
├── Cargo.toml
└── src
│ ├── ac.rs
│ ├── docs.rs
│ ├── lib.rs
│ └── parser.rs
├── onetagger-shared
├── Cargo.toml
├── build.rs
└── src
│ └── lib.rs
├── onetagger-tag
├── Cargo.toml
└── src
│ ├── flac.rs
│ ├── id3.rs
│ ├── lib.rs
│ ├── mp4.rs
│ ├── vorbis.rs
│ └── wav.rs
├── onetagger-tagger
├── Cargo.toml
└── src
│ ├── custom.rs
│ └── lib.rs
├── onetagger-ui
├── Cargo.toml
└── src
│ ├── browser.rs
│ ├── lib.rs
│ ├── quicktag.rs
│ ├── socket.rs
│ └── tageditor.rs
└── onetagger
├── Cargo.toml
├── build.rs
└── src
└── main.rs
/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | # Mac OS libraries
2 | [target.x86_64-apple-darwin]
3 | rustflags = ["-lz", "-lbz2", "-llzma", "-C", "link-args=-framework AudioUnit"]
4 |
5 | # Windows static build
6 | [target.stable-x86_64-pc-windows-msvc]
7 | rustflags = ["-Ctarget-feature=+crt-static"]
8 |
9 | # Use ldd for faster compile on Linux
10 | [target.x86_64-unknown-linux-gnu]
11 | rustflags = ["-C", "link-arg=-fuse-ld=lld"]
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: marekkon5
2 | patreon: onetagger
3 | custom: ["https://paypal.me/marekkon5"]
4 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build-linux:
6 | name: Linux
7 | runs-on: ubuntu-22.04
8 |
9 | steps:
10 |
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 |
14 | - name: Rust Cache
15 | uses: actions/cache@v3
16 | with:
17 | path: |
18 | ~/.cargo/bin/
19 | ~/.cargo/registry/index/
20 | ~/.cargo/registry/cache/
21 | ~/.cargo/git/db/
22 | target/
23 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
24 |
25 | - name: Install dependencies
26 | run: |
27 | sudo apt update
28 | sudo apt install -y lld autogen libasound2-dev pkg-config make libssl-dev gcc g++ curl wget git libwebkit2gtk-4.1-dev
29 |
30 | - name: Install NodeJS
31 | uses: actions/setup-node@v3
32 | with:
33 | node-version: 18
34 |
35 | - name: Install pnpm
36 | uses: pnpm/action-setup@v2
37 | with:
38 | version: 8
39 |
40 | - name: Build
41 | run: |
42 | cd client
43 | pnpm i
44 | pnpm run build
45 | cd ..
46 | cargo update
47 | cargo build --release
48 |
49 | - name: Bundle
50 | run: |
51 | tar zcf OneTagger-linux.tar.gz -C target/release onetagger
52 | tar zcf OneTagger-linux-cli.tar.gz -C target/release onetagger-cli
53 | mkdir dist
54 | mv OneTagger-linux.tar.gz dist/
55 | mv OneTagger-linux-cli.tar.gz dist/
56 |
57 | - name: Upload Linux
58 | uses: actions/upload-artifact@v4
59 | with:
60 | name: onetagger-linux
61 | path: dist/OneTagger-linux.tar.gz
62 |
63 | - name: Upload Linux CLI
64 | uses: actions/upload-artifact@v4
65 | with:
66 | name: onetagger-linux-cli
67 | path: dist/OneTagger-linux-cli.tar.gz
68 |
69 |
70 | build-win:
71 | name: Windows
72 | runs-on: windows-2019
73 |
74 | steps:
75 | - name: Checkout
76 | uses: actions/checkout@v3
77 |
78 | - name: Rust Cache
79 | uses: actions/cache@v3
80 | with:
81 | path: |
82 | ~/.cargo/bin/
83 | ~/.cargo/registry/index/
84 | ~/.cargo/registry/cache/
85 | ~/.cargo/git/db/
86 | target/
87 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
88 |
89 | - name: Install NodeJS
90 | uses: actions/setup-node@v3
91 | with:
92 | node-version: 18
93 |
94 | - name: Install pnpm
95 | uses: pnpm/action-setup@v2
96 | with:
97 | version: 8
98 |
99 | - name: Install Dependencies
100 | run: |
101 | choco install nsis -y
102 | rustup update
103 |
104 | - name: Build
105 | run: |
106 | cd client
107 | pnpm i
108 | pnpm run build
109 | cd ..
110 | cargo update
111 | cargo build --release
112 |
113 | - name: Bundle
114 | run: |
115 | mkdir dist
116 | powershell -command "(new-object System.Net.WebClient).DownloadFile('https://aka.ms/vs/16/release/vc_redist.x64.exe','vc_redist.x64.exe')"
117 | powershell -command "(new-object System.Net.WebClient).DownloadFile('https://go.microsoft.com/fwlink/p/?LinkId=2124703','MicrosoftEdgeWebview2Setup.exe')"
118 | &'C:\Program Files (x86)\NSIS\makensis.exe' 'assets\installer.nsi'
119 | copy target\release\onetagger.exe dist\OneTagger-windows.exe
120 | copy target\release\onetagger-cli.exe dist\OneTagger-windows-cli.exe
121 |
122 | - name: Upload Archive
123 | uses: actions/upload-artifact@v4
124 | with:
125 | name: onetagger-win
126 | path: |
127 | dist/OneTagger-windows.exe
128 |
129 | - name: Upload Archive
130 | uses: actions/upload-artifact@v4
131 | with:
132 | name: onetagger-win-setup
133 | path: dist/OneTagger-windows-setup.exe
134 |
135 | - name: Upload CLI
136 | uses: actions/upload-artifact@v4
137 | with:
138 | name: onetagger-win-cli
139 | path: |
140 | dist/OneTagger-windows-cli.exe
141 |
142 | build-mac:
143 | name: Mac
144 | runs-on: macos-13
145 |
146 | steps:
147 |
148 | - name: Checkout
149 | uses: actions/checkout@v3
150 |
151 | - name: Rust Cache
152 | uses: actions/cache@v3
153 | with:
154 | path: |
155 | ~/.cargo/bin/
156 | ~/.cargo/registry/index/
157 | ~/.cargo/registry/cache/
158 | ~/.cargo/git/db/
159 | target/
160 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
161 |
162 | - name: Install dependencies
163 | run: |
164 | brew install rustup nodejs pnpm
165 | rustup install stable
166 | cargo install cargo-bundle || true
167 |
168 | - name: Build
169 | run: |
170 | cd client
171 | pnpm i
172 | pnpm run build
173 | cd ..
174 | cargo update
175 | cargo build --release
176 | cargo bundle --release
177 |
178 | - name: Bundle
179 | run: |
180 | mkdir dist
181 |
182 | cd target/release/bundle/osx
183 | chmod +x OneTagger.app/Contents/MacOS/onetagger
184 | zip -r OneTagger-mac.zip .
185 | cd -
186 |
187 | cd target/release
188 | chmod +x onetagger-cli
189 | zip OneTagger-mac-cli.zip onetagger-cli
190 | cd -
191 |
192 | mv target/release/bundle/osx/OneTagger-mac.zip dist/
193 | mv target/release/OneTagger-mac-cli.zip dist/
194 |
195 | - name: Upload Mac
196 | uses: actions/upload-artifact@v4
197 | with:
198 | name: onetagger-mac
199 | path: dist/OneTagger-mac.zip
200 |
201 | - name: Upload Mac CLI
202 | uses: actions/upload-artifact@v4
203 | with:
204 | name: onetagger-mac-cli
205 | path: dist/OneTagger-mac-cli.zip
206 |
207 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | sample/
3 | GPUCache/
4 | *.log
5 | client/node_modules/
6 | client/dist/
7 | .idea/
8 | .spotify*
9 | dist/
10 | osxcross/
11 | .vscode/
12 | test/
13 | vc_redist.x64.exe
14 | assets/vc_redist.x64.exe
15 | *.dll
16 |
17 | perf.data
18 | perf.data.old
19 | flamegraph.svg
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.7.0
2 | **(03.08.2023)**
3 |
4 | ### Auto Tag:
5 | - Updated Beatport to support the new site
6 | - Improved match rates
7 | - Selectable overwrite
8 | - Internal refactor to how tags work
9 | - Explicit tag
10 | - BPMSupreme > Latino
11 |
12 | ### Quick Tag:
13 | - **Multiple files mode**
14 | - Thin view mode
15 | - External player support
16 |
17 | ### Renamer:
18 | - Fixed input updating
19 | - BPM in MP4
20 |
21 | ### Other:
22 | - Various bug fixes and improvements in all sections
23 |
24 |
25 | # 1.6.0
26 | **(14.04.2023)**
27 |
28 | ### OneTagger:
29 | - Support for WAV (ID3) and OGG files
30 |
31 | ### Auto Tag:
32 | - **Android version**
33 | - Added Bandcamp
34 | - Added Musixmatch
35 | - Added Deezer
36 | - Lyrics support
37 | - Logging in custom platforms
38 | - Stop button
39 | - Better platform info (supported tags, whether auth is required)
40 | - Profiles (multiple configurations)
41 |
42 | ### Other
43 | - Many bug fixes in all sections
44 | - Dropped dependency on libsndfile, so compilling 1T should be less painful and more portable
45 |
46 |
47 | # 1.5.1
48 | **(31.10.2022)**
49 |
50 | ### OneTagger:
51 | - Older MacOS warning and server version restart
52 |
53 | ### Auto Tag:
54 | - HOTFIX: Make sure the Move files feature is more safe
55 |
56 |
57 | # 1.5.0
58 | **(14.10.2022)**
59 |
60 | ### OneTagger:
61 | - **Rewrite entire frontend to Vue3 + Typescript**
62 |
63 | ### Auto Tag:
64 | - Only write year
65 | - Track/disc number/total tags
66 | - BPMSupreme, iTunes improvements
67 | - Tag each track using multiple platforms
68 | - Merging styles and genres fixes
69 | - Regex title cleanup
70 | - Move failed/successful files
71 |
72 | ### Audio Features:
73 | - Spofiy rate limits
74 | - Bug fixes
75 |
76 | ### Quick Tag:
77 | - List of failed files with reasons
78 |
79 |
80 | # 1.4.0
81 | **(20.04.2022)**
82 |
83 | ### OneTagger:
84 | - Added Auto Rename tab (rename your files by to your tags)
85 | - CLI version
86 |
87 | ### Auto Tag:
88 | - Internal platform system redesign
89 | - Custom platforms support (https://github.com/Marekkon5/onetagger-platform-template/)
90 | - Update Spotify implementation
91 | - Improved match rates, less skipped files
92 | - Various minor platform fixes
93 | - Added reason of failure into status list
94 |
95 | ### Quick Tag:
96 | - Redesign
97 | - Sorting
98 | - Multiple genres
99 | - File browser
100 | - Subgenres
101 |
102 |
103 | ### Other:
104 | - Windows install to registry
105 | - New logging system
106 | - Split to bunch of different crates
107 | - Minor improvements and bug fixes
108 |
109 |
110 | # 1.3.0:
111 | **(18.11.2021)**
112 |
113 | ### Auto Tag:
114 | - Match by exact ID for Discogs, Beatport
115 | - Filename template fixes
116 | - Duration tag
117 | - `VINYLTRACK` Tag for Discogs
118 | - Discogs now faster for smaller batches
119 | - Album artist tag
120 | - iTunes, Musicbrainz, Beatsource and Spotify support
121 | - Beatport subgenres, more tags
122 | - Meta tag
123 | - Remixer tag
124 | - Track number tag
125 | - ISRC tag
126 | - Shazam to find songs without tag and filename parsing
127 | - Filter in status page
128 |
129 | ### Audio Features:
130 | - Added popularity tag
131 | - Renamed danceability value to `dance-high, dance-med, dance-low`
132 |
133 | ### Quick Tag:
134 | - Internal rewrite, cleaner code, more stable
135 | - Search and filter
136 |
137 | ### Tag Editor:
138 | - CTRL + S
139 | - Filtering
140 | - Refactored some code
141 |
142 | ### Other:
143 | - General UI improvements
144 | - Windows: Replace CEF with webview2 - smaller install sizes, more portable.
145 | - `--expose` command line option to make the server listen on 0.0.0.0
146 | - Updated dependencies
147 | - Bug fixes
148 | - .mp4 Extension support
149 |
150 |
151 | # 1.2.1:
152 | **(03.07.2021)**
153 |
154 | ### Auto Tagger
155 | - Fixed bug in 1.2.0 causing Beatport and Traxsource having low match rate
156 |
157 | ### Other
158 | - Added more info to logs for debugging
159 | - Fixed path pickers not opening with bad path
160 |
161 |
162 | # 1.2.0
163 | **(02.07.2021)**
164 |
165 | ### Shared:
166 | - Added M3U playlist support with drag and drop
167 |
168 | ### Auto Tag:
169 | - Added catalog number, track ID, release ID, version, URL tags
170 | - Added duration matching (WARNING: strict, should be used only in specific situations)
171 | - Tag files without metadata (using filename with custom templates)
172 | - Single page design changes
173 | - Improved matching rates, bug fixes
174 |
175 | ### Quick Tag:
176 | - Option to load recursively
177 |
178 | ### Tag Editor:
179 | - Minor design changes
180 |
181 | ### Other
182 | - Benchmark mode (for testing / debugging purproses, can be ran with `--benchmark` command line argument)
183 | - If you specify path as command line argument, it will be automatically prefilled.
184 |
185 |
186 |
187 | # 1.1.0
188 | **(31.05.2021)**
189 |
190 | ### Shared:
191 | - Added MP4/M4A support
192 |
193 | ### Auto Tag:
194 | - Redesign
195 | - Better status page
196 | - Single page setup (enable in settings)
197 | - Camelot key notation
198 | - Juno Download is single thread now
199 | - Bug fixes and improvements related to matching
200 |
201 | ### Audio Features:
202 | - Fix searching in some edge cases
203 | - Cache Spotify token
204 |
205 | ### Quick Tag:
206 | - Player UI improvements
207 | - Autosave, Autoplay (can be enabled in settings)
208 | - Bug fixes
209 |
210 | ## **WARNING: Due to many breaking changes, SETTINGS WILL BE RESET TO DEFAULT. This will hopefully not happen in future again. Sorry for the inconvenience.**
211 |
212 |
213 |
214 | # 1.0.0
215 | **(13.05.2021)**
216 |
217 | First public release
218 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = [
4 | "crates/onetagger",
5 | "crates/onetagger-ui",
6 | "crates/onetagger-cli",
7 | "crates/onetagger-tag",
8 | "crates/onetagger-shared",
9 | "crates/onetagger-player",
10 | "crates/onetagger-tagger",
11 | "crates/onetagger-renamer",
12 | "crates/onetagger-autotag",
13 | "crates/onetagger-playlist",
14 | "crates/onetagger-platforms"
15 | ]
16 |
17 | # Workaround for MacOS
18 | [workspace.dependencies]
19 | lzma-sys = { version = "*", features = ["static"] }
20 |
21 | [profile.release]
22 | opt-level = 3
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The ultimate cross-platform tagger for DJs
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Cross-platform music tagger.
21 | It can fetch metadata from Beatport, Traxsource, Juno Download, Discogs, Musicbrainz and Spotify.
22 | It is also able to fetch Spotify's Audio Features based on ISRC & exact match.
23 | There is a manual tag editor and quick tag editor which lets you use keyboard shortcuts. Written in Rust, Vue.js and Quasar.
24 |
25 | MP3, AIFF, FLAC, M4A (AAC, ALAC) supported.
26 |
27 | *For more info and tutorials check out our [website](https://onetagger.github.io/).*
28 |
29 | https://user-images.githubusercontent.com/15169286/193469224-cbf3af71-f6d7-4ecd-bdbf-5a1dca2d99c8.mp4
30 |
31 |
32 | ## Installing
33 |
34 | You can download latest binaries from [releases](https://github.com/Marekkon5/onetagger/releases)
35 |
36 |
37 | ## Credits
38 | Bas Curtiz - UI, Idea, Help
39 | SongRec (Shazam support) - https://github.com/marin-m/SongRec
40 |
41 | ## Support
42 | You can support this project by donating on [PayPal](https://paypal.me/marekkon5) or [Patreon](https://www.patreon.com/onetagger)
43 |
44 | ## Compilling
45 |
46 | ### Linux & Mac
47 | Install dependencies: [rustup](https://rustup.rs), [node](https://nodejs.org/en/download/package-manager/), [pnpm](https://pnpm.io/installation)
48 |
49 | **Install remaining dependencies**
50 | ```
51 | sudo apt install -y lld autogen libasound2-dev pkg-config make libssl-dev gcc g++ curl wget git libwebkit2gtk-4.1-dev
52 | ```
53 |
54 | **Compile UI**
55 | ```
56 | cd client
57 | pnpm i
58 | pnpm run build
59 | cd ..
60 | ```
61 |
62 | **Compile**
63 | ```
64 | cargo build --release
65 | ```
66 | Output will be in: `target/release/onetagger`
67 |
68 |
69 | ### Windows
70 | You need to install dependencies: [rustup](https://rustup.rs), [nodejs](https://nodejs.org/en/download/), [Visual Studio 2019 Build Tools](https://aka.ms/vs/16/release/vs_buildtools.exe), [pnpm](https://pnpm.io/installation)
71 |
72 | **Compile UI:**
73 | ```
74 | cd client
75 | pnpm i
76 | pnpm run build
77 | cd ..
78 | ```
79 |
80 | **Compile OneTagger:**
81 | ```
82 | cargo build --release
83 | ```
84 |
85 | Output will be inside `target\release` folder.
86 |
87 |
--------------------------------------------------------------------------------
/assets/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/128x128.png
--------------------------------------------------------------------------------
/assets/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/128x128@2x.png
--------------------------------------------------------------------------------
/assets/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/32x32.png
--------------------------------------------------------------------------------
/assets/64x64.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/64x64.bin
--------------------------------------------------------------------------------
/assets/headerimage.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/headerimage.bmp
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/icon.ico
--------------------------------------------------------------------------------
/assets/installer-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/installer-icon.ico
--------------------------------------------------------------------------------
/assets/installer.nsi:
--------------------------------------------------------------------------------
1 | ; Source: https://gist.github.com/drewchapin/246de6d0c404a79ee66a5ead35b480bc
2 |
3 | ;-------------------------------------------------------------------------------
4 | ; Includes
5 | !include "MUI2.nsh"
6 | !include "LogicLib.nsh"
7 | !include "WinVer.nsh"
8 | !include "x64.nsh"
9 |
10 | ;-------------------------------------------------------------------------------
11 | ; Constants
12 | !define PRODUCT_NAME "One Tagger"
13 | !define PRODUCT_DESCRIPTION "App to tag your music library."
14 | !define COPYRIGHT "Marekkon5"
15 | !define PRODUCT_VERSION "1.0.0.0"
16 | !define SETUP_VERSION 1.0.0.0
17 |
18 | ;-------------------------------------------------------------------------------
19 | ; Attributes
20 | Name "One Tagger"
21 | OutFile "..\dist\OneTagger-windows-setup.exe"
22 | InstallDir "$PROGRAMFILES\OneTagger"
23 | RequestExecutionLevel admin ; user|highest|admin
24 | SetCompressor /SOLID lzma
25 |
26 | ;-------------------------------------------------------------------------------
27 | ; Version Info
28 | VIProductVersion "${PRODUCT_VERSION}"
29 | VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
30 | VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}"
31 | VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}"
32 | VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
33 | VIAddVersionKey "FileVersion" "${SETUP_VERSION}"
34 |
35 | ;-------------------------------------------------------------------------------
36 | ; Modern UI Appearance
37 | !define MUI_ICON "..\assets\installer-icon.ico"
38 | !define MUI_HEADERIMAGE
39 | !define MUI_HEADERIMAGE_BITMAP "..\assets\headerimage.bmp"
40 | !define MUI_WELCOMEFINISHPAGE_BITMAP "..\assets\welcomebanner.bmp"
41 | !define MUI_FINISHPAGE_NOAUTOCLOSE
42 |
43 | ; Modern UI Desktop Shortcut
44 | !define MUI_FINISHPAGE_SHOWREADME ""
45 | !define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED
46 | !define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut"
47 | !define MUI_FINISHPAGE_SHOWREADME_FUNCTION desktopshortcut
48 |
49 | ;-------------------------------------------------------------------------------
50 | ; Installer Pages
51 | !insertmacro MUI_PAGE_WELCOME
52 | ;!insertmacro MUI_PAGE_LICENSE "${NSISDIR}\Docs\Modern UI\License.txt"
53 | ;!insertmacro MUI_PAGE_COMPONENTS
54 | !insertmacro MUI_PAGE_DIRECTORY
55 | !insertmacro MUI_PAGE_INSTFILES
56 | !insertmacro MUI_PAGE_FINISH
57 |
58 | ;-------------------------------------------------------------------------------
59 | ; Uninstaller Pages
60 | !insertmacro MUI_UNPAGE_WELCOME
61 | !insertmacro MUI_UNPAGE_CONFIRM
62 | !insertmacro MUI_UNPAGE_INSTFILES
63 | !insertmacro MUI_UNPAGE_FINISH
64 |
65 | ;-------------------------------------------------------------------------------
66 | ; Languages
67 | !insertmacro MUI_LANGUAGE "English"
68 |
69 | ;-------------------------------------------------------------------------------
70 | ; Installer Sections
71 | Section "One Tagger" OneTagger
72 | ; Clean old
73 | ExecWait "taskkill /f /im onetagger.exe"
74 | RMDir /r "$INSTDIR\*"
75 | ; Copy new
76 | SetOutPath $INSTDIR
77 | File "..\target\release\onetagger.exe"
78 | File "..\assets\icon.ico"
79 | File "..\vc_redist.x64.exe"
80 | File "..\MicrosoftEdgeWebview2Setup.exe"
81 | ; Uninstaller
82 | WriteUninstaller "$INSTDIR\Uninstall.exe"
83 | CreateDirectory "$SMPROGRAMS\OneTagger"
84 | CreateShortcut "$SMPROGRAMS\OneTagger\${PRODUCT_NAME}.lnk" "$INSTDIR\onetagger.exe" "" "$INSTDIR\icon.ico"
85 | ; Registry
86 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
87 | "DisplayName" "${PRODUCT_NAME}"
88 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
89 | "UninstallString" "$INSTDIR\Uninstall.exe"
90 | WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
91 | "EstimatedSize" "70656"
92 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
93 | "DisplayIcon" "$INSTDIR\icon.ico"
94 | WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \
95 | "InstallLocation" "$INSTDIR"
96 | ; Dependencies
97 | ExecWait '"$INSTDIR\vc_redist.x64.exe" /install /quiet /norestart'
98 | ExecWait '"$INSTDIR\MicrosoftEdgeWebview2Setup.exe'
99 | Delete "$INSTDIR\vc_redist.x64.exe"
100 | Delete "$INSTDIR\MicrosoftEdgeWebview2Setup.exe"
101 | SectionEnd
102 |
103 | ;-------------------------------------------------------------------------------
104 | ; Uninstaller Sections
105 | Section "Uninstall"
106 | Delete "$SMPROGRAMS\OneTagger\${PRODUCT_NAME}.lnk"
107 | Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
108 | RMDir "$SMPROGRAMS\OneTagger"
109 | Delete "$INSTDIR\*"
110 | RMDir /r "$INSTDIR\*"
111 | RMDir "$INSTDIR"
112 | SectionEnd
113 |
114 | ;------
115 | ; Desktop icon
116 | Function desktopshortcut
117 | CreateShortcut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\onetagger.exe" "" "$INSTDIR\icon.ico"
118 | FunctionEnd
119 |
--------------------------------------------------------------------------------
/assets/mac-cross.toml:
--------------------------------------------------------------------------------
1 | [target.x86_64-apple-darwin]
2 | linker = "x86_64-apple-darwin14-clang"
3 | ar = "x86_64-apple-darwin14-ar"
4 | rustflags = ["-lz", "-lbz2", "-llzma", "-l", "framework=WebKit", "-C", "link-args=-framework AudioUnit"]
5 |
--------------------------------------------------------------------------------
/assets/onetagger-logo-github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/onetagger-logo-github.png
--------------------------------------------------------------------------------
/assets/spotify_callback.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Spotify authorized successfully, you can close this window.
5 |
6 |
--------------------------------------------------------------------------------
/assets/welcomebanner.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/assets/welcomebanner.bmp
--------------------------------------------------------------------------------
/client/.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 |
26 | pnpm-lock.yaml
27 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | One Tagger
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vue-tsc --noEmit && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@quasar/extras": "1.16.16",
13 | "axios": "1.7.9",
14 | "compare-versions": "6.1.1",
15 | "quasar": "2.17.7",
16 | "vue": "3.5.13",
17 | "vue-router": "4.5.0",
18 | "vuedraggable": "4.1.0"
19 | },
20 | "devDependencies": {
21 | "@quasar/vite-plugin": "1.9.0",
22 | "@vitejs/plugin-vue": "5.2.1",
23 | "path": "^0.12.7",
24 | "sass": "1.83.4",
25 | "typescript": "5.7.3",
26 | "vite": "6.0.11",
27 | "vue-tsc": "2.2.0"
28 | }
29 | }
--------------------------------------------------------------------------------
/client/src/assets/apps/crossdj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/crossdj.png
--------------------------------------------------------------------------------
/client/src/assets/apps/dex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/dex.png
--------------------------------------------------------------------------------
/client/src/assets/apps/djaypro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/djaypro.png
--------------------------------------------------------------------------------
/client/src/assets/apps/djuced.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/djuced.png
--------------------------------------------------------------------------------
/client/src/assets/apps/engineprime.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/engineprime.png
--------------------------------------------------------------------------------
/client/src/assets/apps/mixxx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/mixxx.png
--------------------------------------------------------------------------------
/client/src/assets/apps/rekordbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/rekordbox.png
--------------------------------------------------------------------------------
/client/src/assets/apps/serato.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/serato.png
--------------------------------------------------------------------------------
/client/src/assets/apps/traktor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/traktor.png
--------------------------------------------------------------------------------
/client/src/assets/apps/virtualdj.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/apps/virtualdj.png
--------------------------------------------------------------------------------
/client/src/assets/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/bg.jpg
--------------------------------------------------------------------------------
/client/src/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/favicon.png
--------------------------------------------------------------------------------
/client/src/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/icon.png
--------------------------------------------------------------------------------
/client/src/assets/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/logo-full.png
--------------------------------------------------------------------------------
/client/src/assets/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/assets/placeholder.png
--------------------------------------------------------------------------------
/client/src/assets/shazam_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/src/components/AddAlbumArt.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Add new album art
5 |
6 |
7 |
15 |
22 |
30 |
31 | Drag & drop image here
32 |
33 |
34 |
35 |
36 | Cancel
37 |
38 |
39 | Add
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/client/src/components/AdvancedSettingsToggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{label}}
6 |
7 |
8 | {{tooltip}}
9 |
10 |
11 |
12 |
13 |
14 | emit("update:modelValue", v)'>
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/src/components/AutotaggerPlatformSpecific.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{platform.name}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{option.label}}: {{$1t.config.value.custom[platform.id][option.id]}}
15 |
16 | {{option.tooltip}}
17 |
18 |
19 |
20 |
29 |
30 |
31 |
32 |
33 |
34 |
{{option.label}}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{option.tooltip}}
42 |
43 |
52 |
53 |
54 |
55 |
66 |
67 |
68 |
77 |
78 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | No platform specific settings available for the selected platform(s)
96 |
97 |
98 |
99 |
100 |
101 |
Spotify
102 |
103 |
104 |
105 |
106 |
You are successfully logged in to Spotify
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/client/src/components/AutotaggerProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/client/src/components/AutotaggerTags.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
SELECT INPUT
8 |
Drag & drop folder, copy/paste path directly orCLICK the browse icon
9 |
23 |
24 |
25 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
SELECT TAGS
46 |
Check the box to fetch stated tag
47 |
48 |
68 |
69 |
70 |
71 | Enable All
72 | Disable All
73 | Toggle
74 |
75 |
76 |
77 |
78 |
79 |
126 |
127 |
--------------------------------------------------------------------------------
/client/src/components/CliDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Command line version
5 |
6 |
7 | 1. Copy and save this config into config.json
8 |
9 | {{JSON.stringify(config, null, 2)}}
10 |
11 |
12 |
13 | 2. Start onetagger-cli
14 |
15 | {{bin}} {{command}} --config config.json --path {{config.path}} {{extra}}
16 |
17 |
18 |
19 |
20 |
21 |
39 |
40 |
--------------------------------------------------------------------------------
/client/src/components/DJAppIcons.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
47 |
48 |
--------------------------------------------------------------------------------
/client/src/components/DevTools.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DEV TOOLS
7 |
8 |
9 |
10 |
11 | Version: {{ $1t.info.value.version }}
12 | Commit: {{ $1t.info.value.commit }}
13 | OS: {{ $1t.info.value.os }}
14 | Custom Platforms: {{ $1t.info.value.platforms.filter(p => !p.builtIn).map(p => p.platform.id) }}
15 | Data Directory: {{ $1t.info.value.dataDir }}
16 | Working Directory: {{ $1t.info.value.workDir }}
17 | Start Context: {{ $1t.info.value.startContext }}
18 | Custom Platforms Compatibility: {{ $1t.info.value.customPlatformCompat }}
19 |
20 | {$1t.settings.value.devtools = v; $1t.saveSettings(true)}" :model-value='$1t.settings.value.devtools' label='Enable webview devtools (requires restart)'>
21 |
22 |
23 |
24 |
25 | Reload
26 | Go to dev server
27 | Open webview devtools
28 | Open data dir
29 |
30 |
31 |
32 |
33 | Log:
34 |
35 |
36 |
37 | {{ item.time }}
38 | {{ item.level }}
39 | {{ item.module }}
40 | x{{ item.count }}
41 |
42 | {{ item.line }}
43 |
44 |
45 |
46 |
47 |
SHOW FULL
48 |
TO BOTTOM
49 |
50 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
171 |
172 |
--------------------------------------------------------------------------------
/client/src/components/ExitDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Unsaved changes
7 | Tagging in progress
8 |
9 |
10 |
11 | Do you want to save pending changes before exitting?
12 | Tagging is in progress, do you really want to exit?
13 |
14 |
15 |
16 |
17 | Cancel
18 |
19 | Exit without saving
20 | Exit anyway
21 |
22 | Save and exit
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/client/src/components/FolderBrowser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Select folder
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
{{prop.node.label}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Cancel
24 | OK
25 |
26 |
27 |
28 |
29 |
30 |
31 |
155 |
156 |
--------------------------------------------------------------------------------
/client/src/components/HelpRenamerExamples.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
% artist % - % title %
6 |
% track % . % artist % - % title %
7 |
% artist % - % title % - % bpm % - % key %
8 |
% artist % - % album % / % track % - % title %
9 |
% year % - % album % / % track % - % artist % - % title %
10 |
11 |
--------------------------------------------------------------------------------
/client/src/components/Keybind.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Set keybind
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Reset
27 | Cancel
28 | Save
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
115 |
116 |
--------------------------------------------------------------------------------
/client/src/components/PlatformsRepo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | CUSTOM PLATFORMS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{platform.name}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | This platform allows up to {{platform.maxThreads}} concurrent searches
29 | This platform allows unlimited concurrent searches
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Platform requires an account
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Author: {{ platform.author }}
49 |
50 | {{platform.id}}@{{platform.version}}
51 |
52 |
53 | Platform potentially incompatible!
54 | Platform is incompatible!
55 |
56 |
57 |
58 |
59 |
60 | {selectedPlatform = platform; downloadDialog = true}'>
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | INSTALL {{ selectedPlatform.name.toUpperCase() }}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
Version: {{ version }} ,
88 | Compatibility: {{ compat }}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/client/src/components/PlayerBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{ $1t.player.value.title }}
27 |
28 |
29 |
30 |
31 |
32 | {{ $1t.player.value.artists.join(', ') }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
81 |
82 |
83 |
84 | $1t.player.value.setVolume(v)"
90 | @change="$1t.saveSettings(false)"
91 | style="margin-top: 6px"
92 | >
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/client/src/components/PlaylistDropZone.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
14 |
15 | Drag & drop M3U Playlist file
16 |
17 |
18 |
19 | {{filename}}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
36 |
37 | Drag & drop playlist here / click to remove it, or export playlist from selected / filtered tracks
38 |
39 |
40 |
41 |
42 |
43 |
44 |
98 |
99 |
--------------------------------------------------------------------------------
/client/src/components/QuickTagContextMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Manual Tag
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Edit tags
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Delete
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
86 |
87 |
--------------------------------------------------------------------------------
/client/src/components/QuickTagFileBrowser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{path}}
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Parent folder
29 |
30 |
31 |
32 |
33 |
38 |
39 |
40 |
41 | {{file.filename}}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/client/src/components/QuickTagGenreBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 | {{genre.genre}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
{{subgenre}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
61 |
62 |
--------------------------------------------------------------------------------
/client/src/components/QuickTagMoods.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
40 |
41 |
--------------------------------------------------------------------------------
/client/src/components/QuickTagRight.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Add custom note
8 |
9 |
10 |
11 |
12 |
20 |
21 |
22 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 | MANUAL TAG
51 |
52 |
53 |
54 |
55 |
56 |
99 |
100 |
--------------------------------------------------------------------------------
/client/src/components/QuickTagTileThin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ track.title }}
6 | {{ track.artists.join(", ") }}
7 |
8 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
39 | {{genre}},
40 |
41 |
42 | {{track.genres.join(', ')}}
43 |
44 |
45 |
46 |
51 |
52 |
53 |
66 |
67 |
68 |
69 |
70 |
123 |
124 |
--------------------------------------------------------------------------------
/client/src/components/RenamerTokenName.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{token.name}}
5 | (
6 | {{param.name}}
9 | : {{param.type}}
10 | ,
11 | )
12 |
13 |
14 |
15 |
16 |
23 |
24 |
--------------------------------------------------------------------------------
/client/src/components/Separators.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
18 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/client/src/components/SpotifyLogin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1. Open
5 | Spotify Developer account & create an app
6 | 2. In settings set the Callback URL to: {{redirectUrl}}
7 | 3. Enter your Client ID & Client Secret below &CLICK login
8 | video demo
9 |
10 |
11 |
16 |
17 |
18 |
19 |
37 |
38 |
--------------------------------------------------------------------------------
/client/src/components/TagEditorAlbumArt.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
14 |
15 | Drop the image to replace
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
{{image.kind}}
24 |
{{image.description}}
25 |
{{image.mime}} {{image.width}}x{{image.height}}
26 |
Remove
27 |
28 |
29 |
30 |
88 |
89 |
--------------------------------------------------------------------------------
/client/src/components/TagField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/src/components/TagFields.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
21 |
22 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/client/src/components/Waveform.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{time}}
5 |
6 |
14 |
15 | {{waveChar(wave)}}
16 |
17 |
18 |
19 |
{{duration($1t.player.value.duration)}}
20 |
21 |
22 |
23 |
24 |
25 |
94 |
95 |
--------------------------------------------------------------------------------
/client/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import { Quasar, Dialog, Notify } from 'quasar';
3 | import { get1t } from './scripts/onetagger';
4 | import router from './scripts/router';
5 | import iconSet from 'quasar/icon-set/mdi-v6';
6 |
7 | // Style
8 | import '@quasar/extras/mdi-v6/mdi-v6.css';
9 | import 'quasar/src/css/index.sass';
10 | import './style/app.scss';
11 |
12 | import App from './App.vue';
13 |
14 |
15 | // Handle WebView events
16 | // @ts-ignore
17 | window.onWebviewEvent = (e) => {
18 | get1t().onOSMessage(e);
19 | }
20 |
21 |
22 | createApp(App)
23 | .use(router)
24 | .use(Quasar, {
25 | plugins: {
26 | Dialog, Notify
27 | },
28 | iconSet
29 | })
30 | .mount('#app');
31 |
--------------------------------------------------------------------------------
/client/src/scripts/manualtag.ts:
--------------------------------------------------------------------------------
1 | import { AutotaggerConfig, Track } from "./autotagger";
2 | import { get1t } from "./onetagger";
3 | import { wsUrl } from "./utils";
4 |
5 | class ManualTag {
6 |
7 | ws?: WebSocket;
8 | busy = false;
9 | done = false;
10 | matches: TrackMatch[] = [];
11 | errors: ManualTagError[] = [];
12 |
13 |
14 | _resolveSaving?: Function;
15 |
16 | constructor() {}
17 |
18 | /// Reset current state
19 | reset() {
20 | if (this.ws) {
21 | this.ws.close();
22 | this.ws = undefined;
23 | }
24 | this.matches = [];
25 | this.errors = [];
26 | this.busy = false;
27 | this.done = false;
28 | }
29 |
30 | /// Start tagging a track
31 | tagTrack(path: string, config: AutotaggerConfig) {
32 | this.reset();
33 | this.busy = true;
34 |
35 | // Open new WS connection because separate thread
36 | this.ws = new WebSocket(wsUrl());
37 | this.ws.addEventListener('message', (ev) => {
38 | this.onWsMessage(JSON.parse(ev.data));
39 | });
40 | this.ws.addEventListener('open', () => {
41 | this.ws!.send(JSON.stringify({
42 | action: 'manualTag',
43 | config: config,
44 | path
45 | }))
46 | });
47 | }
48 |
49 | /// Apply matches
50 | async apply(matches: TrackMatch[], path: string, config: AutotaggerConfig) {
51 | // Send to socket and wait for response
52 | const $1t = get1t();
53 | let promise = new Promise((res, rej) => this._resolveSaving = res);
54 | $1t.send('manualTagApply', { matches, path, config });
55 | let r = await promise;
56 | this._resolveSaving = undefined;
57 | return r;
58 | }
59 |
60 | /// WebSocket message handler
61 | onWsMessage(json: any) {
62 | switch (json.action) {
63 | // New result
64 | case 'manualTag':
65 | switch (json.status) {
66 | case 'ok':
67 | this.addMatches(json.matches, json.platform);
68 | break;
69 | case 'error':
70 | this.errors.push({ platform: json.platform, error: json.error });
71 | break;
72 | }
73 | break;
74 |
75 | // Finished
76 | case 'manualTagDone':
77 | this.busy = false;
78 | this.done = true;
79 | this.ws?.close();
80 | this.ws = undefined;
81 | break;
82 | }
83 | }
84 |
85 | /// Add new matches to array
86 | addMatches(matches: TrackMatch[], platform: string) {
87 | this.matches.push(...matches.map((m) => {
88 | m.track.platform = platform;
89 | return m;
90 | }));
91 | this.matches.sort((a, b) => b.accuracy - a.accuracy);
92 | }
93 |
94 | }
95 |
96 | /// Matched track
97 | interface TrackMatch {
98 | accuracy: number;
99 | track: Track;
100 | reason: string;
101 | }
102 |
103 | interface ManualTagError {
104 | platform: string;
105 | error: string;
106 | }
107 |
108 | export type { TrackMatch };
109 | export { ManualTag };
110 |
--------------------------------------------------------------------------------
/client/src/scripts/router.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 |
3 | import Index from '../views/Index.vue';
4 | import TagEditor from '../views/TagEditor.vue';
5 | import Renamer from '../views/Renamer.vue';
6 |
7 | // Required for hot reload, idk why it broke
8 | const AutotaggerStatus = () => import('../views/AutotaggerStatus.vue');
9 | const Autotagger = () => import('../views/Autotagger.vue');
10 | const QuickTag = () => import('../views/QuickTag.vue');
11 | const AudioFeatures = () => import('../views/AudioFeatures.vue');
12 |
13 | const history = createWebHashHistory();
14 |
15 | const routes = [
16 | {
17 | path: '/',
18 | component: Index
19 | },
20 | {
21 | path: '/autotagger',
22 | component: Autotagger
23 | },
24 | {
25 | path: '/autotagger/status',
26 | component: AutotaggerStatus
27 | },
28 | {
29 | path: '/quicktag',
30 | component: QuickTag
31 | },
32 | {
33 | path: '/audiofeatures',
34 | component: AudioFeatures
35 | },
36 | {
37 | path: '/audiofeatures/status',
38 | component: AutotaggerStatus
39 | },
40 | {
41 | path: '/tageditor',
42 | component: TagEditor
43 | },
44 | {
45 | path: '/renamer',
46 | component: Renamer
47 | }
48 | ];
49 |
50 | const router = createRouter({
51 | history,
52 | routes
53 | });
54 |
55 | export default router;
56 |
--------------------------------------------------------------------------------
/client/src/scripts/tags.ts:
--------------------------------------------------------------------------------
1 | const ABSTRACTIONS: Record = {
2 | "TIT2": "Title",
3 | "TCON": "Genre",
4 | "TALB": "Album",
5 | "TPE2": "Album Artist",
6 | "TCOM": "Composer",
7 | "TEXT": "Lyricist",
8 | "TIT3": "Mix Name",
9 | "TOPE": "Original Artist",
10 | "TIT1": "Content Group",
11 | "GRP1": "Grouping iTunes",
12 | "TPUB": "Label",
13 | "TPE4": "Remixer",
14 | "IPLS": "Producer ID3v2.3",
15 | "TIPL": "Producer ID3v2.4",
16 | "TPE3": "Conductor",
17 | "TBPM": "BPM",
18 | "TCOP": "Copyright",
19 | "TDAT": "Release Date",
20 | "TDOR": "Original Year",
21 | "TDRL": "Releasetime",
22 | "TKEY": "Key",
23 | "TLEN": "Length",
24 | "TMOO": "Mood",
25 | "TPE1": "Artist",
26 | "TPOS": "Discnumber",
27 | "TRCK": "Tracknumber",
28 | "TSRC": "ISRC",
29 | "TYER": "Year",
30 | "TDRC": "Date Recorded",
31 | "©alb": "Album",
32 | "aART": "Album Artist",
33 | "tmpo": "BPM",
34 | "©cmt": "Comment",
35 | "cpil": "Compilation iTunes",
36 | "©wrt": "Composer",
37 | "cond": "Conductor",
38 | "cprt": "Copyright",
39 | "©day": "Year/Date",
40 | "desc": "Description",
41 | "disk": "Disk Number",
42 | "©too": "Encoded By",
43 | "©gen": "Genre",
44 | "©ART": "Artist",
45 | "©lyr": "Lyrics",
46 | "©grp": "Grouping",
47 | "©ope": "Original Artist",
48 | "©wrk": "Work",
49 | "soaa": "Album Artist Sort Order",
50 | "soal": "Album Sort Order",
51 | "soar": "Artist Sort Order",
52 | "soco": "Composer Sort Order",
53 | "MVNM": "Movement Name",
54 | "©prd": "(Producer)",
55 | "©mvn": "(Movement Name)",
56 | "©nam": "(Title)"
57 | };
58 |
59 | const MP4 = [
60 | "©nam (Title)",
61 | "©alb (Album)",
62 | "©ART (Artist)",
63 | "tmpo (BPM)",
64 | "©cmt (Comment)",
65 | "cpil (Compilation iTunes)",
66 | "©wrt (Composer)",
67 | "cond (Conductor)",
68 | "cprt (Copyright)",
69 | "desc (Description)",
70 | "disk (Disk Number)",
71 | "©too (Encoded By)",
72 | "©gen (Genre)",
73 | "©grp (Grouping)",
74 | "©lyr (Lyrics)",
75 | "©ope (Original Artist)",
76 | "©wrk (Work)",
77 | "©day (Year/Date)",
78 | "aART (Album Artist)",
79 | "©prd (Producer)",
80 | "©mvn (Movement Name)",
81 | "LABEL",
82 | "CATALOGNUMBER"
83 | ];
84 |
85 | const VORBIS = [
86 | "ALBUM",
87 | "ALBUMARTIST",
88 | "COMMENT",
89 | "COMPOSER",
90 | "CONDUCTOR",
91 | "GENRE",
92 | "GROUPING",
93 | "LABEL",
94 | "LYRICS",
95 | "ORGANIZATION",
96 | "PUBLISHER",
97 | "MIXARTIST",
98 | "REMIXER",
99 | "VERSION",
100 | ];
101 |
102 | const ID3 = [
103 | "TIT2 (Title)",
104 | "TPE1 (Artist)",
105 | "TALB (Album)",
106 | "TPE2 (Album Artist)",
107 | "COMM (Comment)",
108 | "TCOM (Composer)",
109 | "TPE3 (Conductor)",
110 | "TIT1 (Content Group)",
111 | "TCON (Genre)",
112 | "GRP1 (Grouping iTunes)",
113 | "TPUB (Label)",
114 | "TEXT (Lyricist)",
115 | "TIT3 (Mix Name)",
116 | "TOPE (Original Artist)",
117 | "IPLS (Producer ID3v2.3)",
118 | "TIPL (Producer ID3v2.4)",
119 | "TPE4 (Remixer)",
120 | "USLT (Unsynchronized Lyrics)",
121 | "MVNM (Movement Name)"
122 | ];
123 |
124 | export { ABSTRACTIONS, MP4, VORBIS, ID3 };
--------------------------------------------------------------------------------
/client/src/scripts/utils.ts:
--------------------------------------------------------------------------------
1 | // Returns the WebSocket server URL
2 | function wsUrl(): string {
3 | return import.meta.env.DEV ? `ws://${window.location.hostname}:36913/ws` : `ws://${window.location.host}/ws`;
4 | }
5 |
6 | // Returns the HTTP server URL
7 | function httpUrl(): string {
8 | return import.meta.env.DEV ? `http://${window.location.hostname}:36913` : `${window.location.origin}`;
9 | }
10 |
11 | // Returns the Spotify redirect URL
12 | function spotifyUrl(): string {
13 | return `${httpUrl()}/spotify`;
14 | }
15 |
16 | // Tag value separators
17 | class Separators {
18 | id3: string = ', ';
19 | vorbis?: string;
20 | mp4: string = ', ';
21 |
22 | constructor(id3?: string, vorbis?: string, mp4?: string) {
23 | this.id3 = id3??', ';
24 | this.vorbis = vorbis;
25 | this.mp4 = mp4??', ';
26 | }
27 | }
28 |
29 | // Frame name in different formats
30 | class FrameName {
31 | constructor(public id3: string, public vorbis: string, public mp4: string) {}
32 |
33 | // Create new FrameName where the name is same for all formats
34 | public static same(name: string): FrameName {
35 | return new FrameName(name, name, name);
36 | }
37 |
38 | // Create class from JSON
39 | public static fromJson(json: any): FrameName {
40 | return Object.assign(FrameName.same(''), json);
41 | }
42 |
43 | // Get value by audio or tag format
44 | byFormat(format: string) {
45 | switch (format) {
46 | case 'mp3':
47 | case 'aiff':
48 | case 'aif':
49 | case 'id3':
50 | case 'wav':
51 | return this.id3;
52 | case 'flac':
53 | case 'ogg':
54 | case 'vorbis':
55 | return this.vorbis;
56 | case 'mp4':
57 | case 'm4a':
58 | return this.mp4;
59 | default:
60 | throw new Error(`Invalid format: ${format}`);
61 | }
62 | }
63 | }
64 |
65 | // Keybind data
66 | class Keybind {
67 | ctrl: boolean = false;
68 | key?: string;
69 | alt: boolean = false;
70 | shift: boolean = false;
71 |
72 | // Create class from JSON
73 | public static fromJson(json: any): Keybind {
74 | return Object.assign(new Keybind(), json);
75 | }
76 |
77 | // Check if keybind pressed
78 | public static check(e: KeyboardEvent, keybind?: Keybind) {
79 | if (!keybind) return false;
80 | if (e.code.match(/F\d{1,2}/) || e.code.startsWith('Key') || e.code.startsWith("Digit") || e.code.startsWith("Numpad")) {
81 | let key = e.code.toLowerCase().replace("key", "").replace("digit", "").replace("numpad", "");
82 | return (key == keybind.key &&
83 | e.altKey == keybind.alt &&
84 | e.shiftKey == keybind.shift &&
85 | (e.ctrlKey || e.metaKey) == keybind.ctrl);
86 | }
87 | }
88 |
89 | // Clear the keybind
90 | clear() {
91 | this.ctrl = false;
92 | this.alt = false;
93 | this.shift = false;
94 | this.key = undefined;
95 | }
96 | }
97 |
98 | // Spotify auth data
99 | class Spotify {
100 | clientId?: string;
101 | clientSecret?: string;
102 | authorized: boolean = false;
103 | }
104 |
105 | // Playlist data
106 | interface Playlist {
107 | data?: string;
108 | format?: string;
109 | filename?: string;
110 | }
111 |
112 | /// Duration from Rust
113 | interface RustDuration {
114 | secs: number;
115 | nanos: number;
116 | }
117 |
118 | export type { Playlist, RustDuration };
119 | export { wsUrl, httpUrl, spotifyUrl, Separators, FrameName, Keybind, Spotify };
--------------------------------------------------------------------------------
/client/src/style/app.scss:
--------------------------------------------------------------------------------
1 | @import 'quasar.scss';
2 |
3 | @font-face {
4 | font-family: Dosis;
5 | src: url('./fonts/Dosis-Light.ttf');
6 | font-weight: 300;
7 | }
8 |
9 | @font-face {
10 | font-family: Dosis;
11 | src: url('./fonts/Dosis-Regular.ttf');
12 | font-weight: 400;
13 | }
14 |
15 | @font-face {
16 | font-family: Dosis;
17 | src: url('./fonts/Dosis-Medium.ttf');
18 | font-weight: 500;
19 | }
20 |
21 | @font-face {
22 | font-family: Dosis;
23 | src: url('./fonts/Dosis-SemiBold.ttf');
24 | font-weight: 600;
25 | }
26 |
27 | @font-face {
28 | font-family: Dosis;
29 | src: url('./fonts/Dosis-Bold.ttf');
30 | font-weight: 700;
31 | }
32 |
33 | @font-face {
34 | font-family: Dosis;
35 | src: url('./fonts/Dosis-ExtraBold.ttf');
36 | font-weight: 800;
37 | }
38 |
39 | @font-face {
40 | font-family: wavefont;
41 | src: url('./fonts/wavefont.woff2') format('woff2');
42 | }
43 |
44 | @font-face {
45 | font-family: blank;
46 | /* src: url(./AdobeBlank2VF.ttf.woff2); */
47 | src: url('data:application/x-font-woff;charset=utf-8;base64,d09GMgABAAAAAARMABMAAAAAC1AAAAPjAAEAxQAAAAAAAAAAAAAAAAAAAAAAAAAAGUYcID9IVkFSVAZgP1NUQVQkP1ZWQVJjAIIAL0YKWFwwLgE2AiQDCAsGAAQgBYsyBzEXJBgIGwEKEcXkJfmqgCdD4yWAULaeeBovVzstIc6riunXpnmtXivg+BLWsFA1x8PTXP+eO5PNAn8AUCwJsYCgqxxZ1JUELrLsXkOo3TUpDjbRB8Tvo61HSFe6NVBVO1SvlxDEvYxsxE4DfNN8ImIOJ0gqtv/9fq0+Wat/NsTds46FRuqU8lf0gse1iqY3dLfkGRqhqiTVConpREoxCY1FPHelp1Ny+uwFWpZYoBA7dh04oaUEhFLUFDri7o2bj2BaFYGOOeZ1vLhvdQ0IMAUABHoC6yWZLbYAgJ9NX06XhIBcaa5iDdL76Qz2k7njkrV+ci1cx5uLNbeOtx22hbrOrMaqxrO/9QL9NwVgAUJoSZgClkjP3HjyTmDP7j0nIoQAM3AGAJfCk0F+cI8EIeugpw/+AUwGvq0R/IaJbxBM/EE9gRAIQEJCRkaBAg00UKJEE0200EIbbXTQQRdd9NBDH30MMMAQQ4wwEuEmSaYkBnnYDCBXEDnpwW9EJK5WZJBAgQZGmA0oQIMiXw91sjku790e3LzZfHZ461Z5+/ozFVTN2diLFz9/ql67cl8k+nvMWO4OBBKYtXsY6LwaespXbYFXt+TqtTcIhK83bvTx5eGGf1s5V/D9qt/p9najlt/Mj6KUQFCxsYrB4sZY4VoloVbYkpHGKGGx3RT/yaJo4wSfCEucQDLwLLLV3u5asKHhhk9QmpVZaOnEerTNyqaunQW61ucM+umj+BAD6xtRwIJe7SdfPJ0j6BTPIkzxelO3WVhf//c2jpwWnwkGXVHJ7/FoDWDAQVkQ+OBTFKZJu8lXljDZnk7YqITmExH/y0MxUyoCDOBqChwdIU8mZhKKCJ2upGVgbGinpSoKOgThuVdIkAJUCkD1UYqNlj3J5kSRrqElYYogo7sLvSE3eCVefdEJa/SRA1H0BWVMIMAKSoG1TMqBSbtr5Q+DNGe+gASmqJQCTiiX8mISEDDtHgvYUEpCDjCuCZD0oQJ1YQ3sjHRPlXdfK+E/rSY28Bg4cI6cV00RDZaMcjGklsyXggUDJ2BRVwCGvwLAfxa/WTib27k4LwoOHB0NbJMLE/HNdo7AyM7W2AINfIZwPg2I0KNAtGALqE+jIEEkfiTfWAAkgSiiBZJkLDgSCXf6EaZ2NnDRspAUq7MFXOYSaLDGnozJK4dkPCmTRSmRKSxxWiDAMFMxKVLnoZ7iPA0oLgj+UBlRkrmlg3DbW9ieSDMnJbu4KqNIxL1kjISZ9GjJ5+t2TALq0U3AcYLmZeFeX9Ww6+F30lD/+kXetcg6ZJg3EalzO/5jvn7dbo3cSO6f/4dc8dJzzBkJMwsAAAA=') format('woff2');
48 | }
49 |
50 | :root {
51 | --waveform-wave: 40;
52 | }
53 |
54 | * {
55 | font-family: Dosis !important;
56 | user-select: none;
57 | }
58 |
59 | .text-h5 {
60 | padding-bottom: 6px;
61 | }
62 |
63 | .bg-darker {
64 | background: #181818;
65 | }
66 |
67 | .bg-highlight {
68 | background: #303030;
69 | }
70 |
71 | .bg-onetagger-icon {
72 | background: #1A1A1A;
73 | }
74 |
75 | .q-tab__indicator {
76 | color: var(--q-color-primary);
77 | }
78 |
79 | .q-field__control {
80 | background-color: #99999910 !important;
81 | }
82 |
83 | // Checkbox fix
84 | .checkbox svg {
85 | color: #000;
86 | }
87 |
88 | // Input fix
89 | .input input {
90 | color: #fff !important;
91 | }
92 |
93 | // Select fix
94 | .select span {
95 | color: #fff !important;
96 | }
97 |
98 | // Tooltip fix
99 | .q-slider__pin-text {
100 | color: #fff;
101 | }
102 |
103 | .q-tooltip {
104 | font-size: 13px !important;
105 | }
106 |
107 | // Card shadow fix
108 | .q-card {
109 | box-shadow: none !important;
110 | }
111 |
112 | // https://medium.com/cloud-native-the-gathering/how-to-fix-your-angular-material-input-field-from-being-broken-in-safari-1419b12007ee
113 | input {
114 | -webkit-user-select: text !important;
115 | }
116 |
117 | // Fix the input fields on Webkit
118 | input[type="search"] {
119 | -webkit-appearance: textfield;
120 | }
121 |
122 | .selectable {
123 | user-select: text !important;
124 | cursor: text;
125 | }
126 |
127 | .clickable {
128 | cursor: pointer;
129 | }
130 |
131 | .monospace {
132 | font-family: monospace !important;
133 | }
134 |
135 | .dotted-underline {
136 | border-bottom: 1px dotted;
137 | }
138 | .dotted-underline:hover {
139 | border-bottom: 1px dotted white;
140 | }
141 |
142 |
143 | /* Renamer syntax higlighting colors */
144 | .__renamer_syntax_text {
145 | color: #9e9e9e;
146 | font-family: monospace !important;
147 | }
148 | .__renamer_syntax_operator {
149 | color: #78909c;
150 | font-family: monospace !important;
151 | }
152 | .__renamer_syntax_string {
153 | color: #4caf50;
154 | font-family: monospace !important;
155 | }
156 | .__renamer_syntax_number {
157 | color: #ff5722;
158 | font-family: monospace !important;
159 | }
160 | .__renamer_syntax_function {
161 | color: #2196f3;
162 | font-family: monospace !important;
163 | }
164 | .__renamer_syntax_property {
165 | color: #cfd8dc;
166 | font-family: monospace !important;
167 | }
168 | .__renamer_syntax_variable {
169 | color: #cfd8dc;
170 | font-family: monospace !important;
171 | }
172 |
173 | code {
174 | font-family: monospace !important;
175 | font-size: 85%;
176 | }
177 |
178 |
179 | // Scrollbar
180 | ::-webkit-scrollbar {
181 | width: 10px;
182 | }
183 |
184 | ::-webkit-scrollbar:horizontal {
185 | height: 10px;
186 | }
187 |
188 | ::-webkit-scrollbar-track {
189 | background-color: #10101044;
190 | border-radius: 5px;
191 | }
192 |
193 | ::-webkit-scrollbar-thumb {
194 | border-radius: 5px;
195 | background-color: #363636;
196 | }
197 |
198 | ::-webkit-scrollbar-thumb:hover {
199 | background-color: #404040;
200 | }
201 |
202 | ::-webkit-scrollbar-thumb:horizontal {
203 | background-color: #36363690;
204 | }
205 |
206 | ::-webkit-scrollbar-thumb:horizontal:hover {
207 | background-color: #404040;
208 | }
209 |
210 | ::-webkit-scrollbar-corner {
211 | background-color: transparent !important;
212 | }
--------------------------------------------------------------------------------
/client/src/style/fonts/Dosis-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/Dosis-Bold.ttf
--------------------------------------------------------------------------------
/client/src/style/fonts/Dosis-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/Dosis-ExtraBold.ttf
--------------------------------------------------------------------------------
/client/src/style/fonts/Dosis-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/Dosis-Light.ttf
--------------------------------------------------------------------------------
/client/src/style/fonts/Dosis-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/Dosis-Medium.ttf
--------------------------------------------------------------------------------
/client/src/style/fonts/Dosis-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/Dosis-Regular.ttf
--------------------------------------------------------------------------------
/client/src/style/fonts/Dosis-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/Dosis-SemiBold.ttf
--------------------------------------------------------------------------------
/client/src/style/fonts/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com),
2 | Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com),
3 | with Reserved Font Names "Dosis".
4 |
5 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
6 | This license is copied below, and is also available with a FAQ at:
7 | http://scripts.sil.org/OFL
8 |
9 |
10 | -----------------------------------------------------------
11 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
12 | -----------------------------------------------------------
13 |
14 | PREAMBLE
15 | The goals of the Open Font License (OFL) are to stimulate worldwide
16 | development of collaborative font projects, to support the font creation
17 | efforts of academic and linguistic communities, and to provide a free and
18 | open framework in which fonts may be shared and improved in partnership
19 | with others.
20 |
21 | The OFL allows the licensed fonts to be used, studied, modified and
22 | redistributed freely as long as they are not sold by themselves. The
23 | fonts, including any derivative works, can be bundled, embedded,
24 | redistributed and/or sold with any software provided that any reserved
25 | names are not used by derivative works. The fonts and derivatives,
26 | however, cannot be released under any other type of license. The
27 | requirement for fonts to remain under this license does not apply
28 | to any document created using the fonts or their derivatives.
29 |
30 | DEFINITIONS
31 | "Font Software" refers to the set of files released by the Copyright
32 | Holder(s) under this license and clearly marked as such. This may
33 | include source files, build scripts and documentation.
34 |
35 | "Reserved Font Name" refers to any names specified as such after the
36 | copyright statement(s).
37 |
38 | "Original Version" refers to the collection of Font Software components as
39 | distributed by the Copyright Holder(s).
40 |
41 | "Modified Version" refers to any derivative made by adding to, deleting,
42 | or substituting -- in part or in whole -- any of the components of the
43 | Original Version, by changing formats or by porting the Font Software to a
44 | new environment.
45 |
46 | "Author" refers to any designer, engineer, programmer, technical
47 | writer or other person who contributed to the Font Software.
48 |
49 | PERMISSION & CONDITIONS
50 | Permission is hereby granted, free of charge, to any person obtaining
51 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
52 | redistribute, and sell modified and unmodified copies of the Font
53 | Software, subject to the following conditions:
54 |
55 | 1) Neither the Font Software nor any of its individual components,
56 | in Original or Modified Versions, may be sold by itself.
57 |
58 | 2) Original or Modified Versions of the Font Software may be bundled,
59 | redistributed and/or sold with any software, provided that each copy
60 | contains the above copyright notice and this license. These can be
61 | included either as stand-alone text files, human-readable headers or
62 | in the appropriate machine-readable metadata fields within text or
63 | binary files as long as those fields can be easily viewed by the user.
64 |
65 | 3) No Modified Version of the Font Software may use the Reserved Font
66 | Name(s) unless explicit written permission is granted by the corresponding
67 | Copyright Holder. This restriction only applies to the primary font name as
68 | presented to the users.
69 |
70 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
71 | Software shall not be used to promote, endorse or advertise any
72 | Modified Version, except to acknowledge the contribution(s) of the
73 | Copyright Holder(s) and the Author(s) or with their explicit written
74 | permission.
75 |
76 | 5) The Font Software, modified or unmodified, in part or in whole,
77 | must be distributed entirely under this license, and must not be
78 | distributed under any other license. The requirement for fonts to
79 | remain under this license does not apply to any document created
80 | using the Font Software.
81 |
82 | TERMINATION
83 | This license becomes null and void if any of the above conditions are
84 | not met.
85 |
86 | DISCLAIMER
87 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
88 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
89 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
90 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
91 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
92 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
93 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
94 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
95 | OTHER DEALINGS IN THE FONT SOFTWARE.
96 |
--------------------------------------------------------------------------------
/client/src/style/fonts/wavefont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/client/src/style/fonts/wavefont.woff2
--------------------------------------------------------------------------------
/client/src/style/quasar.scss:
--------------------------------------------------------------------------------
1 | $primary : #00D2BF;
2 | $secondary : #106c61;
3 | $accent : #181818;
4 |
5 | $dark : #202020;
6 | $dark-page : #181818;
7 |
8 | $positive : #21BA45;
9 | $negative : #C10015;
10 | $info : #31CCEC;
11 | $warning : #F2C037;
12 |
13 | $tooltip-background: #080808 !default;
--------------------------------------------------------------------------------
/client/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 | const component: DefineComponent<{}, {}, any>
6 | export default component
7 | }
8 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "isolatedModules": true,
12 | "esModuleInterop": true,
13 | "lib": ["ESNext", "DOM"],
14 | "skipLibCheck": true
15 | },
16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
17 | "references": [{ "path": "./tsconfig.node.json" }]
18 | }
19 |
--------------------------------------------------------------------------------
/client/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { defineConfig } from 'vite';
3 | import { quasar, transformAssetUrls } from '@quasar/vite-plugin';
4 | import vue from '@vitejs/plugin-vue';
5 |
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [
10 | vue({
11 | template: { transformAssetUrls }
12 | }),
13 | quasar({
14 | sassVariables: path.resolve('src/style/quasar.scss')
15 | })
16 | ],
17 | server: {
18 | port: 8080
19 | },
20 | build: {
21 | assetsInlineLimit: 16 * 1024
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/crates/onetagger-autotag/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-autotag"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | rand = "0.8"
11 | regex = "1.10"
12 | dunce = "1.0"
13 | image = "0.25"
14 | anyhow = "1.0"
15 | chrono = "0.4"
16 | base64 = "0.22"
17 | execute = "0.2"
18 | walkdir = "2.5"
19 | libloading = "0.8"
20 | serde_json = "1.0"
21 | lazy_static = "1.5"
22 | crossbeam-channel = "0.5"
23 |
24 | serde = { version = "1.0", features = ["derive"] }
25 | reqwest = { version = "0.12", features = ["json", "blocking", "rustls-tls"], default-features = false }
26 |
27 | songrec = { git = "https://github.com/Marekkon5/SongRec.git" }
28 |
29 | onetagger-tag = { path = "../onetagger-tag" }
30 | onetagger-tagger = { path = "../onetagger-tagger" }
31 | onetagger-player = { path = "../onetagger-player" }
32 | onetagger-shared = { path = "../onetagger-shared" }
33 | onetagger-renamer = { path = "../onetagger-renamer" }
34 | onetagger-platforms = { path = "../onetagger-platforms" }
35 |
--------------------------------------------------------------------------------
/crates/onetagger-autotag/src/repo.rs:
--------------------------------------------------------------------------------
1 | use std::io::BufWriter;
2 | use std::fs::File;
3 | use anyhow::Error;
4 | use onetagger_shared::Settings;
5 | use serde_json::Value;
6 |
7 | const MANIFEST_URL: &str = "https://raw.githubusercontent.com/Marekkon5/onetagger-platforms/master/platforms.json";
8 | const DOWNLOAD_URL: &str = "https://github.com/Marekkon5/onetagger-platforms/releases/download/platforms";
9 |
10 | /// Fetch custom platforms manifest
11 | /// TODO: Serialization not implemented right now since it just gets passed to UI
12 | pub fn fetch_manifest() -> Result {
13 | Ok(reqwest::blocking::get(MANIFEST_URL)?.json()?)
14 | }
15 |
16 | /// Fetch custom platforms repo async version because WS is handled by async runtime
17 | pub async fn fetch_manifest_async() -> Result {
18 | Ok(reqwest::get(MANIFEST_URL).await?.error_for_status()?.json().await?)
19 | }
20 |
21 | /// Download and install custom platform
22 | pub fn install_platform(id: &str, version: &str, is_native: bool) -> Result<(), Error> {
23 | info!("Installing platform {id}@{version}");
24 |
25 | // Generate filename
26 | let name = format!("{id}_{version}");
27 | let name = match is_native {
28 | true => {
29 | let name = format!("{name}_{}_{}", std::env::consts::OS, std::env::consts::ARCH);
30 | match std::env::consts::OS {
31 | "windows" => format!("{name}.dll"),
32 | "linux" => format!("{name}.so"),
33 | "macos" => format!("{name}.dylib"),
34 | // Fallback assume linux
35 | _ => format!("{name}.so")
36 | }
37 | },
38 | false => format!("{name}"),
39 | };
40 |
41 | // Get paths
42 | let platforms_dir = Settings::get_folder()?.join("platforms");
43 | let path = platforms_dir.join(&name);
44 |
45 | // Download native
46 | if is_native {
47 | std::io::copy(&mut reqwest::blocking::get(format!("{DOWNLOAD_URL}/{name}"))?, &mut BufWriter::new(File::create(path)?))?;
48 | return Ok(())
49 | }
50 |
51 | Ok(())
52 | }
--------------------------------------------------------------------------------
/crates/onetagger-autotag/src/shazam.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use std::path::Path;
3 | use std::thread::Builder;
4 | use onetagger_player::rodio::source::UniformSourceIterator;
5 | use serde::{Serialize, Deserialize};
6 | use songrec::SignatureGenerator;
7 | use onetagger_player::AudioSources;
8 |
9 | pub struct Shazam;
10 |
11 | impl Shazam {
12 | /// Recognize song on Shazam from path, returns Track, Duration
13 | pub fn recognize_from_file(path: impl AsRef) -> Result<(ShazamTrack, u128), Error> {
14 | // Load file
15 | let source = AudioSources::from_path(path)?;
16 | let duration = source.duration();
17 | let conv = UniformSourceIterator::new(source.get_source()?, 1, 16000);
18 | // Get 12s part from middle
19 | let buffer = if duration >= 12000 {
20 | // ((duration / 1000) * 16KHz) / 2 (half duration) - (6 * 16KHz) seconds.
21 | conv.skip((duration * 8 - 96000) as usize).take(16000 * 12).collect::>()
22 | } else {
23 | conv.collect::>()
24 | };
25 | // Calculating singnature requires 6MB stack, because it allocates >2MB of buffers for some reason
26 | let signature = Builder::new()
27 | .stack_size(1024 * 1024 * 6)
28 | .spawn(move || { SignatureGenerator::make_signature_from_buffer(&buffer) })
29 | .unwrap()
30 | .join()
31 | .unwrap();
32 | let response = songrec::recognize_song_from_signature(&signature, 0).map_err(|e| anyhow!("{e:?}"))?;
33 | let response: ShazamResponse = serde_json::from_value(response)?;
34 | let track = response.track.ok_or(anyhow!("Shazam returned no matches!"))?;
35 | Ok((track, duration))
36 | }
37 | }
38 |
39 | #[derive(Debug, Clone, Serialize, Deserialize)]
40 | pub struct ShazamResponse {
41 | pub timestamp: u64,
42 | pub tagid: String,
43 | pub track: Option
44 | }
45 |
46 | #[derive(Debug, Clone, Serialize, Deserialize)]
47 | pub struct ShazamTrack {
48 | pub albumadamid: Option,
49 | pub artists: Option>,
50 | pub genres: Option,
51 | pub images: Option,
52 | pub isrc: Option,
53 | pub key: String,
54 | pub sections: Vec,
55 | /// Song title
56 | pub title: String,
57 | /// Artist
58 | pub subtitle: String,
59 | pub url: String,
60 | }
61 |
62 | #[derive(Debug, Clone, Serialize, Deserialize)]
63 | pub struct ShazamSmall {
64 | pub adamid: String,
65 | pub id: String
66 | }
67 |
68 | #[derive(Debug, Clone, Serialize, Deserialize)]
69 | pub struct ShazamGenres {
70 | pub primary: Option
71 | }
72 |
73 | #[derive(Debug, Clone, Serialize, Deserialize)]
74 | pub struct ShazamImages {
75 | pub background: String,
76 | pub coverart: String,
77 | pub coverarthq: String
78 | }
79 |
80 | #[derive(Debug, Clone, Serialize, Deserialize)]
81 | #[serde(untagged)]
82 | pub enum ShazamSection {
83 | MetaSection {
84 | metadata: Vec
85 | },
86 | ArtistSection {
87 | id: String,
88 | name: String,
89 | tabname: String,
90 | // Has to == "ARTIST"
91 | #[serde(rename = "type")]
92 | _type: String
93 | },
94 | Other {}
95 | }
96 |
97 | #[derive(Debug, Clone, Serialize, Deserialize)]
98 | pub struct ShazamMetadataSection {
99 | pub text: String,
100 | pub title: String
101 | }
--------------------------------------------------------------------------------
/crates/onetagger-cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-cli"
3 | version = "1.7.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | anyhow = "1.0"
11 | serde_json = "1.0"
12 | convert_case = "0.6"
13 |
14 | clap = { version = "4.5", features = ["derive"] }
15 |
16 | onetagger-ui = { path = "../onetagger-ui" }
17 | onetagger-tagger = { path = "../onetagger-tagger" }
18 | onetagger-shared = { path = "../onetagger-shared" }
19 | onetagger-autotag = { path = "../onetagger-autotag" }
20 | onetagger-renamer = { path = "../onetagger-renamer" }
21 | onetagger-playlist = { path = "../onetagger-playlist" }
22 | onetagger-platforms = { path = "../onetagger-platforms" }
23 |
24 | [target.'cfg(windows)'.build-dependencies]
25 | winres = "0.1"
--------------------------------------------------------------------------------
/crates/onetagger-cli/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | // Set Windows icon
3 | #[cfg(windows)]
4 | {
5 | let mut res = winres::WindowsResource::new();
6 | res.set_icon("..\\..\\assets\\icon.ico");
7 | res.compile().unwrap();
8 | }
9 | }
--------------------------------------------------------------------------------
/crates/onetagger-platforms/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-platforms"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | url = "2.5"
11 | rand = "0.8"
12 | regex = "1.10"
13 | anyhow = "1.0"
14 | scraper = "0.20"
15 | serde_json = "1.0"
16 | minify-html = "0.15"
17 |
18 | serde = { version = "1.0", features = ["derive"] }
19 | chrono = { version = "0.4", features = ["serde"] }
20 | reqwest = { version = "0.12", features = ["json", "blocking", "rustls-tls"], default-features = false }
21 | rspotify = { version = "0.13", features = ["client-ureq", "ureq-rustls-tls"], default-features = false }
22 |
23 |
24 | onetagger-tag = { path = "../onetagger-tag" }
25 | onetagger-shared = { path = "../onetagger-shared" }
26 | onetagger-tagger = { path = "../onetagger-tagger" }
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/bandcamp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/bandcamp.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/beatport.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/beatport.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/beatsource.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/beatsource.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/bpmsupreme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/bpmsupreme.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/deezer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/deezer.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/discogs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/discogs.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/itunes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/itunes.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/junodownload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/junodownload.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/musicbrainz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/musicbrainz.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/musixmatch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/musixmatch.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/spotify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/spotify.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/assets/traxsource.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Marekkon5/onetagger/f3e1409c6c8ebfbaa80759915a2f56a760e3f407/crates/onetagger-platforms/assets/traxsource.png
--------------------------------------------------------------------------------
/crates/onetagger-platforms/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[macro_use] extern crate log;
2 | #[macro_use] extern crate anyhow;
3 | #[macro_use] extern crate onetagger_shared;
4 |
5 | pub mod beatport;
6 | pub mod traxsource;
7 | pub mod discogs;
8 | pub mod junodownload;
9 | pub mod spotify;
10 | pub mod itunes;
11 | pub mod musicbrainz;
12 | pub mod beatsource;
13 | pub mod bpmsupreme;
14 | pub mod deezer;
15 | pub mod musixmatch;
16 | pub mod bandcamp;
17 | mod bandcamp_genres;
18 |
19 |
--------------------------------------------------------------------------------
/crates/onetagger-player/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-player"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | alac = "0.5"
11 | hound = "3.5"
12 | lofty = "0.21"
13 | anyhow = "1.0"
14 | pacmog = "0.4.2"
15 | mp4parse = "0.17"
16 |
17 | rodio = { version = "0.19", features = ["symphonia-aac", "symphonia-isomp4", "flac", "vorbis", "wav", "minimp3"], default-features = false }
18 |
19 |
--------------------------------------------------------------------------------
/crates/onetagger-player/src/aiff.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 | use anyhow::Error;
3 | use lofty::file::AudioFile;
4 | use std::fs::File;
5 | use std::io::Read;
6 | use std::time::Duration;
7 | use pacmog::PcmReader;
8 | use rodio::Source;
9 |
10 | use crate::AudioSource;
11 |
12 | pub struct AIFFSource {
13 | path: PathBuf,
14 | duration: Duration
15 | }
16 |
17 | impl AIFFSource {
18 | // Load from path
19 | pub fn new(path: impl AsRef) -> Result {
20 | // Get duration
21 | let file = lofty::read_from_path(&path)?;
22 | let duration = file.properties().duration();
23 |
24 | Ok(AIFFSource {
25 | path: path.as_ref().to_owned(),
26 | duration
27 | })
28 | }
29 |
30 | }
31 |
32 | impl AudioSource for AIFFSource {
33 | // Get duration
34 | fn duration(&self) -> u128 {
35 | self.duration.as_millis()
36 |
37 | }
38 |
39 | // Get rodio source
40 | fn get_source(&self) -> Result + Send>, Error> {
41 | let source = AIFFDecoder::load(&self.path)?;
42 | Ok(Box::new(source.convert_samples()))
43 | }
44 | }
45 |
46 | struct AIFFDecoder {
47 | channels: u32,
48 | samples: u32,
49 | sample_rate: u32,
50 | index: usize,
51 | buffer: Vec
52 | }
53 |
54 | impl AIFFDecoder {
55 | /// Load file into memory
56 | pub fn load(path: impl AsRef) -> Result {
57 | // Load file
58 | let mut data = vec![];
59 | File::open(path)?.read_to_end(&mut data)?;
60 |
61 | // Parse metadata (catch panic, because weird library)
62 | let reader = std::panic::catch_unwind(|| {
63 | PcmReader::new(&data)
64 | }).map_err(|e| anyhow!("Not an AIFF file: {e:?}"))?;
65 | let specs = reader.get_pcm_specs();
66 |
67 | // Decode the file (because the library is weeeird)
68 | // TODO: Make better using symphonia / new rodio
69 | let mut samples = vec![0i16; specs.num_channels as usize * specs.num_samples as usize];
70 | let mut i = 0;
71 | for sample in 0..specs.num_samples {
72 | for channel in 0..specs.num_channels {
73 | let s = std::panic::catch_unwind(|| {
74 | reader.read_sample(channel as u32, sample)
75 | }).map_err(|e| anyhow!("Failed decoding AIFF: {e:?}"))?.map_err(|e| anyhow!("Failed decoding AIFF: {e}"))?;
76 | samples[i] = (s * i16::MAX as f32) as i16;
77 | i += 1;
78 | }
79 | }
80 |
81 | Ok(AIFFDecoder {
82 | channels: specs.num_channels as u32,
83 | samples: specs.num_samples,
84 | sample_rate: specs.sample_rate,
85 | index: 0,
86 | buffer: samples,
87 | })
88 | }
89 | }
90 |
91 | impl Source for AIFFDecoder {
92 | fn current_frame_len(&self) -> Option {
93 | None
94 | }
95 |
96 | fn channels(&self) -> u16 {
97 | self.channels as u16
98 | }
99 |
100 | fn sample_rate(&self) -> u32 {
101 | self.sample_rate
102 | }
103 |
104 | fn total_duration(&self) -> Option {
105 | Some(Duration::from_secs_f32(self.samples as f32 / self.sample_rate as f32))
106 | }
107 | }
108 |
109 | impl Iterator for AIFFDecoder {
110 | type Item = i16;
111 |
112 | fn next(&mut self) -> Option {
113 | if self.index >= self.buffer.len() {
114 | return None;
115 | }
116 | let sample = self.buffer[self.index];
117 | self.index += 1;
118 | Some(sample)
119 | }
120 | }
--------------------------------------------------------------------------------
/crates/onetagger-player/src/alac.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use std::path::Path;
3 | use std::fs::File;
4 | use std::io::BufReader;
5 | use std::time::Duration;
6 | use rodio::Source;
7 | use alac::{Reader, Samples, StreamInfo};
8 |
9 | pub struct ALACSource {
10 | samples: Samples, i32>,
11 | stream_info: StreamInfo
12 | }
13 |
14 | impl ALACSource {
15 | // Read alac from file
16 | pub fn new(path: impl AsRef) -> Result {
17 | let file = File::open(path)?;
18 | let r = BufReader::new(file);
19 | let reader = Reader::new(r)?;
20 | let stream_info = reader.stream_info().to_owned();
21 | Ok(ALACSource {
22 | samples: reader.into_samples(),
23 | stream_info
24 | })
25 | }
26 | }
27 |
28 | impl Source for ALACSource {
29 | fn current_frame_len(&self) -> Option {
30 | None
31 | }
32 |
33 | fn channels(&self) -> u16 {
34 | self.stream_info.channels() as u16
35 | }
36 |
37 | fn sample_rate(&self) -> u32 {
38 | self.stream_info.sample_rate()
39 | }
40 |
41 | fn total_duration(&self) -> Option {
42 | None
43 | }
44 | }
45 |
46 | impl Iterator for ALACSource {
47 | type Item = i16;
48 |
49 | fn next(&mut self) -> Option {
50 | // Wrapper against samples
51 | if let Some(r) = self.samples.next() {
52 | if let Ok(s) = r {
53 | return Some((s >> 16) as i16);
54 | }
55 | }
56 | None
57 | }
58 | }
--------------------------------------------------------------------------------
/crates/onetagger-player/src/flac.rs:
--------------------------------------------------------------------------------
1 | use std::path::{PathBuf, Path};
2 | use anyhow::Error;
3 | use std::io::BufReader;
4 | use std::fs::File;
5 | use rodio::{Source, Decoder};
6 | use crate::AudioSource;
7 |
8 | pub struct FLACSource {
9 | path: PathBuf,
10 | duration: u128
11 | }
12 |
13 | impl FLACSource {
14 | pub fn new(path: impl AsRef) -> Result {
15 | let mut flac = FLACSource {
16 | path: path.as_ref().to_owned(),
17 | duration: 0
18 | };
19 | // Get duration from decoder
20 | flac.duration = flac.get_source()?.total_duration().ok_or(anyhow!("Missing duration"))?.as_millis();
21 |
22 | Ok(flac)
23 | }
24 | }
25 |
26 | impl AudioSource for FLACSource {
27 | // Get duration
28 | fn duration(&self) -> u128 {
29 | self.duration
30 | }
31 |
32 | // Get rodio decoder
33 | fn get_source(&self) -> Result + Send>, Error> {
34 | Ok(Box::new(Decoder::new_flac(BufReader::new(File::open(&self.path)?))?))
35 | }
36 | }
--------------------------------------------------------------------------------
/crates/onetagger-player/src/mp3.rs:
--------------------------------------------------------------------------------
1 | use std::path::{Path, PathBuf};
2 | use anyhow::Error;
3 | use lofty::file::AudioFile;
4 | use std::io::BufReader;
5 | use std::fs::File;
6 | use rodio::{Source, Decoder};
7 | use crate::AudioSource;
8 |
9 | pub struct MP3Source {
10 | path: PathBuf,
11 | duration: u128
12 | }
13 | impl MP3Source {
14 | pub fn new(path: impl AsRef) -> Result {
15 | // Get duration
16 | let file = lofty::read_from_path(&path)?;
17 | let duration = file.properties().duration();
18 |
19 | Ok(MP3Source {
20 | path: path.as_ref().to_owned(),
21 | duration: duration.as_millis()
22 | })
23 | }
24 | }
25 |
26 | impl AudioSource for MP3Source {
27 | // Get duration
28 | fn duration(&self) -> u128 {
29 | self.duration
30 | }
31 |
32 | // Get rodio decoder
33 | fn get_source(&self) -> Result + Send>, Error> {
34 | Ok(Box::new(Decoder::new_mp3(BufReader::new(File::open(&self.path)?))?))
35 | }
36 | }
--------------------------------------------------------------------------------
/crates/onetagger-player/src/mp4.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use mp4parse::{SampleEntry, CodecType};
3 | use rodio::decoder::Mp4Type;
4 | use std::io::BufReader;
5 | use std::fs::File;
6 | use std::path::{PathBuf, Path};
7 | use rodio::{Source, Decoder};
8 |
9 | use crate::AudioSource;
10 | use crate::alac::ALACSource;
11 |
12 | pub struct MP4Source {
13 | path: PathBuf,
14 | duration: u128,
15 | alac: bool
16 | }
17 |
18 | impl MP4Source {
19 | pub fn new(path: impl AsRef) -> Result {
20 | let file = File::open(&path)?;
21 | let mp4 = mp4parse::read_mp4(&mut BufReader::new(file))?;
22 | let track = mp4.tracks.first().ok_or(anyhow!("No MP4 tracks"))?;
23 | let duration = track.duration.ok_or(anyhow!("Missing duration"))?.0 as f32 / track.timescale.ok_or(anyhow!("Missing timescale"))?.0 as f32;
24 | // Check if alac
25 | let mut alac = false;
26 | if let SampleEntry::Audio(entry) = track.stsd.as_ref().ok_or(anyhow!("Missing stsd"))?.descriptions.first().ok_or(anyhow!("Missing first stsd"))? {
27 | alac = entry.codec_type == CodecType::ALAC
28 | }
29 |
30 | debug!("Creating MP4 source ok, alac: {}", alac);
31 |
32 | Ok(MP4Source {
33 | path: path.as_ref().to_owned(),
34 | duration: (duration * 1000.0) as u128,
35 | alac
36 | })
37 | }
38 | }
39 |
40 | impl AudioSource for MP4Source {
41 | fn duration(&self) -> u128 {
42 | self.duration
43 | }
44 |
45 | fn get_source(&self) -> Result + Send>, Error> {
46 | // ALAC MP4
47 | if self.alac {
48 | let alac = ALACSource::new(&self.path)?;
49 | return Ok(Box::new(alac));
50 | }
51 |
52 | // Symphonia
53 | let decoder = Decoder::new_mp4(BufReader::new(File::open(&self.path)?), Mp4Type::M4a)?;
54 | return Ok(Box::new(decoder));
55 |
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/crates/onetagger-player/src/ogg.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 | use anyhow::Error;
3 | use lofty::file::AudioFile;
4 | use std::path::{PathBuf, Path};
5 | use std::io::BufReader;
6 | use std::fs::File;
7 | use rodio::{Source, Decoder};
8 |
9 | use crate::AudioSource;
10 |
11 | pub struct OGGSource {
12 | path: PathBuf,
13 | duration: Duration,
14 | }
15 |
16 | impl OGGSource {
17 | pub fn new(path: impl AsRef) -> Result {
18 | // Get duration
19 | let file = lofty::read_from_path(&path)?;
20 | let duration = file.properties().duration();
21 |
22 | Ok(OGGSource {
23 | duration,
24 | path: path.as_ref().into()
25 | })
26 | }
27 | }
28 |
29 | impl AudioSource for OGGSource {
30 | fn duration(&self) -> u128 {
31 | self.duration.as_millis()
32 | }
33 |
34 | fn get_source(&self) -> Result + Send>, Error> {
35 | // Use rodio vorbis
36 | Ok(Box::new(Decoder::new_vorbis(BufReader::new(File::open(&self.path)?))?))
37 | }
38 | }
39 |
40 |
--------------------------------------------------------------------------------
/crates/onetagger-player/src/wav.rs:
--------------------------------------------------------------------------------
1 | use std::path::{PathBuf, Path};
2 | use anyhow::Error;
3 | use std::io::BufReader;
4 | use std::fs::File;
5 | use rodio::{Source, Decoder};
6 | use crate::AudioSource;
7 |
8 | pub struct WAVSource {
9 | path: PathBuf,
10 | duration: u128
11 | }
12 |
13 | impl WAVSource {
14 | pub fn new(path: impl AsRef) -> Result {
15 | let mut wav = WAVSource {
16 | path: path.as_ref().to_owned(),
17 | duration: 0
18 | };
19 | // Get duration from decoder
20 | wav.duration = wav.get_source()?.total_duration().ok_or(anyhow!("Missing duration"))?.as_millis();
21 |
22 | Ok(wav)
23 | }
24 | }
25 |
26 | impl AudioSource for WAVSource {
27 | // Get duration
28 | fn duration(&self) -> u128 {
29 | self.duration
30 | }
31 |
32 | // Get rodio decoder
33 | fn get_source(&self) -> Result + Send>, Error> {
34 | Ok(Box::new(Decoder::new_wav(BufReader::new(File::open(&self.path)?))?))
35 | }
36 | }
--------------------------------------------------------------------------------
/crates/onetagger-playlist/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-playlist"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | anyhow = "1.0"
11 | base64 = "0.22"
12 | urlencoding = "2.1"
13 |
14 | serde = { version = "1.0", features = ["derive"] }
15 |
16 | onetagger-tag = { path = "../onetagger-tag" }
--------------------------------------------------------------------------------
/crates/onetagger-playlist/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[macro_use] extern crate log;
2 | #[macro_use] extern crate anyhow;
3 |
4 | use anyhow::Error;
5 | use std::fs::File;
6 | use std::io::prelude::*;
7 | use std::path::{Path, PathBuf};
8 | use serde::{Serialize, Deserialize};
9 | use base64::Engine;
10 | use onetagger_tag::EXTENSIONS;
11 |
12 | pub const PLAYLIST_EXTENSIONS: [&str; 2] = ["m3u", "m3u8"];
13 |
14 | // Playlist info from UI
15 | #[derive(Debug, Clone, Serialize, Deserialize)]
16 | pub struct UIPlaylist {
17 | // base64
18 | pub data: String,
19 | pub filename: String,
20 | pub format: PlaylistFormat
21 | }
22 |
23 | impl UIPlaylist {
24 | pub fn get_files(&self) -> Result, Error> {
25 | let files = match self.format {
26 | PlaylistFormat::M3U => {
27 | // Decode base64 from JS
28 | let bytes = base64::engine::general_purpose::STANDARD.decode(self.data[self.data.find(';').ok_or(anyhow!("Invalid data!"))? + 8..].trim())?;
29 | let m3u = String::from_utf8(bytes)?;
30 | get_files_from_m3u(&m3u, None)
31 | }
32 | };
33 | // Filter extensions
34 | let out = files
35 | .iter()
36 | .filter(|f| EXTENSIONS.iter().any(|e| f.extension().unwrap_or_default().to_ascii_lowercase() == *e))
37 | .map(PathBuf::from).collect();
38 | Ok(out)
39 | }
40 | }
41 |
42 | #[derive(Debug, Clone, Serialize, Deserialize)]
43 | #[serde(rename_all = "lowercase")]
44 | pub enum PlaylistFormat {
45 | M3U
46 | }
47 |
48 |
49 | /// Get files from any playlist format
50 | pub fn get_files_from_playlist_file(path: impl AsRef) -> Result, Error> {
51 | // Validate extension
52 | if !PLAYLIST_EXTENSIONS.iter().any(|e| &&path.as_ref().extension().unwrap_or_default().to_string_lossy().to_lowercase() == e) {
53 | return Err(anyhow!("Unsupported playlist!").into());
54 | };
55 |
56 | // Load file
57 | let mut file = File::open(&path)?;
58 | let mut buf = vec![];
59 | file.read_to_end(&mut buf)?;
60 |
61 | // TODO: Check format if multiple
62 |
63 | // M3U
64 | let data = String::from_utf8(buf)?;
65 | Ok(get_files_from_m3u(&data, Some(path.as_ref().parent().unwrap().to_owned())))
66 | }
67 |
68 |
69 | /// Get file list from M3U playlist
70 | pub fn get_files_from_m3u(m3u: &str, base_path: Option) -> Vec {
71 | let clean = m3u.replace("\r", "\n").replace("\n\n", "\n");
72 | let entries = clean.split("\n");
73 | let mut out = vec![];
74 | for entry in entries {
75 | if !entry.starts_with("#") && !entry.starts_with("http://") && !entry.is_empty() {
76 | // Decode
77 | let entry = match urlencoding::decode(entry) {
78 | Ok(e) => e.to_string(),
79 | Err(e) => {
80 | warn!("Failed URLDecode: {e}");
81 | entry.to_string()
82 | }
83 | };
84 |
85 | if base_path.is_none() {
86 | out.push(entry.trim().to_string());
87 | } else {
88 | // Add base path
89 | out.push(base_path.clone().unwrap().join(entry).to_str().unwrap().to_string());
90 | }
91 | }
92 | }
93 | out.into_iter().map(|i| i.into()).collect()
94 | }
95 |
96 | /// Generate m3u playlist from paths
97 | pub fn create_m3u_playlist(paths: &Vec) -> String {
98 | let mut playlist = "#EXTM3U\r\n".to_string();
99 | for path in paths {
100 | playlist = format!("{playlist}{}\r\n", path.to_string_lossy());
101 | }
102 | playlist
103 | }
104 |
--------------------------------------------------------------------------------
/crates/onetagger-renamer/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-renamer"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | pad = "0.1"
11 | dunce = "1.0"
12 | regex = "1.10"
13 | anyhow = "1.0"
14 | titlecase = "3.3"
15 | lazy_static = "1.5"
16 | pulldown-cmark = "0.12"
17 |
18 | serde = { version = "1.0", features = ["derive"] }
19 |
20 | onetagger-tag = { path = "../onetagger-tag" }
21 | onetagger-tagger = { path = "../onetagger-tagger" }
22 |
--------------------------------------------------------------------------------
/crates/onetagger-shared/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-shared"
3 | version = "1.7.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | anyhow = "1.0"
11 | chrono = "0.4"
12 | backtrace = "0.3"
13 | crossterm = "0.28"
14 | serde_json = "1.0"
15 | directories = "5.0"
16 | lazy_static = "1.5"
17 |
18 | fern = { version = "0.6", features = ["colored"] }
19 | serde = { version = "1.0", features = ["derive"] }
--------------------------------------------------------------------------------
/crates/onetagger-shared/build.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::Read;
3 | use std::path::Path;
4 |
5 | fn main() {
6 | // Github Actions commit
7 | let mut commit = if let Ok(commit) = std::env::var("GITHUB_SHA") {
8 | commit
9 | } else {
10 | // Local commit
11 | if let Ok(mut f) = File::open(Path::new("../../.git").join("refs").join("heads").join("master")) {
12 | let mut buf = String::new();
13 | f.read_to_string(&mut buf).ok();
14 | buf
15 | } else {
16 | String::new()
17 | }
18 | };
19 | // Trim
20 | if commit.len() > 8 {
21 | commit = commit[..8].to_string()
22 | }
23 | if commit.is_empty() {
24 | commit = "unknown".to_string();
25 | }
26 | println!("cargo:rustc-env=COMMIT={}", commit);
27 | }
--------------------------------------------------------------------------------
/crates/onetagger-tag/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-tag"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | chrono = "0.4"
10 | anyhow = "1.0"
11 |
12 | log = { version = "0.4", optional = true }
13 | id3 = { version = "1.12.0", optional = true }
14 | riff = { version = "2.0.0", optional = true }
15 | lofty = { version = "0.21", optional = true }
16 | base64 = { version = "0.22", optional = true }
17 | mp4ameta = { version = "0.11", optional = true }
18 | metaflac = { version = "0.2.5", optional = true }
19 | once_cell = { version = "1.19", optional = true }
20 |
21 | serde = { version = "1.0", features = ["derive"] }
22 |
23 | [features]
24 | default = ["tag"]
25 | tag = ["id3", "mp4ameta", "metaflac", "base64", "log", "riff", "lofty", "once_cell"]
--------------------------------------------------------------------------------
/crates/onetagger-tag/src/wav.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use std::io::{BufReader, BufWriter, Cursor, Seek, SeekFrom};
3 | use std::fs::File;
4 | use std::collections::HashMap;
5 | use std::path::Path;
6 | use id3::{Tag, TagLike, Frame, Version};
7 | use once_cell::sync::Lazy;
8 | use riff::{Chunk, ChunkId, LIST_ID, RIFF_ID, ChunkContents};
9 |
10 | /// RIFF `id3 ` chunk
11 | static ID3_ID_1: ChunkId = ChunkId { value: [0x69, 0x64, 0x33, 0x20] };
12 | static ID3_ID_2: ChunkId = ChunkId { value: [0x49, 0x44, 0x33, 0x20] };
13 | /// RIFF WAVE chunk
14 | static WAVE_ID: ChunkId = ChunkId { value: [0x57, 0x41, 0x56, 0x45] };
15 | static INFO_ID: ChunkId = ChunkId { value: [0x49, 0x4E, 0x46, 0x4F] };
16 |
17 | /// ID3 Frame to RIFF ChunkID
18 | static ID3_RIFF: Lazy> = Lazy::new(|| {
19 | let mut m = HashMap::new();
20 | m.insert("TIT2", ChunkId::new("INAM").unwrap());
21 | m.insert("TALB", ChunkId::new("IPRD").unwrap());
22 | m.insert("TPE1", ChunkId::new("IART").unwrap());
23 | m.insert("COMM", ChunkId::new("ICMT").unwrap());
24 | m.insert("TCON", ChunkId::new("IGNR").unwrap());
25 | m.insert("TSRC", ChunkId::new("ISRC").unwrap());
26 | m
27 | });
28 |
29 | /// Write wav to path
30 | /// Will copy ID3 meta into RIFF INFO chunk
31 | pub(crate) fn write_wav(path: impl AsRef, tag: Tag, version: Version) -> Result<(), Error> {
32 | let mut file = BufReader::new(File::open(&path)?);
33 | let mut offset = 0;
34 | // Read all the chunks
35 | let mut riff_chunks = vec![];
36 | let mut list_chunks = vec![];
37 | loop {
38 | let chunk = match Chunk::read(&mut file, offset) {
39 | Ok(chunk) => chunk,
40 | Err(_) => break
41 | };
42 | // Search for parent chunks
43 | if chunk.id() != LIST_ID && chunk.id() != RIFF_ID {
44 | offset += 4;
45 | continue;
46 | }
47 | // Save chunks
48 | for child in chunk.iter(&mut file) {
49 | let child = child?;
50 | if chunk.id() == LIST_ID {
51 | list_chunks.push(child);
52 | } else {
53 | riff_chunks.push(child);
54 | }
55 | }
56 |
57 | offset += chunk.len() as u64;
58 | }
59 |
60 |
61 | // Get all the RIFF chunks
62 | let mut resolved_riff_chunks = vec![];
63 | for chunk in riff_chunks {
64 | // Resolve LIST chunks
65 | if chunk.id() == LIST_ID {
66 | for child in chunk.iter(&mut file) {
67 | let child = child?;
68 | if child.id() != RIFF_ID && child.id() != LIST_ID {
69 | list_chunks.push(child);
70 | }
71 | }
72 | continue;
73 | }
74 | // Resolve nested (because of SOME apps)
75 | if chunk.id() == RIFF_ID {
76 | for child in chunk.iter(&mut file) {
77 | let child = child?;
78 | if child.id() != RIFF_ID && child.id() != LIST_ID {
79 | resolved_riff_chunks.push(child);
80 | }
81 | }
82 | continue;
83 | }
84 | resolved_riff_chunks.push(chunk);
85 | }
86 | let riff_chunks = resolved_riff_chunks;
87 |
88 | // Generate the RIFF chunk
89 | let mut riff_data = vec![];
90 | for chunk in riff_chunks {
91 | // skip old ID3 chunk
92 | if chunk.id() == ID3_ID_1 || chunk.id() == ID3_ID_2 {
93 | continue;
94 | }
95 | // Passthru
96 | let data = ChunkContents::Data(chunk.id(), chunk.read_contents(&mut file)?);
97 | riff_data.push(data);
98 | }
99 | // Add ID3 chunk
100 | let mut out = vec![];
101 | tag.write_to(Cursor::new(&mut out), version)?;
102 | riff_data.push(ChunkContents::Data(ID3_ID_1.clone(), out));
103 | let riff_chunk = ChunkContents::Children(RIFF_ID.clone(), WAVE_ID.clone(), riff_data);
104 |
105 | // Generate LIST chunk
106 | let mut list_data = vec![];
107 | for frame in tag.frames() {
108 | if let Some(chunk_id) = ID3_RIFF.get(frame.id()) {
109 | if let Some(text) = frame.content().text() {
110 | let data = ChunkContents::Data(chunk_id.clone(), format!("{text}").as_bytes().to_owned());
111 | list_data.push(data);
112 | }
113 | }
114 | }
115 |
116 | // Add original LIST chunks
117 | for chunk in list_chunks {
118 | if list_data.iter().any(|c| matches!(c, ChunkContents::Data(id, _) if id == &chunk.id())) {
119 | continue;
120 | }
121 | let data = ChunkContents::Data(chunk.id(), chunk.read_contents(&mut file)?);
122 | list_data.push(data);
123 | }
124 | let list_chunk = ChunkContents::Children(LIST_ID.clone(), INFO_ID.clone(), list_data);
125 |
126 | // Write to file
127 | let mut file = BufWriter::new(File::create(path)?);
128 | riff_chunk.write(&mut file)?;
129 | list_chunk.write(&mut file)?;
130 |
131 | Ok(())
132 | }
133 |
134 | /// Read WAV from file, will copy missing tags from RIFF to ID3
135 | pub(crate) fn read_wav(path: impl AsRef) -> Result {
136 | let mut file = BufReader::new(File::open(path)?);
137 | let mut offset = 0;
138 | // Read all the chunks
139 | let mut chunks = vec![];
140 | loop {
141 | let chunk = match Chunk::read(&mut file, offset) {
142 | Ok(chunk) => chunk,
143 | Err(_) => break
144 | };
145 | // Search for parent chunks
146 | if chunk.id() != LIST_ID && chunk.id() != RIFF_ID {
147 | offset += 4;
148 | continue;
149 | }
150 | for child in chunk.iter(&mut file) {
151 | chunks.push(child?);
152 | }
153 | offset += chunk.len() as u64;
154 | }
155 |
156 | // Resolve nested chunks because SOME apps do that
157 | let mut new_chunks = vec![];
158 | for chunk in chunks {
159 | // Resolve nested
160 | if chunk.id() == RIFF_ID || chunk.id() == LIST_ID {
161 | for c in chunk.iter(&mut file) {
162 | new_chunks.push(c?);
163 | }
164 | continue;
165 | }
166 | new_chunks.push(chunk);
167 | }
168 | let chunks = new_chunks;
169 |
170 | // Read ID3 using the new ID3 reader
171 | file.seek(SeekFrom::Start(0))?;
172 | let mut id3 = Tag::read_from2(&mut file).unwrap_or(Tag::new());
173 |
174 | // Copy tags from RIFF to ID3 if missing
175 | for (frame_name, chunk_id) in ID3_RIFF.iter() {
176 | if id3.get(frame_name).is_none() {
177 | if let Some(chunk) = chunks.iter().find(|c| &c.id() == chunk_id) {
178 | let data = chunk.read_contents(&mut file)?;
179 | if let Ok(data) = String::from_utf8(data) {
180 | id3.add_frame(Frame::text(frame_name, data.replace("\0", "")));
181 | }
182 | }
183 | }
184 | }
185 |
186 | Ok(id3)
187 | }
--------------------------------------------------------------------------------
/crates/onetagger-tagger/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-tagger"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | log = "0.4"
10 | regex = "1.10"
11 | anyhow = "1.0"
12 | strsim = "0.11"
13 | unidecode = "0.3"
14 | serde_json = "1.0"
15 |
16 | serde = { version = "1.0", features = ["derive"] }
17 | chrono = { version = "0.4", features = ["serde"] }
18 |
19 | onetagger-tag = { path = "../onetagger-tag", default-features = false }
20 |
21 | [features]
22 | default = []
--------------------------------------------------------------------------------
/crates/onetagger-ui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger-ui"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 |
7 | [dependencies]
8 | log = "0.4"
9 | mime = "0.3"
10 | dunce = "1.0"
11 | trash = "5.1"
12 | image = "0.25"
13 | anyhow = "1.0"
14 | opener = "0.7"
15 | base64 = "0.22"
16 | walkdir = "2.3"
17 | serde_json = "1.0"
18 | webbrowser = "1.0"
19 | mime_guess = "2.0"
20 | urlencoding = "2.1"
21 | include_dir = "0.7"
22 | directories = "5.0"
23 | tinyfiledialogs = "3.9"
24 |
25 | axum = { version = "0.7", features = ["ws"] }
26 | serde = { version = "1.0", features = ["derive"] }
27 | tokio = { version = "1.39", features = ["rt-multi-thread"] }
28 |
29 | onetagger-tag = { path = "../onetagger-tag" }
30 | onetagger-shared = { path = "../onetagger-shared" }
31 | onetagger-tagger = { path = "../onetagger-tagger" }
32 | onetagger-player = { path = "../onetagger-player" }
33 | onetagger-autotag = { path = "../onetagger-autotag" }
34 | onetagger-renamer = { path = "../onetagger-renamer" }
35 | onetagger-playlist = { path = "../onetagger-playlist" }
36 | onetagger-platforms = { path = "../onetagger-platforms" }
37 |
38 | # Windows specific
39 | [target.'cfg(windows)'.dependencies]
40 | sysinfo = "0.31"
41 |
42 |
--------------------------------------------------------------------------------
/crates/onetagger-ui/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[macro_use] extern crate log;
2 | #[macro_use] extern crate anyhow;
3 | #[macro_use] extern crate include_dir;
4 | #[macro_use] extern crate onetagger_shared;
5 |
6 | use std::net::SocketAddr;
7 | use std::time::Duration;
8 | use anyhow::Error;
9 | use axum::body::Body;
10 | use axum::extract::{Query, WebSocketUpgrade, State, Request};
11 | use axum::http::StatusCode;
12 | use axum::http::header::CONTENT_TYPE;
13 | use axum::response::IntoResponse;
14 | use axum::Router;
15 | use axum::routing::get;
16 | use include_dir::Dir;
17 | use onetagger_player::AudioSources;
18 | use serde::{Serialize, Deserialize};
19 | use quicktag::QuickTagFile;
20 | use tokio::runtime::Builder;
21 | use tokio::net::TcpListener;
22 | use onetagger_shared::{PORT, WEBSERVER_CALLBACKS};
23 |
24 | pub mod socket;
25 | pub mod browser;
26 | pub mod quicktag;
27 | pub mod tageditor;
28 |
29 | static CLIENT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../client/dist");
30 |
31 | // Should have data from arguments and other flags (eg. port / host in future)
32 | #[derive(Debug, Clone, Serialize, Deserialize)]
33 | #[serde(rename_all = "camelCase")]
34 | pub struct StartContext {
35 | pub server_mode: bool,
36 | pub start_path: Option,
37 | pub expose: bool,
38 | pub browser: bool,
39 | }
40 |
41 | fn start_async_runtime(context: StartContext) -> Result<(), Error> {
42 | let expose = context.expose;
43 | Builder::new_multi_thread().enable_all().build()?.block_on(async move {
44 | // Register routes
45 | let app = Router::new()
46 | .route("/thumb", get(get_thumb))
47 | .route("/audio", get(get_audio))
48 | .route("/ws", get(get_ws))
49 | .route("/spotify", get(get_spotify_callback))
50 | .route("/*path", get(get_static_file))
51 | .route("/", get(get_static_file))
52 | .with_state(context);
53 |
54 | // Start http server
55 | let host = match expose {
56 | true => format!("0.0.0.0:{PORT}"),
57 | false => format!("127.0.0.1:{PORT}")
58 | };
59 | info!("Starting web server on: http://{host}");
60 | let listener = TcpListener::bind(host).await?;
61 | axum::serve(listener, app.into_make_service_with_connect_info::()).await?;
62 |
63 | Ok::<(), Error>(())
64 | })?;
65 | Ok(())
66 | }
67 |
68 | /// Serve assets file
69 | async fn get_static_file(request: Request) -> impl IntoResponse {
70 | let mut path = request.uri().to_string();
71 | // Index HTML
72 | if path == "/" {
73 | path = "/index.html".to_string();
74 | }
75 | path = path[1..].to_string();
76 |
77 | // Static files
78 | if let Some(file) = CLIENT_DIR.get_file(&path) {
79 | let mime = mime_guess::from_path(&path).first().unwrap_or(mime::APPLICATION_OCTET_STREAM);
80 | return (StatusCode::OK, [(CONTENT_TYPE, mime.to_string())], file.contents().to_vec());
81 | }
82 |
83 | (StatusCode::NOT_FOUND, [(CONTENT_TYPE, "text/plain".to_string())], "Not found".as_bytes().to_vec())
84 | }
85 |
86 | #[derive(Debug, Clone, Deserialize)]
87 | struct GetQueryPath {
88 | path: String
89 | }
90 |
91 | /// Serve thumbnail
92 | async fn get_thumb(Query(GetQueryPath { path }): Query) -> impl IntoResponse {
93 | match QuickTagFile::get_art(&path) {
94 | Ok(art) => (StatusCode::OK, [(CONTENT_TYPE, "image/jpeg".to_string())], art),
95 | Err(e) => {
96 | warn!("Error loading album art: {} File: {}", e, path);
97 | (StatusCode::NOT_FOUND, [(CONTENT_TYPE, "text/plain".to_string())], format!("Error loading album art: {} File: {}", e, path).into_bytes())
98 | }
99 | }
100 | }
101 |
102 | /// Serve audio
103 | async fn get_audio(Query(GetQueryPath { path }): Query) -> impl IntoResponse {
104 | let data = tokio::task::spawn_blocking(move || {
105 | match AudioSources::from_path(&path).map(|s| s.generate_wav()) {
106 | Ok(Ok(wav)) => wav,
107 | Ok(Err(e)) => {
108 | warn!("Failed generating wav: {e}");
109 | vec![]
110 | },
111 | Err(e) => {
112 | warn!("Failed opening audio file {path}: {e}");
113 | vec![]
114 | }
115 | }
116 | }).await.unwrap_or(vec![]);
117 |
118 | // Empty 404 on error
119 | if data.is_empty() {
120 | return (StatusCode::NOT_FOUND, [(CONTENT_TYPE, "text/plain")], vec![]);
121 | }
122 | (StatusCode::OK, [(CONTENT_TYPE, "audio/wav")], data)
123 | }
124 |
125 | /// WS connection
126 | async fn get_ws(ws: WebSocketUpgrade, State(context): State) -> impl IntoResponse {
127 | ws.on_upgrade(move |socket| {
128 | debug!("WS Connected!");
129 | async move {
130 | match socket::handle_ws_connection(socket, context).await {
131 | Ok(_) => {},
132 | Err(e) => warn!("WS connection error: {e}"),
133 | }
134 | debug!("WS Disconnected!");
135 | }
136 | })
137 | }
138 |
139 | /// Spotify token callback
140 | async fn get_spotify_callback(request: Request) -> impl IntoResponse {
141 | info!("Got Spotify token from callback");
142 | WEBSERVER_CALLBACKS.lock().unwrap().insert("spotify".to_string(), request.uri().to_string());
143 | (StatusCode::OK, [(CONTENT_TYPE, "text/html")], include_str!("../../../assets/spotify_callback.html"))
144 | }
145 |
146 | // Start everything
147 | pub fn start_all(context: StartContext) -> Result<(), Error> {
148 | if context.expose {
149 | warn!("Server is exposed to public!");
150 | }
151 |
152 | // Open in browser with 1s delay to allow the srever to load
153 | if context.browser {
154 | std::thread::spawn(move || {
155 | std::thread::sleep(Duration::from_secs(1));
156 | webbrowser::open(&format!("http://127.0.0.1:{PORT}")).ok();
157 | });
158 | }
159 |
160 | start_async_runtime(context.clone())?;
161 | Ok(())
162 | }
163 |
164 |
--------------------------------------------------------------------------------
/crates/onetagger-ui/src/quicktag.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use std::path::PathBuf;
3 | use std::collections::HashMap;
4 | use std::fs::read_dir;
5 | use std::path::Path;
6 | use std::io::Cursor;
7 | use walkdir::WalkDir;
8 | use image::{ImageFormat, ImageReader};
9 | use serde::{Deserialize, Serialize};
10 | use onetagger_tag::{AudioFileFormat, Field, Tag, EXTENSIONS, TagSeparators};
11 | use onetagger_playlist::{UIPlaylist, get_files_from_playlist_file};
12 |
13 | pub struct QuickTag {}
14 |
15 | impl QuickTag {
16 | /// Load all files from folder
17 | pub fn load_files_path(path: impl AsRef, recursive: bool, separators: &TagSeparators, skip: usize, limit: usize) -> Result {
18 | // Check if path to playlist
19 | if !path.as_ref().is_dir() {
20 | return QuickTag::load_files(get_files_from_playlist_file(path)?, separators);
21 | }
22 |
23 | let mut files = vec![];
24 | // Load recursivly
25 | if recursive {
26 | for e in WalkDir::new(path) {
27 | if let Ok(entry) = e {
28 | // Filter extensions to not make large arrays
29 | if EXTENSIONS.iter().any(|e| entry.path().extension().unwrap_or_default().to_ascii_lowercase() == *e) {
30 | files.push(entry.path().to_owned());
31 | }
32 | }
33 | }
34 | } else {
35 | // Load just dir
36 | for entry in read_dir(path)? {
37 | // Check if valid
38 | if entry.is_err() {
39 | continue;
40 | }
41 | let entry = entry.unwrap();
42 | // Skip dirs
43 | if entry.path().is_dir() {
44 | continue;
45 | }
46 | // Filter extensions to not make large arrays
47 | if EXTENSIONS.iter().any(|e| entry.path().extension().unwrap_or_default().to_ascii_lowercase() == *e) {
48 | files.push(entry.path());
49 | }
50 | }
51 | }
52 |
53 | let files = files.into_iter().skip(skip).take(limit).collect();
54 | QuickTag::load_files(files, separators)
55 | }
56 |
57 | /// Load all files from playlist
58 | pub fn load_files_playlist(playlist: &UIPlaylist, separators: &TagSeparators) -> Result {
59 | QuickTag::load_files(playlist.get_files()?, separators)
60 | }
61 |
62 | /// Check extension and load file
63 | pub fn load_files(files: Vec, separators: &TagSeparators) -> Result {
64 | let mut out = vec![];
65 | let mut failed = vec![];
66 | for path in files {
67 | if EXTENSIONS.iter().any(|e| path.extension().unwrap_or_default().to_ascii_lowercase() == *e) {
68 | match QuickTagFile::from_path(&path, separators) {
69 | Ok(t) => out.push(t),
70 | Err(e) => {
71 | failed.push(QuickTagFailed::new(&path, e.to_string()));
72 | error!("Error loading file: {:?} {}", path, e);
73 | }
74 | }
75 | }
76 | }
77 | Ok(QuickTagData {
78 | files: out,
79 | failed
80 | })
81 | }
82 |
83 | }
84 |
85 | #[derive(Debug, Clone, Serialize, Deserialize, Default)]
86 | pub struct QuickTagData {
87 | pub files: Vec,
88 | pub failed: Vec
89 | }
90 |
91 | #[derive(Debug, Clone, Serialize, Deserialize)]
92 | pub struct QuickTagFailed {
93 | pub path: PathBuf,
94 | pub error: String
95 | }
96 |
97 | impl QuickTagFailed {
98 | /// Create new instance
99 | pub fn new(path: impl AsRef, error: impl Into) -> QuickTagFailed {
100 | QuickTagFailed { path: path.as_ref().into(), error: error.into() }
101 | }
102 | }
103 |
104 | #[derive(Serialize, Deserialize, Clone, Debug)]
105 | #[serde(rename_all = "camelCase")]
106 | pub struct QuickTagFile {
107 | path: PathBuf,
108 | format: AudioFileFormat,
109 | title: String,
110 | artists: Vec,
111 | genres: Vec,
112 | bpm: Option,
113 | rating: u8,
114 | tags: HashMap>,
115 | year: Option,
116 | key: Option
117 | }
118 |
119 | impl QuickTagFile {
120 | /// Load tags from path
121 | pub fn from_path(path: impl AsRef, separators: &TagSeparators) -> Result {
122 | let mut tag_wrap = Tag::load_file(&path, false)?;
123 | tag_wrap.set_separators(separators);
124 | Ok(QuickTagFile::from_tag(path, &tag_wrap)?)
125 | }
126 |
127 | /// Load tags from `Tag`
128 | pub fn from_tag(path: impl AsRef, tag_wrap: &Tag) -> Result {
129 | let tag = tag_wrap.tag();
130 | let mut all_tags = tag.all_tags();
131 | // Insert overriden tags
132 | if let Some(v) = tag.get_raw("COMM") {
133 | all_tags.insert("COMM".to_string(), v);
134 | }
135 | if let Some(v) = tag.get_raw("USLT") {
136 | all_tags.insert("USLT".to_string(), v);
137 | }
138 |
139 | // Filter null bytes
140 | let all_tags = all_tags.into_iter().map(|(k, v)| (k, v.into_iter().map(|v| v.replace("\0", "")).collect::>())).collect();
141 |
142 | Ok(QuickTagFile {
143 | path: path.as_ref().to_owned(),
144 | format: tag_wrap.format(),
145 | title: tag.get_field(Field::Title).ok_or(anyhow!("Missing title tag"))?.first().ok_or(anyhow!("Missing title"))?.to_string(),
146 | artists: tag.get_field(Field::Artist).ok_or(anyhow!("Missing artist tag"))?,
147 | genres: tag.get_field(Field::Genre).unwrap_or(vec![]),
148 | rating: tag.get_rating().unwrap_or(0),
149 | bpm: match tag.get_field(Field::BPM) {
150 | Some(t) => t.first().unwrap_or(&"can't parse".to_string()).parse().ok(),
151 | None => None
152 | },
153 | tags: all_tags,
154 | year: tag.get_date().map(|d| d.year),
155 | key: tag.get_field(Field::Key).map(|f| f.first().map(String::from)).flatten()
156 | })
157 | }
158 |
159 | /// Load album art from tag and downscale
160 | pub fn get_art(path: impl AsRef) -> Result, Error> {
161 | // Load
162 | let tag_wrap = Tag::load_file(path, false)?;
163 | let tag = tag_wrap.tag();
164 | let pictures = tag.get_art();
165 | let picture = pictures.first().ok_or(anyhow!("Missing album art!"))?;
166 | let img = ImageReader::new(Cursor::new(&picture.data)).with_guessed_format()?.decode()?;
167 | // Downscale and save
168 | let scaled = img.thumbnail_exact(50, 50);
169 | let mut out = vec![];
170 | scaled.write_to(&mut Cursor::new(&mut out), ImageFormat::Jpeg)?;
171 | Ok(out)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/crates/onetagger-ui/src/tageditor.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Error;
2 | use std::io::Cursor;
3 | use std::collections::HashMap;
4 | use std::path::{Path, PathBuf};
5 | use base64::Engine;
6 | use serde::{Serialize, Deserialize};
7 | use image::{GenericImageView, ImageReader};
8 |
9 | use onetagger_tag::{AudioFileFormat, CoverType, Picture, Tag};
10 | use onetagger_tag::id3::{ID3Comment, ID3Popularimeter};
11 |
12 | pub struct TagEditor {}
13 |
14 | impl TagEditor {
15 | // Load tags from file
16 | pub fn load_file(path: impl AsRef) -> Result {
17 | let filename = path.as_ref().file_name().ok_or(anyhow!("Invalid filename"))?.to_str().ok_or(anyhow!("Invalid filename!"))?;
18 | let tag_wrap = Tag::load_file(&path, true)?;
19 | let id3_binary = ID3Binary::from_tag(&tag_wrap);
20 | // Load tags
21 | let tag = tag_wrap.tag();
22 | let tags = tag.all_tags().iter().map(|(k, v)| {
23 | (k.to_owned(), v.join(",").replace('\0', ""))
24 | }).collect();
25 |
26 | // Load images
27 | let mut images = vec![];
28 | for picture in tag.get_art() {
29 | if let Ok(art) = TagEditor::load_art(picture) {
30 | images.push(art);
31 | }
32 | }
33 |
34 | Ok(TagEditorFile {
35 | tags,
36 | filename: filename.to_owned(),
37 | format: tag_wrap.format(),
38 | path: path.as_ref().to_owned(),
39 | images,
40 | id3: id3_binary
41 | })
42 | }
43 |
44 | // Load art and encode
45 | fn load_art(picture: Picture) -> Result {
46 | let img = ImageReader::new(Cursor::new(&picture.data)).with_guessed_format()?.decode()?;
47 | Ok(TagEditorImage {
48 | mime: picture.mime.to_string(),
49 | data: format!("data:{};base64,{}", &picture.mime, base64::engine::general_purpose::STANDARD.encode(picture.data)),
50 | kind: picture.kind.to_owned(),
51 | description: picture.description.to_owned(),
52 | width: img.dimensions().0,
53 | height: img.dimensions().1,
54 | })
55 | }
56 | }
57 |
58 | #[derive(Debug, Clone, Serialize, Deserialize)]
59 | pub struct FolderEntry {
60 | pub path: PathBuf,
61 | pub filename: String,
62 | pub dir: bool,
63 | pub playlist: bool
64 | }
65 |
66 | #[derive(Debug, Clone, Serialize, Deserialize)]
67 | pub struct TagEditorFile {
68 | pub tags: HashMap,
69 | pub filename: String,
70 | pub format: AudioFileFormat,
71 | pub path: PathBuf,
72 | pub images: Vec,
73 | pub id3: Option
74 | }
75 |
76 | #[derive(Debug, Clone, Serialize, Deserialize)]
77 | pub struct TagEditorImage {
78 | pub mime: String,
79 | pub data: String,
80 | pub kind: CoverType,
81 | pub description: String,
82 | pub width: u32,
83 | pub height: u32
84 | }
85 |
86 | // Binary ID3 tags
87 | #[derive(Debug, Clone, Serialize, Deserialize)]
88 | pub struct ID3Binary {
89 | pub comments: Vec,
90 | pub unsync_lyrics: Vec,
91 | pub popularimeter: Option
92 | }
93 |
94 | impl ID3Binary {
95 | pub fn from_tag(tag: &Tag) -> Option {
96 | match tag {
97 | Tag::ID3(t) => Some(ID3Binary {
98 | comments: t.get_comments(),
99 | unsync_lyrics: t.get_unsync_lyrics(),
100 | popularimeter: t.get_popularimeter()
101 | }),
102 | _ => None
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/crates/onetagger/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "onetagger"
3 | version = "1.7.0"
4 | edition = "2021"
5 | description = "App to tag your music library."
6 | keywords = ["gui", "audio"]
7 | categories = ["multimedia::audio"]
8 |
9 | [dependencies]
10 | log = "0.4"
11 | anyhow = "1.0"
12 | urlencoding = "2.1"
13 |
14 | tao = { version = "0.29", features = ["rwh_05"] }
15 | wry = { version = "0.42", features = ["devtools"] }
16 | clap = { version = "4.5", features = ["derive"] }
17 | serde = { version = "1.0", features = ["derive"] }
18 |
19 | onetagger-ui = { path = "../onetagger-ui" }
20 | onetagger-tag = { path = "../onetagger-tag" }
21 | onetagger-shared = { path = "../onetagger-shared" }
22 | onetagger-tagger = { path = "../onetagger-tagger" }
23 | onetagger-player = { path = "../onetagger-player" }
24 | onetagger-autotag = { path = "../onetagger-autotag" }
25 | onetagger-renamer = { path = "../onetagger-renamer" }
26 | onetagger-playlist = { path = "../onetagger-playlist" }
27 | onetagger-platforms = { path = "../onetagger-platforms" }
28 |
29 | [target.'cfg(windows)'.build-dependencies]
30 | winres = "0.1"
31 |
32 | # MacOS specific
33 | [target.'cfg(target_os = "macos")'.dependencies]
34 | muda = "0.14"
35 | native-dialog = "0.7.0"
36 |
37 |
38 | [package.metadata.bundle]
39 | name = "OneTagger"
40 | identifier = "com.marekkon5.onetagger"
41 | icon = ["../../assets/32x32.png", "../../assets/128x128.png", "../../assets/128x128@2x.png",
42 | "../../assets/icon.icns", "../../assets/icon.ico"]
43 | version = "1.7.0"
44 | resources = []
45 | copyright = "Copyright (c) Marekkon5 2022. All rights reserved."
46 | category = "Music"
47 | short_description = "Tool to tag your audio library."
48 | long_description = """
49 | Tool to tag your audio library.
50 | """
--------------------------------------------------------------------------------
/crates/onetagger/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | // Set Windows icon
3 | #[cfg(windows)]
4 | {
5 | let mut res = winres::WindowsResource::new();
6 | res.set_icon("..\\..\\assets\\icon.ico");
7 | res.compile().unwrap();
8 | }
9 | }
--------------------------------------------------------------------------------