music_player_types/
types.rs

1use std::time::Duration;
2
3use local_ip_addr::get_local_ip_address;
4use lofty::{Accessor, FileProperties, ItemKey, Tag};
5use mdns_sd::ServiceInfo;
6use music_player_discovery::{SERVICE_NAME, XBMC_SERVICE_NAME};
7use music_player_settings::{read_settings, Settings};
8use tantivy::{
9    schema::{Schema, SchemaBuilder, STORED, STRING, TEXT},
10    Document,
11};
12use upnp_client::types::Metadata;
13use url::Url;
14
15pub const CHROMECAST_SERVICE_NAME: &str = "_googlecast._tcp.local.";
16pub const AIRPLAY_SERVICE_NAME: &str = "_raop._tcp.local.";
17
18pub const AIRPLAY_DEVICE: &str = "AirPlay";
19pub const CHROMECAST_DEVICE: &str = "Chromecast";
20pub const XBMC_DEVICE: &str = "XBMC";
21pub const MUSIC_PLAYER_DEVICE: &str = "MusicPlayer";
22pub const UPNP_DLNA_DEVICE: &str = "UPnP/DLNA";
23
24#[derive(Debug, Clone, Default)]
25pub struct Playback {
26    pub current_track: Option<Track>,
27    pub index: u32,
28    pub current_item_id: Option<i32>,
29    pub position_ms: u32,
30    pub is_playing: bool,
31    pub items: Vec<(Track, i32)>,
32}
33
34pub struct CurrentPlayback {
35    pub current: Option<Playback>,
36}
37
38impl CurrentPlayback {
39    pub fn new() -> Self {
40        Self { current: None }
41    }
42}
43
44#[derive(Debug, Clone, Default)]
45pub struct Song {
46    pub title: String,
47    pub artist: String,
48    pub album: String,
49    pub genre: String,
50    pub year: Option<u32>,
51    pub track: Option<u32>,
52    pub bitrate: Option<u32>,
53    pub sample_rate: Option<u32>,
54    pub bit_depth: Option<u8>,
55    pub channels: Option<u8>,
56    pub duration: Duration,
57    pub uri: Option<String>,
58    pub cover: Option<String>,
59    pub album_artist: String,
60}
61
62#[derive(Debug, Clone, Default)]
63pub struct SimplifiedSong {
64    pub id: String,
65    pub title: String,
66    pub artist: String,
67    pub album: String,
68    pub genre: String,
69    pub duration: Duration,
70    pub cover: Option<String>,
71    pub artist_id: String,
72    pub album_id: String,
73}
74
75#[derive(Debug, Clone, Default)]
76pub struct Album {
77    pub id: String,
78    pub title: String,
79    pub artist: String,
80    pub artist_id: Option<String>,
81    pub year: Option<u32>,
82    pub cover: Option<String>,
83    pub tracks: Vec<Track>,
84}
85
86#[derive(Debug, Clone, Default)]
87pub struct Artist {
88    pub id: String,
89    pub name: String,
90    pub picture: Option<String>,
91    pub albums: Vec<Album>,
92    pub songs: Vec<Track>,
93}
94
95impl From<Document> for Album {
96    fn from(doc: Document) -> Self {
97        let mut schema_builder: SchemaBuilder = Schema::builder();
98
99        let id_field = schema_builder.add_text_field("id", STRING | STORED);
100        let title_field = schema_builder.add_text_field("title", TEXT | STORED);
101        let artist_field = schema_builder.add_text_field("artist", TEXT | STORED);
102        let year_field = schema_builder.add_i64_field("year", STORED);
103        let cover_field = schema_builder.add_text_field("cover", STRING | STORED);
104
105        let id = doc
106            .get_first(id_field)
107            .unwrap()
108            .as_text()
109            .unwrap()
110            .to_string();
111        let title = doc
112            .get_first(title_field)
113            .unwrap()
114            .as_text()
115            .unwrap()
116            .to_string();
117        let artist = doc
118            .get_first(artist_field)
119            .unwrap()
120            .as_text()
121            .unwrap()
122            .to_string();
123        let year = Some(doc.get_first(year_field).unwrap().as_i64().unwrap() as u32);
124        let cover = match doc.get_first(cover_field) {
125            Some(cover) => cover.as_text(),
126            None => None,
127        };
128        let cover = match cover {
129            Some("") => None,
130            Some(cover) => Some(cover.to_string()),
131            None => None,
132        };
133
134        Self {
135            id,
136            title,
137            artist,
138            year,
139            cover,
140            ..Default::default()
141        }
142    }
143}
144
145impl From<Document> for Artist {
146    fn from(doc: Document) -> Self {
147        let mut schema_builder: SchemaBuilder = Schema::builder();
148
149        let id_field = schema_builder.add_text_field("id", TEXT | STORED);
150        let name_field = schema_builder.add_text_field("name", TEXT | STORED);
151
152        let id = doc
153            .get_first(id_field)
154            .unwrap()
155            .as_text()
156            .unwrap()
157            .to_string();
158        let name = doc
159            .get_first(name_field)
160            .unwrap()
161            .as_text()
162            .unwrap()
163            .to_string();
164
165        Self {
166            id,
167            name,
168            ..Default::default()
169        }
170    }
171}
172
173impl From<Document> for SimplifiedSong {
174    fn from(doc: Document) -> Self {
175        let mut schema_builder: SchemaBuilder = Schema::builder();
176
177        let id_field = schema_builder.add_text_field("id", STRING | STORED);
178        let title_field = schema_builder.add_text_field("title", TEXT | STORED);
179        let artist_field = schema_builder.add_text_field("artist", TEXT | STORED);
180        let album_field = schema_builder.add_text_field("album", TEXT | STORED);
181        let genre_field = schema_builder.add_text_field("genre", TEXT);
182        let cover_field = schema_builder.add_text_field("cover", STRING | STORED);
183        let duration_field = schema_builder.add_i64_field("duration", STORED);
184        let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED);
185        let album_id_field = schema_builder.add_text_field("album_id", STRING | STORED);
186
187        let id = doc
188            .get_first(id_field)
189            .unwrap()
190            .as_text()
191            .unwrap()
192            .to_string();
193
194        let title = match doc.get_first(title_field) {
195            Some(title) => title.as_text().unwrap().to_string(),
196            None => String::from(""),
197        };
198        let artist = match doc.get_first(artist_field) {
199            Some(artist) => artist.as_text().unwrap().to_string(),
200            None => String::from(""),
201        };
202        let album = match doc.get_first(album_field) {
203            Some(album) => album.as_text().unwrap().to_string(),
204            None => String::from(""),
205        };
206        let genre = match doc.get_first(genre_field) {
207            Some(genre) => genre.as_text().unwrap().to_string(),
208            None => String::from(""),
209        };
210        let duration = match doc.get_first(duration_field) {
211            Some(duration) => Duration::from_secs(duration.as_i64().unwrap_or_default() as u64),
212            None => Duration::from_secs(0),
213        };
214        let cover = match doc.get_first(cover_field) {
215            Some(cover) => cover.as_text(),
216            None => None,
217        };
218        let cover = match cover {
219            Some("") => None,
220            Some(cover) => Some(cover.to_string()),
221            None => None,
222        };
223        let artist_id = doc
224            .get_first(artist_id_field)
225            .unwrap()
226            .as_text()
227            .unwrap()
228            .to_string();
229        let album_id = doc
230            .get_first(album_id_field)
231            .unwrap()
232            .as_text()
233            .unwrap()
234            .to_string();
235        Self {
236            id,
237            title,
238            artist,
239            album,
240            genre,
241            duration,
242            cover,
243            artist_id,
244            album_id,
245            ..Default::default()
246        }
247    }
248}
249
250impl From<&Tag> for Song {
251    fn from(tag: &Tag) -> Self {
252        Self {
253            title: tag.title().unwrap_or("None").to_string(),
254            artist: tag.artist().unwrap_or("None").to_string(),
255            album: tag.album().unwrap_or("None").to_string(),
256            genre: tag.genre().unwrap_or("None").to_string(),
257            year: tag.year(),
258            track: tag.track(),
259            album_artist: tag
260                .get_string(&ItemKey::AlbumArtist)
261                .unwrap_or(tag.artist().unwrap_or("None"))
262                .to_string(),
263            ..Default::default()
264        }
265    }
266}
267
268impl From<&Tag> for Artist {
269    fn from(tag: &Tag) -> Self {
270        let id = format!(
271            "{:x}",
272            md5::compute(
273                tag.get_string(&ItemKey::AlbumArtist)
274                    .unwrap_or(tag.artist().unwrap_or("None"))
275                    .to_string()
276            )
277        );
278        Self {
279            id,
280            name: tag
281                .get_string(&ItemKey::AlbumArtist)
282                .unwrap_or(tag.artist().unwrap_or("None"))
283                .to_string(),
284            ..Default::default()
285        }
286    }
287}
288
289impl From<&Tag> for Album {
290    fn from(tag: &Tag) -> Self {
291        let id = format!(
292            "{:x}",
293            md5::compute(tag.album().unwrap_or("None").to_string())
294        );
295        let artist_id = Some(format!(
296            "{:x}",
297            md5::compute(
298                tag.get_string(&ItemKey::AlbumArtist)
299                    .unwrap_or(tag.artist().unwrap_or("None"))
300                    .to_string()
301            )
302        ));
303        Self {
304            id,
305            title: tag.album().unwrap_or("None").to_string(),
306            artist: tag
307                .get_string(&ItemKey::AlbumArtist)
308                .unwrap_or(tag.artist().unwrap_or("None"))
309                .to_string(),
310            year: tag.year(),
311            artist_id,
312            ..Default::default()
313        }
314    }
315}
316
317impl Song {
318    pub fn with_properties(&mut self, properties: &FileProperties) -> Self {
319        self.bitrate = properties.audio_bitrate();
320        self.sample_rate = properties.sample_rate();
321        self.bit_depth = properties.bit_depth();
322        self.channels = properties.channels();
323        self.duration = properties.duration();
324        self.clone()
325    }
326}
327
328#[derive(Default, Clone)]
329pub struct Device {
330    pub id: String,
331    pub name: String,
332    pub host: String,
333    pub ip: String,
334    pub port: u16,
335    pub service: String,
336    pub app: String,
337    pub is_connected: bool,
338    pub base_url: Option<String>,
339    pub is_cast_device: bool,
340    pub is_source_device: bool,
341    pub is_current_device: bool,
342}
343
344impl Device {
345    pub fn with_base_url(&mut self, base_url: Option<String>) -> Self {
346        self.base_url = base_url;
347        self.clone()
348    }
349}
350
351impl From<ServiceInfo> for Device {
352    fn from(srv: ServiceInfo) -> Self {
353        if srv.get_fullname().contains("xbmc") {
354            return Self {
355                id: srv.get_fullname().to_owned(),
356                name: srv
357                    .get_fullname()
358                    .replace(XBMC_SERVICE_NAME, "")
359                    .replace(".", "")
360                    .to_owned(),
361                host: srv
362                    .get_hostname()
363                    .split_at(srv.get_hostname().len() - 1)
364                    .0
365                    .to_owned(),
366                ip: srv.get_addresses().iter().next().unwrap().to_string(),
367                port: srv.get_port(),
368                service: srv.get_fullname().to_owned(),
369                app: "xbmc".to_owned(),
370                is_connected: false,
371                base_url: None,
372                is_cast_device: true,
373                is_source_device: true,
374                is_current_device: false,
375            };
376        }
377
378        if srv.get_fullname().contains(SERVICE_NAME) {
379            let device_id = srv
380                .get_fullname()
381                .replace(SERVICE_NAME, "")
382                .split("-")
383                .collect::<Vec<&str>>()[1]
384                .replace(".", "")
385                .to_owned();
386
387            let config = read_settings().unwrap();
388            let settings = config.try_deserialize::<Settings>().unwrap();
389
390            let is_current_device = device_id == settings.device_id
391                && srv.get_fullname().split("-").collect::<Vec<&str>>()[0].to_owned() == "http";
392
393            let mut addresses = srv.get_addresses().iter();
394            let mut ip = addresses.next().unwrap().to_string();
395
396            if is_current_device {
397                ip = get_local_ip_address().unwrap();
398            }
399
400            return Self {
401                id: device_id.clone(),
402                name: srv
403                    .get_properties()
404                    .get("device_name")
405                    .unwrap_or(&device_id.clone())
406                    .to_owned(),
407                host: srv
408                    .get_hostname()
409                    .split_at(srv.get_hostname().len() - 1)
410                    .0
411                    .to_owned(),
412                ip,
413                port: srv.get_port(),
414                service: srv.get_fullname().split("-").collect::<Vec<&str>>()[0].to_owned(),
415                app: "music-player".to_owned(),
416                is_connected: false,
417                base_url: None,
418                is_cast_device: true,
419                is_source_device: true,
420                is_current_device,
421            };
422        }
423
424        if srv.get_fullname().contains(CHROMECAST_SERVICE_NAME) {
425            return Self {
426                id: srv.get_properties().get("id").unwrap().to_owned(),
427                name: srv.get_properties().get("fn").unwrap().to_owned(),
428                host: srv
429                    .get_hostname()
430                    .split_at(srv.get_hostname().len() - 1)
431                    .0
432                    .to_owned(),
433                ip: srv.get_addresses().iter().next().unwrap().to_string(),
434                port: srv.get_port(),
435                service: srv.get_fullname().to_owned(),
436                app: "chromecast".to_owned(),
437                is_connected: false,
438                base_url: None,
439                is_cast_device: true,
440                is_source_device: false,
441                is_current_device: false,
442            };
443        }
444
445        if srv.get_fullname().contains(AIRPLAY_SERVICE_NAME) {
446            let name = srv.get_fullname().split("@").collect::<Vec<&str>>()[1]
447                .replace(AIRPLAY_SERVICE_NAME, "")
448                .to_owned();
449            let name = name.split_at(name.len() - 1).0.to_owned();
450            return Self {
451                id: srv.get_fullname().to_owned(),
452                name,
453                host: srv
454                    .get_hostname()
455                    .split_at(srv.get_hostname().len() - 1)
456                    .0
457                    .to_owned(),
458                ip: srv.get_addresses().iter().next().unwrap().to_string(),
459                port: srv.get_port(),
460                service: srv.get_fullname().to_owned(),
461                app: "airplay".to_owned(),
462                is_connected: false,
463                base_url: None,
464                is_cast_device: true,
465                is_source_device: false,
466                is_current_device: false,
467            };
468        }
469
470        Self {
471            ..Default::default()
472        }
473    }
474}
475
476impl From<upnp_client::types::Device> for Device {
477    fn from(device: upnp_client::types::Device) -> Self {
478        let (host, port) = Url::parse(&device.location)
479            .map(|url| {
480                let host = url.host_str().unwrap();
481                let port = url.port().unwrap();
482                (host.to_string(), port)
483            })
484            .unwrap();
485        let is_cast_device = device
486            .device_type
487            .contains("urn:schemas-upnp-org:device:MediaRenderer");
488        let is_source_device = device
489            .device_type
490            .contains("urn:schemas-upnp-org:device:MediaServer");
491
492        Self {
493            id: device.udn,
494            name: device.friendly_name,
495            host: host.clone(),
496            ip: host.clone(),
497            port,
498            service: device.device_type,
499            app: "dlna".to_owned(),
500            is_connected: false,
501            base_url: Some(device.location),
502            is_cast_device,
503            is_source_device,
504            is_current_device: false,
505        }
506    }
507}
508
509pub trait Connected {
510    fn is_connected(&self, current: Option<&Device>) -> Self;
511}
512
513impl Connected for Device {
514    fn is_connected(&self, current: Option<&Device>) -> Self {
515        match current {
516            Some(current) => Self {
517                is_connected: self.id == current.id,
518                ..self.clone()
519            },
520            None => Self {
521                is_connected: false,
522                ..self.clone()
523            },
524        }
525    }
526}
527
528#[derive(Default, Debug, Clone)]
529pub struct Track {
530    pub id: String,
531    pub title: String,
532    pub duration: Option<f32>,
533    pub disc_number: u32,
534    pub track_number: Option<u32>,
535    pub uri: String,
536    pub artists: Vec<Artist>,
537    pub album: Option<Album>,
538    pub artist: String,
539}
540
541#[derive(Default, Clone)]
542pub struct Playlist {
543    pub id: String,
544    pub name: String,
545    pub description: Option<String>,
546    pub tracks: Vec<Track>,
547}
548
549#[derive(Default, Clone)]
550pub struct Folder {
551    pub id: String,
552    pub name: String,
553    pub playlists: Vec<Playlist>,
554}
555
556impl Into<Metadata> for Track {
557    fn into(self) -> Metadata {
558        Metadata {
559            title: self.title,
560            artist: Some(self.artist),
561            album: self.album.clone().map(|a| a.title),
562            album_art_uri: self.album.map(|a| a.cover.unwrap()),
563            ..Default::default()
564        }
565    }
566}
567
568pub trait RemoteTrackUrl {
569    fn with_remote_track_url(&self, base_url: &str) -> Self;
570}
571
572pub trait RemoteCoverUrl {
573    fn with_remote_cover_url(&self, base_url: &str) -> Self;
574}
575
576impl RemoteTrackUrl for Track {
577    fn with_remote_track_url(&self, base_url: &str) -> Self {
578        Self {
579            uri: format!("{}/tracks/{}", base_url, self.id),
580            ..self.clone()
581        }
582    }
583}
584
585impl RemoteCoverUrl for Track {
586    fn with_remote_cover_url(&self, base_url: &str) -> Self {
587        Self {
588            album: match self.album {
589                Some(ref album) => Some(album.with_remote_cover_url(base_url)),
590                None => None,
591            },
592            ..self.clone()
593        }
594    }
595}
596
597impl RemoteCoverUrl for Album {
598    fn with_remote_cover_url(&self, base_url: &str) -> Self {
599        let cover_url = match self.cover {
600            Some(ref cover) => match cover.starts_with("http") {
601                true => Some(cover.to_owned()),
602                false => Some(format!("{}/covers/{}", base_url, cover)),
603            },
604            None => None,
605        };
606        Self {
607            cover: cover_url,
608            tracks: self
609                .tracks
610                .iter()
611                .map(|track| track.with_remote_cover_url(base_url))
612                .collect(),
613            ..self.clone()
614        }
615    }
616}
617
618impl RemoteTrackUrl for Album {
619    fn with_remote_track_url(&self, base_url: &str) -> Self {
620        Self {
621            tracks: self
622                .tracks
623                .iter()
624                .map(|track| track.with_remote_track_url(base_url))
625                .collect(),
626            ..self.clone()
627        }
628    }
629}
630
631impl RemoteCoverUrl for Artist {
632    fn with_remote_cover_url(&self, base_url: &str) -> Self {
633        Self {
634            albums: self
635                .albums
636                .iter()
637                .map(|album| album.with_remote_cover_url(base_url))
638                .collect(),
639            songs: self
640                .songs
641                .iter()
642                .map(|track| track.with_remote_cover_url(base_url))
643                .collect(),
644            ..self.clone()
645        }
646    }
647}
648
649impl RemoteTrackUrl for Artist {
650    fn with_remote_track_url(&self, base_url: &str) -> Self {
651        Self {
652            songs: self
653                .songs
654                .iter()
655                .map(|track| track.with_remote_track_url(base_url))
656                .collect(),
657            ..self.clone()
658        }
659    }
660}
661
662impl RemoteTrackUrl for Playlist {
663    fn with_remote_track_url(&self, base_url: &str) -> Self {
664        Self {
665            tracks: self
666                .tracks
667                .iter()
668                .map(|track| track.with_remote_track_url(base_url))
669                .collect(),
670            ..self.clone()
671        }
672    }
673}
674
675impl RemoteCoverUrl for Playlist {
676    fn with_remote_cover_url(&self, base_url: &str) -> Self {
677        Self {
678            tracks: self
679                .tracks
680                .iter()
681                .map(|track| track.with_remote_cover_url(base_url))
682                .collect(),
683            ..self.clone()
684        }
685    }
686}