diff options
Diffstat (limited to '')
| -rw-r--r-- | src/daemon.rs | 183 |
1 files changed, 160 insertions, 23 deletions
diff --git a/src/daemon.rs b/src/daemon.rs index c794495..d34305a 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -747,30 +747,40 @@ fn process_command( | |||
| 747 | } else { | 747 | } else { |
| 748 | match Config::load() { | 748 | match Config::load() { |
| 749 | Ok(config) => match crate::database::Database::open(&config) { | 749 | Ok(config) => match crate::database::Database::open(&config) { |
| 750 | Ok(db) => match db.search_tracks(query_str) { | 750 | Ok(db) => { |
| 751 | Ok(tracks) if !tracks.is_empty() => { | 751 | let tracks = match db.resolve_path(query_str) { |
| 752 | let db_track = &tracks[0]; | 752 | Ok(Some(t)) => t, |
| 753 | let album_tracks = db | 753 | Ok(None) => match db.search_tracks(query_str) { |
| 754 | .get_album_tracks_after( | 754 | Ok(t) => t, |
| 755 | db_track.album_id, | 755 | Err(e) => return format!("Database error: {}\n", e), |
| 756 | db_track.track_number, | 756 | }, |
| 757 | &db_track.path, | 757 | Err(e) => return format!("Database error: {}\n", e), |
| 758 | ) | 758 | }; |
| 759 | .unwrap_or_default(); | 759 | if tracks.is_empty() { |
| 760 | let display = format!( | ||
| 761 | "{} - {}", | ||
| 762 | db_track.artist_name.as_deref().unwrap_or("unknown"), | ||
| 763 | db_track.title, | ||
| 764 | ); | ||
| 765 | let paths: Vec<PathBuf> = | ||
| 766 | album_tracks.into_iter().map(|t| t.path).collect(); | ||
| 767 | (db_track.path.clone(), paths, display) | ||
| 768 | } | ||
| 769 | Ok(_) => { | ||
| 770 | return format!("No tracks found matching '{}'\n", query_str); | 760 | return format!("No tracks found matching '{}'\n", query_str); |
| 771 | } | 761 | } |
| 772 | Err(e) => return format!("Database error: {}\n", e), | 762 | let db_track = &tracks[0]; |
| 773 | }, | 763 | let album_paths: Vec<PathBuf> = if tracks.len() > 1 { |
| 764 | // resolve_path returned full album — use remaining tracks | ||
| 765 | tracks[1..].iter().map(|t| t.path.clone()).collect() | ||
| 766 | } else { | ||
| 767 | db.get_album_tracks_after( | ||
| 768 | db_track.album_id, | ||
| 769 | db_track.track_number, | ||
| 770 | &db_track.path, | ||
| 771 | ) | ||
| 772 | .unwrap_or_default() | ||
| 773 | .into_iter() | ||
| 774 | .map(|t| t.path) | ||
| 775 | .collect() | ||
| 776 | }; | ||
| 777 | let display = format!( | ||
| 778 | "{} - {}", | ||
| 779 | db_track.artist_name.as_deref().unwrap_or("unknown"), | ||
| 780 | db_track.title, | ||
| 781 | ); | ||
| 782 | (db_track.path.clone(), album_paths, display) | ||
| 783 | } | ||
| 774 | Err(e) => return format!("Database error: {}\n", e), | 784 | Err(e) => return format!("Database error: {}\n", e), |
| 775 | }, | 785 | }, |
| 776 | Err(e) => return format!("Config error: {}\n", e), | 786 | Err(e) => return format!("Config error: {}\n", e), |
| @@ -862,6 +872,8 @@ fn process_command( | |||
| 862 | } else { | 872 | } else { |
| 863 | path_str.to_string() | 873 | path_str.to_string() |
| 864 | }; | 874 | }; |
| 875 | // Strip trailing slash so autocompleted Artist/Album/ resolves cleanly | ||
| 876 | let path_str = path_str.trim_end_matches('/').to_string(); | ||
| 865 | 877 | ||
| 866 | if path_str.is_empty() { | 878 | if path_str.is_empty() { |
| 867 | return "Usage: play [now|next|last] <path>\n".to_string(); | 879 | return "Usage: play [now|next|last] <path>\n".to_string(); |
| @@ -1052,7 +1064,132 @@ fn process_command( | |||
| 1052 | _ => format!("Error: No audio files found in '{}'\n", path_str), | 1064 | _ => format!("Error: No audio files found in '{}'\n", path_str), |
| 1053 | } | 1065 | } |
| 1054 | } else { | 1066 | } else { |
| 1055 | format!("File not found: {}\n", path_str) | 1067 | match Config::load() { |
| 1068 | Ok(config) => match crate::database::Database::open(&config) { | ||
| 1069 | Ok(db) => { | ||
| 1070 | // Collect tracks: try hierarchical path first, fallback to search | ||
| 1071 | let tracks = match db.resolve_path(&path_str) { | ||
| 1072 | Ok(Some(t)) => t, | ||
| 1073 | Ok(None) => { | ||
| 1074 | let query = path_str | ||
| 1075 | .rsplit_once('/') | ||
| 1076 | .and_then(|(_, after)| { | ||
| 1077 | (!after.is_empty()).then_some(after) | ||
| 1078 | }) | ||
| 1079 | .unwrap_or(&path_str); | ||
| 1080 | match db.search_tracks(query) { | ||
| 1081 | Ok(t) => t, | ||
| 1082 | Err(e) => return format!("Database error: {}\n", e), | ||
| 1083 | } | ||
| 1084 | } | ||
| 1085 | Err(e) => return format!("Database error: {}\n", e), | ||
| 1086 | }; | ||
| 1087 | |||
| 1088 | if tracks.is_empty() { | ||
| 1089 | return format!("No tracks found matching '{}'\n", path_str); | ||
| 1090 | } | ||
| 1091 | |||
| 1092 | let is_album_play = tracks.len() > 1; | ||
| 1093 | if let Ok(mut q) = queue.lock() { | ||
| 1094 | let was_empty = q.is_empty(); | ||
| 1095 | match position { | ||
| 1096 | "now" => { | ||
| 1097 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 1098 | try_scrobble_current(scrobbler, &mut s); | ||
| 1099 | } | ||
| 1100 | for t in tracks.iter().rev() { | ||
| 1101 | q.insert_after_current(t.path.clone()); | ||
| 1102 | } | ||
| 1103 | if !was_empty { | ||
| 1104 | let new_pos = q.position() + 1; | ||
| 1105 | q.set_position(new_pos); | ||
| 1106 | } | ||
| 1107 | if let Some(track) = q.track_at(q.position()) { | ||
| 1108 | audio.play(track.path.clone()); | ||
| 1109 | send_now_playing(scrobbler, track); | ||
| 1110 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 1111 | s.start(track.clone()); | ||
| 1112 | } | ||
| 1113 | let fmt = Config::load() | ||
| 1114 | .ok() | ||
| 1115 | .and_then(|c| c.display_format.clone()); | ||
| 1116 | notification::notify_track(track, fmt.as_deref()); | ||
| 1117 | } | ||
| 1118 | if is_album_play { | ||
| 1119 | format!( | ||
| 1120 | "Playing now: {} tracks from {}\n", | ||
| 1121 | tracks.len(), | ||
| 1122 | path_str | ||
| 1123 | ) | ||
| 1124 | } else { | ||
| 1125 | format!("Playing now: {}\n", path_str) | ||
| 1126 | } | ||
| 1127 | } | ||
| 1128 | "next" => { | ||
| 1129 | for t in tracks.iter().rev() { | ||
| 1130 | q.insert_after_current(t.path.clone()); | ||
| 1131 | } | ||
| 1132 | if is_album_play { | ||
| 1133 | format!( | ||
| 1134 | "Queued next: {} tracks from {}\n", | ||
| 1135 | tracks.len(), | ||
| 1136 | path_str | ||
| 1137 | ) | ||
| 1138 | } else { | ||
| 1139 | format!("Queued next: {}\n", path_str) | ||
| 1140 | } | ||
| 1141 | } | ||
| 1142 | _ => { | ||
| 1143 | let was_playing = audio.is_playing(); | ||
| 1144 | for t in &tracks { | ||
| 1145 | q.add_track(t.path.clone()); | ||
| 1146 | } | ||
| 1147 | if was_empty && !was_playing { | ||
| 1148 | if is_album_play { | ||
| 1149 | q.shuffle_initial(); | ||
| 1150 | } | ||
| 1151 | if let Some(track) = q.current() { | ||
| 1152 | audio.play(track.path.clone()); | ||
| 1153 | send_now_playing(scrobbler, track); | ||
| 1154 | if let Ok(mut s) = scrobble_state.lock() { | ||
| 1155 | s.start(track.clone()); | ||
| 1156 | } | ||
| 1157 | let fmt = Config::load() | ||
| 1158 | .ok() | ||
| 1159 | .and_then(|c| c.display_format.clone()); | ||
| 1160 | notification::notify_track( | ||
| 1161 | track, | ||
| 1162 | fmt.as_deref(), | ||
| 1163 | ); | ||
| 1164 | } | ||
| 1165 | if is_album_play { | ||
| 1166 | format!( | ||
| 1167 | "Playing: {} tracks from {}\n", | ||
| 1168 | tracks.len(), | ||
| 1169 | path_str | ||
| 1170 | ) | ||
| 1171 | } else { | ||
| 1172 | format!("Playing: {}\n", path_str) | ||
| 1173 | } | ||
| 1174 | } else if is_album_play { | ||
| 1175 | format!( | ||
| 1176 | "Queued {} tracks from: {}\n", | ||
| 1177 | tracks.len(), | ||
| 1178 | path_str | ||
| 1179 | ) | ||
| 1180 | } else { | ||
| 1181 | format!("Queued: {}\n", path_str) | ||
| 1182 | } | ||
| 1183 | } | ||
| 1184 | } | ||
| 1185 | } else { | ||
| 1186 | "Error accessing queue\n".to_string() | ||
| 1187 | } | ||
| 1188 | } | ||
| 1189 | Err(e) => format!("Database error: {}\n", e), | ||
| 1190 | }, | ||
| 1191 | Err(e) => format!("Config error: {}\n", e), | ||
| 1192 | } | ||
| 1056 | } | 1193 | } |
| 1057 | } | 1194 | } |
| 1058 | } | 1195 | } |
