[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.

Move state to Rust

Kasper (Aug 25, 2021, 10:57 PM +0200) 89ae4032 d10db3b8

+118 -78
+69 -39
src-tauri/src/cmd.rs
··· 2 2 use crate::throw; 3 3 use base64; 4 4 use serde::Serialize; 5 + use serde_json::Value; 5 6 use std::path::PathBuf; 6 7 use std::sync::{Arc, Mutex}; 7 8 use std::thread; 8 9 use tauri::api::dialog; 9 10 use tauri::{command, State}; 10 11 11 - #[derive(Debug, Clone)] 12 - pub struct Item { 12 + #[derive(Debug, Clone, Serialize)] 13 + pub struct File { 13 14 path: PathBuf, 15 + #[serde(skip_serializing)] 14 16 metadata: Metadata, 15 17 } 16 18 17 - #[derive(Debug, Default)] 19 + #[derive(Debug, Default, Serialize)] 18 20 pub struct App { 19 - item: Option<Item>, 21 + current_index: usize, 22 + files: Vec<File>, 23 + } 24 + impl App { 25 + fn current_file(&mut self) -> Result<&mut File, String> { 26 + match self.files.get_mut(self.current_index) { 27 + Some(file) => Ok(file), 28 + None => throw!("Error getting open file"), 29 + } 30 + } 20 31 } 21 32 22 33 #[derive(Default)] ··· 32 43 33 44 fn get_metadata(path: &PathBuf) -> Result<Metadata, String> { 34 45 let ext = path.extension().unwrap_or_default().to_string_lossy(); 46 + let path_str = path.to_string_lossy(); 35 47 let metadata = match ext.as_ref() { 36 - "mp3" | "aiff" | "wav" => { 48 + "mp3" | "aiff" => { 37 49 let tag = match id3::Tag::read_from_path(&path) { 38 50 Ok(tag) => tag, 39 - Err(_) => id3::Tag::new(), 51 + Err(e) => match e.kind { 52 + id3::ErrorKind::NoTag => id3::Tag::default(), 53 + _ => throw!("Error reading tag for file {}: {}", path_str, e.description), 54 + }, 40 55 }; 41 - // for frame in tag.frames() { 42 - // println!("MP3 FRAME {:?}", frame); 43 - // } 44 56 Metadata::Id3(tag) 45 57 } 46 58 "m4a" | "mp4" | "m4p" | "m4b" | "m4r" | "m4v" => { 47 59 let tag = match mp4ameta::Tag::read_from_path(&path) { 48 60 Ok(tag) => tag, 49 - Err(_) => throw!("No tags found"), 61 + Err(e) => match e.kind { 62 + mp4ameta::ErrorKind::NoTag => mp4ameta::Tag::default(), 63 + _ => throw!("Error reading tag for file {}: {}", path_str, e.description), 64 + }, 50 65 }; 51 - // for (ident, data) in tag.data() { 52 - // println!("M4A FRAME {:?}, {:?}", ident, data); 53 - // } 54 66 Metadata::Mp4(tag) 55 67 } 56 68 _ => throw!("Unsupported file type"), ··· 58 70 Ok(metadata) 59 71 } 60 72 73 + #[command] 74 + pub async fn open_files(paths: Vec<PathBuf>, app: State<'_, Data>) -> Result<Value, String> { 75 + let mut app = app.0.lock().unwrap(); 76 + let initial_len = app.files.len(); 77 + for path in paths { 78 + let is_duplicate = app.files.iter().any(|f| f.path == path); 79 + if !is_duplicate { 80 + let metadata = get_metadata(&path)?; 81 + app.files.push(File { 82 + path: path.clone(), 83 + metadata: metadata.clone(), 84 + }); 85 + } 86 + } 87 + if initial_len == 0 { 88 + app.current_index = app.files.len() - 1; 89 + } 90 + Ok(serde_json::to_value(&*app).unwrap()) 91 + } 92 + 93 + #[command] 94 + pub fn show(index: usize, app: State<'_, Data>) -> Result<Value, String> { 95 + let mut app = app.0.lock().unwrap(); 96 + app.current_index = index; 97 + Ok(serde_json::to_value(&*app).unwrap()) 98 + } 99 + 61 100 #[derive(Serialize)] 62 - pub struct Info { 101 + pub struct Page { 63 102 path: PathBuf, 64 103 frames: Vec<Frame>, 65 104 } 66 105 67 106 #[command] 68 - pub async fn open(path: PathBuf, app: State<'_, Data>) -> Result<Option<Info>, String> { 107 + pub fn get_page(app: State<'_, Data>) -> Result<Page, String> { 69 108 let mut app = app.0.lock().unwrap(); 70 - let metadata = get_metadata(&path)?; 71 - app.item = Some(Item { 72 - path: path.clone(), 73 - metadata: metadata.clone(), 74 - }); 75 - let info = Info { 76 - path, 77 - frames: get_frames(&metadata), 78 - }; 79 - Ok(Some(info)) 109 + let file = app.current_file()?; 110 + Ok(Page { 111 + path: file.path.clone(), 112 + frames: get_frames(&file.metadata), 113 + }) 80 114 } 81 115 82 116 #[derive(Serialize)] ··· 88 122 } 89 123 90 124 #[command] 91 - pub fn get_image(index: Option<usize>, app: State<'_, Data>) -> Result<Option<Image>, String> { 92 - let app = app.0.lock().unwrap(); 93 - let item = app.item.as_ref().unwrap(); 125 + pub fn get_image(index: Option<usize>, app: State<'_, Data>) -> Option<Image> { 126 + let mut app = app.0.lock().unwrap(); 127 + let file = app.current_file().ok()?; 94 128 let index = match index { 95 129 Some(index) => index, 96 - None => match item.metadata { 130 + None => match file.metadata { 97 131 Metadata::Id3(ref tag) => { 98 132 let mut index = match tag.pictures().next() { 99 133 Some(_pic) => 0, 100 - None => return Ok(None), 134 + None => return None, 101 135 }; 102 136 for (i, current_pic) in tag.pictures().enumerate() { 103 137 if current_pic.picture_type == id3::frame::PictureType::CoverFront { ··· 109 143 } 110 144 Metadata::Mp4(ref tag) => match tag.artwork() { 111 145 Some(_artwork) => 0, 112 - None => return Ok(None), 146 + None => return None, 113 147 }, 114 148 }, 115 149 }; 116 - let image = match item.metadata { 150 + match file.metadata { 117 151 Metadata::Id3(ref tag) => match tag.pictures().nth(index) { 118 152 Some(pic) => Some(Image { 119 153 index, ··· 136 170 }), 137 171 None => None, 138 172 }, 139 - }; 140 - Ok(image) 173 + } 141 174 } 142 175 143 176 #[command] 144 177 pub fn remove_image(index: usize, app: State<'_, Data>) -> Result<(), String> { 145 178 let mut app = app.0.lock().unwrap(); 146 - let item = match &mut app.item { 147 - Some(item) => item, 148 - None => throw!("No open item"), 149 - }; 150 - match item.metadata { 179 + let file = app.current_file().unwrap(); 180 + match file.metadata { 151 181 Metadata::Id3(ref mut tag) => { 152 182 let mut pic_frames: Vec<_> = tag 153 183 .frames()
+3 -1
src-tauri/src/main.rs
··· 80 80 let ctx = tauri::generate_context!(); 81 81 tauri::Builder::default() 82 82 .invoke_handler(tauri::generate_handler![ 83 - cmd::open, 83 + cmd::open_files, 84 + cmd::show, 85 + cmd::get_page, 84 86 cmd::error_popup, 85 87 cmd::get_image, 86 88 cmd::remove_image
+42 -34
src/App.svelte
··· 1 1 <script lang="ts"> 2 2 import { invoke, dialog } from '@tauri-apps/api' 3 - import { checkShortcut, popup } from './scripts/helpers' 4 - import ItemView from './components/Item.svelte' 5 - import type { Item } from './components/Item.svelte' 3 + import { checkShortcut, popup, runCmd } from './scripts/helpers' 4 + import PageView from './components/Item.svelte' 5 + import type { Page } from './components/Item.svelte' 6 6 import FileDrop from './components/FileDrop.svelte' 7 7 8 - let openFiles: string[] = [] 9 - let selected: number | null = null 10 - async function addFiles(files: string[]) { 11 - for (const file of files) { 12 - if (!openFiles.includes(file)) { 13 - openFiles.push(file) 14 - openFiles = openFiles 15 - } 16 - } 17 - if (selected === null && openFiles.length > 0) { 18 - open(openFiles.length - 1) 8 + type File = { 9 + path: string 10 + } 11 + type App = { 12 + current_index: number 13 + files: File[] 14 + } 15 + let app: App = { 16 + current_index: 0, 17 + files: [], 18 + } 19 + ;(async () => { 20 + app = await runCmd<App>('open_files', { paths: [] }) 21 + })() 22 + 23 + let page: Page | null = null 24 + $: if (app) getPage() 25 + async function getPage() { 26 + let newPage = await runCmd<Page>('get_page') 27 + if (!page || newPage.path !== page.path) { 28 + page = newPage 19 29 } 20 30 } 21 - let item: Item | null = null 31 + 32 + async function openFiles(paths: string[]) { 33 + app = await runCmd<App>('open_files', { paths }) 34 + } 22 35 async function openDialog() { 23 36 let paths = await dialog.open({ 24 37 filters: [{ name: 'Audio file', extensions: ['mp3', 'm4a', 'wav', 'aiff'] }], ··· 29 42 paths = [paths] 30 43 } 31 44 if (paths !== null) { 32 - await addFiles(paths) 45 + await openFiles(paths) 33 46 } 34 47 } 35 - async function open(index: number) { 36 - if (selected !== index) { 37 - item = (await invoke('open', { path: openFiles[index] }).catch(popup)) as any 38 - selected = index 48 + async function show(index: number) { 49 + if (app.current_index !== index) { 50 + app = await runCmd<App>('show', { index }) 39 51 } 40 52 } 41 53 async function filesKeydown(e: KeyboardEvent) { 42 54 if (checkShortcut(e, 'ArrowUp')) { 43 55 e.preventDefault() 44 - if (selected !== null && selected > 0) { 45 - open(selected - 1) 46 - } else if (selected === null && openFiles.length >= 1) { 47 - open(openFiles.length - 1) 56 + if (app.current_index >= 1) { 57 + show(app.current_index - 1) 48 58 } 49 59 } else if (checkShortcut(e, 'ArrowDown')) { 50 60 e.preventDefault() 51 - if (selected !== null && selected < openFiles.length - 1) { 52 - open(selected + 1) 53 - } else if (selected === null && openFiles.length >= 1) { 54 - open(0) 61 + if (app.current_index < app.files.length - 1) { 62 + show(app.current_index + 1) 55 63 } 56 64 } 57 65 } ··· 63 71 <button on:click={openDialog}>Open Files</button> 64 72 </div> 65 73 <div class="files" tabindex="0" on:keydown={filesKeydown}> 66 - {#each openFiles as file, i} 67 - <div class="row" class:selected={i === selected} on:click={() => open(i)} 68 - >{file.replace(/^.*[\\\/]/, '')}</div> 74 + {#each app.files as file, i} 75 + <div class="row" class:selected={i === app.current_index} on:click={() => show(i)} 76 + >{file.path.replace(/^.*[\\\/]/, '')}</div> 69 77 {/each} 70 78 </div> 71 79 <FileDrop 72 80 fileExtensions={['mp3', 'aiff', 'wav', 'm4a', 'mp4', 'm4p', 'm4b', 'm4r', 'm4v']} 73 - handleFiles={addFiles} 81 + handleFiles={openFiles} 74 82 msg="" /> 75 83 </div> 76 84 <div class="main"> 77 - {#if item} 78 - <ItemView {item} /> 85 + {#if page} 86 + <PageView item={page} /> 79 87 {/if} 80 88 </div> 81 89 </main>
+2 -2
src/components/Item.svelte
··· 8 8 data: Uint8Array 9 9 mime_type: string 10 10 } 11 - export type Item = { 11 + export type Page = { 12 12 path: string 13 13 frames: Frame[] 14 14 } ··· 18 18 <script lang="ts"> 19 19 import { runCmd } from '../scripts/helpers' 20 20 21 - export let item: Item 21 + export let item: Page 22 22 console.log(item) 23 23 24 24 let image: Image | null = null
+2 -2
src/scripts/helpers.ts
··· 5 5 invoke('error_popup', { msg }) 6 6 } 7 7 8 - export async function runCmd(cmd: string, options: { [key: string]: unknown } = {}) { 9 - return await invoke(cmd, options).catch(popup) 8 + export async function runCmd<T = unknown>(cmd: string, options: { [key: string]: unknown } = {}) { 9 + return (await invoke(cmd, options).catch(popup)) as T 10 10 } 11 11 12 12 export function extractUnlistener(futureUnlistener: Promise<event.UnlistenFn>) {