diff --git a/music-player/client/src/client.ts b/music-player/client/src/client.ts index b42b023..61f1ba8 100644 --- a/music-player/client/src/client.ts +++ b/music-player/client/src/client.ts @@ -1,6 +1,7 @@ export interface TrackInfo { id: string; track_number?: number; + duration?: number; name?: string; album?: string; artist?: string; diff --git a/music-player/client/src/blocks/track.ts b/music-player/client/src/components/track.ts similarity index 87% rename from music-player/client/src/blocks/track.ts rename to music-player/client/src/components/track.ts index 46a742e..55c034e 100644 --- a/music-player/client/src/blocks/track.ts +++ b/music-player/client/src/components/track.ts @@ -35,7 +35,7 @@ export class TrackName extends HTMLElement { export class TrackCard extends HTMLElement { static get observedAttributes() { - return ["id", "trackNumber", "name", "album", "artist"]; + return ["id", "trackNumber", "name", "album", "artist", "duration"]; } constructor() { @@ -124,10 +124,14 @@ export class TrackCard extends HTMLElement { trackName.name = this["name"]; container.appendChild(trackName); } - this["length"] && container.appendChild(document.createTextNode("1:23")); - this["album"] && - container.appendChild(document.createTextNode("Shatter Me")); - this["artist"] && - container.appendChild(document.createTextNode("Lindsey Stirling")); + if (this["length"]) { + container.appendChild(document.createTextNode(this["length"])); + } + if (this["album"]) { + container.appendChild(document.createTextNode(this["album"])); + } + if (this["artist"]) { + container.appendChild(document.createTextNode(this["artist"])); + } } } diff --git a/music-player/client/src/main.ts b/music-player/client/src/main.ts index ec15f2e..657a044 100644 --- a/music-player/client/src/main.ts +++ b/music-player/client/src/main.ts @@ -1,6 +1,6 @@ import * as _ from "lodash"; import { TrackInfo, getTracks } from "./client"; -import { TrackName, TrackCard } from "./blocks/track"; +import { TrackName, TrackCard } from "./components/track"; window.customElements.define("track-name", TrackName); window.customElements.define("track-card", TrackCard); @@ -12,19 +12,15 @@ declare global { } } -const replaceTitle = () => { - const title = document.querySelector(".js-title"); - if (title && title.innerHTML) { - title.innerHTML = "Music Player Paused"; - } -}; - const updateTrackList = (tracks: TrackInfo[]) => { const track_list = document.querySelector(".track-list__tracks"); if (track_list) { let track_formats = _.map(tracks, (info) => { let card: TrackCard = document.createElement("track-card"); card.name = info.name || null; + card.album = info.album || null; + card.artist = info.artist || null; + card.length = (info.duration && `${info.duration}`) || null; return card; }); _.map(track_formats, (trackCard) => { diff --git a/music-player/server/Cargo.lock b/music-player/server/Cargo.lock index 6e0e4e4..4682b1f 100644 --- a/music-player/server/Cargo.lock +++ b/music-player/server/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" version = "0.7.6" @@ -77,6 +83,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -87,63 +102,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote 1.0.23", - "strsim", - "syn 1.0.107", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core", - "quote 1.0.23", - "syn 1.0.107", -] - -[[package]] -name = "dbus" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" -dependencies = [ - "libc", - "libdbus-sys", - "winapi", -] - -[[package]] -name = "derive_is_enum_variant" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ac8859845146979953797f03cc5b282fb4396891807cdb3d04929a88418197" -dependencies = [ - "heck", - "quote 0.3.15", - "syn 0.11.11", -] - [[package]] name = "digest" version = "0.10.6" @@ -154,17 +112,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "enum-kinds" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462" -dependencies = [ - "proc-macro2", - "quote 1.0.23", - "syn 1.0.107", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -186,6 +133,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flow" version = "0.1.0" @@ -205,27 +162,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "from_variants" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6e0c314d32a210f213d2de7fc205a415498c32477022bffdb8927a95f44493" -dependencies = [ - "from_variants_impl", -] - -[[package]] -name = "from_variants_impl" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243da8644e4e4ac28a22f7f30187f9d3f17eafbc219884ff8935edb5ebfe621c" -dependencies = [ - "darling", - "proc-macro2", - "quote 1.0.23", - "syn 1.0.107", -] - [[package]] name = "futures-channel" version = "0.3.25" @@ -351,15 +287,6 @@ dependencies = [ "http", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "hermit-abi" version = "0.2.6" @@ -428,10 +355,15 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "id3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "19d7a833474b30425eb64132d1f9b727b4e39537418bcc3288497c8d2f5c8948" +dependencies = [ + "bitflags", + "byteorder", + "flate2", +] [[package]] name = "idna" @@ -474,15 +406,6 @@ version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" -[[package]] -name = "libdbus-sys" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2264f9d90a9b4e60a2dc722ad899ea0374f03c2e96e755fe22a8f551d4d5fb3c" -dependencies = [ - "pkg-config", -] - [[package]] name = "libsqlite3-sys" version = "0.25.2" @@ -534,6 +457,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.5" @@ -546,19 +478,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "mpris" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3d377fd27d9d5c7145341cd3affcb83839c24c73e7460488b3ae0a3f9c5166" -dependencies = [ - "dbus", - "derive_is_enum_variant", - "enum-kinds", - "from_variants", - "thiserror", -] - [[package]] name = "multipart" version = "0.18.0" @@ -581,10 +500,10 @@ dependencies = [ name = "music-player" version = "0.1.0" dependencies = [ - "dbus", "flow", + "id3", + "mime", "mime_guess", - "mpris", "rusqlite", "serde", "thiserror", @@ -655,8 +574,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -698,12 +617,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quote" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" - [[package]] name = "quote" version = "1.0.23" @@ -824,8 +737,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -907,23 +820,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" -dependencies = [ - "quote 0.3.15", - "synom", - "unicode-xid", -] - [[package]] name = "syn" version = "1.0.107" @@ -931,19 +827,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", - "quote 1.0.23", + "quote", "unicode-ident", ] -[[package]] -name = "synom" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" -dependencies = [ - "unicode-xid", -] - [[package]] name = "tempfile" version = "3.3.0" @@ -974,8 +861,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -1020,8 +907,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", - "quote 1.0.23", - "syn 1.0.107", + "quote", + "syn", ] [[package]] @@ -1158,18 +1045,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" - -[[package]] -name = "unicode-xid" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" - [[package]] name = "url" version = "2.3.1" diff --git a/music-player/server/Cargo.toml b/music-player/server/Cargo.toml index a2fbd78..c849ff4 100644 --- a/music-player/server/Cargo.toml +++ b/music-player/server/Cargo.toml @@ -6,10 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dbus = { version = "0.9.7" } flow = { path = "../../flow" } -mime_guess = "2.0.4" -mpris = { version = "2.0" } +id3 = { version = "1.6" } +mime_guess = { version = "2.0" } +mime = { version = "0.3" } rusqlite = { version = "0.28" } serde = { version = "1.0", features = ["derive"] } thiserror = { version = "1.0" } diff --git a/music-player/server/src/bin/server.rs b/music-player/server/src/bin/server.rs index 2532edb..ae1d08c 100644 --- a/music-player/server/src/bin/server.rs +++ b/music-player/server/src/bin/server.rs @@ -7,10 +7,10 @@ use std::{ use warp::Filter; use music_player::{ - audio::TrackInfo, core::Core, database::{MemoryIndex, MusicIndex}, - music_scanner::FileScanner, + media::TrackInfo, + scanner::FileScanner, }; fn tracks(index: &Arc) -> Vec { diff --git a/music-player/server/src/core.rs b/music-player/server/src/core.rs index 5126a2a..290ccce 100644 --- a/music-player/server/src/core.rs +++ b/music-player/server/src/core.rs @@ -1,6 +1,4 @@ -use crate::{ - audio::TrackInfo, database::MusicIndex, music_scanner::MusicScanner, Error, FatalError, -}; +use crate::{database::MusicIndex, media::TrackInfo, scanner::MusicScanner, Error, FatalError}; use flow::{ok, Flow}; use std::{ sync::{ @@ -108,7 +106,7 @@ impl Core { #[cfg(test)] mod test { use super::*; - use crate::{audio::TrackId, database::MemoryIndex, music_scanner::factories::MockScanner}; + use crate::{database::MemoryIndex, media::TrackId, scanner::factories::MockScanner}; use std::collections::HashSet; fn with_example_index(f: F) diff --git a/music-player/server/src/database.rs b/music-player/server/src/database.rs index 72a05ac..4407c9d 100644 --- a/music-player/server/src/database.rs +++ b/music-player/server/src/database.rs @@ -1,5 +1,5 @@ use crate::{ - audio::{TrackId, TrackInfo}, + media::{TrackId, TrackInfo}, FatalError, }; use flow::{error, ok, Flow}; @@ -129,6 +129,8 @@ mod test { name: None, album: None, artist: None, + duration: None, + filetype: "text/plain".parse::().unwrap(), }; index.add_track(info.clone()); diff --git a/music-player/server/src/lib.rs b/music-player/server/src/lib.rs index cbf09e5..66dfc1d 100644 --- a/music-player/server/src/lib.rs +++ b/music-player/server/src/lib.rs @@ -1,7 +1,7 @@ -pub mod audio; pub mod core; pub mod database; -pub mod music_scanner; +pub mod media; +pub mod scanner; use database::DatabaseError; use thiserror::Error; diff --git a/music-player/server/src/audio.rs b/music-player/server/src/media.rs similarity index 94% rename from music-player/server/src/audio.rs rename to music-player/server/src/media.rs index e109506..bb8b9cb 100644 --- a/music-player/server/src/audio.rs +++ b/music-player/server/src/media.rs @@ -10,7 +10,7 @@ Luminescent Dreams Tools is distributed in the hope that it will be useful, but You should have received a copy of the GNU General Public License along with Lumeto. If not, see . */ -use serde::Serialize; +use serde::{Serialize, Serializer}; use std::time::Duration; use thiserror::Error; @@ -46,28 +46,10 @@ pub enum AudioError { #[error("Play was ordered, but nothing is in the queue")] NothingInQueue, - #[error("Unknown dbus error")] - DbusError(dbus::Error), - - #[error("Unknown problem with mpris")] - MprisError(mpris::DBusError), - #[error("url parse error {0}")] UrlError(url::ParseError), } -impl From for AudioError { - fn from(err: dbus::Error) -> Self { - Self::DbusError(err) - } -} - -impl From for AudioError { - fn from(err: mpris::DBusError) -> Self { - Self::MprisError(err) - } -} - impl From for AudioError { fn from(err: url::ParseError) -> Self { Self::UrlError(err) @@ -102,6 +84,16 @@ pub struct TrackInfo { pub name: Option, pub album: Option, pub artist: Option, + pub duration: Option, + #[serde(serialize_with = "serialize_mime")] + pub filetype: mime::Mime, +} + +fn serialize_mime(val: &mime::Mime, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(val.essence_str()) } #[derive(Clone, Debug, Serialize)] diff --git a/music-player/server/src/music_scanner.rs b/music-player/server/src/scanner.rs similarity index 60% rename from music-player/server/src/music_scanner.rs rename to music-player/server/src/scanner.rs index dddcc63..4634d3e 100644 --- a/music-player/server/src/music_scanner.rs +++ b/music-player/server/src/scanner.rs @@ -1,16 +1,19 @@ -use crate::audio::{TrackId, TrackInfo}; +use crate::media::{TrackId, TrackInfo}; +use id3::{Tag, TagLike}; use std::{ fs::{DirEntry, ReadDir}, - path::PathBuf, + path::{Path, PathBuf}, }; use thiserror::Error; #[derive(Debug, Error)] pub enum ScannerError { - #[error("Cannot scan {0}")] - CannotScan(PathBuf), - #[error("Not found {0}")] - NotFound(PathBuf), + #[error("Cannot scan file")] + CannotScan, + #[error("File not found")] + FileNotFound, + #[error("Tag not found")] + TagNotFound, #[error("IO error {0}")] IO(std::io::Error), } @@ -21,6 +24,19 @@ impl From for ScannerError { } } +impl From for ScannerError { + fn from(err: id3::Error) -> ScannerError { + match err.kind { + id3::ErrorKind::Io(err) => ScannerError::IO(err), + id3::ErrorKind::StringDecoding(_) => ScannerError::CannotScan, + id3::ErrorKind::NoTag => ScannerError::TagNotFound, + id3::ErrorKind::Parsing => ScannerError::CannotScan, + id3::ErrorKind::InvalidInput => ScannerError::CannotScan, + id3::ErrorKind::UnsupportedFeature => ScannerError::CannotScan, + } + } +} + pub trait MusicScanner: Send { fn scan<'a>(&'a self) -> Box> + 'a>; } @@ -42,15 +58,62 @@ pub struct FileIterator { impl FileIterator { fn scan_file(&self, path: PathBuf) -> Result { + println!( + "[{:?}] {}", + mime_guess::from_path(path.clone()).first(), + path.to_str().unwrap() + ); + let mimetype = mime_guess::from_path(path.clone()) + .first() + .ok_or(ScannerError::CannotScan)?; + match (mimetype.type_(), mimetype.subtype().as_str()) { + (mime::AUDIO, "mpeg") => TrackInfo::scan_id3(path, mimetype), + + /* + (mime::AUDIO, "ogg") => Ok(TrackInfo { + id: TrackId::from(path.to_str().unwrap().to_owned()), + album: None, + artist: None, + name: path + .file_stem() + .and_then(|s| s.to_str()) + .map(|s| s.to_owned()), + track_number: None, + filetype: mimetype, + }), + (mime::AUDIO, "flac") => Ok(TrackInfo { + id: TrackId::from(path.to_str().unwrap().to_owned()), + album: None, + artist: None, + name: path + .file_stem() + .and_then(|s| s.to_str()) + .map(|s| s.to_owned()), + track_number: None, + filetype: mimetype, + }), + */ + _ => Err(ScannerError::CannotScan), + } + } +} + +impl TrackInfo { + fn scan_id3(path: PathBuf, mimetype: mime::Mime) -> Result { + let tags = Tag::read_from_path(path.clone()).map_err(ScannerError::from)?; + Ok(TrackInfo { id: TrackId::from(path.to_str().unwrap().to_owned()), - album: None, - artist: None, - name: path - .file_stem() - .and_then(|s| s.to_str()) - .map(|s| s.to_owned()), + album: tags.album().map(|s| s.to_owned()), + artist: tags.artist().map(|s| s.to_owned()), + name: tags.title().map(|s| s.to_owned()).or_else(|| { + path.file_stem() + .and_then(|s| s.to_str()) + .map(|s| s.to_owned()) + }), + duration: tags.duration(), track_number: None, + filetype: mimetype, }) } } @@ -100,7 +163,7 @@ impl Iterator for FileIterator { } Err(err) => { if err.kind() == std::io::ErrorKind::NotFound { - Some(Err(ScannerError::NotFound(dir))) + Some(Err(ScannerError::FileNotFound)) } else { Some(Err(ScannerError::from(err))) } @@ -124,7 +187,7 @@ impl MusicScanner for FileScanner { #[cfg(test)] pub mod factories { use super::*; - use crate::audio::TrackId; + use crate::media::TrackId; pub struct MockScanner { data: Vec, @@ -140,6 +203,8 @@ pub mod factories { name: Some("Track 1".to_owned()), album: Some("Savanni's Demo".to_owned()), artist: Some("Savanni".to_owned()), + duration: Some(15), + filetype: "audio/mpeg".parse::().unwrap(), }, TrackInfo { id: TrackId::from("/home/savanni/Track 2.mp3".to_owned()), @@ -147,6 +212,8 @@ pub mod factories { name: Some("Track 2".to_owned()), album: Some("Savanni's Demo".to_owned()), artist: Some("Savanni".to_owned()), + duration: Some(15), + filetype: "audio/mpeg".parse::().unwrap(), }, TrackInfo { id: TrackId::from("/home/savanni/Track 3.mp3".to_owned()), @@ -154,6 +221,8 @@ pub mod factories { name: Some("Track 3".to_owned()), album: Some("Savanni's Demo".to_owned()), artist: Some("Savanni".to_owned()), + duration: Some(15), + filetype: "audio/mpeg".parse::().unwrap(), }, TrackInfo { id: TrackId::from("/home/savanni/Track 4.mp3".to_owned()), @@ -161,6 +230,8 @@ pub mod factories { name: Some("Track 4".to_owned()), album: Some("Savanni's Demo".to_owned()), artist: Some("Savanni".to_owned()), + duration: Some(15), + filetype: "audio/mpeg".parse::().unwrap(), }, TrackInfo { id: TrackId::from("/home/savanni/Track 5.mp3".to_owned()), @@ -168,6 +239,8 @@ pub mod factories { name: Some("Track 5".to_owned()), album: Some("Savanni's Demo".to_owned()), artist: Some("Savanni".to_owned()), + duration: Some(15), + filetype: "audio/mpeg".parse::().unwrap(), }, ], }