Set up the infrastructure to play music #29

Merged
savanni merged 3 commits from feature/play-music into main 2023-03-10 14:35:18 +00:00
14 changed files with 671 additions and 94 deletions

View File

@ -9,3 +9,10 @@ export interface TrackInfo {
export const getTracks = (): Promise<TrackInfo[]> => export const getTracks = (): Promise<TrackInfo[]> =>
fetch("/api/v1/tracks").then((r) => r.json()); fetch("/api/v1/tracks").then((r) => r.json());
export const playTrack = (id: string): Promise<Response> =>
fetch("/api/v1/play", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ id: id }),
});

View File

@ -0,0 +1,34 @@
export class PlaylistRow extends HTMLElement {
onPlay: (trackId: string) => void;
static get observedAttributes() {
return ["trackId"];
}
constructor() {
super();
this.onPlay = (_) => {};
}
get trackId(): string | null {
return this.getAttribute("trackId");
}
set trackId(id: string | null) {
id ? this.setAttribute("trackId", id) : this.removeAttribute("trackId");
}
connectedCallback() {
this.classList.add("playlist-row");
const playButton = document.createElement("button");
playButton.innerHTML = "Play";
playButton.addEventListener("click", (_) => {
if (this.trackId) {
this.onPlay(this.trackId);
}
});
this.appendChild(playButton);
}
}

View File

