Skip to content

Commit 829de97

Browse files
committed
0.4.0 - library manager
1 parent a411ff4 commit 829de97

11 files changed

+231
-40
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Get the AppImage [here](https://github.com/ronanru/ronix/releases/tag/v0.2.0)
2828
- [x] Plays music (All traditional music player features)
2929
- [x] Fuzzy search
3030
- [x] Multiple themes
31-
- [ ] Song manager (Ability to edit song tags, delete songs)
31+
- [x] Song manager (Ability to edit song tags, delete songs)
3232
- [x] Song downloader with yt-dlp
3333
- [x] Publish on the AUR
3434
- [ ] Publish on flathub

src-tauri/Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ronix"
3-
version = "0.3.0"
3+
version = "0.4.0"
44
description = "Music Player and Library Manager"
55
authors = ["Matvey Ryabchikov"]
66
edition = "2021"

src-tauri/src/download.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub fn get_router() -> RouterBuilder<Context> {
3333
.current_dir(&folders[0])
3434
.status();
3535
if !sacad.map(|s| s.success()).unwrap_or(false) {
36-
return "Failed to convert video with sacad_r";
36+
return "Failed to download cover art with sacad_r";
3737
}
3838
*ctx.library.lock().unwrap() = library::read_from_dirs(&folders);
3939
"Download successful"

src-tauri/src/library.rs

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
use crate::{Context, PlayerScope};
22
use fuse_rust::Fuse;
3-
use lofty::{Accessor, AudioFile, MimeType, Probe, TaggedFileExt};
3+
use lofty::{Accessor, AudioFile, MimeType, Probe, TagExt, TaggedFileExt};
44
use nanoid::nanoid;
55
use rand::prelude::*;
66
use rspc::{Router, RouterBuilder, Type};
7-
use serde::Serialize;
7+
use serde::{Deserialize, Serialize};
88
use std::{
99
collections::HashMap,
1010
fs::{self, create_dir_all, File},
1111
hash::Hash,
1212
io::Write,
1313
path::PathBuf,
14+
process::Command,
1415
};
1516
use walkdir::WalkDir;
1617

@@ -187,6 +188,14 @@ struct SearchResults {
187188
songs: Vec<String>,
188189
}
189190

191+
#[derive(Deserialize, Type)]
192+
struct EditSongInput {
193+
id: String,
194+
title: String,
195+
album: String,
196+
artist: String,
197+
}
198+
190199
pub fn get_router() -> RouterBuilder<Context> {
191200
Router::<Context>::new()
192201
.query("get", |t| {
@@ -243,4 +252,55 @@ pub fn get_router() -> RouterBuilder<Context> {
243252
}
244253
})
245254
})
255+
.mutation("editSong", |t| {
256+
t(|ctx, input: EditSongInput| {
257+
let mut library = ctx.library.lock().unwrap();
258+
match library.songs.get(&input.id) {
259+
Some(song) => {
260+
match Probe::open(&song.path)
261+
.ok()
262+
.map(|f| f.read().ok())
263+
.flatten()
264+
{
265+
Some(mut tagged_file) => {
266+
let tags_option = match tagged_file.primary_tag_mut() {
267+
Some(primary_tag) => Some(primary_tag),
268+
None => tagged_file.first_tag_mut(),
269+
};
270+
match tags_option {
271+
Some(tags) => {
272+
tags.set_title(input.title);
273+
tags.set_album(input.album);
274+
tags.set_artist(input.artist);
275+
for i in 0..tags.picture_count() {
276+
tags.remove_picture(i as usize);
277+
}
278+
if tags.save_to_path(&song.path).is_err() {
279+
return "Failed to edit song";
280+
}
281+
let sacad = Command::new("sacad_r")
282+
.args(["-f", ".", "600", "+"])
283+
.current_dir(&song.path.parent().unwrap())
284+
.status();
285+
if !sacad.map(|s| s.success()).unwrap_or(false) {
286+
return "Failed to download cover art with sacad_r";
287+
}
288+
*library = read_from_dirs(&ctx.config.lock().unwrap().music_folders);
289+
"Successfully edited"
290+
}
291+
None => "Could not edit song",
292+
}
293+
}
294+
None => "Could not find song to edit",
295+
}
296+
}
297+
None => "Could not find song to edit",
298+
}
299+
})
300+
})
301+
.mutation("refresh", |t| {
302+
t(|ctx, _: ()| {
303+
*ctx.library.lock().unwrap() = read_from_dirs(&ctx.config.lock().unwrap().music_folders);
304+
})
305+
})
246306
}

