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}