diff --git a/README.md b/README.md
index 02f0544..c170c6c 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Get the AppImage [here](https://github.com/ronanru/ronix/releases/tag/v0.2.0)
- [x] Plays music (All traditional music player features)
- [x] Fuzzy search
- [x] Multiple themes
-- [ ] Song manager (Ability to edit song tags, delete songs)
+- [x] Song manager (Ability to edit song tags, delete songs)
- [x] Song downloader with yt-dlp
- [x] Publish on the AUR
- [ ] Publish on flathub
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 61990dd..00d14ed 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -2567,7 +2567,7 @@ dependencies = [
[[package]]
name = "ronix"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"async-stream",
"directories",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f9b9a72..d66e099 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "ronix"
-version = "0.3.0"
+version = "0.4.0"
description = "Music Player and Library Manager"
authors = ["Matvey Ryabchikov"]
edition = "2021"
diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs
index ef48d97..f6c8be1 100644
--- a/src-tauri/src/download.rs
+++ b/src-tauri/src/download.rs
@@ -33,7 +33,7 @@ pub fn get_router() -> RouterBuilder {
.current_dir(&folders[0])
.status();
if !sacad.map(|s| s.success()).unwrap_or(false) {
- return "Failed to convert video with sacad_r";
+ return "Failed to download cover art with sacad_r";
}
*ctx.library.lock().unwrap() = library::read_from_dirs(&folders);
"Download successful"
diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs
index 369b152..8443719 100644
--- a/src-tauri/src/library.rs
+++ b/src-tauri/src/library.rs
@@ -1,16 +1,17 @@
use crate::{Context, PlayerScope};
use fuse_rust::Fuse;
-use lofty::{Accessor, AudioFile, MimeType, Probe, TaggedFileExt};
+use lofty::{Accessor, AudioFile, MimeType, Probe, TagExt, TaggedFileExt};
use nanoid::nanoid;
use rand::prelude::*;
use rspc::{Router, RouterBuilder, Type};
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::{self, create_dir_all, File},
hash::Hash,
io::Write,
path::PathBuf,
+ process::Command,
};
use walkdir::WalkDir;
@@ -187,6 +188,14 @@ struct SearchResults {
songs: Vec,
}
+#[derive(Deserialize, Type)]
+struct EditSongInput {
+ id: String,
+ title: String,
+ album: String,
+ artist: String,
+}
+
pub fn get_router() -> RouterBuilder {
Router::::new()
.query("get", |t| {
@@ -243,4 +252,55 @@ pub fn get_router() -> RouterBuilder {
}
})
})
+ .mutation("editSong", |t| {
+ t(|ctx, input: EditSongInput| {
+ let mut library = ctx.library.lock().unwrap();
+ match library.songs.get(&input.id) {
+ Some(song) => {
+ match Probe::open(&song.path)
+ .ok()
+ .map(|f| f.read().ok())
+ .flatten()
+ {
+ Some(mut tagged_file) => {
+ let tags_option = match tagged_file.primary_tag_mut() {
+ Some(primary_tag) => Some(primary_tag),
+ None => tagged_file.first_tag_mut(),
+ };
+ match tags_option {
+ Some(tags) => {
+ tags.set_title(input.title);
+ tags.set_album(input.album);
+ tags.set_artist(input.artist);
+ for i in 0..tags.picture_count() {
+ tags.remove_picture(i as usize);
+ }
+ if tags.save_to_path(&song.path).is_err() {
+ return "Failed to edit song";
+ }
+ let sacad = Command::new("sacad_r")
+ .args(["-f", ".", "600", "+"])
+ .current_dir(&song.path.parent().unwrap())
+ .status();
+ if !sacad.map(|s| s.success()).unwrap_or(false) {
+ return "Failed to download cover art with sacad_r";
+ }
+ *library = read_from_dirs(&ctx.config.lock().unwrap().music_folders);
+ "Successfully edited"
+ }
+ None => "Could not edit song",
+ }
+ }
+ None => "Could not find song to edit",
+ }
+ }
+ None => "Could not find song to edit",
+ }
+ })
+ })
+ .mutation("refresh", |t| {
+ t(|ctx, _: ()| {
+ *ctx.library.lock().unwrap() = read_from_dirs(&ctx.config.lock().unwrap().music_folders);
+ })
+ })
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 9d86610..18a1c4b 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -8,7 +8,7 @@
},
"package": {
"productName": "ronix",
- "version": "0.3.0"
+ "version": "0.4.0"
},
"tauri": {
"allowlist": {
diff --git a/src/components/menu.tsx b/src/components/menu.tsx
index 2bf5144..3268d47 100644
--- a/src/components/menu.tsx
+++ b/src/components/menu.tsx
@@ -1,5 +1,13 @@
+import { api } from '@/api';
+import { refetchLibrary } from '@/library';
import { navigate } from '@/router';
-import { FolderIcon, MenuIcon, PlusIcon, SettingsIcon } from 'lucide-solid';
+import {
+ FolderIcon,
+ MenuIcon,
+ PlusIcon,
+ RefreshCcwIcon,
+ SettingsIcon,
+} from 'lucide-solid';
import {
For,
Show,
@@ -36,6 +44,12 @@ const Menu: Component<{
},
id: 'downloadSong',
},
+ {
+ icon: RefreshCcwIcon,
+ name: 'Refresh library',
+ onClick: () => api.mutation(['library.refresh']).then(refetchLibrary),
+ id: 'refreshLibrary',
+ },
{
icon: FolderIcon,
name: 'Library Manager',
diff --git a/src/components/ui/textInput.tsx b/src/components/ui/textInput.tsx
new file mode 100644
index 0000000..5437d19
--- /dev/null
+++ b/src/components/ui/textInput.tsx
@@ -0,0 +1,30 @@
+import {
+ ComponentProps,
+ createUniqueId,
+ splitProps,
+ type Component,
+} from 'solid-js';
+
+const TextInput: Component<
+ {
+ label: string;
+ } & ComponentProps<'input'>
+> = (props) => {
+ const id = createUniqueId();
+
+ const [local, otherProps] = splitProps(props, ['label']);
+
+ return (
+
+
+
+
+ );
+};
+
+export default TextInput;
diff --git a/src/gen/tauri-types.ts b/src/gen/tauri-types.ts
index 7884db7..2895418 100644
--- a/src/gen/tauri-types.ts
+++ b/src/gen/tauri-types.ts
@@ -11,6 +11,8 @@ export type Procedures = {
mutations:
{ key: "config.set", input: Config, result: null } |
{ key: "library.deleteSong", input: string, result: string } |
+ { key: "library.editSong", input: EditSongInput, result: string } |
+ { key: "library.refresh", input: never, result: null } |
{ key: "player.nextSong", input: never, result: null } |
{ key: "player.playSong", input: PlaySongInput, result: null } |
{ key: "player.previousSong", input: never, result: null } |
@@ -25,15 +27,17 @@ export type Procedures = {
export type RepeatMode = "None" | "One" | "All"
-export type PlaySongInput = { song_id: string; scope: PlayerScope }
+export type Song = { title: string; path: string; duration: number; album: string }
-export type Artist = { name: string }
+export type PlaySongInput = { song_id: string; scope: PlayerScope }
export type MainColor = "Slate" | "Gray" | "Zinc" | "Neutral" | "Stone"
+export type SearchResults = { artists: string[]; albums: string[]; songs: string[] }
+
export type Library = { artists: { [key: string]: Artist }; albums: { [key: string]: Album }; songs: { [key: string]: Song } }
-export type Song = { title: string; path: string; duration: number; album: string }
+export type EditSongInput = { id: string; title: string; album: string; artist: string }
export type CurrentSongData = { current_song: string | null; song_started_at: number; paused_at: number | null; volume: number }
@@ -43,6 +47,6 @@ export type Config = { music_folders: string[]; dark_mode: boolean; main_color:
export type PlayerScope = "Library" | { Album: string } | { Artist: string }
-export type SearchResults = { artists: string[]; albums: string[]; songs: string[] }
+export type Artist = { name: string }
export type AccentColor = "Red" | "Orange" | "Amber" | "Yellow" | "Lime" | "Green" | "Emerald" | "Teal" | "Cyan" | "Blue" | "Indigo" | "Violet" | "Purple" | "Fuchsia" | "Pink" | "Rose"
diff --git a/src/songButton.tsx b/src/songButton.tsx
index 8cd8dfa..ae233cb 100644
--- a/src/songButton.tsx
+++ b/src/songButton.tsx
@@ -1,7 +1,7 @@
+import { PencilIcon, Trash2Icon } from 'lucide-solid';
import { Show, type Component } from 'solid-js';
import CoverArt from './components/coverArt';
import Button from './components/ui/button';
-import { Trash2Icon } from 'lucide-solid';
const SongButton: Component<{
title: string;
@@ -13,6 +13,7 @@ const SongButton: Component<{
onClick?: (e: MouseEvent) => void;
isManager?: boolean;
onDelete?: () => void;
+ onEdit?: () => void;
}> = (props) => {
return (
-
{Math.floor(props.duration / 60)}:
{(props.duration % 60).toString().padStart(2, '0')}
-
}
+
+ }
>
+
diff --git a/src/views/songList.tsx b/src/views/songList.tsx
index 6caa757..d4a548d 100644
--- a/src/views/songList.tsx
+++ b/src/views/songList.tsx
@@ -1,9 +1,17 @@
import { api } from '@/api';
import Button from '@/components/ui/button';
import Modal from '@/components/ui/modal';
+import TextInput from '@/components/ui/textInput';
import { library, refetchLibrary } from '@/library';
import SongButton from '@/songButton';
-import { For, Show, createSignal, type Component } from 'solid-js';
+import {
+ For,
+ Match,
+ Show,
+ Switch,
+ createSignal,
+ type Component,
+} from 'solid-js';
const SongList: Component<{
albums?: string[];
@@ -12,7 +20,12 @@ const SongList: Component<{
noSort?: boolean;
isManager?: boolean;
}> = (props) => {
- const [songToDelete, setSongToDelete] = createSignal(null);
+ const [songToEdit, setSongToEdit] = createSignal(null);
+ const [operation, setOperation] = createSignal<'EDIT' | 'DELETE'>('EDIT');
+
+ const songEditData = () => library()?.songs[songToEdit()!];
+ const albumEditData = () => library()?.albums[songEditData()!.album!];
+ const artistEditData = () => library()?.artists[albumEditData()!.artist!];
const [isLoading, setIsLoading] = createSignal(false);
const [returnText, setReturnText] = createSignal(null);
@@ -47,37 +60,95 @@ const SongList: Component<{
return (
<>
setSongToDelete(null) !== null}
+ isOpen={!!props.isManager && !!songToEdit()}
+ onClose={() => setSongToEdit(null) !== null}
title={
returnText() ??
(isLoading()
- ? 'Deleting...'
- : `Do you really want to delete "${library()?.songs[songToDelete()!]
- .title}"?`)
+ ? {
+ DELETE: 'Deleting...',
+ EDIT: 'Editing...',
+ }[operation()]
+ : `${
+ {
+ DELETE: 'Do you really want to delete',
+ EDIT: 'Edit',
+ }[operation()]
+ } "${library()?.songs[songToEdit()!].title}"?`)
}
disableClosing={!returnText()}
>
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
{
- setSongToDelete(song.id);
+ setSongToEdit(song.id);
+ setOperation('DELETE');
+ setIsLoading(false);
+ }}
+ onEdit={() => {
+ setSongToEdit(song.id);
+ setOperation('EDIT');
setIsLoading(false);
}}
onClick={() =>