danceinterpreter_rs/traktor_api/
data_provider.rs

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::mem;
10use std::net::SocketAddr;
11
12pub const TRAKTOR_SERVER_DEFAULT_ADDR: &str = "127.0.0.1:8080";
13
14#[derive(Debug, Copy, Clone, Eq, PartialEq)]
15pub enum TraktorNextMode {
16    DeckByPosition,
17    DeckByNumber,
18    PlaylistByNumber,
19    PlaylistByName,
20}
21
22#[derive(Debug, Copy, Clone, Eq, PartialEq)]
23pub enum TraktorSyncMode {
24    Relative,
25    AbsoluteByNumber,
26    AbsoluteByName,
27}
28
29#[derive(Debug, Copy, Clone, Eq, PartialEq)]
30pub enum TraktorSyncAction {
31    Relative(isize),
32    PlaylistAbsolute(usize),
33}
34
35pub struct TraktorDataProvider {
36    pub is_enabled: bool,
37    pub address: String,
38    pub submitted_address: String,
39
40    pub next_mode: Option<TraktorNextMode>,
41    pub next_mode_fallback: Option<TraktorNextMode>,
42    pub sync_mode: Option<TraktorSyncMode>,
43
44    channel: Option<UnboundedSender<AppMessage>>,
45
46    time_offset_ms: i64,
47    pub state: Option<State>,
48    covers: HashMap<String, image::Handle>,
49
50    sync_x_fader_is_left: bool,
51
52    cached_song_info: Option<SongInfo>,
53    cached_next_song_info: Option<SongInfo>,
54    cached_sync_action: TraktorSyncAction,
55    should_scroll: bool,
56
57    pub debug_logging: bool,
58    log: Vec<String>,
59}
60
61impl Default for TraktorDataProvider {
62    fn default() -> Self {
63        Self {
64            is_enabled: false,
65            address: String::new(),
66            submitted_address: String::new(),
67            channel: None,
68
69            next_mode: Some(TraktorNextMode::DeckByNumber),
70            next_mode_fallback: None,
71            sync_mode: None,
72
73            time_offset_ms: 0,
74            state: None,
75            covers: HashMap::new(),
76
77            sync_x_fader_is_left: true,
78
79            cached_song_info: None,
80            cached_next_song_info: None,
81            cached_sync_action: TraktorSyncAction::Relative(0),
82            should_scroll: false,
83
84            debug_logging: false,
85            log: Vec::new(),
86        }
87    }
88}
89
90impl TraktorDataProvider {
91    pub fn is_ready(&self) -> bool {
92        self.is_enabled && self.channel.as_ref().is_some_and(|c| !c.is_closed())
93    }
94
95    #[allow(dead_code)]
96    pub fn get_log(&self) -> &[String] {
97        &self.log
98    }
99
100    #[allow(dead_code)]
101    pub fn clear_log(&mut self) {
102        self.log.clear();
103    }
104
105    pub fn reconnect(&mut self) {
106        self.time_offset_ms = 0;
107        self.state = None;
108        self.sync_x_fader_is_left = true;
109        self.update_song_info(&[]);
110
111        self.send_message(AppMessage::Reconnect {
112            debug_logging: self.debug_logging,
113        });
114    }
115
116    pub fn get_socket_addr(&self) -> Option<SocketAddr> {
117        if !self.is_enabled {
118            return None;
119        }
120
121        if self.submitted_address.is_empty() {
122            return TRAKTOR_SERVER_DEFAULT_ADDR.parse().ok();
123        }
124
125        self.submitted_address.parse().ok()
126    }
127
128    pub fn get_song_info(&self) -> Option<&SongInfo> {
129        if !self.is_ready() {
130            return None;
131        }
132
133        self.cached_song_info.as_ref()
134    }
135
136    pub fn get_next_song_info(&self) -> Option<&SongInfo> {
137        if !self.is_ready() {
138            return None;
139        }
140
141        self.cached_next_song_info.as_ref()
142    }
143
144    fn get_deck_score(&self, deck: &DeckState, channel: &ChannelState, mixer: &MixerState) -> f64 {
145        if !deck.content.is_loaded || deck.play_state.speed == 0.0 || channel.volume == 0.0 {
146            return 0.0;
147        }
148
149        if channel.x_fader_left && mixer.x_fader > 0.5 {
150            (1.0 - mixer.x_fader) * 2.0
151        } else if channel.x_fader_right && mixer.x_fader < 0.5 {
152            mixer.x_fader * 2.0
153        } else {
154            1.0
155        }
156    }
157
158    fn update_song_info(&mut self, playlist: &[SongInfo]) {
159        let old_song_info = self.cached_song_info.take();
160        self.cached_next_song_info = None;
161
162        if !self.is_ready() {
163            return;
164        }
165
166        let Some(state) = self.state.as_ref() else {
167            return;
168        };
169
170        let scores = [
171            self.get_deck_score(&state.decks.0, &state.channels.0, &state.mixer),
172            self.get_deck_score(&state.decks.1, &state.channels.1, &state.mixer),
173            self.get_deck_score(&state.decks.2, &state.channels.2, &state.mixer),
174            self.get_deck_score(&state.decks.3, &state.channels.3, &state.mixer),
175        ];
176
177        let Some(max) = scores
178            .iter()
179            .enumerate()
180            .max_by(|(_, a), (_, b)| a.total_cmp(b))
181        else {
182            return;
183        };
184        let max_index = if *max.1 > 0.0 {
185            max.0
186        } else {
187            return;
188        };
189
190        let content = match max_index {
191            0 => &state.decks.0.content,
192            1 => &state.decks.1.content,
193            2 => &state.decks.2.content,
194            3 => &state.decks.3.content,
195            _ => return,
196        };
197
198        let channel = match max_index {
199            0 => &state.channels.0,
200            1 => &state.channels.1,
201            2 => &state.channels.2,
202            3 => &state.channels.3,
203            _ => return,
204        };
205
206        let current_song_info = self.copy_song_info_from_deck(content, playlist);
207        self.cached_song_info = Some(current_song_info.clone());
208
209        if old_song_info != self.cached_song_info {
210            self.should_scroll = true;
211        }
212
213        self.cached_next_song_info = self
214            .try_get_next_with_mode(self.next_mode, channel, playlist)
215            .or_else(|| self.try_get_next_with_mode(self.next_mode_fallback, channel, playlist));
216
217        match self.sync_mode {
218            Some(TraktorSyncMode::AbsoluteByNumber) => {
219                let current_index = playlist
220                    .iter()
221                    .position(|s| content.number == s.track_number);
222
223                self.cached_sync_action = match current_index {
224                    None => TraktorSyncAction::Relative(0),
225                    Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
226                };
227            }
228            Some(TraktorSyncMode::AbsoluteByName) => {
229                let current_index = playlist
230                    .iter()
231                    .position(|s| Self::songs_name_match(&current_song_info, s));
232
233                self.cached_sync_action = match current_index {
234                    None => TraktorSyncAction::Relative(0),
235                    Some(ci) => TraktorSyncAction::PlaylistAbsolute(ci),
236                };
237            }
238            _ => {}
239        };
240    }
241
242    fn try_get_next_with_mode(
243        &self,
244        mode: Option<TraktorNextMode>,
245        current_channel: &ChannelState,
246        playlist: &[SongInfo],
247    ) -> Option<SongInfo> {
248        let mode = mode?;
249
250        if !self.is_ready() {
251            return None;
252        }
253
254        let state = self.state.as_ref()?;
255
256        let current_song_info = self.cached_song_info.as_ref()?;
257
258        match mode {
259            TraktorNextMode::DeckByPosition => {
260                let is_on_left = if current_channel.x_fader_left {
261                    true
262                } else if current_channel.x_fader_right {
263                    false
264                } else {
265                    return None;
266                };
267
268                let other_side = vec![
269                    &state.channels.0,
270                    &state.channels.1,
271                    &state.channels.2,
272                    &state.channels.3,
273                ]
274                    .into_iter()
275                    .position(|c| {
276                        if is_on_left {
277                            c.x_fader_right
278                        } else {
279                            c.x_fader_left
280                        }
281                    });
282
283                let deck = other_side.and_then(|o| match o {
284                    0 => Some(&state.decks.0),
285                    1 => Some(&state.decks.1),
286                    2 => Some(&state.decks.2),
287                    3 => Some(&state.decks.3),
288                    _ => None,
289                });
290
291                deck.filter(|d| d.play_state.position < 0.5 * d.content.track_length)
292                    .map(|d| self.copy_song_info_from_deck(&d.content, playlist))
293            }
294            TraktorNextMode::DeckByNumber => {
295                let deck = vec![
296                    &state.decks.0,
297                    &state.decks.1,
298                    &state.decks.2,
299                    &state.decks.3,
300                ]
301                    .into_iter()
302                    .find(|d| d.content.number == current_song_info.track_number + 1);
303
304                deck.map(|d| self.copy_song_info_from_deck(&d.content, playlist))
305            }
306            TraktorNextMode::PlaylistByNumber => {
307                let current_index = playlist
308                    .iter()
309                    .position(|s| current_song_info.track_number == s.track_number);
310
311                current_index.and_then(|ci| playlist.get(ci + 1).cloned())
312            }
313            TraktorNextMode::PlaylistByName => {
314                let current_index = playlist
315                    .iter()
316                    .position(|s| Self::songs_name_match(current_song_info, s));
317
318                current_index.and_then(|ci| playlist.get(ci + 1).cloned())
319            }
320        }
321    }
322
323    fn copy_song_info_from_deck(
324        &self,
325        content: &DeckContentState,
326        playlist: &[SongInfo],
327    ) -> SongInfo {
328        let mut song_info = SongInfo::new(
329            content.number,
330            content.title.to_owned(),
331            content.artist.to_owned(),
332            content.genre.to_owned(),
333            self.covers.get(&content.file_path).cloned(),
334        );
335
336        if song_info.album_art.is_none() {
337            song_info.album_art = playlist
338                .iter()
339                .find(|s| Self::songs_name_match(&song_info, s))
340                .and_then(|s| s.album_art.clone());
341        }
342
343        song_info
344    }
345
346    pub fn songs_name_match(a: &SongInfo, b: &SongInfo) -> bool {
347        // TODO: maybe change this to levenshtein or sth
348        a.artist == b.artist && a.title == b.title
349    }
350
351    fn get_loaded_files(&self) -> Vec<String> {
352        let Some(state) = self.state.as_ref() else {
353            return Vec::new();
354        };
355
356        let mut files: Vec<String> = vec![
357            &state.decks.0.content.file_path,
358            &state.decks.1.content.file_path,
359            &state.decks.2.content.file_path,
360            &state.decks.3.content.file_path,
361        ]
362            .into_iter()
363            .filter(|&f| !f.is_empty())
364            .map(|f| f.to_owned())
365            .collect();
366        files.dedup();
367
368        files
369    }
370
371    pub fn process_message(&mut self, message: ServerMessage, playlist: &[SongInfo]) {
372        match message {
373            ServerMessage::Ready(channel) => {
374                self.channel = Some(channel);
375
376                self.time_offset_ms = 0;
377                self.state = None;
378                self.sync_x_fader_is_left = true;
379                self.update_song_info(playlist);
380
381                self.reconnect();
382            }
383            ServerMessage::Connect {
384                time_offset_ms,
385                initial_state,
386            } => {
387                println!("{:?}", initial_state);
388
389                self.time_offset_ms = time_offset_ms;
390                self.sync_x_fader_is_left = initial_state.mixer.x_fader < 0.5;
391                self.state = Some(*initial_state);
392                self.update_song_info(playlist);
393            }
394            ServerMessage::Update(update) => {
395                println!("{:?}", update);
396
397                if let Some(state) = self.state.as_mut() {
398                    if matches!(self.sync_mode, Some(TraktorSyncMode::Relative))
399                        && let StateUpdate::Mixer(new_mixer_state) = &update
400                    {
401                        let x_fader_old = state.mixer.x_fader;
402                        let x_fader_new = new_mixer_state.x_fader;
403
404                        let mut offset = 0;
405                        if x_fader_old > 0.5 && x_fader_new <= 0.5 {
406                            if self.sync_x_fader_is_left {
407                                offset -= 1;
408                            } else {
409                                offset += 1;
410                            }
411                        } else if x_fader_old <= 0.5 && x_fader_new > 0.5 {
412                            if self.sync_x_fader_is_left {
413                                offset += 1;
414                            } else {
415                                offset -= 1;
416                            }
417                        }
418
419                        if x_fader_new < 0.2 {
420                            self.sync_x_fader_is_left = true;
421                        } else if x_fader_new > 0.8 {
422                            self.sync_x_fader_is_left = false;
423                        }
424
425                        self.cached_sync_action = match self.cached_sync_action {
426                            TraktorSyncAction::Relative(prev) => {
427                                TraktorSyncAction::Relative(prev + offset)
428                            }
429                            TraktorSyncAction::PlaylistAbsolute(_) => {
430                                TraktorSyncAction::Relative(offset)
431                            }
432                        };
433                    }
434
435                    state.apply_update(update);
436                }
437
438                self.update_song_info(playlist);
439            }
440            ServerMessage::CoverImage { path, data } => {
441                self.covers.insert(path, image::Handle::from_bytes(data));
442
443                let loaded_files = self.get_loaded_files();
444                self.covers.retain(|path, _| loaded_files.contains(path));
445            }
446            ServerMessage::Log(msg) => {
447                if self.debug_logging {
448                    self.log.push(msg);
449                }
450            }
451        }
452    }
453
454    pub fn take_sync_action(&mut self) -> TraktorSyncAction {
455        mem::replace(&mut self.cached_sync_action, TraktorSyncAction::Relative(0))
456    }
457
458    pub fn take_should_scroll(&mut self) -> bool {
459        let should_scroll = self.should_scroll;
460        self.should_scroll = false;
461        should_scroll
462    }
463
464    pub fn get_current_index(&self, playlist: &[SongInfo]) -> Option<usize> {
465        let traktor_song = self.get_song_info()?;
466
467        playlist
468            .iter()
469            .enumerate()
470            .find(|(_i, s)| TraktorDataProvider::songs_name_match(s, traktor_song))
471            .map(|(i, _s)| i)
472    }
473
474    fn send_message(&mut self, message: AppMessage) {
475        if let Some(channel) = self.channel.as_ref()
476            && channel.unbounded_send(message).is_err()
477        {
478            self.channel = None;
479        }
480    }
481}