1use crate::dataloading::songinfo::SongInfo;
2use crate::traktor_api::{
3 AppMessage, ChannelState, DeckContentState, DeckState, MixerState, ServerMessage, State,
4 StateUpdate,
5};
6use iced::futures::channel::mpsc::UnboundedSender;
7use iced::widget::image;
8use std::collections::HashMap;
9use std::fmt::Display;
10use std::mem;
11use std::net::SocketAddr;
12
13pub const TRAKTOR_SERVER_DEFAULT_ADDR: &str = "127.0.0.1:8080";
14
15#[derive(Debug, Copy, Clone, Eq, PartialEq)]
16pub enum TraktorNextMode {
17 DeckByPosition,
18 DeckByNumber,
19 PlaylistByNumber,
20 PlaylistByName,
21}
22
23#[derive(Debug, Copy, Clone, Eq, PartialEq)]
24pub enum TraktorSyncMode {
25 Relative,
26 AbsoluteByNumber,
27 AbsoluteByName,
28}
29
30#[derive(Debug, Copy, Clone, Eq, PartialEq)]
31pub enum TraktorSyncAction {
32 Relative(isize),
33 PlaylistAbsolute(usize),
34}
35
36impl Display for TraktorNextMode {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{:?}", self)
39 }
40}
41
42impl Display for TraktorSyncMode {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "{:?}", self)
45 }
46}
47
48impl Display for TraktorSyncAction {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "{:?}", self)
51 }
52}
53
54#[derive(Debug, Clone)]
55pub enum TraktorMessage {
56 ServerMessage(Box<ServerMessage>),
57 EnableSync(bool),
58 SetSyncMode(TraktorSyncMode),
59 SetNextMode(TraktorNextMode),
60 SetNextModeFallback(TraktorNextMode),
61 EnableServer(bool),
62 ChangeAddress(String),
63 SubmitAddress,
64 ChangeAndSubmitAddress(String),
65 EnableDebugLogging(bool),
66 Reconnect,
67}
68
69#[derive(Debug, Clone, Copy)]
70pub enum ConnectionState {
71 Disconnected,
72 CoverLoader,
73 Traktor,
74 Connected,
75}
76
77pub struct TraktorDataProvider {
78 pub address: String,
79 pub submitted_address: String,
80 enabled: bool,
81
82 pub next_mode: TraktorNextMode,
83 pub next_mode_fallback: TraktorNextMode,
84 pub sync: bool,
85 pub sync_mode: TraktorSyncMode,
86
87 channel: Option<UnboundedSender<AppMessage>>,
88
89 time_offset_ms: i64,
90 pub state: Option<State>,
91 covers: HashMap<String, image::Handle>,
92 pub cover_loader_addr: Option<SocketAddr>,
93
94 sync_x_fader_is_left: bool,
95
96 cached_song_info: Option<SongInfo>,
97 cached_next_song_info: Option<SongInfo>,
98 cached_sync_action: TraktorSyncAction,
99 should_scroll: bool,
100
101 pub debug_logging: bool,
102 log: Vec<String>,
103}
104
105impl Default for TraktorDataProvider {
106 fn default() -> Self {
107 Self {
108 enabled: false,
109 address: String::new(),
110 submitted_address: String::new(),
111 channel: None,
112
113 next_mode: TraktorNextMode::DeckByNumber,
114 next_mode_fallback: TraktorNextMode::DeckByPosition,
115 sync: false,
116 sync_mode: TraktorSyncMode::Relative,
117
118 time_offset_ms: 0,
119 state: None,
120 covers: HashMap::new(),
121
122 sync_x_fader_is_left: true,
123
124 cached_song_info: None,
125 cached_next_song_info: None,
126 cached_sync_action: TraktorSyncAction::Relative(0),
127 should_scroll: false,
128
129 cover_loader_addr: None,
130
131 debug_logging: false,
132 log: Vec::new(),
133 }
134 }
135}
136
137impl TraktorDataProvider {
138 pub fn set_enabled(&mut self, enabled: bool) {
139 self.enabled = enabled;
140 self.cover_loader_addr = None;
141 self.state = None;
142 }
143 pub fn is_ready(&self) -> bool {
144 self.enabled && self.channel.as_ref().is_some_and(|c| !c.is_closed())
145 }
146
147 #[allow(dead_code)]
148 pub fn get_log(&self) -> &[String] {
149 &self.log
150 }
151
152 #[allow(dead_code)]
153 pub fn clear_log(&mut self) {
154 self.log.clear();
155 }
156
157 pub fn reconnect(&mut self) {
158 self.time_offset_ms = 0;
159 self.state = None;
160 self.sync_x_fader_is_left = true;
161 self.update_song_info(&[]);
162
163 self.send_message(AppMessage::Reconnect {
164 debug_logging: self.debug_logging,
165 });
166 }
167
168 pub fn get_socket_addr(&self) -> Option<SocketAddr> {
169 if !self.enabled {
170 return None;
171 }
172
173 if self.submitted_address.is_empty() {
174 return TRAKTOR_SERVER_DEFAULT_ADDR.parse().ok();
175 }
176
177 self.submitted_address.parse().ok()
178 }
179
180 pub fn get_song_info(&self) -> Option<&SongInfo> {
181 if !self.is_ready() {
182 return None;
183 }
184
185 self.cached_song_info.as_ref()
186 }
187
188 pub fn get_next_song_info(&self) -> Option<&SongInfo> {
189 if !self.is_ready() {
190 return None;
191 }
192
193 self.cached_next_song_info.as_ref()
194 }
195
196 pub fn get_connection_state(&self) -> ConnectionState {
197 if !self.enabled {
198 ConnectionState::Disconnected
199 } else if self.cover_loader_addr.is_some() && self.state.is_some() {
200 ConnectionState::Connected
201 } else if self.state.is_some() {
202 ConnectionState::Traktor
203 } else if self.cover_loader_addr.is_some() {
204 ConnectionState::CoverLoader
205 } else {
206 ConnectionState::Disconnected
207 }
208 }
209
210 fn get_deck_score(&self, deck: &DeckState, channel: &ChannelState, mixer: &MixerState) -> f64 {
211 if !deck.content.is_loaded || deck.play_state.speed == 0.0 || channel.volume == 0.0 {
212 return 0.0;
213 }
214
215 if channel.x_fader_left && mixer.x_fader > 0.5 {
216 (1.0 - mixer.x_fader) * 2.0
217 } else if channel.x_fader_right && mixer.x_fader < 0.5 {
218 mixer.x_fader * 2.0
219 } else {
220 1.0
221 }
222 }
223
224 fn update_song_info(&mut self, playlist: &[SongInfo]) {
225 let old_song_info = self.cached_song_info.take();
226 self.cached_next_song_info = None;
227
228 if !self.is_ready() {
229 return;
230 }
231
232 let Some(state) = self.state.as_ref() else {
233 return;
234 };
235
236 let scores = (0..4)
237 .map(|i| self.get_deck_score(&state.decks[i], &state.channels[i], &state.mixer))
238 .collect::<Vec<f64>>();
239
240 let Some(max) = scores
241 .iter()
242 .enumerate()
243 .max_by(|(_, a), (_, b)| a.total_cmp(b))
244 else {
245 return;
246 };
247 let max_index = if *max.1 > 0.0 {
248 max.0
249 } else {
250 return;
251 };
252
253 let content = &state.decks[max_index].content;
254 let channel = &state.channels[max_index];
255
256 let current_song_info = self.copy_song_info_from_deck(content, playlist);
257 self.cached_song_info = Some(current_song_info.clone());
258
259 if old_song_info != self.cached_song_info {
260 self.should_scroll = true;
261 }
262
263 self.cached_next_song_info = self
264 .try_get_next_with_mode(false, channel, playlist)
265 .or_else(|| self.try_get_next_with_mode(true, channel, playlist));
266
267 match self.sync_mode {
268 TraktorSyncMode::AbsoluteByNumber => {
269 let current_index = playlist
270 .iter()
271 .position(|s| content.number == s.track_number);
272
273 self.cached_sync_action = match current_index {
274 None => TraktorSyncAction::Relative(0),
275 Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
276 };
277 }
278 TraktorSyncMode::AbsoluteByName => {
279 let current_index = playlist
280 .iter()
281 .position(|s| Self::songs_name_match(¤t_song_info, s));
282
283 self.cached_sync_action = match current_index {
284 None => TraktorSyncAction::Relative(0),
285 Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
286 };
287 }
288 _ => {}
289 };
290 }
291
292 fn try_get_next_with_mode(
293 &self,
294 fallback: bool,
295 current_channel: &ChannelState,
296 playlist: &[SongInfo],
297 ) -> Option<SongInfo> {
298 let mode = if !fallback {
299 self.next_mode
300 } else {
301 self.next_mode_fallback
302 };
303
304 if !self.is_ready() {
305 return None;
306 }
307
308 let state = self.state.as_ref()?;
309
310 let current_song_info = self.cached_song_info.as_ref()?;
311
312 match mode {
313 TraktorNextMode::DeckByPosition => {
314 let is_on_left = if current_channel.x_fader_left {
315 true
316 } else if current_channel.x_fader_right {
317 false
318 } else {
319 return None;
320 };
321
322 let other_side = state.channels.iter().position(|c| {
323 if is_on_left {
324 c.x_fader_right
325 } else {
326 c.x_fader_left
327 }
328 });
329
330 let deck = other_side.map(|o| &state.decks[o]);
331 deck.filter(|d| d.play_state.position < 0.5 * d.content.track_length)
332 .map(|d| self.copy_song_info_from_deck(&d.content, playlist))
333 }
334 TraktorNextMode::DeckByNumber => {
335 let deck = state
336 .decks
337 .iter()
338 .find(|d| d.content.number == current_song_info.track_number + 1);
339
340 deck.map(|d| self.copy_song_info_from_deck(&d.content, playlist))
341 }
342 TraktorNextMode::PlaylistByNumber => {
343 let current_index = playlist
344 .iter()
345 .position(|s| current_song_info.track_number == s.track_number);
346
347 current_index.and_then(|ci| playlist.get(ci + 1).cloned())
348 }
349 TraktorNextMode::PlaylistByName => {
350 let current_index = playlist
351 .iter()
352 .position(|s| Self::songs_name_match(current_song_info, s));
353
354 current_index.and_then(|ci| playlist.get(ci + 1).cloned())
355 }
356 }
357 }
358
359 fn copy_song_info_from_deck(
360 &self,
361 content: &DeckContentState,
362 playlist: &[SongInfo],
363 ) -> SongInfo {
364 let mut song_info = SongInfo::new(
365 content.number,
366 content.title.to_owned(),
367 content.artist.to_owned(),
368 content.genre.to_owned(),
369 self.covers.get(&content.file_path).cloned(),
370 );
371
372 if song_info.album_art.is_none() {
373 song_info.album_art = playlist
374 .iter()
375 .find(|s| Self::songs_name_match(&song_info, s))
376 .and_then(|s| s.album_art.clone());
377 }
378
379 song_info
380 }
381
382 pub fn songs_name_match(a: &SongInfo, b: &SongInfo) -> bool {
383 a.artist == b.artist && a.title == b.title
385 }
386
387 fn get_loaded_files(&self) -> Vec<String> {
388 let Some(state) = self.state.as_ref() else {
389 return Vec::new();
390 };
391
392 let mut files: Vec<String> = state
393 .decks
394 .iter()
395 .map(|d| &d.content.file_path)
396 .filter(|&f| !f.is_empty())
397 .map(|f| f.to_owned())
398 .collect();
399 files.dedup();
400
401 files
402 }
403
404 pub fn process_message(&mut self, message: ServerMessage, playlist: &[SongInfo]) {
405 match message {
406 ServerMessage::Ready(channel) => {
407 self.channel = Some(channel);
408
409 self.time_offset_ms = 0;
410 self.state = None;
411 self.sync_x_fader_is_left = true;
412 self.update_song_info(playlist);
413
414 self.reconnect();
415 }
416 ServerMessage::Connect {
417 time_offset_ms,
418 initial_state,
419 } => {
420 self.time_offset_ms = time_offset_ms;
421 self.sync_x_fader_is_left = initial_state.mixer.x_fader < 0.5;
422 self.state = Some(*initial_state);
423 self.update_song_info(playlist);
424 }
425 ServerMessage::Update(update) => {
426 if let Some(state) = self.state.as_mut() {
427 if matches!(self.sync_mode, TraktorSyncMode::Relative)
428 && let StateUpdate::Mixer(new_mixer_state) = &update
429 {
430 let x_fader_old = state.mixer.x_fader;
431 let x_fader_new = new_mixer_state.x_fader;
432
433 let mut offset = 0;
434 if x_fader_old > 0.5 && x_fader_new <= 0.5 {
435 if self.sync_x_fader_is_left {
436 offset -= 1;
437 } else {
438 offset += 1;
439 }
440 } else if x_fader_old <= 0.5 && x_fader_new > 0.5 {
441 if self.sync_x_fader_is_left {
442 offset += 1;
443 } else {
444 offset -= 1;
445 }
446 }
447
448 if x_fader_new < 0.2 {
449 self.sync_x_fader_is_left = true;
450 } else if x_fader_new > 0.8 {
451 self.sync_x_fader_is_left = false;
452 }
453
454 self.cached_sync_action = match self.cached_sync_action {
455 TraktorSyncAction::Relative(prev) => {
456 TraktorSyncAction::Relative(prev + offset)
457 }
458 TraktorSyncAction::PlaylistAbsolute(_) => {
459 TraktorSyncAction::Relative(offset)
460 }
461 };
462 }
463
464 state.apply_update(update);
465 }
466 self.update_song_info(playlist);
467 }
468 ServerMessage::CoverImage { path, data } => {
469 self.covers.insert(path, image::Handle::from_bytes(data));
470
471 let loaded_files = self.get_loaded_files();
472 self.covers.retain(|path, _| loaded_files.contains(path));
473 }
474 ServerMessage::Log(msg) => {
475 if self.debug_logging {
476 self.log.push(msg);
477 }
478 }
479 ServerMessage::ClientChanged(addr) => {
480 self.cover_loader_addr = addr;
481 }
482 }
483 }
484
485 pub fn take_sync_action(&mut self) -> TraktorSyncAction {
486 mem::replace(&mut self.cached_sync_action, TraktorSyncAction::Relative(0))
487 }
488
489 pub fn take_should_scroll(&mut self) -> bool {
490 let should_scroll = self.should_scroll;
491 self.should_scroll = false;
492 should_scroll
493 }
494
495 pub fn get_current_index(&self, playlist: &[SongInfo]) -> Option<usize> {
496 let traktor_song = self.get_song_info()?;
497
498 playlist
499 .iter()
500 .enumerate()
501 .find(|(_i, s)| TraktorDataProvider::songs_name_match(s, traktor_song))
502 .map(|(i, _s)| i)
503 }
504
505 fn send_message(&mut self, message: AppMessage) {
506 if let Some(channel) = self.channel.as_ref()
507 && channel.unbounded_send(message).is_err()
508 {
509 self.channel = None;
510 }
511 }
512
513 pub fn enabled(&self) -> bool {
514 self.enabled
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::traktor_api::DeckPlayState;
522 use iced::futures::channel::mpsc;
523 use std::array;
524
525 #[test]
528 fn connecting_cover_loader_sets_connection_state() {
529 let (tx, _rx) = mpsc::unbounded();
531 let mut provider = TraktorDataProvider {
532 enabled: true,
533 channel: Some(tx),
534 ..Default::default()
535 };
536
537 assert!(matches!(
538 provider.get_connection_state(),
539 ConnectionState::Disconnected
540 ));
541
542 provider.process_message(
543 ServerMessage::ClientChanged(Some("127.0.0.1:8080".parse().unwrap())),
544 &[],
545 );
546 assert!(matches!(
547 provider.get_connection_state(),
548 ConnectionState::CoverLoader
549 ));
550
551 provider.process_message(ServerMessage::ClientChanged(None), &[]);
552 assert!(matches!(
553 provider.get_connection_state(),
554 ConnectionState::Disconnected
555 ));
556 }
557
558 #[test]
560 fn connecting_traktor_sets_connection_state() {
561 let (tx, _rx) = mpsc::unbounded();
563 let mut provider = TraktorDataProvider {
564 enabled: true,
565 channel: Some(tx),
566 ..Default::default()
567 };
568
569 assert!(matches!(
570 provider.get_connection_state(),
571 ConnectionState::Disconnected
572 ));
573
574 let connect_message = ServerMessage::Connect {
575 time_offset_ms: 0,
576 initial_state: Box::new(get_sample_state()),
577 };
578
579 provider.process_message(connect_message, &[]);
580 assert!(matches!(
581 provider.get_connection_state(),
582 ConnectionState::Traktor
583 ));
584 }
585
586 #[test]
590 fn connecting_cover_loader_and_traktor_sets_connection_state() {
591 let (tx, _rx) = mpsc::unbounded();
593 let mut provider = TraktorDataProvider {
594 enabled: true,
595 channel: Some(tx),
596 ..Default::default()
597 };
598
599 assert!(matches!(
600 provider.get_connection_state(),
601 ConnectionState::Disconnected
602 ));
603
604 provider.process_message(
605 ServerMessage::ClientChanged(Some("127.0.0.1:8080".parse().unwrap())),
606 &[],
607 );
608 assert!(matches!(
609 provider.get_connection_state(),
610 ConnectionState::CoverLoader
611 ));
612
613 let connect_message = ServerMessage::Connect {
614 time_offset_ms: 0,
615 initial_state: Box::new(get_sample_state()),
616 };
617
618 provider.process_message(connect_message, &[]);
619 assert!(matches!(
620 provider.get_connection_state(),
621 ConnectionState::Connected
622 ));
623
624 provider.process_message(ServerMessage::ClientChanged(None), &[]);
625 assert!(matches!(
626 provider.get_connection_state(),
627 ConnectionState::Traktor
628 ));
629
630 provider.set_enabled(false);
631
632 assert!(matches!(
633 provider.get_connection_state(),
634 ConnectionState::Disconnected
635 ));
636 }
637
638 fn get_sample_state() -> State {
639 let deck_state = DeckState {
640 content: DeckContentState {
641 is_loaded: false,
642 number: 0,
643 title: "".to_string(),
644 artist: "".to_string(),
645 album: "".to_string(),
646 genre: "".to_string(),
647 comment: "".to_string(),
648 comment2: "".to_string(),
649 label: "".to_string(),
650 key: "".to_string(),
651 file_path: "".to_string(),
652 track_length: 0.0,
653 bpm: 0.0,
654 },
655 play_state: DeckPlayState {
656 timestamp: 0,
657 position: 0.0,
658 speed: 0.0,
659 },
660 };
661 State {
662 mixer: MixerState {
663 x_fader: 0.0,
664 master_volume: 0.0,
665 cue_volume: 0.0,
666 cue_mix: 0.0,
667 mic_volume: 0.0,
668 },
669 channels: [ChannelState {
670 cue: false,
671 volume: 0.0,
672 x_fader_left: false,
673 x_fader_right: false,
674 }; 4],
675 decks: array::from_fn(|_| deck_state.clone()),
676 }
677 }
678}