src-tauri/tauri.conf.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"package": {
1010
"productName": "ronix",
11-
"version": "0.3.0"
11+
"version": "0.4.0"
1212
},
1313
"tauri": {
1414
"allowlist": {

src/components/menu.tsx

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
import { api } from '@/api';
2+
import { refetchLibrary } from '@/library';
13
import { navigate } from '@/router';
2-
import { FolderIcon, MenuIcon, PlusIcon, SettingsIcon } from 'lucide-solid';
4+
import {
5+
FolderIcon,
6+
MenuIcon,
7+
PlusIcon,
8+
RefreshCcwIcon,
9+
SettingsIcon,
10+
} from 'lucide-solid';
311
import {
412
For,
513
Show,
@@ -36,6 +44,12 @@ const Menu: Component<{
3644
},
3745
id: 'downloadSong',
3846
},
47+
{
48+
icon: RefreshCcwIcon,
49+
name: 'Refresh library',
50+
onClick: () => api.mutation(['library.refresh']).then(refetchLibrary),
51+
id: 'refreshLibrary',
52+
},
3953
{
4054
icon: FolderIcon,
4155
name: 'Library Manager',

src/components/ui/textInput.tsx

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
ComponentProps,
3+
createUniqueId,
4+
splitProps,
5+
type Component,
6+
} from 'solid-js';
7+
8+
const TextInput: Component<
9+
{
10+
label: string;
11+
} & ComponentProps<'input'>
12+
> = (props) => {
13+
const id = createUniqueId();
14+
15+
const [local, otherProps] = splitProps(props, ['label']);
16+
17+
return (
18+
<div>
19+
<label for={id}>{local.label}</label>
20+
<input
21+
class="mt-1 block w-full rounded-md bg-primary-800 px-2 py-1"
22+
type="text"
23+
id={id}
24+
{...otherProps}
25+
/>
26+
</div>
27+
);
28+
};
29+
30+
export default TextInput;

src/gen/tauri-types.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export type Procedures = {
1111
mutations:
1212
{ key: "config.set", input: Config, result: null } |
1313
{ key: "library.deleteSong", input: string, result: string } |
14+
{ key: "library.editSong", input: EditSongInput, result: string } |
15+
{ key: "library.refresh", input: never, result: null } |
1416
{ key: "player.nextSong", input: never, result: null } |
1517
{ key: "player.playSong", input: PlaySongInput, result: null } |
1618
{ key: "player.previousSong", input: never, result: null } |
@@ -25,15 +27,17 @@ export type Procedures = {
2527

2628
export type RepeatMode = "None" | "One" | "All"
2729

28-
export type PlaySongInput = { song_id: string; scope: PlayerScope }
30+
export type Song = { title: string; path: string; duration: number; album: string }
2931

30-
export type Artist = { name: string }
32+
export type PlaySongInput = { song_id: string; scope: PlayerScope }
3133

3234
export type MainColor = "Slate" | "Gray" | "Zinc" | "Neutral" | "Stone"
3335

36+
export type SearchResults = { artists: string[]; albums: string[]; songs: string[] }
37+
3438
export type Library = { artists: { [key: string]: Artist }; albums: { [key: string]: Album }; songs: { [key: string]: Song } }
3539

36-
export type Song = { title: string; path: string; duration: number; album: string }
40+
export type EditSongInput = { id: string; title: string; album: string; artist: string }
3741

3842
export type CurrentSongData = { current_song: string | null; song_started_at: number; paused_at: number | null; volume: number }
3943

@@ -43,6 +47,6 @@ export type Config = { music_folders: string[]; dark_mode: boolean; main_color:
4347

4448
export type PlayerScope = "Library" | { Album: string } | { Artist: string }
4549

46-
export type SearchResults = { artists: string[]; albums: string[]; songs: string[] }
50+
export type Artist = { name: string }
4751

4852
export type AccentColor = "Red" | "Orange" | "Amber" | "Yellow" | "Lime" | "Green" | "Emerald" | "Teal" | "Cyan" | "Blue" | "Indigo" | "Violet" | "Purple" | "Fuchsia" | "Pink" | "Rose"

src/songButton.tsx

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { PencilIcon, Trash2Icon } from 'lucide-solid';
12
import { Show, type Component } from 'solid-js';
23
import CoverArt from './components/coverArt';
34
import Button from './components/ui/button';
4-
import { Trash2Icon } from 'lucide-solid';
55

66
const SongButton: Component<{
77
title: string;
@@ -13,6 +13,7 @@ const SongButton: Component<{
1313
onClick?: (e: MouseEvent) => void;
1414
isManager?: boolean;
1515
onDelete?: () => void;
16+
onEdit?: () => void;
1617
}> = (props) => {
1718
return (
1819
<button
@@ -31,13 +32,18 @@ const SongButton: Component<{
3132
<button class="block truncate">{props.artist}</button>
3233
</Show>
3334
</div>
34-
<Show when={props.isManager}
35+
<Show
36+
when={props.isManager}
3537
fallback={
3638
<p class="flex-shrink-0">
3739
{Math.floor(props.duration / 60)}:
3840
{(props.duration % 60).toString().padStart(2, '0')}
39-
</p>}
41+
</p>
42+
}
4043
>
44+
<Button variant="accent" size="icon" onClick={props.onEdit}>
45+
<PencilIcon />
46+
</Button>
4147
<Button variant="danger" size="icon" onClick={props.onDelete}>
4248
<Trash2Icon />
4349
</Button>

0 commit comments

Comments
 (0)