@ -8,7 +8,7 @@ export class TrackCard extends HTMLElement {
durationContainer: TextField; durationContainer: TextField;
static get observedAttributes() { static get observedAttributes() {
return ["id", "trackNumber", "name", "album", "artist", "duration"]; return ["trackId", "trackNumber", "name", "album", "artist", "duration"];
} }
constructor() { constructor() {
@ -29,6 +29,18 @@ export class TrackCard extends HTMLElement {
this.durationContainer.classList.add("track-card__duration"); this.durationContainer.classList.add("track-card__duration");
} }
get trackId(): string | null {
return this.getAttribute("id");
}
set trackId(id: string | null) {
if (id) {
this.setAttribute("trackId", id);
} else {
this.removeAttribute("trackId");
}
}
get name(): string | null { get name(): string | null {
return this.getAttribute("name"); return this.getAttribute("name");
} }

View File

@ -1,14 +1,16 @@
import * as _ from "lodash"; import * as _ from "lodash";
import { TrackInfo, getTracks } from "./client"; import { TrackInfo, getTracks, playTrack } from "./client";
import { DataCard } from "./components/DataCard"; import { DataCard } from "./components/DataCard";
import { NowPlaying } from "./components/NowPlaying"; import { NowPlaying } from "./components/NowPlaying";
import { TextField } from "./components/TextField"; import { TextField } from "./components/TextField";
import { TrackCard } from "./components/TrackCard"; import { TrackCard } from "./components/TrackCard";
import { PlaylistRow } from "./components/PlaylistRow";
window.customElements.define("data-card", DataCard); window.customElements.define("data-card", DataCard);
window.customElements.define("now-playing", NowPlaying); window.customElements.define("now-playing", NowPlaying);
window.customElements.define("text-field", TextField); window.customElements.define("text-field", TextField);
window.customElements.define("track-card", TrackCard); window.customElements.define("track-card", TrackCard);
window.customElements.define("playlist-row", PlaylistRow);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -16,25 +18,29 @@ declare global {
"now-playing": NowPlaying; "now-playing": NowPlaying;
"text-field": TextField; "text-field": TextField;
"track-card": TrackCard; "track-card": TrackCard;
"playlist-row": PlaylistRow;
} }
} }
const updateTrackList = (tracks: TrackInfo[]) => { const updateTrackList = (tracks: TrackInfo[]) => {
const track_list = document.querySelector(".track-list__tracks"); const playlist = document.querySelector(".track-list__tracks");
if (track_list) { if (playlist) {
let track_formats = _.map(tracks, (info) => { _.map(tracks, (info) => {
let card: TrackCard = document.createElement("track-card"); let card: TrackCard = document.createElement("track-card");
let listItem: PlaylistRow = document.createElement("playlist-row");
card.trackId = info.id;
card.name = info.name || null; card.name = info.name || null;
card.album = info.album || null; card.album = info.album || null;
card.artist = info.artist || null; card.artist = info.artist || null;
card.duration = (info.duration && `${info.duration}`) || null; card.duration = (info.duration && `${info.duration}`) || null;
return card;
}); listItem.appendChild(card);
_.map(track_formats, (trackCard) => { listItem.trackId = info.id;
let listItem = document.createElement("li"); listItem.onPlay = (id: string) => {
listItem.classList.add("track-list__row"); playTrack(id);
listItem.appendChild(trackCard); };
track_list.appendChild(listItem); playlist.appendChild(listItem);
}); });
} else { } else {
console.log("track_list does not exist"); console.log("track_list does not exist");

View File

@ -143,6 +143,11 @@ body {
list-style: none; list-style: none;
} }
.playlist-row {
display: flex;
margin-top: 32px;
}
.track-list__row { .track-list__row {
margin-top: 32px; margin-top: 32px;
} }

View File

@ -19,6 +19,12 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "anyhow"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.1.0"
@ -68,6 +74,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]]
name = "cfg-expr"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa"
dependencies = [
"smallvec",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
@ -178,6 +193,28 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
[[package]]
name = "futures-executor"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-macro"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.25" version = "0.3.25"
@ -197,6 +234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"pin-project-lite", "pin-project-lite",
@ -225,6 +263,113 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gio-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"winapi",
]
[[package]]
name = "glib"
version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"glib-macros",
"glib-sys",
"gobject-sys",
"libc",
"once_cell",
"smallvec",
"thiserror",
]
[[package]]
name = "glib-macros"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf"
dependencies = [
"anyhow",
"heck",
"proc-macro-crate",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "glib-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65"
dependencies = [
"libc",
"system-deps",
]
[[package]]
name = "gobject-sys"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]]
name = "gstreamer"
version = "0.19.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85fc926d081923c840403ec5ec3b2157a7cd236a2587c3031a4f0206f13ed500"
dependencies = [
"bitflags",
"cfg-if",
"futures-channel",
"futures-core",
"futures-util",
"glib",
"gstreamer-sys",
"libc",
"muldiv",
"num-integer",
"num-rational",
"once_cell",
"option-operations",
"paste",
"pretty-hex",
"thiserror",
]
[[package]]
name = "gstreamer-sys"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545f52ad8a480732cc4290fd65dfe42952c8ae374fe581831ba15981fedf18a4"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.15" version = "0.3.15"
@ -287,6 +432,12 @@ dependencies = [
"http", "http",
] ]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.2.6" version = "0.2.6"
@ -478,6 +629,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "muldiv"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
[[package]] [[package]]
name = "multipart" name = "multipart"
version = "0.18.0" version = "0.18.0"
@ -501,6 +658,7 @@ name = "music-player"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"flow", "flow",
"gstreamer",
"id3", "id3",
"mime", "mime",
"mime_guess", "mime_guess",
@ -509,10 +667,41 @@ dependencies = [
"thiserror", "thiserror",
"tokio", "tokio",
"url", "url",
"urlencoding",
"uuid", "uuid",
"warp", "warp",
] ]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.15.0" version = "1.15.0"
@ -529,6 +718,15 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "option-operations"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0"
dependencies = [
"paste",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -552,6 +750,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "paste"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.2.0" version = "2.2.0"
@ -602,6 +806,46 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "pretty-hex"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
dependencies = [
"once_cell",
"toml_edit",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.50" version = "1.0.50"
@ -831,6 +1075,19 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "system-deps"
version = "6.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"version-compare",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.3.0" version = "3.3.0"
@ -948,6 +1205,32 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
[[package]]
name = "toml_edit"
version = "0.19.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@ -1056,6 +1339,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "urlencoding"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
[[package]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -1077,6 +1366,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -1208,3 +1503,12 @@ name = "windows_x86_64_msvc"
version = "0.42.1" version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winnow"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f"
dependencies = [
"memchr",
]

View File

@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
flow = { path = "../../flow" } flow = { path = "../../flow" }
gstreamer = { version = "0.19" }
id3 = { version = "1.6" } id3 = { version = "1.6" }
mime_guess = { version = "2.0" } mime_guess = { version = "2.0" }
mime = { version = "0.3" } mime = { version = "0.3" }
@ -17,5 +18,6 @@ tokio = { version = "1.24", features = ["full"] }
url = { version = "2.3" } url = { version = "2.3" }
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
warp = { version = "0.3" } warp = { version = "0.3" }
urlencoding = { version = "2.1" }
[lib] [lib]

View File

@ -1,17 +1,23 @@
use flow::Flow; use flow::Flow;
use music_player::{
core::{ControlMsg, Core},
database::{MemoryIndex, MusicIndex},
media::{TrackId, TrackInfo},
scanner::FileScanner,
};
use serde::Deserialize;
use std::{ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr}, net::{IpAddr, Ipv4Addr, SocketAddr},
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
thread,
}; };
use warp::Filter; use warp::Filter;
use music_player::{ #[derive(Clone, Debug, Deserialize)]
core::Core, struct TrackRequest {
database::{MemoryIndex, MusicIndex}, id: String,
media::TrackInfo, }
scanner::FileScanner,
};
fn tracks(index: &Arc<impl MusicIndex>) -> Vec<TrackInfo> { fn tracks(index: &Arc<impl MusicIndex>) -> Vec<TrackInfo> {
match index.list_tracks() { match index.list_tracks() {
@ -25,15 +31,6 @@ struct Static(PathBuf);
impl Static { impl Static {
fn read(self, root: PathBuf) -> String { fn read(self, root: PathBuf) -> String {
/*
let mut path = root;
match self {
Bundle::Index => path.push(PathBuf::from("index.html")),
Bundle::App => path.push(PathBuf::from("bundle.js")),
Bundle::Styles => path.push(PathBuf::from("styles.css")),
};
std::fs::read_to_string(path).expect("to find the file")
*/
let mut path = root; let mut path = root;
path.push(self.0); path.push(self.0);
println!("path: {:?}", path); println!("path: {:?}", path);
@ -55,13 +52,15 @@ pub async fn main() {
.unwrap(); .unwrap();
let index = Arc::new(MemoryIndex::new()); let index = Arc::new(MemoryIndex::new());
let scanner = FileScanner::new(vec![music_root.clone()]); let scanner = Arc::new(FileScanner::new(vec![music_root.clone()]));
let _core = match Core::new(index.clone(), scanner) { let (core, api) = match Core::new(index.clone(), scanner) {
Flow::Ok(core) => core, Flow::Ok((core, api)) => (core, api),
Flow::Err(error) => panic!("error: {}", error), Flow::Err(error) => panic!("error: {}", error),
Flow::Fatal(error) => panic!("fatal: {}", error), Flow::Fatal(error) => panic!("fatal: {}", error),
}; };
let _handle = thread::spawn(move || core.start());
println!("config: {:?} {:?} {:?}", dev, bundle_root, music_root); println!("config: {:?} {:?} {:?}", dev, bundle_root, music_root);
let root = warp::path!().and(warp::get()).map({ let root = warp::path!().and(warp::get()).map({
@ -102,6 +101,18 @@ pub async fn main() {
move || warp::reply::json(&tracks(&index)) move || warp::reply::json(&tracks(&index))
}); });
let play_track = warp::path!("api" / "v1" / "play")
.and(warp::post())
.and(warp::body::json())
.map({
let api = api.clone();
move |body: TrackRequest| {
let result = api.play_track(TrackId::from(body.id));
println!("Play result: {:?}", result);
warp::reply::json(&("ok".to_owned()))
}
});
/* /*
let tracks_for_artist = warp::path!("api" / "v1" / "artist" / String) let tracks_for_artist = warp::path!("api" / "v1" / "artist" / String)
.and(warp::get()) .and(warp::get())
@ -128,7 +139,7 @@ pub async fn main() {
.or(queue) .or(queue)
.or(playing_status); .or(playing_status);
*/ */
let routes = root.or(assets).or(track_list); let routes = root.or(assets).or(track_list).or(play_track);
let server = warp::serve(routes); let server = warp::serve(routes);
server server
.run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002)) .run(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8002))

View File

@ -1,9 +1,16 @@
use crate::{database::MusicIndex, media::TrackInfo, scanner::MusicScanner, Error, FatalError}; use crate::{
use flow::{ok, Flow}; database::MusicIndex,
media::{TrackId, TrackInfo},
playback::{Playback, PlaybackControl, PlaybackStatus},
scanner::MusicScanner,
Error, FatalError,
};
use flow::{ok, return_error, Flow};
use gstreamer::{format::ClockTime, prelude::*, MessageView};
use std::{ use std::{
sync::{ sync::{
mpsc::{channel, Receiver, RecvTimeoutError, Sender}, mpsc::{channel, Receiver, RecvTimeoutError, Sender},
Arc, Arc, Mutex,
}, },
thread, thread,
thread::JoinHandle, thread::JoinHandle,
@ -14,7 +21,9 @@ fn scan_frequency() -> Duration {
Duration::from_secs(60) Duration::from_secs(60)
} }
#[derive(Clone)]
pub enum ControlMsg { pub enum ControlMsg {
PlayTrack(TrackId),
Exit, Exit,
} }
@ -32,74 +41,119 @@ pub enum PlaybackMsg {
pub struct Core { pub struct Core {
db: Arc<dyn MusicIndex>, db: Arc<dyn MusicIndex>,
_track_handle: JoinHandle<()>, scanner: Arc<dyn MusicScanner>,
_track_rx: Receiver<TrackMsg>, control_rx: Receiver<ControlMsg>,
_playback_handle: JoinHandle<()>,
_playback_rx: Receiver<PlaybackMsg>, playback_controller: Playback,
control_tx: Sender<ControlMsg>,
} }
impl Core { impl Core {
pub fn new( pub fn new(
db: Arc<dyn MusicIndex>, db: Arc<dyn MusicIndex>,
scanner: impl MusicScanner + 'static, scanner: Arc<dyn MusicScanner>,
) -> Flow<Core, FatalError, Error> { ) -> Flow<(Core, CoreAPI), FatalError, Error> {
let (control_tx, control_rx) = channel::<ControlMsg>(); let (control_tx, control_rx) = channel::<ControlMsg>();
let db = db; let db = db;
let (_track_handle, _track_rx) = { let playback_controller = Playback::new();
let (track_tx, track_rx) = channel();
let db = db.clone(); ok((
let track_handle = thread::spawn(move || { Core {
db,
scanner,
control_rx,
playback_controller,
},
CoreAPI {
control_tx: Arc::new(Mutex::new(control_tx)),
},
))
}
pub fn start(&self) -> Flow<(), FatalError, Error> {
gstreamer::init();
let (scanner_tx, _scanner_rx) = channel();
let mut next_scan = Instant::now(); let mut next_scan = Instant::now();
loop { loop {
if Instant::now() >= next_scan { if Instant::now() >= next_scan {
let _ = track_tx.send(TrackMsg::UpdateInProgress); let scan_start = Instant::now();
for track in scanner.scan() { let _ = scanner_tx.send(TrackMsg::UpdateInProgress);
for track in self.scanner.scan() {
match track { match track {
Ok(track) => db.add_track(track), Ok(track) => self.db.add_track(track),
Err(_) => ok(()), Err(_) => ok(()),
}; };
} }
let _ = track_tx.send(TrackMsg::UpdateComplete); let _ = scanner_tx.send(TrackMsg::UpdateComplete);
next_scan = Instant::now() + scan_frequency(); next_scan = Instant::now() + scan_frequency();
println!("scan duration: {:?}", Instant::now() - scan_start);
} }
match control_rx.recv_timeout(Duration::from_millis(1000)) { match self.control_rx.recv_timeout(Duration::from_millis(1000)) {
Ok(ControlMsg::Exit) => return, Ok(ControlMsg::PlayTrack(id)) => {
let _ = self.play_track(id);
}
Ok(ControlMsg::Exit) => return ok(()),
Err(RecvTimeoutError::Timeout) => (), Err(RecvTimeoutError::Timeout) => (),
Err(RecvTimeoutError::Disconnected) => return, Err(RecvTimeoutError::Disconnected) => return ok(()),
} }
} }
});
(track_handle, track_rx)
};
let (_playback_handle, _playback_rx) = {
let (_playback_tx, playback_rx) = channel();
let playback_handle = thread::spawn(move || {});
(playback_handle, playback_rx)
};
ok(Core {
db,
_track_handle,
_track_rx,
_playback_handle,
_playback_rx,
control_tx,
})
} }
pub fn list_tracks<'a>(&'a self) -> Flow<Vec<TrackInfo>, FatalError, Error> { pub fn list_tracks<'a>(&'a self) -> Flow<Vec<TrackInfo>, FatalError, Error> {
self.db.list_tracks().map_err(Error::DatabaseError) self.db.list_tracks().map_err(Error::DatabaseError)
} }
pub fn exit(&self) { pub fn play_track<'a>(&'a self, id: TrackId) -> Flow<(), FatalError, Error> {
let _ = self.control_tx.send(ControlMsg::Exit);
/* /*
self.track_handle.join(); println!("play_track: {}", id.as_ref());
self.playback_handle.join(); let pipeline = return_error!(Flow::from(
gstreamer::parse_launch(&format!("playbin uri={}", id.as_str()))
.map_err(|err| Error::CannotPlay(err.to_string()),)
));
return_error!(Flow::from(
pipeline
.set_state(gstreamer::State::Playing)
.map_err(|err| Error::CannotPlay(err.to_string()))
));
{
let pipeline = pipeline.clone();
thread::spawn(move || {
println!("starting");
let bus = pipeline.bus().unwrap();
for msg in bus.iter_timed(gstreamer::ClockTime::NONE) {
match msg.view() {
MessageView::Eos(_) => (),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
}
msg => println!("{:?}", msg),
}
}
});
}
*/ */
self.playback_controller.play_track(id);
ok(())
}
}
#[derive(Clone)]
pub struct CoreAPI {
control_tx: Arc<Mutex<Sender<ControlMsg>>>,
}
impl CoreAPI {
pub fn play_track(&self, id: TrackId) -> () {
self.control_tx
.lock()
.unwrap()
.send(ControlMsg::PlayTrack(id))
.unwrap()
} }
} }
@ -115,8 +169,8 @@ mod test {
{ {
let index = MemoryIndex::new(); let index = MemoryIndex::new();
let scanner = MockScanner::new(); let scanner = MockScanner::new();
match Core::new(Arc::new(index), scanner) { match Core::new(Arc::new(index), Arc::new(scanner)) {
Flow::Ok(core) => { Flow::Ok((core, api)) => {
thread::sleep(Duration::from_millis(10)); thread::sleep(Duration::from_millis(10));
f(core) f(core)
} }

View File

@ -1,6 +1,7 @@
pub mod core; pub mod core;
pub mod database; pub mod database;
pub mod media; pub mod media;
pub mod playback;
pub mod scanner; pub mod scanner;
use database::DatabaseError; use database::DatabaseError;
use thiserror::Error; use thiserror::Error;
@ -9,6 +10,15 @@ use thiserror::Error;
pub enum Error { pub enum Error {
#[error("Database error: {0}")] #[error("Database error: {0}")]
DatabaseError(DatabaseError), DatabaseError(DatabaseError),
#[error("Cannot play track")]
CannotPlay,
#[error("Cannot stop playback")]
CannotStop,
#[error("Unmatched glib error: {0}")]
GlibError(gstreamer::glib::Error),
} }
impl From<DatabaseError> for Error { impl From<DatabaseError> for Error {

View File

@ -59,6 +59,12 @@ impl From<url::ParseError> for AudioError {
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
pub struct TrackId(String); pub struct TrackId(String);
impl TrackId {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for TrackId { impl Default for TrackId {
fn default() -> Self { fn default() -> Self {
Self(uuid::Uuid::new_v4().as_hyphenated().to_string()) Self(uuid::Uuid::new_v4().as_hyphenated().to_string())

View File

@ -0,0 +1,131 @@
use crate::{media::TrackId, Error, FatalError};
use flow::{ok, return_error, Flow};
use gstreamer::{format::ClockTime, prelude::*, MessageView, StateChangeError};
use std::{
path::PathBuf,
sync::mpsc::{channel, Receiver, Sender},
thread::{self, JoinHandle},
time::Duration,
};
use urlencoding::encode;
pub enum PlaybackControl {
PlayTrack(TrackId),
Stop,
Exit,
}
pub enum PlaybackStatus {
Stopped,
}
pub struct Playback {
handle: JoinHandle<Flow<(), FatalError, Error>>,
control_tx: Sender<PlaybackControl>,
}
impl Playback {
pub fn new() -> Playback {
let (control_tx, control_rx) = channel::<PlaybackControl>();
let handle = thread::spawn(move || {
let mut pipeline = None;
loop {
match control_rx.recv().unwrap() {
PlaybackControl::PlayTrack(id) => match play_track(id) {
Flow::Ok(pipeline_) => pipeline = Some(pipeline_),
Flow::Fatal(err) => panic!("fatal error: {:?}", err),
Flow::Err(err) => panic!("playback error: {:?}", err),
},
PlaybackControl::Stop => {
if let Some(ref pipeline) = pipeline {
return_error!(Flow::from(
pipeline
.set_state(gstreamer::State::Paused)
.map_err(|_| Error::CannotStop)
));
}
}
PlaybackControl::Exit => return ok(()),
}
}
});
Self { handle, control_tx }
}
pub fn play_track(&self, id: TrackId) {
self.control_tx
.send(PlaybackControl::PlayTrack(id))
.unwrap();
}
}
fn play_track(id: TrackId) -> Flow<gstreamer::Element, FatalError, Error> {
let pb = PathBuf::from(id.as_ref());
let path = pb
.iter()
.skip(1)
.map(|component| encode(&component.to_string_lossy()).into_owned())
.collect::<PathBuf>();
let playbin = format!("playbin uri=file:///{}", path.display());
println!("setting up to play {}", playbin);
let pipeline = return_error!(Flow::from(
gstreamer::parse_launch(&playbin).map_err(|err| Error::GlibError(err))
));
println!("ready to play");
return_error!(Flow::from(
pipeline
.set_state(gstreamer::State::Playing)
.map_err(|_| Error::CannotPlay)
));
println!("playing started");
ok(pipeline)
}
/*
fn play_track(id: TrackId) -> Flow<(), FatalError, Error> {
let playbin = format!("playbin uri=file://{}", id.as_ref());
let pipeline = return_error!(Flow::from(
gstreamer::parse_launch(&playbin).map_err(|err| Error::GlibError(err))
));
return_error!(Flow::from(
pipeline
.set_state(gstreamer::State::Playing)
.map_err(|_| Error::CannotPlay)
));
let message_handler = {
let pipeline = pipeline.clone();
thread::spawn(move || {
let bus = pipeline.bus().unwrap();
for msg in bus.iter_timed(gstreamer::ClockTime::NONE) {
match msg.view() {
MessageView::Eos(_) => (),
MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.src().map(|s| s.path_string()),
err.error(),
err.debug()
);
}
msg => println!("{:?}", msg),
}
}
})
};
let query_handler = {
let pipeline = pipeline.clone();
thread::spawn(move || loop {
let position: Option<ClockTime> = pipeline.query_position();
let duration: Option<ClockTime> = pipeline.query_duration();
println!("Position {:?} {:?}", position, duration);
thread::sleep(Duration::from_millis(100));
})
};
ok(())
}
*/

View File

@ -37,7 +37,7 @@ impl From<id3::Error> for ScannerError {
} }
} }
pub trait MusicScanner: Send { pub trait MusicScanner: Sync + Send {
fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a>; fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a>;
} }
@ -58,11 +58,6 @@ pub struct FileIterator {
impl FileIterator { impl FileIterator {
fn scan_file(&self, path: PathBuf) -> Result<TrackInfo, ScannerError> { 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()) let mimetype = mime_guess::from_path(path.clone())
.first() .first()
.ok_or(ScannerError::CannotScan)?; .ok_or(ScannerError::CannotScan)?;

View File

@ -34,7 +34,7 @@ fn main() {
thread::spawn(move || loop { thread::spawn(move || loop {
let position: Option<ClockTime> = pipeline.query_position(); let position: Option<ClockTime> = pipeline.query_position();
let duration: Option<ClockTime> = pipeline.query_duration(); let duration: Option<ClockTime> = pipeline.query_duration();
println!("{:?} {:?}", position, duration); println!("Position {:?} {:?}", position, duration);
thread::sleep(Duration::from_millis(100)); thread::sleep(Duration::from_millis(100));
}) })
}; };