Scan and render mp3 metadata #27

Merged
savanni merged 2 commits from savanni/music-file-types into main 2023-03-02 15:44:15 +00:00
11 changed files with 178 additions and 237 deletions
Showing only changes of commit 81779c9fc2 - Show all commits

View File

@ -1,6 +1,7 @@
export interface TrackInfo {
id: string;
track_number?: number;
duration?: number;
name?: string;
album?: string;
artist?: string;

View File

@ -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"]));
}
}
}

View File

@ -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) => {

View File

@ -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"

View File

@ -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" }

View File

@ -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<impl MusicIndex>) -> Vec<TrackInfo> {

View File

@ -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: F)

View File

@ -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::<mime::Mime>().unwrap(),
};
index.add_track(info.clone());

View File

@ -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;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<dbus::Error> for AudioError {
fn from(err: dbus::Error) -> Self {
Self::DbusError(err)
}
}
impl From<mpris::DBusError> for AudioError {
fn from(err: mpris::DBusError) -> Self {
Self::MprisError(err)
}
}
impl From<url::ParseError> for AudioError {
fn from(err: url::ParseError) -> Self {
Self::UrlError(err)
@ -102,6 +84,16 @@ pub struct TrackInfo {
pub name: Option<String>,
pub album: Option<String>,
pub artist: Option<String>,
pub duration: Option<u32>,
#[serde(serialize_with = "serialize_mime")]
pub filetype: mime::Mime,
}
fn serialize_mime<S>(val: &mime::Mime, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(val.essence_str())
}
#[derive(Clone, Debug, Serialize)]

View File

@ -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<std::io::Error> for ScannerError {
}
}
impl From<id3::Error> 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<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a>;
}
@ -42,15 +58,62 @@ pub struct FileIterator {
impl FileIterator {
fn scan_file(&self, path: PathBuf) -> Result<TrackInfo, ScannerError> {
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<TrackInfo, ScannerError> {
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<TrackInfo>,
@ -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::<mime::Mime>().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::<mime::Mime>().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::<mime::Mime>().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::<mime::Mime>().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::<mime::Mime>().unwrap(),
},
],
}