[READ-ONLY] Mirror of https://github.com/probablykasper/mr-tagger. Music file tagging app for Mac, Linux and Windows
audio linux macos music tagger tauri windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Show cover art

Kasper (Aug 25, 2021, 3:28 AM +0200) 05ffbb6b 560294c0

+169 -45
+7
src-tauri/Cargo.lock
··· 90 90 checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 91 91 92 92 [[package]] 93 + name = "base64" 94 + version = "0.13.0" 95 + source = "registry+https://github.com/rust-lang/crates.io-index" 96 + checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 97 + 98 + [[package]] 93 99 name = "bincode" 94 100 version = "1.3.3" 95 101 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1454 1460 name = "mr-tagger" 1455 1461 version = "0.1.0" 1456 1462 dependencies = [ 1463 + "base64", 1457 1464 "id3", 1458 1465 "mp4ameta", 1459 1466 "serde",
+1
src-tauri/Cargo.toml
··· 15 15 tauri = { version = "1.0.0-beta.8", features = ["dialog-open", "dialog-save", "shell-open"] } 16 16 id3 = "0.6.4" 17 17 mp4ameta = "0.11.0" 18 + base64 = "0.13.0" 18 19 19 20 [features] 20 21 default = [ "custom-protocol" ]
+60 -16
src-tauri/src/cmd.rs
··· 1 1 use crate::frames::{get_frames, Frame, Metadata}; 2 2 use crate::throw; 3 + use base64; 3 4 use serde::Serialize; 4 5 use std::path::PathBuf; 5 6 use std::sync::{Arc, Mutex}; ··· 29 30 }); 30 31 } 31 32 32 - #[derive(Serialize)] 33 - pub struct Info { 34 - path: PathBuf, 35 - frames: Vec<Frame>, 36 - } 37 - 38 - #[command] 39 - pub async fn open(path: PathBuf, app: State<'_, Data>) -> Result<Option<Info>, String> { 40 - let mut app = app.0.lock().unwrap(); 33 + fn get_metadata(path: &PathBuf) -> Result<Metadata, String> { 41 34 let ext = path.extension().unwrap_or_default().to_string_lossy(); 42 35 let metadata = match ext.as_ref() { 43 36 "mp3" | "aiff" | "wav" => { ··· 45 38 Ok(tag) => tag, 46 39 Err(_) => id3::Tag::new(), 47 40 }; 48 - for frame in tag.frames() { 49 - println!("MP3 FRAME {:?}", frame); 50 - } 41 + // for frame in tag.frames() { 42 + // println!("MP3 FRAME {:?}", frame); 43 + // } 51 44 Metadata::Id3(tag) 52 45 } 53 46 "m4a" | "mp4" | "m4p" | "m4b" | "m4r" | "m4v" => { ··· 55 48 Ok(tag) => tag, 56 49 Err(_) => throw!("No tags found"), 57 50 }; 58 - for (ident, data) in tag.data() { 59 - println!("M4A FRAME {:?}, {:?}", ident, data); 60 - } 51 + // for (ident, data) in tag.data() { 52 + // println!("M4A FRAME {:?}, {:?}", ident, data); 53 + // } 61 54 Metadata::Mp4(tag) 62 55 } 63 56 _ => throw!("Unsupported file type"), 64 57 }; 58 + Ok(metadata) 59 + } 60 + 61 + #[derive(Serialize)] 62 + pub struct Image { 63 + data: String, 64 + mime_type: String, 65 + } 66 + 67 + #[derive(Serialize)] 68 + pub struct Info { 69 + path: PathBuf, 70 + artwork: Option<Image>, 71 + frames: Vec<Frame>, 72 + } 73 + 74 + #[command] 75 + pub async fn open(path: PathBuf, app: State<'_, Data>) -> Result<Option<Info>, String> { 76 + let mut app = app.0.lock().unwrap(); 77 + let metadata = get_metadata(&path)?; 65 78 app.open_item = Some(Item { 66 79 path: path.clone(), 67 80 metadata: metadata.clone(), 68 81 }); 69 82 let info = Info { 70 - path: path, 83 + path, 84 + artwork: match metadata { 85 + Metadata::Id3(ref tag) => { 86 + let mut img = match tag.pictures().next() { 87 + Some(pic) => Some(Image { 88 + data: base64::encode(&pic.data), 89 + mime_type: pic.mime_type.clone(), 90 + }), 91 + None => None, 92 + }; 93 + for pic in tag.pictures() { 94 + if pic.picture_type == id3::frame::PictureType::CoverFront { 95 + img = Some(Image { 96 + data: base64::encode(&pic.data), 97 + mime_type: pic.mime_type.clone(), 98 + }); 99 + } 100 + } 101 + img 102 + } 103 + Metadata::Mp4(ref tag) => match tag.artwork() { 104 + Some(artwork) => Some(Image { 105 + data: base64::encode(&artwork.data), 106 + mime_type: match artwork.fmt { 107 + mp4ameta::ImgFmt::Bmp => "BMP".to_string(), 108 + mp4ameta::ImgFmt::Jpeg => "JPEG".to_string(), 109 + mp4ameta::ImgFmt::Png => "PNG".to_string(), 110 + }, 111 + }), 112 + None => None, 113 + }, 114 + }, 71 115 frames: get_frames(&metadata), 72 116 }; 73 117 Ok(Some(info))
+2 -2
src-tauri/src/main.rs
··· 87 87 .transparent(false) 88 88 .decorations(true) 89 89 .always_on_top(false) 90 - .inner_size(800.0, 600.0) 91 - .min_inner_size(300.0, 150.0) 90 + .inner_size(800.0, 550.0) 91 + .min_inner_size(400.0, 200.0) 92 92 .skip_taskbar(false) 93 93 .fullscreen(false); 94 94 return (win, webview);
+30 -13
src/App.svelte
··· 42 42 43 43 <main> 44 44 <div class="sidebar"> 45 - <div class="row"> 45 + <div class="topbar"> 46 46 <button on:click={openDialog}>Open Files</button> 47 47 </div> 48 - {#each openFiles as file, i} 49 - <div class="row" class:selected={i === selected} on:click={() => open(i)} 50 - >{file.replace(/^.*[\\\/]/, '')}</div> 51 - {/each} 52 - <FileDrop fileExtensions={['mp3']} handleFiles={addFiles} msg="" /> 48 + <div class="files"> 49 + {#each openFiles as file, i} 50 + <div class="row" class:selected={i === selected} on:click={() => open(i)} 51 + >{file.replace(/^.*[\\\/]/, '')}</div> 52 + {/each} 53 + </div> 54 + <FileDrop 55 + fileExtensions={['mp3', 'aiff', 'wav', 'm4a', 'mp4', 'm4p', 'm4b', 'm4r', 'm4v']} 56 + handleFiles={addFiles} 57 + msg="" /> 53 58 </div> 54 - <div class="page"> 59 + <div class="main"> 55 60 {#if item} 56 61 <ItemView {item} /> 57 62 {/if} ··· 65 70 font-size: 18px 66 71 background-color: #191B20 67 72 overflow: hidden 73 + user-select: none 74 + -webkit-user-select: none 68 75 main 69 76 display: flex 70 77 color: #e6e6e6 71 78 height: 100vh 72 - .page 79 + .main 73 80 flex-grow: 1 81 + width: 0px 82 + overflow: auto 74 83 .sidebar 75 84 position: relative 76 - width: 230px 85 + display: flex 86 + flex-direction: column 87 + width: 250px 77 88 height: 100% 78 - background-color: rgba(#ffffff, 0.03) 89 + background-color: #202227 79 90 border-right: 1px solid rgba(#ffffff, 0.1) 80 - font-size: 13px 91 + font-size: 12px 92 + .topbar 93 + padding: 8px 94 + border-bottom: 1px solid rgba(#ffffff, 0.1) 95 + .files 96 + overflow-y: auto 97 + height: 100% 81 98 .row 82 - padding: 8px 99 + padding: 7px 8px 83 100 cursor: default 84 101 .row:nth-child(2n) 85 102 background-color: rgba(#ffffff, 0.05) 86 103 .row.selected 87 - background-color: #074a97 104 + background-color: #7f0644 88 105 </style>
+1 -8
src/components/FileDrop.svelte
··· 9 9 export let fileExtensions: string[] = [] 10 10 export let handleFiles: (files: string[]) => void 11 11 12 - // workaround for https://github.com/tauri-apps/tauri/issues/2323 13 - let readyToListen = false 14 - setTimeout(() => (readyToListen = true), 100) 15 - 16 12 function getValidPaths(paths: string[]) { 17 13 let validPaths = [] 18 14 for (const path of paths) { 19 15 for (const ext of fileExtensions) { 20 16 if (path.endsWith('.' + ext)) { 21 17 validPaths.push(path) 18 + break 22 19 } 23 20 } 24 21 } ··· 26 23 } 27 24 onMount(() => { 28 25 const unlisten = event.listen('tauri://file-drop-hover', (e) => { 29 - if (!readyToListen) return 30 26 const validPaths = getValidPaths(e.payload as string[]) 31 27 if (validPaths.length > 0) { 32 28 droppable = true ··· 36 32 }) 37 33 onMount(() => { 38 34 const unlisten = event.listen('tauri://file-drop', (e) => { 39 - if (!readyToListen) return 40 35 const validPaths = getValidPaths(e.payload as string[]) 41 36 if (validPaths.length > 0) { 42 37 droppable = false ··· 47 42 }) 48 43 onMount(() => { 49 44 const unlisten = event.listen('tauri://file-drop-cancelled', (e) => { 50 - if (!readyToListen) return 51 45 droppable = false 52 46 }) 53 47 return extractUnlistener(unlisten) ··· 55 49 </script> 56 50 57 51 {#if droppable} 58 - <!-- if the overlay is always visible, it's not possible to scroll while dragging tracks --> 59 52 <div class="drag-overlay" transition:fade={{ duration: 100 }}> 60 53 <h1>{msg}</h1> 61 54 </div>
+68 -6
src/components/Item.svelte
··· 2 2 export type Frame = { 3 3 Text?: { id: string; value: string } 4 4 } 5 + export type Image = { 6 + data: Uint8Array 7 + mime_type: string 8 + } 5 9 export type Item = { 6 10 path: string 11 + artwork: Image | null 7 12 frames: Frame[] 8 13 } 9 14 let x = 0 // to fix syntax highlighting ··· 12 17 <script lang="ts"> 13 18 export let item: Item 14 19 console.log(item) 20 + let artworkSrc: string | null = null 21 + $: if (item.artwork) { 22 + artworkSrc = 'data:' + 'item.artwork.mime_type' + ';base64,' + '' + item.artwork.data 23 + // let blob = new Blob([item.artwork.data], { type: item.artwork.mime_type }) 24 + // artworkSrc = URL.createObjectURL(blob) 25 + } else { 26 + artworkSrc = null 27 + } 15 28 </script> 16 29 17 - <p>path: {item.path}</p> 18 - {#each item.frames as frame} 19 - <div class="item"> 20 - {#if frame.Text} 21 - <p>{frame.Text.id}: {frame.Text.value}</p> 30 + <main> 31 + <div class="left"> 32 + {#if artworkSrc} 33 + <div class="cover"> 34 + <img src={artworkSrc} alt="" /> 35 + </div> 36 + {:else} 37 + <div class="svg-cover"> 38 + <svg 39 + xmlns="http://www.w3.org/2000/svg" 40 + preserveAspectRatio="xMidYMin meet" 41 + width="24" 42 + height="24" 43 + viewBox="0 0 24 24"> 44 + <path 45 + d="M23 0l-15.996 3.585v13.04c-2.979-.589-6.004 1.671-6.004 4.154 0 2.137 1.671 3.221 3.485 3.221 2.155 0 4.512-1.528 4.515-4.638v-10.9l12-2.459v8.624c-2.975-.587-6 1.664-6 4.141 0 2.143 1.715 3.232 3.521 3.232 2.14 0 4.476-1.526 4.479-4.636v-17.364z" /> 46 + </svg> 47 + </div> 48 + {/if} 49 + {#if item.artwork} 50 + {item.artwork.mime_type} 22 51 {/if} 23 52 </div> 24 - {/each} 53 + <div class="right"> 54 + <div>{item.path}</div> 55 + {#each item.frames as frame} 56 + <div class="item"> 57 + {#if frame.Text} 58 + <p>{frame.Text.id}: {frame.Text.value}</p> 59 + {/if} 60 + </div> 61 + {/each} 62 + </div> 63 + </main> 64 + 65 + <style lang="sass"> 66 + main 67 + display: flex 68 + margin: 12px 69 + font-size: 14px 70 + .left 71 + margin-right: 12px 72 + min-width: 150px 73 + width: 20% 74 + img 75 + width: 100% 76 + .svg-cover 77 + width: 100% 78 + padding-bottom: 100% 79 + svg 80 + width: 100% 81 + height: 100% 82 + padding: 28% 83 + box-sizing: border-box 84 + background-color: #2b2c31 85 + fill: #45464a 86 + </style>