0.7.1: Sea codec, Slint client redesign

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
Ivan Bushchik 2025-02-21 17:57:43 +03:00
parent 80cef97ca2
commit 0bfd885c70
No known key found for this signature in database
GPG key ID: 2F16FBF3262E090C
33 changed files with 3207 additions and 1456 deletions

2802
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ members = [
[package]
name = "lonelyradio"
description = "TCP radio for lonely ones"
version = "0.7.0"
version = "0.7.1"
edition = "2021"
license = "MIT"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
@ -49,14 +49,16 @@ samplerate = "0.2.4"
flacenc = { version = "0.4.0", default-features = false, optional = true }
alac-encoder = { version = "0.3.0", optional = true }
vorbis_rs = {version = "0.5.4", optional = true }
sea-codec = { version = "0.5.2", optional = true }
[features]
default = ["all-lossless", "all-lossy"]
all-lossless = ["alac", "flac"]
all-lossy = ["vorbis"]
all-lossy = ["vorbis", "sea"]
alac = ["dep:alac-encoder"]
flac = ["dep:flacenc"]
vorbis = ["dep:vorbis_rs"]
sea = ["dep:sea-codec"]
[profile.distribute]
inherits = "release"

View file

@ -3,7 +3,7 @@
# Autogenerated by docker init
# https://docs.docker.com/go/dockerfile-reference/
ARG RUST_VERSION=1.80.1
ARG RUST_VERSION=1.85
ARG APP_NAME=lonelyradio
################################################################################

View file

@ -4,12 +4,12 @@ Shuffles through your [XSPF playlists](https://www.xspf.org) or your entire libr
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia) (supported [decoders](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#codecs-decoders) and [demuxers](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#formats-demuxers))
Streams music using [FLAC](https://crates.io/crates/flacenc), [ALAC](https://crates.io/crates/alac-encoder), [Vorbis](https://crates.io/crates/vorbis_rs) or raw PCM on clients requests.
Streams music using [FLAC](https://crates.io/crates/flacenc), [ALAC](https://crates.io/crates/alac-encoder), [Vorbis](https://crates.io/crates/vorbis_rs), [Sea](https://github.com/Daninet/sea-codec) or raw PCM on clients requests.
### Install server
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 lonelyradio
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 lonelyradio
```
### Run
@ -57,7 +57,7 @@ Only the `<location>` and (playlist's) element would be used and only `file://`
##### Install
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 monoclient-s
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 monoclient-s
```
You may need to install some dependencies for Slint.
@ -71,7 +71,7 @@ Desktop integration will be added later.
##### Install monoclient
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 monoclient
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 monoclient
```
#### Usage

View file

@ -1,3 +1,5 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
@ -99,6 +101,7 @@ pub enum Encoder {
Opus = 5,
Aac = 6,
Vorbis = 7,
Sea = 8,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
@ -119,3 +122,19 @@ pub struct FragmentMetadata {
fn none() -> Option<Vec<u8>> {
None
}
impl Display for Encoder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Pcm16 => "PCM s16",
Self::PcmFloat => "PCM f32",
Self::Flac => "FLAC",
Self::Alac => "ALAC",
Self::WavPack => "WavPack",
Self::Opus => "Opus",
Self::Aac => "AAC",
Self::Vorbis => "Vorbis",
Self::Sea => "Sea",
})
}
}

View file

@ -3,16 +3,20 @@ name = "monoclient-s"
description = "Client for lonelyradio built with Slint"
version = "0.7.0"
edition = "2021"
build = "build.rs"
[dependencies]
slint = { version = "1.6" }
slint = { version = "1.8" }
monolib = { path = "../monolib", version = "0.7.1" }
zune-jpeg = "0.4.13"
[build-dependencies]
slint-build = "1.8"
[package.metadata.bundle]
name = "monoclient-s"
identifier = "dev.ivabus.monoclient-s"
icon = ["lonelyradio.png", "lonelyradio.icns"]
version = "0.5.0"
version = "0.7.1"
copyright = "Copyright (c) 2024 Ivan Bushchik."
category = "Music"

3
monoclient-s/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
slint_build::compile("ui/ui.slint").unwrap();
}

View file

@ -6,146 +6,20 @@ use slint::{
Image, ModelRc, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, SharedString, VecModel, Weak,
};
slint::slint! {
import { ComboBox, Button, VerticalBox, GroupBox, Slider } from "std-widgets.slint";
export component MainWindow inherits Window {
max-height: self.preferred-height;
callback play;
callback stop;
callback next;
callback refreshp;
callback change_volume(float);
callback text_edited;
in-out property <string> addr: address.text;
in-out property <string> mtitle: "";
in-out property <string> malbum: "";
in-out property <string> martist: "";
in-out property <float> volume: svolume.value;
in-out property <bool> start_enabled: false;
in-out property <bool> playing: false;
in-out property <bool> paused: false;
in property <image> cover: @image-url("lonelyradio.png");
in property <[string]> playlists: ["All tracks"];
in-out property <string> selected_playlist: selected.current-value;
title: "monoclient-s";
min-width: 192px;
max-width: 768px;
VerticalBox {
alignment: center;
spacing: 0px;
Image {
source: cover;
max-height: 192px;
max-width: 192px;
min-height: 192px;
min-width: 192px;
}
GroupBox{
max-width: 768px;
address := TextInput {
text: "";
horizontal-alignment: center;
height: 1.25rem;
accepted => {
self.clear_focus()
}
edited => {
text_edited();
}
}
}
VerticalLayout {
max-width: 512px;
VerticalLayout {
spacing: 4px;
Button {
max-width: 256px;
text: playing ? (paused ? "Play" : "Pause") : "Start";
enabled: start_enabled || playing;
clicked => {
play()
}
}
HorizontalLayout {
spacing: 4px;
max-width: 256px;
Button {
text: "Stop";
enabled: playing && !paused;
clicked => {
stop()
}
}
Button {
text: "Next";
enabled: playing && !paused;
clicked => {
next()
}
}
}
HorizontalLayout {
selected := ComboBox {
model: playlists;
current-value: "All tracks";
selected() => {
self.clear_focus()
}
}
}
svolume := Slider {
value: 255;
maximum: 255;
changed(f) => {
change_volume(f)
}
}
}
}
VerticalLayout {
padding: 4px;
tartist := Text {
height: 1.25rem;
font-weight: 600;
text: martist;
overflow: elide;
}
talbum := Text {
height: 1.25rem;
text: malbum;
overflow: elide;
}
ttitle := Text {
height: 1.25rem;
text: mtitle;
overflow: elide;
}
}
}
}
}
slint::include_modules!();
#[allow(dead_code)]
fn start_playback(window_weak: Weak<MainWindow>) {
let window = window_weak.upgrade().unwrap();
let addr = window.get_addr().to_string();
let playlist = window.get_selected_playlist();
let encoder = monolib::SUPPORTED_DECODERS[window.get_selected_encoder() as usize];
let handle = std::thread::spawn(move || {
monolib::run(
&addr,
lonelyradio_types::Settings {
encoder: lonelyradio_types::Encoder::Flac,
cover: 512,
encoder,
cover: 2048,
},
if playlist == "All tracks" {
""
@ -206,6 +80,14 @@ pub fn main() {
)));
});
window.set_supported_encoders(ModelRc::new(VecModel::from(
monolib::SUPPORTED_DECODERS
.iter()
.map(|x| x.to_string())
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
let window_weak = window.as_weak();
window.on_next(move || {
monolib::stop();

View file

@ -0,0 +1,20 @@
MIT License
Copyright (c) 2025 Ivan Bushchik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,5 @@
# Monoclient Icons Set
Copyright (c) 2024 Ivan Bushchik
License: [MIT](./LICENSE)

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="eject.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="13.894537"
inkscape:cx="13.674439"
inkscape:cy="19.288156"
inkscape:window-width="1536"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke-width:0"
id="rect2"
width="7.5"
height="1.5"
x="1.25"
y="7.8000002"
rx="0.75"
ry="0.75" />
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="-0.017735456"
inkscape:transform-center-y="-1.8312623"
transform="matrix(2.0174137,0,0,2.0174137,1.4570468,2.1451198)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="first.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="14.983016"
inkscape:cx="18.621084"
inkscape:cy="16.618817"
inkscape:window-width="1712"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g2"
transform="matrix(-1,0,0,1,9.9524816,0.592)">
<g
id="g1">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-opacity:1"
id="path1"
sodipodi:type="arc"
sodipodi:cx="5.2266302"
sodipodi:cy="5.0154533"
sodipodi:rx="3"
sodipodi:ry="3"
sodipodi:start="1.5707963"
sodipodi:end="4.712389"
sodipodi:arc-type="arc"
d="M 5.2266302,8.0154533 A 3,3 0 0 1 2.628554,6.5154534 a 3,3 0 0 1 0,-3.0000001 3,3 0 0 1 2.5980762,-1.5"
sodipodi:open="true" />
</g>
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="-0.9156312"
inkscape:transform-center-y="0.008867619"
transform="matrix(0,1.0087068,-1.0087069,0,7.6059217,0.2785234)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-opacity:1"
id="rect1"
width="2"
height="1"
x="4.7523584"
y="7.5159998"
rx="0.5"
ry="0.5" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="gear.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="13.894537"
inkscape:cx="13.74641"
inkscape:cy="19.288156"
inkscape:window-width="1536"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:1"
id="path1"
inkscape:flatsided="false"
sodipodi:sides="7"
sodipodi:cx="2.2279439"
sodipodi:cy="0.64743674"
sodipodi:r1="5.8304453"
sodipodi:r2="4.3145294"
sodipodi:arg1="0.77846987"
sodipodi:arg2="1.2272688"
inkscape:rounded="0.68"
inkscape:randomized="0"
d="M 6.3791557,4.7415218 C 5.0907843,6.0478706 5.4087092,4.0919025 3.6811232,4.7098786 1.953537,5.3278548 3.4399287,6.6383994 1.6152975,6.4456051 -0.20933355,6.2528109 1.5181269,5.2818485 -0.04215863,4.3164691 -1.6024442,3.3510897 -1.7003191,5.3303087 -2.6872255,3.7835494 -3.6741317,2.2367901 -1.8379488,2.9819879 -2.0560069,1.1602034 -2.274065,-0.66158125 -3.8825048,0.49591998 -3.2885257,-1.2400632 c 0.5939791,-1.735983 1.1562014,0.1642259 2.44457285,-1.1421229 1.28837157,-1.3063488 -0.61944515,-1.8421875 1.10814107,-2.4601637 1.72758608,-0.6179761 0.59248281,1.0063478 2.41711388,1.199142 1.8246312,0.1927943 1.0540625,-1.6328868 2.6143481,-0.6675074 1.5602855,0.9653794 -0.4173874,1.0906693 0.5695189,2.6374286 0.9869063,1.54675933 1.9338395,-0.1939891 2.1518976,1.62779553 0.2180582,1.82178457 -1.1129563,0.3536946 -1.7069354,2.08967757 -0.5939791,1.7359832 1.357396,1.3909864 0.069024,2.6973353 z"
inkscape:transform-center-x="0.47254043"
inkscape:transform-center-y="-0.49742099"
transform="matrix(0.6544318,0,0,0.6544318,3.4877435,4.5192689)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="last.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="14.983016"
inkscape:cx="18.621084"
inkscape:cy="16.618817"
inkscape:window-width="1712"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g2"
transform="translate(0.04706873,0.592)">
<g
id="g1">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-opacity:1"
id="path1"
sodipodi:type="arc"
sodipodi:cx="5.2266302"
sodipodi:cy="5.0154533"
sodipodi:rx="3"
sodipodi:ry="3"
sodipodi:start="1.5707963"
sodipodi:end="4.712389"
sodipodi:arc-type="arc"
d="M 5.2266302,8.0154533 A 3,3 0 0 1 2.628554,6.5154534 a 3,3 0 0 1 0,-3.0000001 3,3 0 0 1 2.5980762,-1.5"
sodipodi:open="true" />
</g>
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="-0.9156312"
inkscape:transform-center-y="0.008867619"
transform="matrix(0,1.0087068,-1.0087069,0,7.6059217,0.2785234)" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-opacity:1"
id="rect1"
width="2"
height="1"
x="4.7523584"
y="7.5159998"
rx="0.5"
ry="0.5" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="next.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="13.894537"
inkscape:cx="13.674439"
inkscape:cy="19.288156"
inkscape:window-width="1536"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke-width:0"
id="rect2"
width="7.5"
height="1.5"
x="1.25"
y="-9"
rx="0.75"
ry="0.75"
transform="rotate(90)" />
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="-1.8312623"
inkscape:transform-center-y="0.017735426"
transform="matrix(0,2.0174137,-2.0174137,0,6.2118431,1.4570468)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="pause.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="13.894537"
inkscape:cx="13.674439"
inkscape:cy="19.288156"
inkscape:window-width="1536"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke-width:0"
id="rect2"
width="1.5"
height="7.5"
x="2.75"
y="1.25"
rx="0.75"
ry="0.75" />
<rect
style="fill:#000000;stroke-width:0"
id="rect2-9"
width="1.5"
height="7.5"
x="5.75"
y="1.25"
rx="0.75"
ry="0.75" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="play.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="8.3757958"
inkscape:cx="29.489735"
inkscape:cy="38.324717"
inkscape:window-width="1536"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path2"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="3.4656906"
sodipodi:cy="3.2943101"
sodipodi:r1="3.8732622"
sodipodi:r2="3.8401258"
sodipodi:arg1="0.92139558"
sodipodi:arg2="1.9685931"
inkscape:rounded="0.04"
inkscape:randomized="0"
d="M 5.807888,6.3791555 C 5.5941637,6.5414277 -0.3432929,4.0465167 -0.37696257,3.7802899 -0.41063223,3.5140631 4.7187523,-0.38046969 4.9661464,-0.27651507 5.2135404,-0.17256045 6.0216124,6.2168833 5.807888,6.3791555 Z"
transform="matrix(0.55578837,-0.76724136,0.73199924,0.58254681,0.05250644,5.7392433)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="previous.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="13.894537"
inkscape:cx="13.674439"
inkscape:cy="19.288156"
inkscape:window-width="1536"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke-width:0"
id="rect2"
width="7.5"
height="1.5"
x="1.25"
y="-2.5"
rx="0.75"
ry="0.75"
transform="rotate(90)" />
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="1.8312623"
transform="matrix(0,-2.0174137,2.0174137,0,3.7881198,8.5429532)"
inkscape:transform-center-y="-0.017735441" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="random.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="12.005348"
inkscape:cx="13.993764"
inkscape:cy="26.987973"
inkscape:window-width="1536"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="10 : 5 : 1"
inkscape:persp3d-origin="5 : 3.3333333 : 1"
id="perspective1" />
</defs>
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g6"
transform="translate(-0.41149989)">
<rect
style="fill:#000000;stroke-width:0"
id="rect6"
width="10"
height="1"
x="2.0706227"
y="-0.50035352"
rx="0.5"
ry="0.5"
transform="rotate(45)" />
<rect
style="fill:#000000;stroke-width:0"
id="rect6-7"
width="10"
height="1"
x="-5"
y="6.5702691"
rx="0.5"
ry="0.5"
transform="rotate(-45)" />
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="-0.0861241"
inkscape:transform-center-y="-0.087606847"
transform="matrix(0.63138587,0.63138587,-0.63138587,0.63138587,8.0200992,-0.32204334)" />
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3-6"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="-0.087607003"
inkscape:transform-center-y="0.086124089"
transform="matrix(-0.63138587,0.63138587,-0.63138587,-0.63138587,10.22831,8.1200992)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
sodipodi:docname="repeat-one.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="6.1285977"
inkscape:cx="9.4638289"
inkscape:cy="10.116507"
inkscape:window-width="1536"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="0.75466106"
inkscape:transform-center-y="-0.0073088053"
transform="matrix(0,-0.83137379,0.83137379,0,3.7731711,3.93391)" />
<path
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1"
sodipodi:type="arc"
sodipodi:cx="5.1258287"
sodipodi:cy="5.2289081"
sodipodi:rx="3"
sodipodi:ry="3"
sodipodi:start="4.712389"
sodipodi:end="3.8397244"
sodipodi:arc-type="arc"
d="M 5.1258287,2.2289081 A 3,3 0 0 1 8.0547168,4.5795893 3,3 0 0 1 6.3936834,7.9478315 3,3 0 0 1 2.7457687,7.0551923 3,3 0 0 1 2.8276955,3.3005451"
sodipodi:open="true" />
<g
id="g1"
transform="translate(-0.33961362,-0.08264361)">
<rect
style="fill:#000000;stroke-width:0.7;stroke-dasharray:none"
id="rect1"
width="0.5"
height="3"
x="5.347394"
y="4.0818858"
rx="0.25"
ry="0.25" />
<rect
style="fill:#000000;stroke-width:0.7;stroke-dasharray:none"
id="rect1-2"
width="0.5"
height="1.5"
x="6.767818"
y="-1.1417361"
rx="0.25"
ry="0.25"
transform="rotate(45)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
sodipodi:docname="repeat.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="21.325417"
inkscape:cx="18.897637"
inkscape:cy="18.897637"
inkscape:window-width="1536"
inkscape:window-height="1041"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="fill:#000000;stroke-width:0"
id="path3"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="1.7518876"
sodipodi:cy="1.4281693"
sodipodi:r1="1.9958065"
sodipodi:r2="3.9916129"
sodipodi:arg1="0.51914611"
sodipodi:arg2="1.5663437"
inkscape:rounded="0.07"
inkscape:randomized="0"
d="M 3.4847328,2.4183665 C 3.3646778,2.6284629 0.14985015,2.6427775 0.02792892,2.4337586 -0.09399232,2.2247398 1.5010247,-0.56653997 1.7430009,-0.56761742 1.9849772,-0.56869487 3.6047879,2.2082702 3.4847328,2.4183665 Z"
inkscape:transform-center-x="0.75466106"
inkscape:transform-center-y="-0.0073088053"
transform="matrix(0,-0.83137379,0.83137379,0,3.7731711,3.93391)" />
<path
style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.9;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
id="path1"
sodipodi:type="arc"
sodipodi:cx="5.1258287"
sodipodi:cy="5.2289081"
sodipodi:rx="3"
sodipodi:ry="3"
sodipodi:start="4.712389"
sodipodi:end="3.8397244"
sodipodi:arc-type="arc"
d="M 5.1258287,2.2289081 A 3,3 0 0 1 8.0547168,4.5795893 3,3 0 0 1 6.3936834,7.9478315 3,3 0 0 1 2.7457687,7.0551923 3,3 0 0 1 2.8276955,3.3005451"
sodipodi:open="true" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 10 10"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
sodipodi:docname="stop.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff00"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="13.894537"
inkscape:cx="13.710424"
inkscape:cy="19.288156"
inkscape:window-width="1536"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="43"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;stroke-width:0"
id="rect2"
width="7.5"
height="7.5"
x="1.25"
y="1.25"
rx="0.75"
ry="0.75" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

286
monoclient-s/ui/ui.slint Normal file
View file

@ -0,0 +1,286 @@
import { Palette, Slider, ComboBox } from "std-widgets.slint";
component Button {
in-out property icon <=> img.source;
in property <bool> wanted;
in property <bool> enabled: false;
callback clicked;
VerticalLayout {
alignment: center;
spacing: 8px;
img := Image {
// What the actual fuck
x: 7px;
colorize: enabled ? Palette.foreground : Palette.foreground.darker(10%);
width: 16px;
height: 16px;
horizontal-alignment: center;
opacity: 1;
}
rect := Rectangle {
touch := TouchArea {
clicked => {
if enabled {
clicked()
}
}
}
border-radius: root.height / 2;
border-width: 1px;
border-color: Palette.border;
background: enabled ? (touch.pressed ? Palette.control-background.darker(45%) : (touch.has-hover ? Palette.control-background.darker(35%) : Palette.control-background)) : Palette.control-background.darker(35%);
animate background { duration: 166ms; }
drop-shadow-blur: wanted ? pow(abs(mod(animation-tick() / 1s - 3, 6) - 3) / 3, 2) * 128.0 * 1px : 0px;
drop-shadow-color: self.border-color;
width: 32px;
height: 32px;
}
}
}
export component MainWindow inherits Window {
callback play;
callback stop;
callback next;
callback refreshp;
callback change_volume(float);
callback text_edited;
in-out property <string> addr: address.text;
in-out property <string> mtitle: "";
in-out property <string> malbum: "";
in-out property <string> martist: "";
in-out property <float> volume: svolume.value;
in-out property <bool> start_enabled: false;
in-out property <bool> playing: false;
in-out property <bool> paused: false;
in property <image> cover: @image-url("../lonelyradio.png");
in property <[string]> playlists: ["All tracks"];
in property <[string]> supported_encoders: [];
in-out property <string> selected_playlist: selected.current-value;
in-out property <int> selected_encoder: encoder.current-index;
property <bool> settings: false;
title: "monoclient-s";
min-width: 448px;
max-width: 448px * 3;
preferred-width: 448px;
height: main.height;
animate background { duration: 166ms; }
panels := HorizontalLayout {
width: root.width * 2;
x: settings ? -root.width : 0.0;
height: main.height;
animate x {
easing: ease-in-out-expo;
duration: 0.5s;
}
main := HorizontalLayout {
width: root.width;
height: rect.height + self.padding * 2;
spacing: 16px;
padding: 16px;
rect := Rectangle {
opacity: playing ? 1.0 : 0.0;
animate opacity {
duration: 0.25s;
easing: ease-in-out;
}
clip: true;
border-radius: 6px;
animate background { duration: 166ms; }
background: Palette.foreground;
//width: 240px;
height: img.height + 12px * 2 + 1.5rem * 3;
max-width: parent.width;
border-width: 0px;
VerticalLayout {
img := Image {
vertical-alignment: top;
source: cover;
min-width: 240px;
height: self.width;
image-fit: contain;
}
VerticalLayout {
padding: 12px;
tartist := Text {
color: Palette.background;
vertical-alignment: center;
height: 1.5rem;
font-weight: 600;
text: martist;
overflow: elide;
}
talbum := Text {
color: Palette.background;
vertical-alignment: center;
height: 1.5rem;
text: malbum;
overflow: elide;
}
ttitle := Text {
color: Palette.background;
vertical-alignment: center;
height: 1.5rem;
text: mtitle;
overflow: elide;
}
}
}
}
VerticalLayout {
alignment: center;
//max-width: 160px;
spacing: 16px;
VerticalLayout {
spacing: 16px;
HorizontalLayout {
padding: 8px;
alignment: center;
spacing: 16px;
//height: 96px;
Button {
icon: @image-url("icons/stop.svg");
enabled: playing && !paused;
clicked => {
stop()
}
}
Button {
icon: playing ? (paused ? @image-url("icons/play.svg") : @image-url("icons/pause.svg")) : @image-url("icons/random.svg");
enabled: start_enabled || playing;
wanted: start_enabled && !playing;
clicked => {
play()
}
}
Button {
icon: @image-url("icons/next.svg");
enabled: playing && !paused;
clicked => {
next()
}
}
}
Rectangle {
padding: 16px;
clip: true;
background: Palette.background;
border-color: Palette.border;
animate background, border-color { duration: 166ms; }
border-width: 1px;
border-radius: 4px;
drop-shadow-blur: !start_enabled ? pow(abs(mod(animation-tick() / 1s - 3, 6) - 3) / 3, 2) * 128.0 * 1px : 0px;
drop-shadow-color: Palette.border;
VerticalLayout {
alignment: center;
padding: 8px;
address := TextInput {
text: "";
accepted => {
self.clear_focus()
}
edited => {
text_edited();
}
}
}
}
svolume := Slider {
value: 255;
maximum: 255;
changed(f) => {
change_volume(f)
}
}
HorizontalLayout {
alignment: center;
Button {
icon: @image-url("icons/gear.svg");
enabled: true;
wanted: false;
clicked => {
settings = !settings;
}
}
}
}
}
}
VerticalLayout {
width: root.width;
height: root.height;
alignment: center;
VerticalLayout {
padding: 20px;
spacing: 20px;
HorizontalLayout {
alignment: center;
spacing: 8px;
Text {
horizontal-alignment: right;
vertical-alignment: center;
text: "Playlists";
}
selected := ComboBox {
model: playlists;
current-index: 0;
selected() => {
self.clear_focus()
}
}
}
HorizontalLayout {
alignment: center;
spacing: 8px;
Text {
horizontal-alignment: right;
vertical-alignment: center;
text: "Encoder";
}
encoder := ComboBox {
model: supported_encoders;
selected() => {
self.clear_focus()
}
}
}
}
HorizontalLayout {
alignment: center;
Button {
icon: @image-url("icons/first.svg");
enabled: true;
wanted: false;
clicked => {
settings = !settings;
}
}
}
}
}
}

View file

@ -352,12 +352,12 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
LIBRARY_SEARCH_PATHS = "";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.6.0;
MARKETING_VERSION = 0.7.1;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -405,12 +405,12 @@
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 0.6.0;
MARKETING_VERSION = 0.7.1;
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -5,11 +5,10 @@
// Created by ivabus on 13.06.2024.
//
import SwiftUI
import AVFAudio
import MediaPlayer
import MonoLib
import SwiftUI
enum PlayerState {
case NotStarted
@ -17,11 +16,12 @@ enum PlayerState {
case Paused
mutating func update() {
self = switch c_get_state() {
case 2: PlayerState.Playing
case 3: PlayerState.Paused
default: PlayerState.NotStarted
}
self =
switch c_get_state() {
case 2: PlayerState.Playing
case 3: PlayerState.Paused
default: PlayerState.NotStarted
}
}
}
@ -30,10 +30,11 @@ enum EncoderType: UInt8 {
case PCMFloat = 1
case FLAC = 2
case Alac = 3
//WavPack = 4,
//Opus = 5,
//Aac = 6,
//WavPack = 4,
//Opus = 5,
//Aac = 6,
case Vorbis = 7
case Sea = 8
}
enum CoverSize: Int32 {
@ -51,10 +52,10 @@ struct PlayList: Identifiable, Hashable {
var name: String
}
struct Settings {
var encoder: EncoderType = EncoderType.FLAC
var cover_size: CoverSize = CoverSize.High/*
var cover_size: CoverSize = CoverSize
.High /*
init(enc: EncoderType, cov: CoverSize) {
encoder = enc
cover_size = cov
@ -62,9 +63,9 @@ struct Settings {
}
#if os(tvOS)
typealias MyStack = HStack
typealias MyStack = HStack
#else
typealias MyStack = VStack
typealias MyStack = VStack
#endif
struct Player: View {
@ -81,32 +82,30 @@ struct Player: View {
var body: some View {
MyStack(alignment: .center) {
VStack(alignment: .center) {
#if os(macOS)
Image(nsImage: cover.cover)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
.frame(width: 256.0, height: 256.0)
.clipShape(.rect(cornerRadius: 24))
.shadow(radius: 16)
.padding(16)
#else
Image(uiImage: cover.cover)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
.frame(width: 256.0, height: 256.0)
.clipShape(.rect(cornerRadius: 24))
.shadow(radius: 16)
.padding(16)
#endif
#if os(macOS)
Image(nsImage: cover.cover)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
.frame(width: 256.0, height: 256.0)
.clipShape(.rect(cornerRadius: 24))
.shadow(radius: 16)
.padding(16)
#else
Image(uiImage: cover.cover)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
.frame(width: 256.0, height: 256.0)
.clipShape(.rect(cornerRadius: 24))
.shadow(radius: 16)
.padding(16)
#endif
VStack(alignment: .center){
VStack(alignment: .center) {
Text(metadata.title).bold()
Text(metadata.album)
@ -119,14 +118,15 @@ struct Player: View {
cover.update()
}
let image = cover.cover
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) { (size: CGSize) -> PlatformImage in
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) {
(size: CGSize) -> PlatformImage in
return image
}
#if os(macOS)
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
#endif
#if os(macOS)
MPNowPlayingInfoCenter.default().playbackState =
state == PlayerState.Playing ? .playing : .paused
#endif
let nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyArtist: metadata.artist,
@ -144,11 +144,11 @@ struct Player: View {
"Server",
text: $server,
onCommit: {
#if os(macOS)
DispatchQueue.main.async {
NSApp.keyWindow?.makeFirstResponder(nil)
}
#endif
#if os(macOS)
DispatchQueue.main.async {
NSApp.keyWindow?.makeFirstResponder(nil)
}
#endif
}
)
.disableAutocorrection(true)
@ -158,7 +158,7 @@ struct Player: View {
.multilineTextAlignment(.center)
HStack(spacing: 8) {
Button(action: stop){
Button(action: stop) {
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
}
.disabled(state == PlayerState.NotStarted)
@ -166,16 +166,22 @@ struct Player: View {
.font(.system(size: 20))
.buttonBorderShape(.capsule)
Button(action: play){
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
.font(.system(size: 30))
.padding(4)
Button(action: play) {
Image(
systemName: state == PlayerState.NotStarted
? "infinity.circle"
: (state == PlayerState.Playing)
? "pause.circle.fill" : "play.circle"
)
.font(.system(size: 30))
.padding(4)
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(.capsule)
Button(action: next){
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
Button(action: next) {
Image(systemName: "forward.end.fill").padding(4).frame(
width: 32, height: 24)
}.disabled(state == PlayerState.NotStarted)
.buttonStyle(.bordered)
.font(.system(size: 20))
@ -185,7 +191,7 @@ struct Player: View {
}
Menu {
Picker("Playlist", selection: $playlist) {
ForEach ($playlists) { pl in
ForEach($playlists) { pl in
Text(pl.wrappedValue.name).tag(pl.wrappedValue)
}
@ -202,6 +208,8 @@ struct Player: View {
.tag(EncoderType.Alac)
Text("Vorbis (lossy)")
.tag(EncoderType.Vorbis)
Text("Sea (lossy)")
.tag(EncoderType.Sea)
}.pickerStyle(.menu)
Picker("Cover size", selection: $settings.cover_size) {
@ -227,18 +235,21 @@ struct Player: View {
.padding(32)
.onReceive(timer_playlists) { _ in
var id = -1
playlists = (["All tracks"] + String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({elem in
if elem.isEmpty {
return PlayList(id: -1, name: elem)
}
id += 1;
return PlayList(id: id, name: elem)
}).filter({elem in elem.id != -1})
playlists =
(["All tracks"]
+ String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({
elem in
if elem.isEmpty {
return PlayList(id: -1, name: elem)
}
id += 1
return PlayList(id: id, name: elem)
}).filter({ elem in elem.id != -1 })
}
.onAppear() {
#if os(iOS)
UIApplication.shared.beginReceivingRemoteControlEvents()
#endif
.onAppear {
#if os(iOS)
UIApplication.shared.beginReceivingRemoteControlEvents()
#endif
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
@ -256,49 +267,52 @@ struct Player: View {
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: { _ in
play()
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: { _ in
next()
return MPRemoteCommandHandlerStatus.success
})
}
.animation(.spring, value: UUID())
}
.animation(.spring, value: UUID())
}
func play() {
switch state {
case PlayerState.NotStarted: do {
/*#if os(macOS)
#else*/
let audioSession = AVAudioSession.sharedInstance()
case PlayerState.NotStarted:
do {
try audioSession.setCategory(
.playback, mode: .default)
try audioSession.setActive(true)
#if os(macOS)
#else
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(
.playback, mode: .default)
try audioSession.setActive(true)
} catch {
print("Failed to set the audio session configuration")
} catch {
print("Failed to set the audio session configuration")
}
#endif
Thread.detachNewThread {
c_start(
server,
CSettings(
encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue),
playlist.name == "All tracks" ? "" : playlist.name)
}
}
/*#endif*/
Thread.detachNewThread {
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue), playlist.name == "All tracks" ? "" : playlist.name)
default:
do {
c_toggle()
state.update()
}
}
default: do {
c_toggle()
state.update()
}
}
}
func stop() {

View file

@ -57,7 +57,7 @@ fn main() {
monolib::run(
&args.address,
Settings {
encoder: Encoder::Flac,
encoder: Encoder::Sea,
cover: -1,
},
&args.playlist,

View file

@ -3,7 +3,7 @@ name = "monolib"
version = "0.7.1"
edition = "2021"
license = "MIT"
description = "A library implementing the lonely radio audio streaming protocol"
description = "A library implementing the lonelyradio audio streaming protocol"
repository = "https://github.com/ivabus/lonelyradio"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
@ -22,15 +22,17 @@ anyhow = "1.0.86"
claxon = { version = "0.4.3", optional = true }
symphonia-codec-alac = {version = "0.5.4", optional = true }
symphonia-core = {version = "0.5.4", optional = true }
vorbis_rs = {version = "0.5.4", optional = true }
lewton = {version = "0.10.2", optional = true }
sea-codec = { version = "0.5.2", optional = true }
[features]
default = ["all-lossless", "all-lossy"]
all-lossless = ["alac", "flac"]
all-lossy = ["vorbis"]
all-lossy = ["vorbis", "sea"]
alac = ["dep:symphonia-codec-alac", "dep:symphonia-core"]
flac = ["dep:claxon"]
vorbis = ["dep:vorbis_rs"]
vorbis = ["dep:lewton"]
sea = ["dep:sea-codec"]
[package.metadata.xcframework]
include-dir = "src"
@ -38,5 +40,4 @@ lib-type = "cdylib"
zip = false
macOS = true
iOS = true
#tvOS = true
simulators = true
simulators = false

View file

@ -35,6 +35,7 @@ pub extern "C" fn c_start(server: *const c_char, settings: CSettings, playlist:
2 => Encoder::Flac,
3 => Encoder::Alac,
7 => Encoder::Vorbis,
8 => Encoder::Sea,
_ => return,
},
cover: settings.cover,

View file

@ -18,8 +18,7 @@ pub(crate) fn decode(
Encoder::Pcm16 => {
let mut samples_i16 = vec![0; fmd.length as usize / 2];
stream.read_i16_into::<LittleEndian>(&mut samples_i16)?;
samples
.append(&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect());
samples.extend(samples_i16.iter().map(|sample| *sample as f32 / 32767.0));
}
Encoder::PcmFloat => {
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
@ -31,12 +30,8 @@ pub(crate) fn decode(
{
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
let mut reader = claxon::FlacReader::new(take)?;
samples.append(
&mut reader
.samples()
.map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
.collect::<Vec<f32>>(),
);
samples
.extend(&mut reader.samples().map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0));
}
#[cfg(not(feature = "flac"))]
@ -73,19 +68,9 @@ pub(crate) fn decode(
{
let mut buf = vec![];
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
let mut decoder = vorbis_rs::VorbisDecoder::new(Cursor::new(buf))?;
let mut interleaved = vec![];
while let Some(decoded_block) = decoder.decode_audio_block()? {
let s = decoded_block.samples();
interleaved.resize(s[0].len() * s.len(), 0f32);
for (ind, channel) in s.iter().enumerate() {
for (samind, sample) in channel.iter().enumerate() {
interleaved[ind + samind * md.channels as usize] = *sample;
}
}
samples.extend(interleaved);
interleaved = vec![];
let mut srr = lewton::inside_ogg::OggStreamReader::new(Cursor::new(buf))?;
while let Some(pck_samples) = srr.read_dec_packet_itl()? {
samples.extend(pck_samples.iter().map(|x| *x as f32 / 32768.0));
}
}
#[cfg(not(feature = "vorbis"))]
@ -93,6 +78,19 @@ pub(crate) fn decode(
unimplemented!("vorbis decoding is disabled in library")
}
}
Encoder::Sea => {
#[cfg(feature = "sea")]
{
let mut buf = vec![];
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
let dec = sea_codec::sea_decode(buf.as_slice());
samples.extend(dec.samples.iter().map(|x| *x as f32 / 32768.0));
}
#[cfg(not(feature = "sea"))]
{
unimplemented!("sea decoding is disabled in library")
}
}
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
};
Ok(samples)

View file

@ -38,15 +38,17 @@ mod decode;
const CACHE_SIZE_PCM: usize = 32;
const CACHE_SIZE_COMPRESSED: usize = 4;
const SUPPORTED_DECODERS: &[Encoder] = &[
Encoder::Pcm16,
Encoder::PcmFloat,
pub const SUPPORTED_DECODERS: &[Encoder] = &[
#[cfg(feature = "flac")]
Encoder::Flac,
#[cfg(feature = "alac")]
Encoder::Alac,
#[cfg(feature = "vorbis")]
Encoder::Vorbis,
#[cfg(feature = "sea")]
Encoder::Sea,
Encoder::PcmFloat,
Encoder::Pcm16,
];
static SINK: RwLock<Option<Sink>> = RwLock::new(None);

View file

@ -11,7 +11,7 @@ use symphonia::core::units::Time;
use crate::Args;
pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time) {
pub fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time) {
let file = Box::new(std::fs::File::open(file_path).unwrap());
let mut hint = Hint::new();
hint.with_extension(file_path.extension().unwrap().to_str().unwrap());
@ -41,12 +41,7 @@ pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time)
let mut sample_rate = 0;
let track_length =
track.codec_params.time_base.unwrap().calc_time(track.codec_params.n_frames.unwrap());
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
_ => break,
};
while let Ok(packet) = format.next_packet() {
if packet.track_id() != track_id {
continue;
}
@ -112,12 +107,7 @@ pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream
.expect("unsupported codec");
let track_id = track.id;
stream! {
loop {
let packet = match format.next_packet() {
Ok(packet) => packet,
_ => break,
};
while let Ok(packet) = format.next_packet() {
if packet.track_id() != track_id {
continue;
}

View file

@ -127,6 +127,32 @@ pub fn encode(
unimplemented!()
}
}
Encoder::Sea => {
#[cfg(feature = "sea")]
{
Some((
sea_codec::sea_encode(
samples
.iter_mut()
.map(|x| (*x * 32768.0) as i16)
.collect::<Vec<_>>()
.as_slice(),
sample_rate,
channels as u32,
sea_codec::encoder::EncoderSettings {
residual_bits: 5.0,
..Default::default()
},
),
None,
))
}
#[cfg(not(feature = "sea"))]
{
unimplemented!()
}
}
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
}
}

View file

@ -14,6 +14,7 @@ use encode::encode;
use futures_util::pin_mut;
use futures_util::StreamExt;
use image::ImageReader;
use image::RgbImage;
use lofty::Accessor;
use lofty::TaggedFileExt;
use lonelyradio_types::Encoder;
@ -33,7 +34,7 @@ use xspf::Playlist;
use crate::decode::decode_file_stream;
use crate::decode::get_meta;
#[derive(Parser)]
#[derive(Parser, Clone)]
struct Args {
/// Directory with audio files
dir: PathBuf,
@ -67,6 +68,8 @@ const SUPPORTED_ENCODERS: &[Encoder] = &[
Encoder::Alac,
#[cfg(feature = "vorbis")]
Encoder::Vorbis,
#[cfg(feature = "sea")]
Encoder::Sea,
];
async fn stream_track(
@ -94,6 +97,7 @@ async fn stream_track(
Encoder::Flac => 16,
Encoder::Alac => 32,
Encoder::Vorbis => 64,
Encoder::Sea => 64,
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
})
.next()
@ -106,7 +110,8 @@ async fn stream_track(
| Encoder::PcmFloat
| Encoder::Flac
| Encoder::Alac
| Encoder::Vorbis => {
| Encoder::Vorbis
| Encoder::Sea => {
let (encoded, magic_cookie) =
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
let _md = PlayMessage::F(FragmentMetadata {
@ -294,6 +299,56 @@ fn track_valid(track: &Path) -> bool {
}
}
struct Metadata {
title: String,
album: String,
artist: String,
cover: Option<RgbImage>,
}
fn get_metadata(track: impl AsRef<Path>, args: &Args, settings: &Settings) -> Option<Metadata> {
let mut title = String::new();
let mut artist = String::new();
let mut album = String::new();
let mut cover = std::thread::spawn(|| None);
let mut file = std::fs::File::open(&track).unwrap();
let tagged = lofty::read_from(&mut file).ok()?;
if let Some(id3v2) = tagged.primary_tag() {
title = id3v2
.title()
.unwrap_or(track.as_ref().file_stem().unwrap().to_string_lossy())
.to_string();
album = id3v2.album().unwrap_or("".into()).to_string();
artist = id3v2.artist().unwrap_or("".into()).to_string();
if !(id3v2.pictures().is_empty() || args.artwork == -1 || settings.cover == -1) {
let pic = id3v2.pictures()[0].clone();
let args = args.clone();
let settings = settings.clone();
cover = std::thread::spawn(move || {
let dec = ImageReader::new(Cursor::new(pic.into_data()))
.with_guessed_format()
.ok()?
.decode()
.ok()?;
let img = if args.artwork != 0 && settings.cover != 0 {
let size = std::cmp::min(args.artwork as u32, settings.cover as u32);
dec.resize(size, size, image::imageops::FilterType::Lanczos3)
} else {
dec
}
.to_rgb8();
Some(img)
});
};
};
Some(Metadata {
title,
album,
artist,
cover: cover.join().unwrap(),
})
}
async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
let args = Args::parse();
let encoder_wants = match settings.encoder {
@ -304,46 +359,20 @@ async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Setti
loop {
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
let mut title = String::new();
let mut artist = String::new();
let mut album = String::new();
let mut cover = std::thread::spawn(|| None);
let mut file = std::fs::File::open(&track).unwrap();
let tagged = match lofty::read_from(&mut file) {
Ok(f) => f,
let Metadata {
title,
album,
artist,
cover,
} = match get_metadata(&track, &args, &settings) {
Some(m) => m,
_ => continue,
};
if let Some(id3v2) = tagged.primary_tag() {
title =
id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string();
album = id3v2.album().unwrap_or("".into()).to_string();
artist = id3v2.artist().unwrap_or("".into()).to_string();
if !(id3v2.pictures().is_empty() || args.artwork == -1 || settings.cover == -1) {
let pic = id3v2.pictures()[0].clone();
cover = std::thread::spawn(move || {
let dec = ImageReader::new(Cursor::new(pic.into_data()))
.with_guessed_format()
.ok()?
.decode()
.ok()?;
let mut img = Vec::new();
if args.artwork != 0 && settings.cover != 0 {
let size = std::cmp::min(args.artwork as u32, settings.cover as u32);
dec.resize(size, size, image::imageops::FilterType::Lanczos3)
} else {
dec
}
.to_rgb8()
.write_to(&mut Cursor::new(&mut img), image::ImageFormat::Jpeg)
.unwrap();
Some(img)
});
};
};
let track_message = format!("{} - {} - {}", &artist, &album, &title);
println!("[{}] {} ({:?})", Local::now().to_rfc3339(), track_message, settings.encoder);
let (channels, sample_rate, time) = get_meta(track.as_path(), encoder_wants).await;
let (channels, sample_rate, time) = get_meta(track.as_path(), encoder_wants);
let stream = decode_file_stream(track, encoder_wants);
let id = thread_rng().gen();
if stream_track(
@ -352,7 +381,11 @@ async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Setti
track_length_frac: time.frac as f32,
track_length_secs: time.seconds,
encoder: settings.encoder,
cover: cover.join().unwrap(),
cover: cover.map(|x| {
let mut buf = Cursor::new(Vec::new());
x.write_to(&mut buf, image::ImageFormat::Jpeg).unwrap();
buf.into_inner()
}),
id,
album,
artist,