Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
0bfd885c70 | |||
80cef97ca2 | |||
60791dc292 | |||
5d9179e5df | |||
5794359202 | |||
bc3a43e870 | |||
3804653512 | |||
dfb86522fb | |||
d43dcde7a2 | |||
29338f32e3 |
32
.dockerignore
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Include any files or directories that you don't want to be copied to your
|
||||||
|
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||||
|
#
|
||||||
|
# For more help, visit the .dockerignore file reference guide at
|
||||||
|
# https://docs.docker.com/go/build-context-dockerignore/
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
/bin
|
||||||
|
/target
|
||||||
|
LICENSE
|
||||||
|
README.md
|
70
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "[0-9]+.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: taiki-e/create-gh-release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
upload-assets:
|
||||||
|
needs: create-release
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: aarch64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
os: windows-latest
|
||||||
|
- target: aarch64-pc-windows-msvc
|
||||||
|
os: windows-latest
|
||||||
|
- target: riscv64gc-unknown-linux-gnu
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: armv7-unknown-linux-musleabihf
|
||||||
|
os: ubuntu-latest
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install cross-compilation tools
|
||||||
|
uses: taiki-e/setup-cross-toolchain-action@v1
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
if: startsWith(matrix.os, 'ubuntu')
|
||||||
|
- uses: taiki-e/upload-rust-binary-action@v1
|
||||||
|
with:
|
||||||
|
bin: lonelyradio
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
profile: distribute
|
||||||
|
- uses: taiki-e/upload-rust-binary-action@v1
|
||||||
|
with:
|
||||||
|
bin: monoclient
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
profile: distribute
|
||||||
|
manifest-path: monoclient/Cargo.toml
|
||||||
|
if: startsWith(matrix.os , 'ubuntu') == false
|
||||||
|
- uses: taiki-e/upload-rust-binary-action@v1
|
||||||
|
with:
|
||||||
|
bin: monoclient-s
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
profile: distribute
|
||||||
|
manifest-path: monoclient-s/Cargo.toml
|
||||||
|
if: startsWith(matrix.os , 'ubuntu') == false
|
1
.gitignore
vendored
|
@ -50,7 +50,6 @@ fastlane/test_output
|
||||||
### SwiftPackageManager ###
|
### SwiftPackageManager ###
|
||||||
Packages
|
Packages
|
||||||
xcuserdata
|
xcuserdata
|
||||||
*.xcodeproj
|
|
||||||
|
|
||||||
### Xcode ###
|
### Xcode ###
|
||||||
# Xcode
|
# Xcode
|
||||||
|
|
3520
Cargo.lock
generated
34
Cargo.toml
|
@ -10,20 +10,17 @@ members = [
|
||||||
[package]
|
[package]
|
||||||
name = "lonelyradio"
|
name = "lonelyradio"
|
||||||
description = "TCP radio for lonely ones"
|
description = "TCP radio for lonely ones"
|
||||||
version = "0.6.1"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
repository = "https://github.com/ivabus/lonelyradio"
|
repository = "https://github.com/ivabus/lonelyradio"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lonelyradio_types = { version = "0.6.0", path = "./lonelyradio_types" }
|
lonelyradio_types = { version = "0.7.0", path = "./lonelyradio_types" }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
clap = { version = "4.4.18", features = ["derive"] }
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
tokio = { version = "1.35.1", features = [
|
tokio = { version = "1.35.1", features = [
|
||||||
"sync",
|
|
||||||
"fs",
|
|
||||||
"io-util",
|
|
||||||
"net",
|
"net",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"rt",
|
"rt",
|
||||||
|
@ -42,14 +39,29 @@ lofty = "0.18.2"
|
||||||
async-stream = "0.3.5"
|
async-stream = "0.3.5"
|
||||||
tokio-stream = { version = "0.1.15", features = ["sync"] }
|
tokio-stream = { version = "0.1.15", features = ["sync"] }
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
samplerate = "0.2.4"
|
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
flacenc = { version = "0.4.0", default-features = false }
|
image = { version = "0.25.1", default-features = false, features = ["png", "jpeg", "bmp"]}
|
||||||
image = "0.25.1"
|
xspf = "0.4.0"
|
||||||
|
url = "2.5.2"
|
||||||
|
samplerate = "0.2.4"
|
||||||
|
|
||||||
[build-dependencies]
|
# Optional encoders
|
||||||
cc = "1.0.98"
|
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 }
|
||||||
|
|
||||||
[profile.release]
|
[features]
|
||||||
|
default = ["all-lossless", "all-lossy"]
|
||||||
|
all-lossless = ["alac", "flac"]
|
||||||
|
all-lossy = ["vorbis", "sea"]
|
||||||
|
alac = ["dep:alac-encoder"]
|
||||||
|
flac = ["dep:flacenc"]
|
||||||
|
vorbis = ["dep:vorbis_rs"]
|
||||||
|
sea = ["dep:sea-codec"]
|
||||||
|
|
||||||
|
[profile.distribute]
|
||||||
|
inherits = "release"
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
strip = true
|
strip = true
|
||||||
|
lto = "fat"
|
||||||
|
|
91
Dockerfile
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Autogenerated by docker init
|
||||||
|
# https://docs.docker.com/go/dockerfile-reference/
|
||||||
|
|
||||||
|
ARG RUST_VERSION=1.85
|
||||||
|
ARG APP_NAME=lonelyradio
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# xx is a helper for cross-compilation.
|
||||||
|
# See https://github.com/tonistiigi/xx/ for more information.
|
||||||
|
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.3.0 AS xx
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Create a stage for building the application.
|
||||||
|
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine AS build
|
||||||
|
ARG APP_NAME
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy cross compilation utilities from the xx stage.
|
||||||
|
COPY --from=xx / /
|
||||||
|
|
||||||
|
# Install host build dependencies.
|
||||||
|
RUN apk add --no-cache clang lld musl-dev git file cmake make
|
||||||
|
|
||||||
|
# This is the architecture you’re building for, which is passed in by the builder.
|
||||||
|
# Placing it here allows the previous steps to be cached across architectures.
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
|
# Install cross compilation build dependencies.
|
||||||
|
RUN xx-apk add --no-cache musl-dev gcc
|
||||||
|
|
||||||
|
# Build the application.
|
||||||
|
# Leverage a cache mount to /usr/local/cargo/registry/
|
||||||
|
# for downloaded dependencies, a cache mount to /usr/local/cargo/git/db
|
||||||
|
# for git repository dependencies, and a cache mount to /app/target/ for
|
||||||
|
# compiled dependencies which will speed up subsequent builds.
|
||||||
|
# Leverage a bind mount to the src directory to avoid having to copy the
|
||||||
|
# source code into the container. Once built, copy the executable to an
|
||||||
|
# output directory before the cache mounted /app/target is unmounted.
|
||||||
|
RUN --mount=type=bind,source=src,target=src \
|
||||||
|
--mount=type=bind,source=lonelyradio_types,target=lonelyradio_types \
|
||||||
|
--mount=type=bind,source=monoclient,target=monoclient \
|
||||||
|
--mount=type=bind,source=monoclient-s,target=monoclient-s \
|
||||||
|
--mount=type=bind,source=monolib,target=monolib \
|
||||||
|
--mount=type=bind,source=monoloader,target=monoloader \
|
||||||
|
--mount=type=bind,source=Cargo.toml,target=Cargo.toml \
|
||||||
|
--mount=type=bind,source=Cargo.lock,target=Cargo.lock \
|
||||||
|
--mount=type=cache,target=/app/target/,id=rust-cache-${APP_NAME}-${TARGETPLATFORM} \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||||
|
--mount=type=cache,target=/usr/local/cargo/registry/ \
|
||||||
|
<<EOF
|
||||||
|
set -e
|
||||||
|
xx-cargo build --locked --release --target-dir ./target
|
||||||
|
cp ./target/$(xx-cargo --print-target-triple)/release/$APP_NAME /bin/server
|
||||||
|
xx-verify /bin/server
|
||||||
|
EOF
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Create a new stage for running the application that contains the minimal
|
||||||
|
# runtime dependencies for the application. This often uses a different base
|
||||||
|
# image from the build stage where the necessary files are copied from the build
|
||||||
|
# stage.
|
||||||
|
#
|
||||||
|
# The example below uses the alpine image as the foundation for running the app.
|
||||||
|
# By specifying the "3.18" tag, it will use version 3.18 of alpine. If
|
||||||
|
# reproducability is important, consider using a digest
|
||||||
|
# (e.g., alpine@sha256:664888ac9cfd28068e062c991ebcff4b4c7307dc8dd4df9e728bedde5c449d91).
|
||||||
|
FROM alpine:3.18 AS final
|
||||||
|
|
||||||
|
# Create a non-privileged user that the app will run under.
|
||||||
|
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||||
|
ARG UID=10001
|
||||||
|
RUN adduser \
|
||||||
|
--disabled-password \
|
||||||
|
--gecos "" \
|
||||||
|
--home "/nonexistent" \
|
||||||
|
--shell "/sbin/nologin" \
|
||||||
|
--no-create-home \
|
||||||
|
--uid "${UID}" \
|
||||||
|
appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Copy the executable from the "build" stage.
|
||||||
|
COPY --from=build /bin/server /bin/
|
||||||
|
|
||||||
|
# Expose the port that the application listens on.
|
||||||
|
EXPOSE 5894
|
||||||
|
|
||||||
|
# What the container should run when it is started.
|
||||||
|
CMD ["/bin/server", "/music"]
|
66
README.md
|
@ -1,27 +1,44 @@
|
||||||
# lonelyradio
|
# lonelyradio Music Streamer
|
||||||
|
|
||||||
Broadcast lossless audio over the internet.
|
Shuffles through your [XSPF playlists](https://www.xspf.org) or your entire library.
|
||||||
|
|
||||||
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
|
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))
|
||||||
|
|
||||||
Optionally transcodes audio into and from FLAC using [flacenc-rs](https://github.com/yotarok/flacenc-rs/) and [claxon](https://github.com/ruuda/claxon).
|
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 client’s requests.
|
||||||
|
|
||||||
## Install server
|
### Install server
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 lonelyradio
|
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 lonelyradio
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run
|
### Run
|
||||||
|
|
||||||
```
|
```
|
||||||
lonelyradio <MUSIC_FOLDER>
|
lonelyradio <MUSIC_FOLDER>
|
||||||
```
|
```
|
||||||
|
|
||||||
All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr.
|
All files (recursively) will be shuffled and played back. Log will be displayed to stdout.
|
||||||
|
|
||||||
Look into `--help` for detailed info
|
Look into `--help` for detailed info
|
||||||
|
|
||||||
|
#### Run in Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d \
|
||||||
|
--name lonelyradio \
|
||||||
|
--restart=unless-stopped \
|
||||||
|
-v /path/to/music:/music \
|
||||||
|
-p 5894:5894 \
|
||||||
|
ivabuz/lonelyradio:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Playlists
|
||||||
|
|
||||||
|
Specify a directory with playlists with `--playlist-dir`. lonelyradio will scan them on startup and play them on clients’ requests.
|
||||||
|
|
||||||
|
Only the `<location>` and (playlist's) element would be used and only `file://` is supported.
|
||||||
|
|
||||||
### Clients
|
### Clients
|
||||||
|
|
||||||
#### monoclient-x
|
#### monoclient-x
|
||||||
|
@ -37,55 +54,46 @@ Look into `--help` for detailed info
|
||||||
|
|
||||||
[monoclient-s](./monoclient-s) is a GUI player for lonelyradio built with [Slint](https://slint.dev)
|
[monoclient-s](./monoclient-s) is a GUI player for lonelyradio built with [Slint](https://slint.dev)
|
||||||
|
|
||||||
|
|
||||||
##### Install
|
##### Install
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 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.
|
You may need to install some dependencies for Slint.
|
||||||
|
|
||||||
Desktop integration will be added later.
|
Desktop integration will be added later.
|
||||||
|
|
||||||
##### Build
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build -p monoclient-s
|
|
||||||
```
|
|
||||||
|
|
||||||
You may need to install some dependencies for Slint.
|
|
||||||
|
|
||||||
#### monoclient
|
#### monoclient
|
||||||
|
|
||||||
[monoclient](./monoclient) is a CLI player for lonelyradio that uses [monolib](./monolib)
|
[monoclient](./monoclient) is a CLI player for lonelyradio that uses [monolib](./monolib)
|
||||||
|
|
||||||
|
##### Install monoclient
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.1 monoclient
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
monoclient <SERVER>:<PORT>
|
monoclient <SERVER>:<PORT>
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Install monoclient
|
Look into `--help` for detailed info on usage.
|
||||||
|
|
||||||
```shell
|
|
||||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 monoclient
|
|
||||||
```
|
|
||||||
|
|
||||||
# Other things
|
# Other things
|
||||||
|
|
||||||
[monoloader](./monoloader) is a tool, that allows you to download individual audio tracks from lonelyradio-compatible servers.
|
[monoloader](./monoloader) is a tool that allows you to download individual audio tracks from lonelyradio-compatible servers.
|
||||||
|
|
||||||
[monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients.
|
[monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients.
|
||||||
|
|
||||||
The full protocol specification will be available later. If you would like to learn more about it now, please refer to the monolib.
|
[Protocol documentation] shortly describes the protocol used in lonelyradio. Please refer to monolib and verify custom clients with the reference lonelyradio server.
|
||||||
|
|
||||||
#### monolib API stability
|
#### monolib API stability
|
||||||
|
|
||||||
As lonelyradio has not yet reached its first major release, the API may (and will) break at any point.
|
As lonelyradio has not yet reached its first major release, the API may (and will) break at any point.
|
||||||
|
|
||||||
### Microphone server
|
|
||||||
|
|
||||||
Experimental (and uncompatible with versions 0.6+) server (lonelyradio-compatible) for streaming audio from your microphone is available in the [microserve](./microserve) crate.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
lonelyradio, monolib and monoclient, as well as all other crates in this repository, are licensed under the terms of the [MIT license](./LICENSE).
|
lonelyradio, monolib and monoclient, as well as all other crates in this repository, are licensed under the terms of the [MIT license](./LICENSE).
|
||||||
|
|
65
docs/protocol.md
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# lonelyradio protocol (0.7)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
The lonelyradio protocol is designed to be minimal yet functional for music streaming purposes.
|
||||||
|
|
||||||
|
The lonelyradio protocol operates at the application layer, establishing communication between the server and client. In its reference implementation, it runs atop the TCP protocol, but it could also be implemented on top of any other transport protocol, such as UDP, WebSocket, and so on.
|
||||||
|
|
||||||
|
The lonelyradio protocol uses [MessagePack](https://msgpack.org) to encode messages. Structures used in communication are defined in the `lonelyradio_types` crate.
|
||||||
|
|
||||||
|
## Establishing connection
|
||||||
|
|
||||||
|
1. The client sends a «hello» packet («lonelyra», 8 bytes)
|
||||||
|
1. The server checks the hello packet
|
||||||
|
2. The server sends «ServerCapabilities» which informs the client about supported audio encoders (raw pcm s16le must be supported by all server implementations)
|
||||||
|
3. Then the client picks one of the requests:
|
||||||
|
1. Play (p) (see example 1.1)
|
||||||
|
2. ListPlaylist (lpl) (see example 1.2)
|
||||||
|
3. PlayPlayList (ppl) (see example 1.3)
|
||||||
|
4. The server responds with one of RequestResult
|
||||||
|
1. Ok -> The server begins sending PlayMessage’s
|
||||||
|
1. TrackMetadata indicates the start of the new track
|
||||||
|
2. FragmentMetadata indicates the start of a new fragment and defines the number of bytes in it
|
||||||
|
1. FragmentMetadata is always followed by a fragment
|
||||||
|
2. Playlist is only returned on ListPlaylist and shows available playlists
|
||||||
|
3. Error indicates an error
|
||||||
|
|
||||||
|
To get «next track» just reestablish the connection.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Examples show JSON representation of MessagePack
|
||||||
|
|
||||||
|
### 1.1
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"p": { // e and co definicions could be found in lonelyradio_types crate
|
||||||
|
"e": "Pcm16",
|
||||||
|
"co": -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2
|
||||||
|
|
||||||
|
Just string encoded to MessagePack
|
||||||
|
|
||||||
|
```json
|
||||||
|
"lpl"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ppl": [
|
||||||
|
"someplaylist",
|
||||||
|
{
|
||||||
|
"e": "Pcm16",
|
||||||
|
"co": -1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -1,12 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lonelyradio_types"
|
name = "lonelyradio_types"
|
||||||
|
edition = "2021"
|
||||||
|
version = "0.7.0"
|
||||||
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
description = "Shared types for lonelyradio"
|
description = "Shared types for lonelyradio"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
version = "0.6.0"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
|
||||||
repository = "https://github.com/ivabus/lonelyradio"
|
repository = "https://github.com/ivabus/lonelyradio"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = {version = "1.0.209", features = ["derive"]}
|
||||||
serde_bytes = "0.11.15"
|
serde_bytes = {version = "0.11.15"}
|
||||||
|
|
|
@ -1,9 +1,41 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub const HELLO_MAGIC: u64 = 0x104e1374d10;
|
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
pub enum Message {
|
pub enum Request {
|
||||||
|
// Just play what server wants you to give
|
||||||
|
#[serde(rename = "p", alias = "Play")]
|
||||||
|
Play(Settings),
|
||||||
|
#[serde(rename = "lpl", alias = "ListPlaylist")]
|
||||||
|
ListPlaylist,
|
||||||
|
#[serde(rename = "ppl", alias = "PlayPlaylist")]
|
||||||
|
PlayPlaylist(String, Settings),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct PlaylistResponce {
|
||||||
|
pub playlists: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub enum RequestResult {
|
||||||
|
Ok,
|
||||||
|
Playlist(PlaylistResponce),
|
||||||
|
Error(RequestError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub enum RequestError {
|
||||||
|
NoSuchPlaylist,
|
||||||
|
WrongCoverSize,
|
||||||
|
UnsupportedEncoder,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub enum PlayMessage {
|
||||||
T(TrackMetadata),
|
T(TrackMetadata),
|
||||||
F(FragmentMetadata),
|
F(FragmentMetadata),
|
||||||
}
|
}
|
||||||
|
@ -11,10 +43,10 @@ pub enum Message {
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
#[serde(rename = "e")]
|
#[serde(rename = "e", alias = "encoder")]
|
||||||
pub encoder: Encoder,
|
pub encoder: Encoder,
|
||||||
|
|
||||||
#[serde(rename = "co")]
|
#[serde(rename = "co", alias = "cover")]
|
||||||
pub cover: i32,
|
pub cover: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +54,9 @@ pub struct Settings {
|
||||||
pub struct ServerCapabilities {
|
pub struct ServerCapabilities {
|
||||||
#[serde(rename = "e")]
|
#[serde(rename = "e")]
|
||||||
pub encoders: Vec<Encoder>,
|
pub encoders: Vec<Encoder>,
|
||||||
|
// Will be used in the next updates
|
||||||
|
//#[serde(rename = "ar")]
|
||||||
|
//pub available_requests: Vec<Request>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
@ -42,22 +77,64 @@ pub struct TrackMetadata {
|
||||||
pub album: String,
|
pub album: String,
|
||||||
#[serde(rename = "mar")]
|
#[serde(rename = "mar")]
|
||||||
pub artist: String,
|
pub artist: String,
|
||||||
#[serde(rename = "co")]
|
|
||||||
#[serde(with = "serde_bytes")]
|
#[serde(
|
||||||
|
rename = "co",
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "serde_bytes",
|
||||||
|
default = "none"
|
||||||
|
)]
|
||||||
pub cover: Option<Vec<u8>>,
|
pub cover: Option<Vec<u8>>,
|
||||||
|
|
||||||
|
pub id: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WavPack, Opus and Aac are currently unimplemented.
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum Encoder {
|
pub enum Encoder {
|
||||||
Pcm16 = 0,
|
Pcm16 = 0,
|
||||||
PcmFloat = 1,
|
PcmFloat = 1,
|
||||||
Flac = 2,
|
Flac = 2,
|
||||||
|
Alac = 3,
|
||||||
|
WavPack = 4,
|
||||||
|
Opus = 5,
|
||||||
|
Aac = 6,
|
||||||
|
Vorbis = 7,
|
||||||
|
Sea = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
pub struct FragmentMetadata {
|
pub struct FragmentMetadata {
|
||||||
// In bytes
|
// In bytes
|
||||||
#[serde(rename = "l")]
|
#[serde(rename = "le")]
|
||||||
pub length: u64,
|
pub length: u64,
|
||||||
|
|
||||||
|
#[serde(
|
||||||
|
rename = "mc",
|
||||||
|
skip_serializing_if = "Option::is_none",
|
||||||
|
with = "serde_bytes",
|
||||||
|
default = "none"
|
||||||
|
)]
|
||||||
|
pub magic_cookie: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
886
microserve/Cargo.lock
generated
|
@ -1,886 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aho-corasick"
|
|
||||||
version = "1.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "alsa"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce"
|
|
||||||
dependencies = [
|
|
||||||
"alsa-sys",
|
|
||||||
"bitflags 2.5.0",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "alsa-sys"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bindgen"
|
|
||||||
version = "0.69.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.5.0",
|
|
||||||
"cexpr",
|
|
||||||
"clang-sys",
|
|
||||||
"itertools",
|
|
||||||
"lazy_static",
|
|
||||||
"lazycell",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"regex",
|
|
||||||
"rustc-hash",
|
|
||||||
"shlex",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "1.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bumpalo"
|
|
||||||
version = "3.16.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytes"
|
|
||||||
version = "1.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cc"
|
|
||||||
version = "1.0.98"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
|
||||||
dependencies = [
|
|
||||||
"jobserver",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cesu8"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cexpr"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "clang-sys"
|
|
||||||
version = "1.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
|
|
||||||
dependencies = [
|
|
||||||
"glob",
|
|
||||||
"libc",
|
|
||||||
"libloading",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "combine"
|
|
||||||
version = "4.6.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation-sys"
|
|
||||||
version = "0.8.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "coreaudio-rs"
|
|
||||||
version = "0.11.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"coreaudio-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "coreaudio-sys"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpal"
|
|
||||||
version = "0.15.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
|
||||||
dependencies = [
|
|
||||||
"alsa",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"coreaudio-rs",
|
|
||||||
"dasp_sample",
|
|
||||||
"jni",
|
|
||||||
"js-sys",
|
|
||||||
"libc",
|
|
||||||
"mach2",
|
|
||||||
"ndk",
|
|
||||||
"ndk-context",
|
|
||||||
"oboe",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"web-sys",
|
|
||||||
"windows",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dasp_sample"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "either"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "equivalent"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glob"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hashbrown"
|
|
||||||
version = "0.14.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indexmap"
|
|
||||||
version = "2.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
|
||||||
dependencies = [
|
|
||||||
"equivalent",
|
|
||||||
"hashbrown",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni"
|
|
||||||
version = "0.21.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
|
||||||
dependencies = [
|
|
||||||
"cesu8",
|
|
||||||
"cfg-if",
|
|
||||||
"combine",
|
|
||||||
"jni-sys",
|
|
||||||
"log",
|
|
||||||
"thiserror",
|
|
||||||
"walkdir",
|
|
||||||
"windows-sys 0.45.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "js-sys"
|
|
||||||
version = "0.3.69"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazycell"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.155"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libloading"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"windows-targets 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lonelyradio_types"
|
|
||||||
version = "0.4.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mach2"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "microserve"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"cpal",
|
|
||||||
"lonelyradio_types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "microserve-session"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ndk"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.5.0",
|
|
||||||
"jni-sys",
|
|
||||||
"log",
|
|
||||||
"ndk-sys",
|
|
||||||
"num_enum",
|
|
||||||
"thiserror",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ndk-context"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ndk-sys"
|
|
||||||
version = "0.5.0+25.2.9519653"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
|
||||||
dependencies = [
|
|
||||||
"jni-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-derive"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_enum"
|
|
||||||
version = "0.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
|
|
||||||
dependencies = [
|
|
||||||
"num_enum_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_enum_derive"
|
|
||||||
version = "0.7.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-crate",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "oboe"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
|
||||||
dependencies = [
|
|
||||||
"jni",
|
|
||||||
"ndk",
|
|
||||||
"ndk-context",
|
|
||||||
"num-derive",
|
|
||||||
"num-traits",
|
|
||||||
"oboe-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "oboe-sys"
|
|
||||||
version = "0.6.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.19.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pkg-config"
|
|
||||||
version = "0.3.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro-crate"
|
|
||||||
version = "3.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
|
|
||||||
dependencies = [
|
|
||||||
"toml_edit",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.82"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.36"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.10.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.8.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-hash"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.202"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.202"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shlex"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.64"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.61"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.61"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_datetime"
|
|
||||||
version = "0.6.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toml_edit"
|
|
||||||
version = "0.21.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
|
||||||
dependencies = [
|
|
||||||
"indexmap",
|
|
||||||
"toml_datetime",
|
|
||||||
"winnow",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "walkdir"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen"
|
|
||||||
version = "0.2.92"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"wasm-bindgen-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-backend"
|
|
||||||
version = "0.2.92"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
|
||||||
dependencies = [
|
|
||||||
"bumpalo",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-futures"
|
|
||||||
version = "0.4.42"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro"
|
|
||||||
version = "0.2.92"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"wasm-bindgen-macro-support",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro-support"
|
|
||||||
version = "0.2.92"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
"wasm-bindgen-backend",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-shared"
|
|
||||||
version = "0.2.92"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "web-sys"
|
|
||||||
version = "0.3.69"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows"
|
|
||||||
version = "0.54.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
|
||||||
dependencies = [
|
|
||||||
"windows-core",
|
|
||||||
"windows-targets 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-core"
|
|
||||||
version = "0.54.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
|
||||||
dependencies = [
|
|
||||||
"windows-result",
|
|
||||||
"windows-targets 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-result"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.45.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.42.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.52.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.42.2",
|
|
||||||
"windows_aarch64_msvc 0.42.2",
|
|
||||||
"windows_i686_gnu 0.42.2",
|
|
||||||
"windows_i686_msvc 0.42.2",
|
|
||||||
"windows_x86_64_gnu 0.42.2",
|
|
||||||
"windows_x86_64_gnullvm 0.42.2",
|
|
||||||
"windows_x86_64_msvc 0.42.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.52.5",
|
|
||||||
"windows_aarch64_msvc 0.52.5",
|
|
||||||
"windows_i686_gnu 0.52.5",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc 0.52.5",
|
|
||||||
"windows_x86_64_gnu 0.52.5",
|
|
||||||
"windows_x86_64_gnullvm 0.52.5",
|
|
||||||
"windows_x86_64_msvc 0.52.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.42.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.52.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winnow"
|
|
||||||
version = "0.5.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
|
@ -1,24 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "microserve"
|
|
||||||
version = "0.5.0"
|
|
||||||
license = "MIT"
|
|
||||||
edition = "2021"
|
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
|
||||||
repository = "https://github.com/ivabus/lonelyradio"
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
cpal = "0.15.3"
|
|
||||||
lonelyradio_types = { path = "../lonelyradio_types" }
|
|
||||||
once_cell = "1.19.0"
|
|
||||||
queues = "1.1.0"
|
|
||||||
rmp-serde = "1.3.0"
|
|
||||||
tokio = { version = "1.35.1", features = [
|
|
||||||
"sync",
|
|
||||||
"fs",
|
|
||||||
"io-util",
|
|
||||||
"net",
|
|
||||||
"rt-multi-thread",
|
|
||||||
"rt",
|
|
||||||
"macros",
|
|
||||||
] }
|
|
|
@ -1,96 +0,0 @@
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
||||||
use lonelyradio_types::{FragmentMetadata, TrackMetadata};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::io::Write;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
use std::sync::RwLock;
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
static QUEUE: Lazy<Arc<RwLock<VecDeque<Vec<i16>>>>> =
|
|
||||||
Lazy::new(|| Arc::new(RwLock::new(VecDeque::new())));
|
|
||||||
|
|
||||||
static START_INDEX: Mutex<usize> = Mutex::new(0);
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
tokio::spawn(listen_mic());
|
|
||||||
println!("Started buffering");
|
|
||||||
let listener = TcpListener::bind("0.0.0.0:5894").await.unwrap();
|
|
||||||
std::thread::sleep(Duration::from_secs(5));
|
|
||||||
tokio::spawn(update_start());
|
|
||||||
println!("Accepting connections");
|
|
||||||
loop {
|
|
||||||
let (socket, _) = listener.accept().await.unwrap();
|
|
||||||
let socket = socket.into_std().unwrap();
|
|
||||||
tokio::spawn(stream(socket));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_start() {
|
|
||||||
loop {
|
|
||||||
std::thread::sleep(Duration::from_secs(1));
|
|
||||||
*START_INDEX.lock().unwrap() = QUEUE.read().unwrap().len() - 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stream(mut s: std::net::TcpStream) {
|
|
||||||
println!("Playing for {}", s.peer_addr().unwrap());
|
|
||||||
let md = lonelyradio_types::Message::T(TrackMetadata {
|
|
||||||
cover: None,
|
|
||||||
encoder: lonelyradio_types::Encoder::Pcm,
|
|
||||||
track_length_secs: 0,
|
|
||||||
track_length_frac: 0.0,
|
|
||||||
channels: 1,
|
|
||||||
sample_rate: 44100,
|
|
||||||
title: "microserve instance".to_string(),
|
|
||||||
album: "".to_string(),
|
|
||||||
artist: "".to_string(),
|
|
||||||
});
|
|
||||||
s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).unwrap();
|
|
||||||
let mut ind = *START_INDEX.lock().unwrap();
|
|
||||||
dbg!(ind);
|
|
||||||
loop {
|
|
||||||
let front = QUEUE.read().unwrap()[ind].clone();
|
|
||||||
ind += 1;
|
|
||||||
let md = lonelyradio_types::Message::F(FragmentMetadata {
|
|
||||||
length: front.len() as u64,
|
|
||||||
});
|
|
||||||
s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).unwrap();
|
|
||||||
|
|
||||||
if s.write_all(unsafe { front.as_slice().align_to::<u8>().1 }).is_err() {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
while ind >= QUEUE.read().unwrap().len() - 5 {
|
|
||||||
std::thread::sleep(Duration::from_millis(100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn listen_mic() {
|
|
||||||
let host = cpal::default_host();
|
|
||||||
|
|
||||||
let device = host.default_input_device().unwrap();
|
|
||||||
let config = device.default_input_config().unwrap();
|
|
||||||
let stream = match config.sample_format() {
|
|
||||||
cpal::SampleFormat::F32 => device.build_input_stream(
|
|
||||||
&config.into(),
|
|
||||||
move |data: &[f32], _: &_| {
|
|
||||||
let samples = data.iter().map(|x| (*x * 32767.0) as i16).collect();
|
|
||||||
QUEUE.write().unwrap().push_back(samples);
|
|
||||||
},
|
|
||||||
|e| eprintln!("Error while reading: {}", e),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
_ => {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.unwrap();
|
|
||||||
loop {
|
|
||||||
stream.play().unwrap();
|
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +1,22 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monoclient-s"
|
name = "monoclient-s"
|
||||||
description = "Client for lonelyradio built with Slint"
|
description = "Client for lonelyradio built with Slint"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
slint = { version = "1.6", features = ["backend-android-activity-06"] }
|
slint = { version = "1.8" }
|
||||||
monolib = { path = "../monolib", version = "0.6.0" }
|
monolib = { path = "../monolib", version = "0.7.1" }
|
||||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
zune-jpeg = "0.4.13"
|
||||||
zune-jpeg = "0.4.11"
|
|
||||||
|
|
||||||
[lib]
|
[build-dependencies]
|
||||||
crate-type = [ "cdylib" ]
|
slint-build = "1.8"
|
||||||
|
|
||||||
[package.metadata.bundle]
|
[package.metadata.bundle]
|
||||||
name = "monoclient-s"
|
name = "monoclient-s"
|
||||||
identifier = "dev.ivabus.monoclient-s"
|
identifier = "dev.ivabus.monoclient-s"
|
||||||
icon = ["lonelyradio.png", "lonelyradio.icns"]
|
icon = ["lonelyradio.png", "lonelyradio.icns"]
|
||||||
version = "0.5.0"
|
version = "0.7.1"
|
||||||
copyright = "Copyright (c) 2024 Ivan Bushchik."
|
copyright = "Copyright (c) 2024 Ivan Bushchik."
|
||||||
category = "Music"
|
category = "Music"
|
||||||
|
|
3
monoclient-s/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
slint_build::compile("ui/ui.slint").unwrap();
|
||||||
|
}
|
|
@ -1,227 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use monolib::State;
|
|
||||||
use slint::{Image, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, Weak};
|
|
||||||
|
|
||||||
slint::slint! {
|
|
||||||
import { AboutSlint, 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 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");
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_playback(window_weak: Weak<MainWindow>) {
|
|
||||||
let window = window_weak.upgrade().unwrap();
|
|
||||||
let addr = window.get_addr().to_string();
|
|
||||||
let handle = std::thread::spawn(move || {
|
|
||||||
monolib::run(
|
|
||||||
&addr,
|
|
||||||
lonelyradio_types::Settings {
|
|
||||||
encoder: lonelyradio_types::Encoder::Flac,
|
|
||||||
cover: 512,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
std::thread::sleep(Duration::from_millis(166));
|
|
||||||
if handle.is_finished() {
|
|
||||||
window.set_playing(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.set_playing(true);
|
|
||||||
window.set_paused(false);
|
|
||||||
while monolib::get_metadata().is_none() {}
|
|
||||||
monolib::set_volume(window.get_volume() as u8);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn _main() {
|
|
||||||
let window = MainWindow::new().unwrap();
|
|
||||||
|
|
||||||
let window_weak = window.as_weak();
|
|
||||||
window.on_text_edited(move || {
|
|
||||||
let window = window_weak.upgrade().unwrap();
|
|
||||||
let addr = window.get_addr().to_string();
|
|
||||||
window.set_start_enabled(addr.contains(':'));
|
|
||||||
});
|
|
||||||
|
|
||||||
let window_weak = window.as_weak();
|
|
||||||
window.on_play(move || match monolib::get_state() {
|
|
||||||
State::NotStarted => start_playback(window_weak.clone()),
|
|
||||||
State::Paused => {
|
|
||||||
let window = window_weak.upgrade().unwrap();
|
|
||||||
window.set_paused(false);
|
|
||||||
monolib::toggle();
|
|
||||||
}
|
|
||||||
State::Resetting => {}
|
|
||||||
State::Playing => {
|
|
||||||
let window = window_weak.upgrade().unwrap();
|
|
||||||
window.set_paused(true);
|
|
||||||
monolib::toggle()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let window_weak = window.as_weak();
|
|
||||||
window.on_next(move || {
|
|
||||||
monolib::stop();
|
|
||||||
start_playback(window_weak.clone())
|
|
||||||
});
|
|
||||||
let window_weak = window.as_weak();
|
|
||||||
window.on_stop(move || {
|
|
||||||
let window = window_weak.upgrade().unwrap();
|
|
||||||
window.set_playing(false);
|
|
||||||
window.set_martist("".into());
|
|
||||||
window.set_malbum("".into());
|
|
||||||
window.set_mtitle("".into());
|
|
||||||
window.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
|
||||||
monolib::stop();
|
|
||||||
});
|
|
||||||
window.on_change_volume(move |vol| monolib::set_volume(vol as u8));
|
|
||||||
let window_weak = window.as_weak();
|
|
||||||
std::thread::spawn(move || loop {
|
|
||||||
let window = window_weak.clone();
|
|
||||||
while monolib::get_metadata().is_none() {
|
|
||||||
std::thread::sleep(Duration::from_millis(25))
|
|
||||||
}
|
|
||||||
let md = monolib::get_metadata().unwrap();
|
|
||||||
let _md = md.clone();
|
|
||||||
if let Some(jpeg) = md.cover {
|
|
||||||
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg);
|
|
||||||
decoder.decode_headers().unwrap();
|
|
||||||
let (w, h) = decoder.dimensions().unwrap();
|
|
||||||
let decoded = decoder.decode().unwrap();
|
|
||||||
let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(w as u32, h as u32);
|
|
||||||
pixel_buffer.make_mut_bytes().copy_from_slice(&decoded);
|
|
||||||
window
|
|
||||||
.upgrade_in_event_loop(|win| {
|
|
||||||
let image = Image::from_rgb8(pixel_buffer);
|
|
||||||
win.set_cover(image);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
window
|
|
||||||
.upgrade_in_event_loop(|win| {
|
|
||||||
win.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
slint::invoke_from_event_loop(move || {
|
|
||||||
let window = window.unwrap();
|
|
||||||
window.set_martist(md.artist.clone().into());
|
|
||||||
window.set_malbum(md.album.clone().into());
|
|
||||||
window.set_mtitle(md.title.clone().into());
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
while monolib::get_metadata() == Some(_md.clone()) {
|
|
||||||
std::thread::sleep(Duration::from_millis(100))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.run().unwrap();
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
mod app;
|
|
||||||
#[cfg(target_os = "andoid")]
|
|
||||||
#[no_mangle]
|
|
||||||
fn android_main(app: slint::android::AndroidApp) {
|
|
||||||
slint::android::init(app).unwrap();
|
|
||||||
app::_main();
|
|
||||||
}
|
|
|
@ -1,4 +1,148 @@
|
||||||
mod app;
|
use std::time::Duration;
|
||||||
fn main() {
|
|
||||||
app::_main()
|
use monolib::lonelyradio_types;
|
||||||
|
use monolib::State;
|
||||||
|
use slint::{
|
||||||
|
Image, ModelRc, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, SharedString, VecModel, Weak,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
cover: 2048,
|
||||||
|
},
|
||||||
|
if playlist == "All tracks" {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
&playlist
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
std::thread::sleep(Duration::from_millis(166));
|
||||||
|
if handle.is_finished() {
|
||||||
|
window.set_playing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.set_playing(true);
|
||||||
|
window.set_paused(false);
|
||||||
|
while monolib::get_metadata().is_none() {}
|
||||||
|
monolib::set_volume(window.get_volume() as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let window = MainWindow::new().unwrap();
|
||||||
|
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
window.on_text_edited(move || {
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
let addr = window.get_addr().to_string();
|
||||||
|
if addr.contains(':') {
|
||||||
|
window.set_start_enabled(true);
|
||||||
|
} else {
|
||||||
|
window.set_start_enabled(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
window.on_play(move || {
|
||||||
|
match monolib::get_state() {
|
||||||
|
State::NotStarted => start_playback(window_weak.clone()),
|
||||||
|
State::Paused => {
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
window.set_paused(false);
|
||||||
|
monolib::toggle();
|
||||||
|
}
|
||||||
|
State::Resetting => {}
|
||||||
|
State::Playing => {
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
window.set_paused(true);
|
||||||
|
monolib::toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
|
||||||
|
let playlists = match monolib::list_playlists(&window.get_addr()) {
|
||||||
|
Some(v) => [vec!["All tracks".to_string()], v].concat(),
|
||||||
|
None => vec!["All tracks".to_string()],
|
||||||
|
};
|
||||||
|
window.set_playlists(ModelRc::new(VecModel::from(
|
||||||
|
playlists.iter().map(SharedString::from).collect::<Vec<_>>(),
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
start_playback(window_weak.clone())
|
||||||
|
});
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
window.on_stop(move || {
|
||||||
|
let window = window_weak.upgrade().unwrap();
|
||||||
|
window.set_playing(false);
|
||||||
|
window.set_martist("".into());
|
||||||
|
window.set_malbum("".into());
|
||||||
|
window.set_mtitle("".into());
|
||||||
|
window.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
||||||
|
monolib::stop();
|
||||||
|
});
|
||||||
|
window.on_change_volume(move |vol| monolib::set_volume(vol as u8));
|
||||||
|
|
||||||
|
let window_weak = window.as_weak();
|
||||||
|
std::thread::spawn(move || loop {
|
||||||
|
let window = window_weak.clone();
|
||||||
|
while monolib::get_metadata().is_none() {
|
||||||
|
std::thread::sleep(Duration::from_millis(25))
|
||||||
|
}
|
||||||
|
let md = monolib::get_metadata().unwrap();
|
||||||
|
let _md = md.clone();
|
||||||
|
if let Some(jpeg) = md.cover {
|
||||||
|
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg);
|
||||||
|
decoder.decode_headers().unwrap();
|
||||||
|
let (w, h) = decoder.dimensions().unwrap();
|
||||||
|
let decoded = decoder.decode().unwrap();
|
||||||
|
let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(w as u32, h as u32);
|
||||||
|
pixel_buffer.make_mut_bytes().copy_from_slice(&decoded);
|
||||||
|
window
|
||||||
|
.upgrade_in_event_loop(|win| {
|
||||||
|
let image = Image::from_rgb8(pixel_buffer);
|
||||||
|
win.set_cover(image);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
window
|
||||||
|
.upgrade_in_event_loop(|win| {
|
||||||
|
win.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
slint::invoke_from_event_loop(move || {
|
||||||
|
let window = window.unwrap();
|
||||||
|
window.set_martist(md.artist.clone().into());
|
||||||
|
window.set_malbum(md.album.clone().into());
|
||||||
|
window.set_mtitle(md.title.clone().into());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
while monolib::get_metadata() == Some(_md.clone()) {
|
||||||
|
std::thread::sleep(Duration::from_millis(100))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.run().unwrap();
|
||||||
}
|
}
|
||||||
|
|
20
monoclient-s/ui/icons/LICENSE
Normal 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.
|
5
monoclient-s/ui/icons/README.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Monoclient Icons Set
|
||||||
|
|
||||||
|
Copyright (c) 2024 Ivan Bushchik
|
||||||
|
|
||||||
|
License: [MIT](./LICENSE)
|
69
monoclient-s/ui/icons/eject.svg
Normal 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 |
89
monoclient-s/ui/icons/first.svg
Normal 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 |
60
monoclient-s/ui/icons/gear.svg
Normal 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 |
89
monoclient-s/ui/icons/last.svg
Normal 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 |
70
monoclient-s/ui/icons/next.svg
Normal 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 |
60
monoclient-s/ui/icons/pause.svg
Normal 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 |
58
monoclient-s/ui/icons/play.svg
Normal 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 |
70
monoclient-s/ui/icons/previous.svg
Normal 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 |
110
monoclient-s/ui/icons/random.svg
Normal 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 |
96
monoclient-s/ui/icons/repeat-one.svg
Normal 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 |
73
monoclient-s/ui/icons/repeat.svg
Normal 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 |
51
monoclient-s/ui/icons/stop.svg
Normal 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
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -352,12 +352,12 @@
|
||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
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 = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = "";
|
LIBRARY_SEARCH_PATHS = "";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 0.6.0;
|
MARKETING_VERSION = 0.7.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
@ -405,12 +405,12 @@
|
||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
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 = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
|
LIBRARY_SEARCH_PATHS = "${PROJECT_DIR/../target/aarch64-apple-darwin/release}";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 0.6.0;
|
MARKETING_VERSION = 0.7.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.ivabus.monoclient-x";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
|
@ -47,7 +47,7 @@ struct Cover {
|
||||||
self.cover = PlatformImage(cgImage: CGImage(jpegDataProviderSource: CGDataProvider(data: data)!, decode: nil, shouldInterpolate: false, intent: CGColorRenderingIntent.absoluteColorimetric)!).preparingForDisplay()!
|
self.cover = PlatformImage(cgImage: CGImage(jpegDataProviderSource: CGDataProvider(data: data)!, decode: nil, shouldInterpolate: false, intent: CGColorRenderingIntent.absoluteColorimetric)!).preparingForDisplay()!
|
||||||
#endif
|
#endif
|
||||||
// deallocating memory
|
// deallocating memory
|
||||||
c_drop(cov.bytes, Int(cov.length))
|
c_drop(cov.bytes, UInt(Int(cov.length)))
|
||||||
print(self.cover.size)
|
print(self.cover.size)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,23 +5,23 @@
|
||||||
// Created by ivabus on 13.06.2024.
|
// Created by ivabus on 13.06.2024.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import AVFAudio
|
import AVFAudio
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import MonoLib
|
import MonoLib
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
enum PlayerState {
|
enum PlayerState {
|
||||||
case NotStarted
|
case NotStarted
|
||||||
case Playing
|
case Playing
|
||||||
case Paused
|
case Paused
|
||||||
|
|
||||||
mutating func update() {
|
mutating func update() {
|
||||||
self = switch c_get_state() {
|
self =
|
||||||
case 2: PlayerState.Playing
|
switch c_get_state() {
|
||||||
case 3: PlayerState.Paused
|
case 2: PlayerState.Playing
|
||||||
default: PlayerState.NotStarted
|
case 3: PlayerState.Paused
|
||||||
}
|
default: PlayerState.NotStarted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,12 @@ enum EncoderType: UInt8 {
|
||||||
case PCM16 = 0
|
case PCM16 = 0
|
||||||
case PCMFloat = 1
|
case PCMFloat = 1
|
||||||
case FLAC = 2
|
case FLAC = 2
|
||||||
|
case Alac = 3
|
||||||
|
//WavPack = 4,
|
||||||
|
//Opus = 5,
|
||||||
|
//Aac = 6,
|
||||||
|
case Vorbis = 7
|
||||||
|
case Sea = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CoverSize: Int32 {
|
enum CoverSize: Int32 {
|
||||||
|
@ -40,110 +46,173 @@ enum CoverSize: Int32 {
|
||||||
case NoCover = -1
|
case NoCover = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PlayList: Identifiable, Hashable {
|
||||||
|
var id: Int
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
}
|
||||||
|
|
||||||
struct Settings {
|
struct Settings {
|
||||||
var encoder: EncoderType = EncoderType.FLAC
|
var encoder: EncoderType = EncoderType.FLAC
|
||||||
var cover_size: CoverSize = CoverSize.High/*
|
var cover_size: CoverSize = CoverSize
|
||||||
init(enc: EncoderType, cov: CoverSize) {
|
.High /*
|
||||||
encoder = enc
|
init(enc: EncoderType, cov: CoverSize) {
|
||||||
cover_size = cov
|
encoder = enc
|
||||||
}*/
|
cover_size = cov
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
typealias MyStack = HStack
|
||||||
|
#else
|
||||||
|
typealias MyStack = VStack
|
||||||
|
#endif
|
||||||
|
|
||||||
struct Player: View {
|
struct Player: View {
|
||||||
|
|
||||||
let timer_state = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
|
let timer_state = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
|
||||||
let timer_meta = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
|
let timer_playlists = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
||||||
@State var metadata: Metadata = Metadata(title: "", album: "", artist: "")
|
@State var metadata: Metadata = Metadata(title: "", album: "", artist: "")
|
||||||
@State var prev_meta: Metadata = Metadata(title: "", album: "", artist: "")
|
@State var prev_meta: Metadata = Metadata(title: "", album: "", artist: "")
|
||||||
@State var cover: Cover = Cover(cover: PlatformImage())
|
@State var cover: Cover = Cover(cover: PlatformImage())
|
||||||
@State var state: PlayerState = PlayerState.NotStarted
|
@State var state: PlayerState = PlayerState.NotStarted
|
||||||
@State var settings: Settings = Settings.init()
|
@State var settings: Settings = Settings.init()
|
||||||
|
@State var playlists: [PlayList] = [PlayList(id: 0, name: "All tracks")]
|
||||||
|
@State var playlist: PlayList = PlayList(id: 0, name: "All tracks")
|
||||||
@AppStorage("ContentView.server") var server: String = ""
|
@AppStorage("ContentView.server") var server: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
VStack(alignment: .center) {
|
MyStack(alignment: .center) {
|
||||||
#if os(macOS)
|
VStack(alignment: .center) {
|
||||||
Image(nsImage: cover.cover)
|
|
||||||
.resizable()
|
#if os(macOS)
|
||||||
.aspectRatio(contentMode: .fit)
|
Image(nsImage: cover.cover)
|
||||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
.resizable()
|
||||||
.frame(width: 256.0, height: 256.0)
|
.aspectRatio(contentMode: .fit)
|
||||||
.clipShape(.rect(cornerRadius: 24))
|
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||||
.shadow(radius: 16)
|
.frame(width: 256.0, height: 256.0)
|
||||||
.padding(16)
|
.clipShape(.rect(cornerRadius: 24))
|
||||||
#else
|
.shadow(radius: 16)
|
||||||
Image(uiImage: cover.cover)
|
.padding(16)
|
||||||
.resizable()
|
#else
|
||||||
.aspectRatio(contentMode: .fit)
|
Image(uiImage: cover.cover)
|
||||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
.resizable()
|
||||||
.frame(width: 256.0, height: 256.0)
|
.aspectRatio(contentMode: .fit)
|
||||||
.clipShape(.rect(cornerRadius: 24))
|
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||||
.shadow(radius: 16)
|
.frame(width: 256.0, height: 256.0)
|
||||||
.padding(16)
|
.clipShape(.rect(cornerRadius: 24))
|
||||||
#endif
|
.shadow(radius: 16)
|
||||||
|
.padding(16)
|
||||||
VStack(alignment: .center){
|
#endif
|
||||||
Text(metadata.title).bold()
|
|
||||||
|
VStack(alignment: .center) {
|
||||||
Text(metadata.album)
|
Text(metadata.title).bold()
|
||||||
|
|
||||||
Text(metadata.artist)
|
Text(metadata.album)
|
||||||
}.frame(minHeight: 64)
|
|
||||||
|
Text(metadata.artist)
|
||||||
TextField(
|
}.frame(minHeight: 64).onReceive(timer_state) { _ in
|
||||||
"Server",
|
metadata.update()
|
||||||
text: $server,
|
if prev_meta != metadata {
|
||||||
onCommit: {
|
prev_meta = metadata
|
||||||
#if os(macOS)
|
cover.update()
|
||||||
DispatchQueue.main.async {
|
|
||||||
NSApp.keyWindow?.makeFirstResponder(nil)
|
|
||||||
}
|
}
|
||||||
#endif
|
let image = cover.cover
|
||||||
|
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) {
|
||||||
|
(size: CGSize) -> PlatformImage in
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
MPNowPlayingInfoCenter.default().playbackState =
|
||||||
|
state == PlayerState.Playing ? .playing : .paused
|
||||||
|
#endif
|
||||||
|
|
||||||
|
let nowPlayingInfo: [String: Any] = [
|
||||||
|
MPMediaItemPropertyArtist: metadata.artist,
|
||||||
|
MPMediaItemPropertyAlbumTitle: metadata.album,
|
||||||
|
MPMediaItemPropertyTitle: metadata.title,
|
||||||
|
MPMediaItemPropertyArtwork: mediaArtwork,
|
||||||
|
MPNowPlayingInfoPropertyIsLiveStream: true,
|
||||||
|
MPMediaItemPropertyPlaybackDuration: c_get_metadata_length(),
|
||||||
|
|
||||||
|
]
|
||||||
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.disableAutocorrection(true)
|
TextField(
|
||||||
.frame(width: 256)
|
"Server",
|
||||||
.textFieldStyle(.roundedBorder)
|
text: $server,
|
||||||
.padding(16)
|
onCommit: {
|
||||||
.multilineTextAlignment(.center)
|
#if os(macOS)
|
||||||
|
DispatchQueue.main.async {
|
||||||
HStack(spacing: 8) {
|
NSApp.keyWindow?.makeFirstResponder(nil)
|
||||||
Button(action: stop){
|
}
|
||||||
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
#endif
|
||||||
}
|
}
|
||||||
.disabled(state == PlayerState.NotStarted)
|
)
|
||||||
.buttonStyle(.bordered)
|
.disableAutocorrection(true)
|
||||||
.font(.system(size: 20))
|
.frame(width: 256)
|
||||||
.buttonBorderShape(.capsule)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.padding(16)
|
||||||
Button(action: play){
|
.multilineTextAlignment(.center)
|
||||||
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
|
|
||||||
.font(.system(size: 30))
|
HStack(spacing: 8) {
|
||||||
.padding(4)
|
Button(action: stop) {
|
||||||
}
|
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
||||||
.buttonStyle(.borderedProminent)
|
}
|
||||||
.buttonBorderShape(.capsule)
|
.disabled(state == PlayerState.NotStarted)
|
||||||
|
|
||||||
Button(action: next){
|
|
||||||
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
|
|
||||||
}.disabled(state == PlayerState.NotStarted)
|
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
.buttonBorderShape(.capsule)
|
.buttonBorderShape(.capsule)
|
||||||
}
|
|
||||||
Menu {
|
Button(action: play) {
|
||||||
Picker("Encoder", selection: $settings.encoder) {
|
Image(
|
||||||
Text("PCM (s16)")
|
systemName: state == PlayerState.NotStarted
|
||||||
.tag(EncoderType.PCM16)
|
? "infinity.circle"
|
||||||
Text("PCM (f32)")
|
: (state == PlayerState.Playing)
|
||||||
.tag(EncoderType.PCMFloat)
|
? "pause.circle.fill" : "play.circle"
|
||||||
Text("FLAC (s24)")
|
)
|
||||||
.tag(EncoderType.FLAC)
|
.font(.system(size: 30))
|
||||||
}.pickerStyle(.menu)
|
.padding(4)
|
||||||
|
}
|
||||||
Picker("Cover size", selection: $settings.cover_size) {
|
.buttonStyle(.borderedProminent)
|
||||||
|
.buttonBorderShape(.capsule)
|
||||||
|
|
||||||
|
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))
|
||||||
|
.buttonBorderShape(.capsule)
|
||||||
|
}.onReceive(timer_state) { _ in
|
||||||
|
state.update()
|
||||||
|
}
|
||||||
|
Menu {
|
||||||
|
Picker("Playlist", selection: $playlist) {
|
||||||
|
ForEach($playlists) { pl in
|
||||||
|
Text(pl.wrappedValue.name).tag(pl.wrappedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
}.pickerStyle(.menu)
|
||||||
|
|
||||||
|
Picker("Encoder", selection: $settings.encoder) {
|
||||||
|
Text("PCM (s16)")
|
||||||
|
.tag(EncoderType.PCM16)
|
||||||
|
Text("PCM (f32)")
|
||||||
|
.tag(EncoderType.PCMFloat)
|
||||||
|
Text("FLAC (s24)")
|
||||||
|
.tag(EncoderType.FLAC)
|
||||||
|
Text("ALAC (s16)")
|
||||||
|
.tag(EncoderType.Alac)
|
||||||
|
Text("Vorbis (lossy)")
|
||||||
|
.tag(EncoderType.Vorbis)
|
||||||
|
Text("Sea (lossy)")
|
||||||
|
.tag(EncoderType.Sea)
|
||||||
|
}.pickerStyle(.menu)
|
||||||
|
|
||||||
|
Picker("Cover size", selection: $settings.cover_size) {
|
||||||
Text("Original")
|
Text("Original")
|
||||||
.tag(CoverSize.Full)
|
.tag(CoverSize.Full)
|
||||||
Text("High (768)")
|
Text("High (768)")
|
||||||
|
@ -157,106 +226,94 @@ struct Player: View {
|
||||||
Text("No cover")
|
Text("No cover")
|
||||||
.tag(CoverSize.NoCover)
|
.tag(CoverSize.NoCover)
|
||||||
}.pickerStyle(.menu)
|
}.pickerStyle(.menu)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Settings", systemImage: "gearshape")
|
Label("Settings", systemImage: "gearshape")
|
||||||
.padding(16)
|
.padding(16)
|
||||||
}.frame(maxWidth: 128)
|
}.frame(maxWidth: 128)
|
||||||
}
|
|
||||||
.padding(32)
|
|
||||||
.onReceive(timer_state) { _ in
|
|
||||||
state.update()
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
|
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
|
||||||
.onReceive(timer_meta) { _ in
|
|
||||||
metadata.update()
|
|
||||||
if prev_meta != metadata || metadata.album == "" || cover.cover == PlatformImage() {
|
|
||||||
prev_meta = metadata
|
|
||||||
cover.update()
|
|
||||||
}
|
}
|
||||||
let image = cover.cover
|
|
||||||
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) { (size: CGSize) -> PlatformImage in
|
.padding(32)
|
||||||
return image
|
.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 })
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
let nowPlayingInfo: [String: Any] = [
|
#if os(iOS)
|
||||||
MPMediaItemPropertyArtist: metadata.artist,
|
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||||
MPMediaItemPropertyAlbumTitle: metadata.album,
|
#endif
|
||||||
MPMediaItemPropertyTitle: metadata.title,
|
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
||||||
MPMediaItemPropertyArtwork: mediaArtwork,
|
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
||||||
MPNowPlayingInfoPropertyIsLiveStream: true,
|
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
||||||
MPMediaItemPropertyPlaybackDuration: c_get_metadata_length(),
|
MPRemoteCommandCenter.shared().skipBackwardCommand.isEnabled = false
|
||||||
|
MPRemoteCommandCenter.shared().pauseCommand.addTarget(handler: { _ in
|
||||||
]
|
if state != PlayerState.Paused {
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
play()
|
||||||
|
}
|
||||||
}
|
return MPRemoteCommandHandlerStatus.success
|
||||||
.onAppear() {
|
})
|
||||||
#if os(iOS)
|
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { _ in
|
||||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
if state != PlayerState.Playing {
|
||||||
#endif
|
play()
|
||||||
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
}
|
||||||
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
return MPRemoteCommandHandlerStatus.success
|
||||||
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
})
|
||||||
MPRemoteCommandCenter.shared().skipBackwardCommand.isEnabled = false
|
|
||||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget(handler: { _ in
|
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: { _ in
|
||||||
if state != PlayerState.Paused {
|
|
||||||
play()
|
play()
|
||||||
}
|
return MPRemoteCommandHandlerStatus.success
|
||||||
return MPRemoteCommandHandlerStatus.success
|
})
|
||||||
})
|
|
||||||
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { _ in
|
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: { _ in
|
||||||
if state != PlayerState.Playing {
|
next()
|
||||||
play()
|
return MPRemoteCommandHandlerStatus.success
|
||||||
}
|
})
|
||||||
return MPRemoteCommandHandlerStatus.success
|
|
||||||
})
|
}
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
|
|
||||||
play()
|
|
||||||
return MPRemoteCommandHandlerStatus.success
|
|
||||||
})
|
|
||||||
|
|
||||||
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
|
|
||||||
next()
|
|
||||||
return MPRemoteCommandHandlerStatus.success
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.animation(.spring, value: UUID())
|
.animation(.spring, value: UUID())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
switch state {
|
switch state {
|
||||||
case PlayerState.NotStarted: do {
|
case PlayerState.NotStarted:
|
||||||
#if os(iOS)
|
|
||||||
let audioSession = AVAudioSession.sharedInstance()
|
|
||||||
do {
|
do {
|
||||||
try audioSession.setCategory(
|
#if os(macOS)
|
||||||
.playback, mode: .default)
|
#else
|
||||||
try audioSession.setActive(true)
|
let audioSession = AVAudioSession.sharedInstance()
|
||||||
|
do {
|
||||||
} catch {
|
try audioSession.setCategory(
|
||||||
print("Failed to set the audio session configuration")
|
.playback, mode: .default)
|
||||||
|
try audioSession.setActive(true)
|
||||||
|
|
||||||
|
} 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
|
default:
|
||||||
Thread.detachNewThread {
|
do {
|
||||||
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue))
|
c_toggle()
|
||||||
|
state.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: do {
|
|
||||||
c_toggle()
|
|
||||||
state.update()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
func stop() {
|
func stop() {
|
||||||
c_stop()
|
c_stop()
|
||||||
|
|
|
@ -25,11 +25,12 @@ struct monoclient_xApp: App {
|
||||||
CommandGroup(replacing: CommandGroupPlacement.newItem) {
|
CommandGroup(replacing: CommandGroupPlacement.newItem) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.defaultSize(width: 256, height: 512)
|
||||||
#else
|
#else
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
}
|
}
|
||||||
.defaultSize(width: 256, height: 512)
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monoclient"
|
name = "monoclient"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
repository = "https://github.com/ivabus/lonelyradio"
|
repository = "https://github.com/ivabus/lonelyradio"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
monolib = { version = "0.6.0", path = "../monolib" }
|
monolib = { version = "0.7.1", path = "../monolib" }
|
||||||
clap = { version = "4.4.18", features = ["derive"] }
|
clap = { version = "4.5.16", features = ["derive"] }
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.28.1"
|
||||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crossterm::cursor::MoveToColumn;
|
||||||
use crossterm::event::{poll, read, Event};
|
use crossterm::event::{poll, read, Event};
|
||||||
use crossterm::style::Print;
|
use crossterm::style::Print;
|
||||||
use crossterm::terminal::{Clear, ClearType};
|
use crossterm::terminal::{Clear, ClearType};
|
||||||
use lonelyradio_types::{Encoder, Settings};
|
use monolib::lonelyradio_types::{Encoder, Settings};
|
||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
@ -17,6 +17,12 @@ struct Args {
|
||||||
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
|
||||||
|
#[arg(short, long, default_value = "")]
|
||||||
|
playlist: String,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const HELP: &str = r#"Keybinds:
|
const HELP: &str = r#"Keybinds:
|
||||||
|
@ -37,17 +43,29 @@ macro_rules! verbose {
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
VERBOSE.set(args.verbose).unwrap();
|
VERBOSE.set(args.verbose).unwrap();
|
||||||
|
if args.list {
|
||||||
|
println!(
|
||||||
|
"Available playlists: {}",
|
||||||
|
match monolib::list_playlists(&args.address) {
|
||||||
|
Some(s) => format!("{:?}", s),
|
||||||
|
None => String::from("None"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
monolib::run(
|
monolib::run(
|
||||||
&args.address,
|
&args.address,
|
||||||
Settings {
|
Settings {
|
||||||
encoder: Encoder::PcmFloat,
|
encoder: Encoder::Sea,
|
||||||
cover: -1,
|
cover: -1,
|
||||||
},
|
},
|
||||||
|
&args.playlist,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
while monolib::get_metadata().is_none() {}
|
while monolib::get_metadata().is_none() {}
|
||||||
let mut md = monolib::get_metadata().unwrap();
|
let mut md = monolib::get_metadata().unwrap();
|
||||||
|
let mut next_md = md.clone();
|
||||||
verbose!("md: {:?}", md);
|
verbose!("md: {:?}", md);
|
||||||
let mut track_start = Instant::now();
|
let mut track_start = Instant::now();
|
||||||
let mut seconds_past = 0;
|
let mut seconds_past = 0;
|
||||||
|
@ -63,7 +81,6 @@ fn main() {
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut next_md = md.clone();
|
|
||||||
crossterm::terminal::enable_raw_mode().unwrap();
|
crossterm::terminal::enable_raw_mode().unwrap();
|
||||||
loop {
|
loop {
|
||||||
if let Ok(true) = poll(std::time::Duration::from_micros(1)) {
|
if let Ok(true) = poll(std::time::Duration::from_micros(1)) {
|
||||||
|
@ -116,8 +133,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if monolib::get_metadata().unwrap() != md
|
if next_md != md
|
||||||
//&& track_length <= (Instant::now() - track_start).as_secs_f64()
|
&& md.track_length_secs as f64 <= (Instant::now() - track_start).as_secs_f64()
|
||||||
{
|
{
|
||||||
md = next_md.clone();
|
md = next_md.clone();
|
||||||
verbose!("md: {:?}", md);
|
verbose!("md: {:?}", md);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
version = "0.6.0"
|
version = "0.7.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
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"
|
repository = "https://github.com/ivabus/lonelyradio"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
|
|
||||||
|
@ -12,11 +12,27 @@ name = "monolib"
|
||||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rodio = { version = "0.17.3", default-features = false }
|
rodio = { version = "0.19.0", default-features = false }
|
||||||
byteorder = "1.5.0"
|
byteorder = "1.5.0"
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde = "1.1.2"
|
||||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
lonelyradio_types = { version = "0.7.0", path = "../lonelyradio_types" }
|
||||||
claxon = "0.4.3"
|
anyhow = "1.0.86"
|
||||||
|
|
||||||
|
# Optional decoders
|
||||||
|
claxon = { version = "0.4.3", optional = true }
|
||||||
|
symphonia-codec-alac = {version = "0.5.4", optional = true }
|
||||||
|
symphonia-core = {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", "sea"]
|
||||||
|
alac = ["dep:symphonia-codec-alac", "dep:symphonia-core"]
|
||||||
|
flac = ["dep:claxon"]
|
||||||
|
vorbis = ["dep:lewton"]
|
||||||
|
sea = ["dep:sea-codec"]
|
||||||
|
|
||||||
[package.metadata.xcframework]
|
[package.metadata.xcframework]
|
||||||
include-dir = "src"
|
include-dir = "src"
|
||||||
|
@ -24,4 +40,4 @@ lib-type = "cdylib"
|
||||||
zip = false
|
zip = false
|
||||||
macOS = true
|
macOS = true
|
||||||
iOS = true
|
iOS = true
|
||||||
simulators = true
|
simulators = false
|
||||||
|
|
|
@ -9,7 +9,8 @@ A library implementing the lonely radio audio streaming protocol
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- [CLI](../monoclient)
|
- [CLI](../monoclient)
|
||||||
- [SwiftUI](../platform/swiftui)
|
- [SwiftUI](../monoclient-x)
|
||||||
|
- [Slint](../monoclient-s)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -11,39 +11,53 @@ pub struct CTrackMetadata {
|
||||||
pub artist: *mut c_char,
|
pub artist: *mut c_char,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ENCODER_PCM16: u8 = 0;
|
|
||||||
pub const ENCODER_PCMFLOAT: u8 = 1;
|
|
||||||
pub const ENCODER_FLAC: u8 = 2;
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct CSettings {
|
pub struct CSettings {
|
||||||
/// See lonelyradio_types -> Encoder
|
/// See lonelyradio_types for numeric representation -> Encoder
|
||||||
pub encoder: u8,
|
pub encoder: u8,
|
||||||
pub cover: i32,
|
pub cover: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||||
pub extern "C" fn c_start(server: *const c_char, settings: CSettings) {
|
/// Starts audio playback using rodio
|
||||||
|
/// Play without playlist => playlist = ""
|
||||||
|
pub extern "C" fn c_start(server: *const c_char, settings: CSettings, playlist: *const c_char) {
|
||||||
let serv = unsafe { CStr::from_ptr(server) };
|
let serv = unsafe { CStr::from_ptr(server) };
|
||||||
|
let playlist = unsafe { CStr::from_ptr(playlist) };
|
||||||
run(
|
run(
|
||||||
match serv.to_str() {
|
serv.to_str().unwrap_or_default(),
|
||||||
Ok(s) => s,
|
|
||||||
_ => "",
|
|
||||||
},
|
|
||||||
Settings {
|
Settings {
|
||||||
encoder: match settings.encoder {
|
encoder: match settings.encoder {
|
||||||
0 => Encoder::Pcm16,
|
0 => Encoder::Pcm16,
|
||||||
1 => Encoder::PcmFloat,
|
1 => Encoder::PcmFloat,
|
||||||
2 => Encoder::Flac,
|
2 => Encoder::Flac,
|
||||||
|
3 => Encoder::Alac,
|
||||||
|
7 => Encoder::Vorbis,
|
||||||
|
8 => Encoder::Sea,
|
||||||
_ => return,
|
_ => return,
|
||||||
},
|
},
|
||||||
cover: settings.cover,
|
cover: settings.cover,
|
||||||
},
|
},
|
||||||
|
playlist.to_str().unwrap_or_default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||||
|
/// Playlists separated by '\n'
|
||||||
|
pub extern "C" fn c_list_playlists(server: *const c_char) -> *mut c_char {
|
||||||
|
let serv = unsafe { CStr::from_ptr(server) };
|
||||||
|
let playlists = list_playlists(serv.to_str().unwrap_or_default());
|
||||||
|
CString::new(match playlists {
|
||||||
|
None => "".to_string(),
|
||||||
|
Some(s) => s.join("\n"),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn c_toggle() {
|
pub extern "C" fn c_toggle() {
|
||||||
toggle()
|
toggle()
|
||||||
|
|
97
monolib/src/decode.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use lonelyradio_types::{Encoder, FragmentMetadata, TrackMetadata};
|
||||||
|
use symphonia_core::{
|
||||||
|
audio::SampleBuffer,
|
||||||
|
codecs::{Decoder, CODEC_TYPE_ALAC},
|
||||||
|
formats::Packet,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn decode(
|
||||||
|
mut stream: impl ReadBytesExt,
|
||||||
|
md: &TrackMetadata,
|
||||||
|
fmd: &FragmentMetadata,
|
||||||
|
) -> anyhow::Result<Vec<f32>> {
|
||||||
|
let mut samples = vec![];
|
||||||
|
match md.encoder {
|
||||||
|
Encoder::Pcm16 => {
|
||||||
|
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
||||||
|
stream.read_i16_into::<LittleEndian>(&mut samples_i16)?;
|
||||||
|
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];
|
||||||
|
stream.read_f32_into::<LittleEndian>(&mut samples_f32)?;
|
||||||
|
samples.append(&mut samples_f32);
|
||||||
|
}
|
||||||
|
Encoder::Flac => {
|
||||||
|
#[cfg(feature = "alac")]
|
||||||
|
{
|
||||||
|
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
||||||
|
let mut reader = claxon::FlacReader::new(take)?;
|
||||||
|
samples
|
||||||
|
.extend(&mut reader.samples().map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "flac"))]
|
||||||
|
{
|
||||||
|
unimplemented!("flac decoding is disabled in library")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Encoder::Alac => {
|
||||||
|
#[cfg(feature = "alac")]
|
||||||
|
{
|
||||||
|
let mut buf = vec![];
|
||||||
|
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
||||||
|
let mut reader = symphonia_codec_alac::AlacDecoder::try_new(
|
||||||
|
symphonia_core::codecs::CodecParameters::default()
|
||||||
|
.for_codec(CODEC_TYPE_ALAC)
|
||||||
|
.with_extra_data(fmd.magic_cookie.clone().unwrap().into_boxed_slice()),
|
||||||
|
&symphonia_core::codecs::DecoderOptions {
|
||||||
|
verify: false,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let decoded = reader.decode(&Packet::new_from_slice(0, 0, 0, &buf))?;
|
||||||
|
let mut byte_buf =
|
||||||
|
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||||
|
byte_buf.copy_interleaved_ref(decoded);
|
||||||
|
samples.extend(byte_buf.samples());
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alac"))]
|
||||||
|
{
|
||||||
|
unimplemented!("alac decoding is disabled in library")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Encoder::Vorbis => {
|
||||||
|
#[cfg(feature = "vorbis")]
|
||||||
|
{
|
||||||
|
let mut buf = vec![];
|
||||||
|
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
||||||
|
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"))]
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
}
|
|
@ -5,8 +5,9 @@
|
||||||
//! extern crate monolib;
|
//! extern crate monolib;
|
||||||
//! use std::thread::{sleep, spawn};
|
//! use std::thread::{sleep, spawn};
|
||||||
//! use std::time::Duration;
|
//! use std::time::Duration;
|
||||||
|
//! use monolib::lonelyradio_types::{Settings, Encoder};
|
||||||
//!
|
//!
|
||||||
//! spawn(|| monolib::run("someserver:someport"));
|
//! spawn(|| monolib::run("someserver:someport", Settings {encoder: Encoder::Flac, cover: -1}, "my_playlist"));
|
||||||
//! while monolib::get_metadata().is_none() {}
|
//! while monolib::get_metadata().is_none() {}
|
||||||
//! let seconds = md.length / md.sample_rate as u64 / 2;
|
//! let seconds = md.length / md.sample_rate as u64 / 2;
|
||||||
//! println!("Playing: {} - {} - {} ({}:{:02})", md.artist, md.album, md.title, seconds / 60, seconds % 60);
|
//! println!("Playing: {} - {} - {} ({}:{:02})", md.artist, md.album, md.title, seconds / 60, seconds % 60);
|
||||||
|
@ -17,19 +18,38 @@
|
||||||
/// Functions, providing C-like API
|
/// Functions, providing C-like API
|
||||||
pub mod c;
|
pub mod c;
|
||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
pub use lonelyradio_types;
|
||||||
use lonelyradio_types::{Encoder, Message, ServerCapabilities, Settings, TrackMetadata};
|
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
use decode::decode;
|
||||||
|
use lonelyradio_types::{
|
||||||
|
Encoder, PlayMessage, Request, RequestResult, ServerCapabilities, Settings, TrackMetadata,
|
||||||
|
};
|
||||||
use rodio::buffer::SamplesBuffer;
|
use rodio::buffer::SamplesBuffer;
|
||||||
use rodio::{OutputStream, Sink};
|
use rodio::{OutputStream, Sink};
|
||||||
use std::error::Error;
|
use std::io::Write;
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::sync::atomic::AtomicU8;
|
use std::sync::atomic::AtomicU8;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
mod decode;
|
||||||
|
|
||||||
const CACHE_SIZE_PCM: usize = 32;
|
const CACHE_SIZE_PCM: usize = 32;
|
||||||
const CACHE_SIZE_COMPRESSED: usize = 2;
|
const CACHE_SIZE_COMPRESSED: usize = 4;
|
||||||
|
|
||||||
|
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);
|
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
||||||
static VOLUME: AtomicU8 = AtomicU8::new(255);
|
static VOLUME: AtomicU8 = AtomicU8::new(255);
|
||||||
|
@ -144,139 +164,146 @@ pub fn set_volume(volume: u8) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Download track as samples
|
/// Download track as samples
|
||||||
pub fn get_track(server: &str, mut settings: Settings) -> Option<(TrackMetadata, Vec<i16>)> {
|
pub fn get_track(
|
||||||
let mut connection = unwrap(TcpStream::connect(server))?;
|
server: &str,
|
||||||
unwrap(connection.write_u64::<LittleEndian>(lonelyradio_types::HELLO_MAGIC))?;
|
mut settings: Settings,
|
||||||
let capabilities: ServerCapabilities = unwrap(rmp_serde::from_read(&mut connection))?;
|
playlist: &str,
|
||||||
|
) -> anyhow::Result<(TrackMetadata, Vec<f32>)> {
|
||||||
|
let mut connection = TcpStream::connect(server)?;
|
||||||
|
connection.write_all(lonelyradio_types::HELLO_MAGIC)?;
|
||||||
|
let capabilities: ServerCapabilities = rmp_serde::from_read(&mut connection)?;
|
||||||
if !capabilities.encoders.contains(&settings.encoder) {
|
if !capabilities.encoders.contains(&settings.encoder) {
|
||||||
settings.encoder = Encoder::Pcm16
|
settings.encoder = Encoder::Pcm16
|
||||||
}
|
}
|
||||||
unwrap(connection.write_all(&rmp_serde::to_vec_named(&settings).unwrap()))?;
|
|
||||||
|
|
||||||
let mut stream = connection;
|
let request = if playlist.is_empty() {
|
||||||
|
Request::Play(settings)
|
||||||
|
} else {
|
||||||
|
Request::PlayPlaylist(playlist.to_string(), settings)
|
||||||
|
};
|
||||||
|
connection.write_all(&rmp_serde::to_vec_named(&request).unwrap())?;
|
||||||
|
|
||||||
|
let response: RequestResult = rmp_serde::from_read(&connection)?;
|
||||||
|
if let RequestResult::Error(e) = response {
|
||||||
|
bail!("{e:?}")
|
||||||
|
}
|
||||||
|
|
||||||
let mut samples = vec![];
|
let mut samples = vec![];
|
||||||
let mut md: Option<TrackMetadata> = None;
|
let mut md: Option<TrackMetadata> = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
let recv_md: PlayMessage = rmp_serde::from_read(&mut connection)?;
|
||||||
match recv_md {
|
match recv_md {
|
||||||
Message::T(tmd) => {
|
PlayMessage::T(tmd) => {
|
||||||
if md.is_some() {
|
if md.is_some() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
md = Some(tmd);
|
md = Some(tmd);
|
||||||
}
|
}
|
||||||
Message::F(fmd) => match md.as_ref().unwrap().encoder {
|
PlayMessage::F(fmd) => {
|
||||||
Encoder::Pcm16 => {
|
samples.extend(decode(&mut connection, md.as_ref().unwrap(), &fmd)?)
|
||||||
let mut buf = vec![0; fmd.length as usize];
|
}
|
||||||
stream.read_i16_into::<LittleEndian>(&mut buf).unwrap();
|
|
||||||
samples.append(&mut buf);
|
|
||||||
}
|
|
||||||
Encoder::PcmFloat => unimplemented!(),
|
|
||||||
Encoder::Flac => {
|
|
||||||
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
|
||||||
let mut reader = claxon::FlacReader::new(take).unwrap();
|
|
||||||
samples.append(
|
|
||||||
&mut reader.samples().map(|x| x.unwrap_or(0) as i16).collect::<Vec<i16>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
md.map(|md| (md, samples))
|
|
||||||
|
if let Some(md) = md {
|
||||||
|
Ok((md, samples))
|
||||||
|
} else {
|
||||||
|
bail!("No metadata")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unwrap<T, E: Error>(thing: Result<T, E>) -> Option<T> {
|
pub fn list_playlists(server: &str) -> Option<Vec<String>> {
|
||||||
if thing.is_err() {
|
let mut connection = TcpStream::connect(server).ok()?;
|
||||||
*STATE.write().unwrap() = State::NotStarted;
|
connection.write_all(lonelyradio_types::HELLO_MAGIC).ok()?;
|
||||||
|
let _: ServerCapabilities = rmp_serde::from_read(&mut connection).ok()?;
|
||||||
|
connection.write_all(&rmp_serde::to_vec_named(&Request::ListPlaylist).ok()?).ok()?;
|
||||||
|
let res: RequestResult = rmp_serde::from_read(connection).ok()?;
|
||||||
|
match res {
|
||||||
|
RequestResult::Playlist(plist) => Some(plist.playlists),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
thing.ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts playing at "server:port"
|
/// Starts playing at "server:port"
|
||||||
pub fn run(server: &str, settings: Settings) {
|
pub fn run(server: &str, settings: Settings, playlist: &str) {
|
||||||
let _ = _run(server, settings);
|
let result = _run(server, settings, playlist);
|
||||||
|
if let Err(e) = result {
|
||||||
|
println!("{:?}", e);
|
||||||
|
*STATE.write().unwrap() = State::NotStarted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _run(server: &str, settings: Settings) -> Option<()> {
|
pub(crate) fn _run(server: &str, mut settings: Settings, playlist: &str) -> anyhow::Result<()> {
|
||||||
let mut settings = settings;
|
if !SUPPORTED_DECODERS.contains(&settings.encoder) {
|
||||||
|
eprintln!(
|
||||||
|
"monolib was built without support for {:?}, falling back to Pcm16",
|
||||||
|
settings.encoder
|
||||||
|
);
|
||||||
|
settings.encoder = Encoder::Pcm16
|
||||||
|
}
|
||||||
let mut state = STATE.write().unwrap();
|
let mut state = STATE.write().unwrap();
|
||||||
if *state == State::Playing || *state == State::Paused {
|
if *state == State::Playing || *state == State::Paused {
|
||||||
return None;
|
return Ok(());
|
||||||
}
|
}
|
||||||
*state = State::Playing;
|
*state = State::Playing;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
let mut connection = unwrap(TcpStream::connect(server))?;
|
let mut connection = TcpStream::connect(server).context("failed to connect to the server")?;
|
||||||
unwrap(connection.write_u64::<LittleEndian>(lonelyradio_types::HELLO_MAGIC))?;
|
connection.write_all(lonelyradio_types::HELLO_MAGIC)?;
|
||||||
let capabilities: ServerCapabilities = unwrap(rmp_serde::from_read(&mut connection))?;
|
let capabilities: ServerCapabilities = rmp_serde::from_read(&mut connection)?;
|
||||||
if !capabilities.encoders.contains(&settings.encoder) {
|
if !capabilities.encoders.contains(&settings.encoder) {
|
||||||
settings.encoder = Encoder::Pcm16
|
settings.encoder = Encoder::Pcm16
|
||||||
}
|
}
|
||||||
unwrap(connection.write_all(&rmp_serde::to_vec_named(&settings).unwrap()))?;
|
|
||||||
|
|
||||||
|
let request = if playlist.is_empty() {
|
||||||
|
Request::Play(settings)
|
||||||
|
} else {
|
||||||
|
Request::PlayPlaylist(playlist.to_string(), settings)
|
||||||
|
};
|
||||||
|
connection.write_all(&rmp_serde::to_vec_named(&request).unwrap())?;
|
||||||
|
|
||||||
|
let response: RequestResult = rmp_serde::from_read(&connection).unwrap();
|
||||||
|
if let RequestResult::Error(e) = response {
|
||||||
|
bail!("{:?}", e)
|
||||||
|
}
|
||||||
let mut stream = connection;
|
let mut stream = connection;
|
||||||
|
|
||||||
let mut sink = SINK.write().unwrap();
|
let mut sink = SINK.write().unwrap();
|
||||||
let (_stream, stream_handle) = unwrap(OutputStream::try_default())?;
|
let (_stream, stream_handle) =
|
||||||
|
OutputStream::try_default().context("failed to determine audio device")?;
|
||||||
|
|
||||||
// Can't reuse old sink for some reason
|
// Can't reuse old sink for some reason
|
||||||
let audio_sink = Sink::try_new(&stream_handle).unwrap();
|
let audio_sink = Sink::try_new(&stream_handle).context("failed to create audio sink")?;
|
||||||
*sink = Some(audio_sink);
|
*sink = Some(audio_sink);
|
||||||
drop(sink);
|
drop(sink);
|
||||||
|
|
||||||
let mut samples = Vec::with_capacity(8192);
|
let mut samples = Vec::with_capacity(8192);
|
||||||
loop {
|
loop {
|
||||||
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
let recv_md: PlayMessage =
|
||||||
|
rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
||||||
match recv_md {
|
match recv_md {
|
||||||
Message::T(tmd) => {
|
PlayMessage::T(tmd) => {
|
||||||
// No metadata shift
|
// No metadata shift
|
||||||
if watching_sleep_until_end() {
|
if watching_sleep_until_end() {
|
||||||
_stop();
|
_stop();
|
||||||
return None;
|
return Ok(());
|
||||||
}
|
}
|
||||||
let mut md = MD.write().unwrap();
|
let mut md = MD.write().unwrap();
|
||||||
*md = Some(tmd.clone());
|
*md = Some(tmd.clone());
|
||||||
|
|
||||||
drop(md);
|
drop(md);
|
||||||
}
|
}
|
||||||
Message::F(fmd) => {
|
PlayMessage::F(fmd) => {
|
||||||
while *STATE.read().unwrap() == State::Paused {
|
while *STATE.read().unwrap() == State::Paused {
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
||||||
}
|
}
|
||||||
if *STATE.read().unwrap() == State::Resetting {
|
if *STATE.read().unwrap() == State::Resetting {
|
||||||
_stop();
|
_stop();
|
||||||
return None;
|
return Ok(());
|
||||||
}
|
}
|
||||||
match MD.read().unwrap().as_ref().unwrap().encoder {
|
|
||||||
Encoder::Pcm16 => {
|
samples.extend(decode(&mut stream, &MD.read().unwrap().clone().unwrap(), &fmd)?);
|
||||||
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
|
||||||
if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
samples.append(
|
|
||||||
&mut samples_i16
|
|
||||||
.iter()
|
|
||||||
.map(|sample| *sample as f32 / 32767.0)
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Encoder::PcmFloat => {
|
|
||||||
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
|
|
||||||
if stream.read_f32_into::<LittleEndian>(&mut samples_f32).is_err() {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
samples.append(&mut samples_f32);
|
|
||||||
}
|
|
||||||
Encoder::Flac => {
|
|
||||||
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
|
||||||
let mut reader = claxon::FlacReader::new(take).unwrap();
|
|
||||||
samples.append(
|
|
||||||
&mut reader
|
|
||||||
.samples()
|
|
||||||
.map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
|
|
||||||
.collect::<Vec<f32>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Synchronizing with sink
|
// Synchronizing with sink
|
||||||
let sink = SINK.read().unwrap();
|
let sink = SINK.read().unwrap();
|
||||||
|
@ -284,8 +311,12 @@ pub fn _run(server: &str, settings: Settings) -> Option<()> {
|
||||||
let md = _md.as_ref().unwrap().clone();
|
let md = _md.as_ref().unwrap().clone();
|
||||||
drop(_md);
|
drop(_md);
|
||||||
if let Some(sink) = sink.as_ref() {
|
if let Some(sink) = sink.as_ref() {
|
||||||
while (sink.len() >= CACHE_SIZE_PCM && md.encoder != Encoder::Flac)
|
while (sink.len() >= CACHE_SIZE_PCM
|
||||||
|| (sink.len() >= CACHE_SIZE_COMPRESSED && md.encoder == Encoder::Flac)
|
&& md.encoder == Encoder::Pcm16
|
||||||
|
&& md.encoder == Encoder::PcmFloat)
|
||||||
|
|| (sink.len() >= CACHE_SIZE_COMPRESSED
|
||||||
|
&& md.encoder != Encoder::Pcm16
|
||||||
|
&& md.encoder != Encoder::PcmFloat)
|
||||||
{
|
{
|
||||||
// Sleeping exactly one buffer and watching for reset signal
|
// Sleeping exactly one buffer and watching for reset signal
|
||||||
if watching_sleep(
|
if watching_sleep(
|
||||||
|
@ -297,7 +328,7 @@ pub fn _run(server: &str, settings: Settings) -> Option<()> {
|
||||||
/ 4.0,
|
/ 4.0,
|
||||||
) {
|
) {
|
||||||
_stop();
|
_stop();
|
||||||
return None;
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sink.append(SamplesBuffer::new(
|
sink.append(SamplesBuffer::new(
|
||||||
|
|
|
@ -1,23 +1,45 @@
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
typedef struct CSettings {
|
||||||
|
/**
|
||||||
|
* See lonelyradio_types for numeric representation -> Encoder
|
||||||
|
*/
|
||||||
|
uint8_t encoder;
|
||||||
|
int32_t cover;
|
||||||
|
} CSettings;
|
||||||
|
|
||||||
typedef struct CImageJpeg {
|
typedef struct CImageJpeg {
|
||||||
uint32_t length;
|
uint32_t length;
|
||||||
uint8_t *bytes;
|
uint8_t *bytes;
|
||||||
} CImageJpeg;
|
} CImageJpeg;
|
||||||
|
|
||||||
typedef struct CSettings {
|
/**
|
||||||
/**
|
* Starts audio playback using rodio
|
||||||
* See lonelyradio_types -> Encoder
|
* Play without playlist => playlist = ""
|
||||||
*/
|
*/
|
||||||
uint8_t encoder;
|
void c_start(const char *server, struct CSettings settings, const char *playlist);
|
||||||
int32_t cover;
|
|
||||||
} CSettings;
|
|
||||||
|
|
||||||
void c_drop(uint8_t *ptr, size_t count);
|
/**
|
||||||
|
* Playlists separated by '\n'
|
||||||
|
*/
|
||||||
|
char *c_list_playlists(const char *server);
|
||||||
|
|
||||||
|
void c_toggle(void);
|
||||||
|
|
||||||
|
void c_stop(void);
|
||||||
|
|
||||||
|
char c_get_state(void);
|
||||||
|
|
||||||
|
char *c_get_metadata_artist(void);
|
||||||
|
|
||||||
|
char *c_get_metadata_album(void);
|
||||||
|
|
||||||
|
char *c_get_metadata_title(void);
|
||||||
|
|
||||||
|
float c_get_metadata_length(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Safety
|
* # Safety
|
||||||
|
@ -25,18 +47,8 @@ void c_drop(uint8_t *ptr, size_t count);
|
||||||
*/
|
*/
|
||||||
struct CImageJpeg c_get_cover_jpeg(void);
|
struct CImageJpeg c_get_cover_jpeg(void);
|
||||||
|
|
||||||
char *c_get_metadata_album(void);
|
/**
|
||||||
|
* # Safety
|
||||||
char *c_get_metadata_artist(void);
|
* None
|
||||||
|
*/
|
||||||
float c_get_metadata_length(void);
|
void c_drop(uint8_t *ptr, uintptr_t count);
|
||||||
|
|
||||||
char *c_get_metadata_title(void);
|
|
||||||
|
|
||||||
char c_get_state(void);
|
|
||||||
|
|
||||||
void c_start(const char *server, struct CSettings settings);
|
|
||||||
|
|
||||||
void c_stop(void);
|
|
||||||
|
|
||||||
void c_toggle(void);
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
../target
|
|
|
@ -1,12 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monoloader"
|
name = "monoloader"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
monolib = { path = "../monolib" }
|
monolib = { version = "0.7.1", path = "../monolib" }
|
||||||
clap = { version = "4.4.18", features = ["derive"] }
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
hound = "3.5.1"
|
hound = "3.5.1"
|
||||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use lonelyradio_types::Settings;
|
use monolib::lonelyradio_types::Settings;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -9,34 +9,53 @@ struct Args {
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
xor_key_file: Option<PathBuf>,
|
xor_key_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(short, long, default_value = "")]
|
||||||
|
playlist: String,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
list: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if args.list {
|
||||||
|
println!(
|
||||||
|
"Available playlists: {}",
|
||||||
|
match monolib::list_playlists(&args.address) {
|
||||||
|
Some(s) => format!("{:?}", s),
|
||||||
|
None => String::from("None"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let (md, samples) = monolib::get_track(
|
let (md, samples) = monolib::get_track(
|
||||||
&args.address,
|
&args.address,
|
||||||
Settings {
|
Settings {
|
||||||
encoder: lonelyradio_types::Encoder::Pcm16,
|
encoder: monolib::lonelyradio_types::Encoder::Flac,
|
||||||
cover: -1,
|
cover: -1,
|
||||||
},
|
},
|
||||||
|
&args.playlist,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!(
|
println!(
|
||||||
"Downloaded: {} - {} - {} ({} MB)",
|
"Downloaded: {} - {} - {} ({:?}, {} MiB)",
|
||||||
md.artist,
|
md.artist,
|
||||||
md.album,
|
md.album,
|
||||||
md.title,
|
md.title,
|
||||||
samples.len() as f32 * 2.0 / 1024.0 / 1024.0
|
md.encoder,
|
||||||
|
samples.len() as f32 / 256.0 / 1024.0
|
||||||
);
|
);
|
||||||
let spec = hound::WavSpec {
|
let spec = hound::WavSpec {
|
||||||
channels: md.channels,
|
channels: md.channels,
|
||||||
sample_rate: md.sample_rate,
|
sample_rate: md.sample_rate,
|
||||||
bits_per_sample: 16,
|
bits_per_sample: 32,
|
||||||
sample_format: hound::SampleFormat::Int,
|
sample_format: hound::SampleFormat::Float,
|
||||||
};
|
};
|
||||||
let mut writer =
|
let mut writer =
|
||||||
hound::WavWriter::create(format!("{} - {}.wav", md.artist, md.title), spec).unwrap();
|
hound::WavWriter::create(format!("{} - {}.wav", md.artist, md.title), spec).unwrap();
|
||||||
let mut writer_i16 = writer.get_i16_writer(samples.len() as u32);
|
samples.iter().for_each(|s| writer.write_sample(*s).unwrap());
|
||||||
samples.iter().for_each(|s| writer_i16.write_sample(*s));
|
writer.flush().unwrap();
|
||||||
writer_i16.flush().unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use symphonia::core::units::Time;
|
||||||
|
|
||||||
use crate::Args;
|
use crate::Args;
|
||||||
|
|
||||||
pub async fn get_meta(file_path: &Path) -> (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 file = Box::new(std::fs::File::open(file_path).unwrap());
|
||||||
let mut hint = Hint::new();
|
let mut hint = Hint::new();
|
||||||
hint.with_extension(file_path.extension().unwrap().to_str().unwrap());
|
hint.with_extension(file_path.extension().unwrap().to_str().unwrap());
|
||||||
|
@ -41,12 +41,7 @@ pub async fn get_meta(file_path: &Path) -> (u16, u32, Time) {
|
||||||
let mut sample_rate = 0;
|
let mut sample_rate = 0;
|
||||||
let track_length =
|
let track_length =
|
||||||
track.codec_params.time_base.unwrap().calc_time(track.codec_params.n_frames.unwrap());
|
track.codec_params.time_base.unwrap().calc_time(track.codec_params.n_frames.unwrap());
|
||||||
loop {
|
while let Ok(packet) = format.next_packet() {
|
||||||
let packet = match format.next_packet() {
|
|
||||||
Ok(packet) => packet,
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
if packet.track_id() != track_id {
|
if packet.track_id() != track_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -65,11 +60,26 @@ pub async fn get_meta(file_path: &Path) -> (u16, u32, Time) {
|
||||||
}
|
}
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
(channels, get_resampling_rate(&sample_rate, &args.max_samplerate), track_length)
|
(
|
||||||
|
channels,
|
||||||
|
if args.no_resampling && encoder_wants == 0 {
|
||||||
|
sample_rate
|
||||||
|
} else {
|
||||||
|
get_resampling_rate(
|
||||||
|
&sample_rate,
|
||||||
|
&if encoder_wants != 0 {
|
||||||
|
args.max_samplerate.min(encoder_wants)
|
||||||
|
} else {
|
||||||
|
args.max_samplerate
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
track_length,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Getting samples
|
/// Getting samples
|
||||||
pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<f32>> {
|
pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream<Item = Vec<f32>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let file = Box::new(std::fs::File::open(&file_path).unwrap());
|
let file = Box::new(std::fs::File::open(&file_path).unwrap());
|
||||||
let mut hint = Hint::new();
|
let mut hint = Hint::new();
|
||||||
|
@ -93,46 +103,41 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<f32>> {
|
||||||
.expect("no supported audio tracks");
|
.expect("no supported audio tracks");
|
||||||
|
|
||||||
let mut decoder = symphonia::default::get_codecs()
|
let mut decoder = symphonia::default::get_codecs()
|
||||||
.make(track.codec_params.clone().with_max_frames_per_packet(65536), &Default::default())
|
.make(&track.codec_params, &Default::default())
|
||||||
.expect("unsupported codec");
|
.expect("unsupported codec");
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
stream! {
|
stream! {
|
||||||
loop {
|
while let Ok(packet) = format.next_packet() {
|
||||||
let packet = match format.next_packet() {
|
|
||||||
Ok(packet) => packet,
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
if packet.track_id() != track_id {
|
if packet.track_id() != track_id {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match decoder.decode(&packet) {
|
match decoder.decode(&packet) {
|
||||||
Ok(decoded) => {
|
Ok(decoded) => {
|
||||||
if decoded.spec().rate > args.max_samplerate {
|
let output_rate = get_resampling_rate(&decoded.spec().rate, &if encoder_wants != 0 {
|
||||||
|
args.max_samplerate.min(encoder_wants)
|
||||||
|
} else {
|
||||||
|
args.max_samplerate
|
||||||
|
});
|
||||||
|
if decoded.spec().rate > output_rate && (!args.no_resampling || encoder_wants != 0) {
|
||||||
let spec = *decoded.spec();
|
let spec = *decoded.spec();
|
||||||
let mut byte_buf =
|
let mut byte_buf =
|
||||||
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||||
byte_buf.copy_interleaved_ref(decoded);
|
byte_buf.copy_interleaved_ref(decoded);
|
||||||
let output_rate = get_resampling_rate(&spec.rate,&args.max_samplerate);
|
|
||||||
|
|
||||||
// About Samplerate struct:
|
// About Samplerate struct:
|
||||||
// We are downsampling, not upsampling, so we should be fine
|
// We are downsampling, not upsampling, so we should be fine
|
||||||
yield (
|
yield (if output_rate == spec.rate {
|
||||||
if output_rate == spec.rate {
|
byte_buf.samples().to_vec()
|
||||||
byte_buf.samples().to_vec()
|
} else {
|
||||||
} else {
|
samplerate::convert(
|
||||||
samplerate::convert(
|
spec.rate,
|
||||||
spec.rate,
|
output_rate,
|
||||||
args.max_samplerate,
|
spec.channels.count(),
|
||||||
spec.channels.count(),
|
samplerate::ConverterType::Linear,
|
||||||
samplerate::ConverterType::Linear,
|
byte_buf.samples(),
|
||||||
byte_buf.samples(),
|
)
|
||||||
)
|
.unwrap()
|
||||||
.unwrap()
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let mut byte_buf =
|
let mut byte_buf =
|
||||||
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||||
|
|
156
src/encode.rs
|
@ -1,52 +1,158 @@
|
||||||
use flacenc::{component::BitRepr, error::Verify, source::MemSource};
|
|
||||||
use lonelyradio_types::Encoder;
|
use lonelyradio_types::Encoder;
|
||||||
|
|
||||||
|
// Return: 0 - encoded bytes, 1 - magic cookie (for alac only)
|
||||||
|
#[allow(unused_variables)]
|
||||||
pub fn encode(
|
pub fn encode(
|
||||||
codec: Encoder,
|
codec: Encoder,
|
||||||
mut samples: Vec<f32>,
|
mut samples: Vec<f32>,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u16,
|
channels: u16,
|
||||||
) -> Option<Vec<u8>> {
|
) -> Option<(Vec<u8>, Option<Vec<u8>>)> {
|
||||||
match codec {
|
match codec {
|
||||||
Encoder::Pcm16 => {
|
Encoder::Pcm16 => {
|
||||||
|
#[allow(unused_mut)]
|
||||||
let mut samples = samples.iter_mut().map(|x| (*x * 32768.0) as i16).collect::<Vec<_>>();
|
let mut samples = samples.iter_mut().map(|x| (*x * 32768.0) as i16).collect::<Vec<_>>();
|
||||||
// Launching lonelyradio on the router moment
|
// Launching lonelyradio on the router moment
|
||||||
if cfg!(target_endian = "big") {
|
#[cfg(target_endian = "big")]
|
||||||
|
{
|
||||||
samples.iter_mut().for_each(|sample| {
|
samples.iter_mut().for_each(|sample| {
|
||||||
*sample = sample.to_le();
|
*sample = sample.to_le();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Sowwy about that
|
// Sowwy about that
|
||||||
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
||||||
Some(samples.to_vec())
|
Some((samples.to_vec(), None))
|
||||||
}
|
}
|
||||||
Encoder::PcmFloat => {
|
Encoder::PcmFloat => {
|
||||||
// Launching lonelyradio on the router moment
|
|
||||||
// Sowwy about that
|
// Sowwy about that
|
||||||
let samples = samples.iter().map(|x| x.to_bits()).collect::<Vec<u32>>();
|
|
||||||
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
||||||
Some(samples.to_vec())
|
Some((samples.to_vec(), None))
|
||||||
}
|
}
|
||||||
Encoder::Flac => {
|
Encoder::Flac => {
|
||||||
let encoded = flacenc::encode_with_fixed_block_size(
|
#[cfg(feature = "flac")]
|
||||||
&flacenc::config::Encoder::default().into_verified().unwrap(),
|
{
|
||||||
MemSource::from_samples(
|
use flacenc::{component::BitRepr, error::Verify, source::MemSource};
|
||||||
// I'm crying (It's just a burning memory)
|
let encoded = flacenc::encode_with_fixed_block_size(
|
||||||
&samples
|
&flacenc::config::Encoder::default().into_verified().unwrap(),
|
||||||
.iter()
|
MemSource::from_samples(
|
||||||
.map(|x| (*x as f64 * 32768.0 * 256.0) as i32)
|
&samples
|
||||||
.collect::<Vec<i32>>(),
|
.iter()
|
||||||
channels as usize,
|
.map(|x| (x.clamp(-1.0, 1.0) as f64 * 32768.0 * 256.0) as i32)
|
||||||
24,
|
.collect::<Vec<i32>>(),
|
||||||
sample_rate as usize,
|
channels as usize,
|
||||||
),
|
24,
|
||||||
256,
|
sample_rate as usize,
|
||||||
)
|
),
|
||||||
.unwrap();
|
256,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut sink = flacenc::bitsink::ByteSink::new();
|
let mut sink = flacenc::bitsink::ByteSink::new();
|
||||||
encoded.write(&mut sink).unwrap();
|
encoded.write(&mut sink).unwrap();
|
||||||
Some(sink.as_slice().to_vec())
|
Some((sink.as_slice().to_vec(), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "flac"))]
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Encoder::Alac => {
|
||||||
|
#[cfg(feature = "alac")]
|
||||||
|
{
|
||||||
|
use alac_encoder::{AlacEncoder, FormatDescription};
|
||||||
|
|
||||||
|
let samples = samples.iter_mut().map(|x| (*x * 32768.0) as i16).collect::<Vec<_>>();
|
||||||
|
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
||||||
|
|
||||||
|
let input_format =
|
||||||
|
FormatDescription::pcm::<i16>(sample_rate as f64, channels as u32);
|
||||||
|
let output_format = FormatDescription::alac(
|
||||||
|
sample_rate as f64,
|
||||||
|
samples.len() as u32,
|
||||||
|
channels as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize the encoder
|
||||||
|
let mut encoder = AlacEncoder::new(&output_format);
|
||||||
|
|
||||||
|
// Allocate a buffer for the encoder to write chunks to.
|
||||||
|
let mut output = vec![0u8; output_format.max_packet_size()];
|
||||||
|
let size = encoder.encode(&input_format, samples, &mut output);
|
||||||
|
|
||||||
|
// Here you can do whatever you want with the result:
|
||||||
|
Some((Vec::from(&output[0..size]), Some(encoder.magic_cookie())))
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "alac"))]
|
||||||
|
{
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Encoder::Vorbis => {
|
||||||
|
#[cfg(feature = "vorbis")]
|
||||||
|
{
|
||||||
|
use std::num::{NonZeroU32, NonZeroU8};
|
||||||
|
let out: Vec<u8> = vec![];
|
||||||
|
let mut encoder = vorbis_rs::VorbisEncoderBuilder::new(
|
||||||
|
NonZeroU32::new(sample_rate).unwrap(),
|
||||||
|
NonZeroU8::new(channels as u8).unwrap(),
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.bitrate_management_strategy(
|
||||||
|
vorbis_rs::VorbisBitrateManagementStrategy::ConstrainedAbr {
|
||||||
|
// I will think about clients asking about bitrate later, now it's just
|
||||||
|
// "enough" 128 kib/s
|
||||||
|
maximum_bitrate: NonZeroU32::new(192 * 1024).unwrap(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let mut samples_channels = vec![];
|
||||||
|
for i in 0..channels as usize {
|
||||||
|
samples_channels.push(
|
||||||
|
samples[i..]
|
||||||
|
.iter()
|
||||||
|
.step_by(channels as usize)
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<f32>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
encoder.encode_audio_block(samples_channels).unwrap();
|
||||||
|
Some((encoder.finish().unwrap(), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "vorbis"))]
|
||||||
|
{
|
||||||
|
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!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
359
src/main.rs
|
@ -1,9 +1,9 @@
|
||||||
mod decode;
|
mod decode;
|
||||||
mod encode;
|
mod encode;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::net::TcpStream;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -13,23 +13,28 @@ use clap::Parser;
|
||||||
use encode::encode;
|
use encode::encode;
|
||||||
use futures_util::pin_mut;
|
use futures_util::pin_mut;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use image::io::Reader as ImageReader;
|
use image::ImageReader;
|
||||||
|
use image::RgbImage;
|
||||||
use lofty::Accessor;
|
use lofty::Accessor;
|
||||||
use lofty::TaggedFileExt;
|
use lofty::TaggedFileExt;
|
||||||
use lonelyradio_types::Encoder;
|
use lonelyradio_types::Encoder;
|
||||||
|
use lonelyradio_types::Request;
|
||||||
|
use lonelyradio_types::RequestResult;
|
||||||
use lonelyradio_types::ServerCapabilities;
|
use lonelyradio_types::ServerCapabilities;
|
||||||
use lonelyradio_types::Settings;
|
use lonelyradio_types::Settings;
|
||||||
use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata};
|
use lonelyradio_types::{FragmentMetadata, PlayMessage, TrackMetadata};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_stream::Stream;
|
use tokio_stream::Stream;
|
||||||
|
use url::Url;
|
||||||
use walkdir::DirEntry;
|
use walkdir::DirEntry;
|
||||||
|
use xspf::Playlist;
|
||||||
|
|
||||||
use crate::decode::decode_file_stream;
|
use crate::decode::decode_file_stream;
|
||||||
use crate::decode::get_meta;
|
use crate::decode::get_meta;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser, Clone)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Directory with audio files
|
/// Directory with audio files
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
|
@ -38,14 +43,6 @@ struct Args {
|
||||||
#[arg(short, default_value = "0.0.0.0:5894")]
|
#[arg(short, default_value = "0.0.0.0:5894")]
|
||||||
address: String,
|
address: String,
|
||||||
|
|
||||||
/// Enable "public" log (without sensitive information)
|
|
||||||
#[arg(short, long)]
|
|
||||||
public_log: bool,
|
|
||||||
|
|
||||||
/// Process all samples to -1 or 1
|
|
||||||
#[arg(short, long)]
|
|
||||||
war: bool,
|
|
||||||
|
|
||||||
/// Resample all tracks, which samplerate exceeds N
|
/// Resample all tracks, which samplerate exceeds N
|
||||||
#[arg(short, long, default_value = "96000")]
|
#[arg(short, long, default_value = "96000")]
|
||||||
max_samplerate: u32,
|
max_samplerate: u32,
|
||||||
|
@ -57,92 +54,110 @@ struct Args {
|
||||||
/// Size of artwork (-1 for no artwork, 0 for original, N for NxN)
|
/// Size of artwork (-1 for no artwork, 0 for original, N for NxN)
|
||||||
#[arg(long, default_value = "96000")]
|
#[arg(long, default_value = "96000")]
|
||||||
artwork: i32,
|
artwork: i32,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
playlist_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUPPORTED_ENCODERS: [Encoder; 3] = [Encoder::Pcm16, Encoder::PcmFloat, Encoder::Flac];
|
const SUPPORTED_ENCODERS: &[Encoder] = &[
|
||||||
|
Encoder::Pcm16,
|
||||||
|
Encoder::PcmFloat,
|
||||||
|
#[cfg(feature = "flac")]
|
||||||
|
Encoder::Flac,
|
||||||
|
#[cfg(feature = "alac")]
|
||||||
|
Encoder::Alac,
|
||||||
|
#[cfg(feature = "vorbis")]
|
||||||
|
Encoder::Vorbis,
|
||||||
|
#[cfg(feature = "sea")]
|
||||||
|
Encoder::Sea,
|
||||||
|
];
|
||||||
|
|
||||||
async fn stream_track(
|
async fn stream_track(
|
||||||
samples_stream: impl Stream<Item = Vec<f32>>,
|
samples_stream: impl Stream<Item = Vec<f32>>,
|
||||||
war: bool,
|
|
||||||
md: TrackMetadata,
|
md: TrackMetadata,
|
||||||
s: &mut TcpStream,
|
mut s: impl Write,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
pin_mut!(samples_stream);
|
pin_mut!(samples_stream);
|
||||||
|
|
||||||
let _md = md.clone();
|
let _md = md.clone();
|
||||||
|
|
||||||
if s.write_all(rmp_serde::encode::to_vec_named(&Message::T(_md)).unwrap().as_slice()).is_err() {
|
if s.write_all(rmp_serde::encode::to_vec_named(&PlayMessage::T(_md)).unwrap().as_slice())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Why chunks?
|
// Why chunks?
|
||||||
// flacenc is broken on low amount of samples (Symphonia's AIFF decoder returns
|
// Different codecs have different quality on different audio lenghts
|
||||||
// ~2304 samples per packet (on bo en's tracks), instead of usual ~8192 on any
|
|
||||||
// other lossless decoder)
|
|
||||||
while let Some(mut _samples) = samples_stream
|
while let Some(mut _samples) = samples_stream
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.chunks(match md.encoder {
|
.chunks(match md.encoder {
|
||||||
Encoder::Pcm16 => 1,
|
Encoder::Pcm16 => 1,
|
||||||
Encoder::PcmFloat => 1,
|
Encoder::PcmFloat => 1,
|
||||||
Encoder::Flac => 16,
|
Encoder::Flac => 16,
|
||||||
|
Encoder::Alac => 32,
|
||||||
|
Encoder::Vorbis => 64,
|
||||||
|
Encoder::Sea => 64,
|
||||||
|
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||||
})
|
})
|
||||||
.next()
|
.next()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let mut _samples = _samples.concat();
|
let mut _samples = _samples.concat();
|
||||||
if war {
|
|
||||||
_samples.iter_mut().for_each(|sample| {
|
|
||||||
*sample = sample.signum();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
match md.encoder {
|
match md.encoder {
|
||||||
Encoder::Pcm16 => {
|
Encoder::Pcm16
|
||||||
let _md = Message::F(FragmentMetadata {
|
| Encoder::PcmFloat
|
||||||
length: _samples.len() as u64 * 2,
|
| Encoder::Flac
|
||||||
});
|
| Encoder::Alac
|
||||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
| Encoder::Vorbis
|
||||||
return true;
|
| Encoder::Sea => {
|
||||||
}
|
let (encoded, magic_cookie) =
|
||||||
if s.write_all(
|
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
||||||
&encode(Encoder::Pcm16, _samples, md.sample_rate, md.channels).unwrap(),
|
let _md = PlayMessage::F(FragmentMetadata {
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Encoder::PcmFloat => {
|
|
||||||
let _md = Message::F(FragmentMetadata {
|
|
||||||
length: _samples.len() as u64 * 4,
|
|
||||||
});
|
|
||||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if s.write_all(
|
|
||||||
&encode(Encoder::PcmFloat, _samples, md.sample_rate, md.channels).unwrap(),
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Encoder::Flac => {
|
|
||||||
let encoded = encode(Encoder::Flac, _samples, md.sample_rate, md.channels).unwrap();
|
|
||||||
let _md = Message::F(FragmentMetadata {
|
|
||||||
length: encoded.as_slice().len() as u64,
|
length: encoded.as_slice().len() as u64,
|
||||||
|
magic_cookie,
|
||||||
});
|
});
|
||||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
if s.write_all(rmp_serde::to_vec_named(&_md).unwrap().as_slice()).is_err() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if s.write_all(encoded.as_slice()).is_err() {
|
if s.write_all(encoded.as_slice()).is_err() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_playlists(dir: impl AsRef<Path>) -> Option<HashMap<String, Arc<Vec<PathBuf>>>> {
|
||||||
|
let mut map: HashMap<String, Arc<Vec<PathBuf>>> = HashMap::new();
|
||||||
|
for playlist in walkdir::WalkDir::new(dir)
|
||||||
|
.into_iter()
|
||||||
|
.filter_entry(is_not_hidden)
|
||||||
|
.filter_map(|v| v.ok())
|
||||||
|
.map(|x| x.into_path())
|
||||||
|
.filter(|x| x.is_file())
|
||||||
|
{
|
||||||
|
let mut name = playlist.file_name().unwrap().to_str().unwrap().to_string();
|
||||||
|
let parsed = Playlist::read_file(playlist).unwrap();
|
||||||
|
if let Some(ref n) = parsed.title {
|
||||||
|
name = n.clone();
|
||||||
|
}
|
||||||
|
let tracklist = parsed
|
||||||
|
.track_list
|
||||||
|
.iter()
|
||||||
|
.flat_map(|x| x.location.iter().flat_map(|l| Url::parse(l.as_str()).ok()))
|
||||||
|
.filter(|x| x.scheme() == "file")
|
||||||
|
.map(|x| x.to_file_path().unwrap())
|
||||||
|
.filter(|x| track_valid(x))
|
||||||
|
.collect();
|
||||||
|
map.insert(name, Arc::new(tracklist));
|
||||||
|
}
|
||||||
|
Some(map)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
@ -156,17 +171,24 @@ async fn main() {
|
||||||
.filter(|x| track_valid(x))
|
.filter(|x| track_valid(x))
|
||||||
.collect::<Vec<PathBuf>>(),
|
.collect::<Vec<PathBuf>>(),
|
||||||
);
|
);
|
||||||
|
let playlists: Option<HashMap<String, Arc<Vec<PathBuf>>>> = match args.playlist_dir.as_ref() {
|
||||||
|
None => None,
|
||||||
|
Some(dir) => get_playlists(dir),
|
||||||
|
};
|
||||||
loop {
|
loop {
|
||||||
let (socket, _) = listener.accept().await.unwrap();
|
let (socket, _) = listener.accept().await.unwrap();
|
||||||
let mut s = socket.into_std().unwrap();
|
let mut s = socket.into_std().unwrap();
|
||||||
s.set_nonblocking(false).unwrap();
|
s.set_nonblocking(false).unwrap();
|
||||||
|
|
||||||
let mut hello = [0u8; 8];
|
let mut hello = [0u8; 8];
|
||||||
if s.read_exact(&mut hello).is_err() {
|
if s.read_exact(&mut hello).is_err() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if hello != lonelyradio_types::HELLO_MAGIC.to_le_bytes() {
|
|
||||||
|
if &hello != lonelyradio_types::HELLO_MAGIC {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.write_all(
|
if s.write_all(
|
||||||
&rmp_serde::to_vec_named(&ServerCapabilities {
|
&rmp_serde::to_vec_named(&ServerCapabilities {
|
||||||
encoders: SUPPORTED_ENCODERS.to_vec(),
|
encoders: SUPPORTED_ENCODERS.to_vec(),
|
||||||
|
@ -177,17 +199,82 @@ async fn main() {
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let settings: Settings = match rmp_serde::from_read(&s) {
|
s.flush().unwrap();
|
||||||
Ok(s) => s,
|
|
||||||
_ => continue,
|
let request: Request = match rmp_serde::from_read(&s) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if settings.cover < -1 {
|
|
||||||
continue;
|
match request {
|
||||||
|
Request::Play(settings) => {
|
||||||
|
if s.write_all(&rmp_serde::to_vec_named(&check_settings(&settings)).unwrap())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokio::spawn(stream(s, tracklist.clone(), settings));
|
||||||
|
}
|
||||||
|
Request::ListPlaylist => match playlists {
|
||||||
|
None => {
|
||||||
|
s.write_all(
|
||||||
|
&rmp_serde::to_vec_named(&RequestResult::Playlist(
|
||||||
|
lonelyradio_types::PlaylistResponce {
|
||||||
|
playlists: vec![],
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Some(ref playlists) => {
|
||||||
|
s.write_all(
|
||||||
|
&rmp_serde::to_vec_named(&RequestResult::Playlist(
|
||||||
|
lonelyradio_types::PlaylistResponce {
|
||||||
|
playlists: playlists.keys().cloned().collect(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
Request::PlayPlaylist(playlist, settings) => {
|
||||||
|
if playlists.is_none() || playlists.as_ref().unwrap().get(&playlist).is_none() {
|
||||||
|
s.write_all(
|
||||||
|
&rmp_serde::to_vec_named(&RequestResult::Error(
|
||||||
|
lonelyradio_types::RequestError::NoSuchPlaylist,
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if s.write_all(&rmp_serde::to_vec_named(&check_settings(&settings)).unwrap())
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tracklist = playlists.as_ref().unwrap().get(&playlist).unwrap().clone();
|
||||||
|
tokio::spawn(stream(s, tracklist, settings));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tokio::spawn(stream(s, tracklist.clone(), settings));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_settings(settings: &Settings) -> RequestResult {
|
||||||
|
if settings.cover < -1 {
|
||||||
|
return RequestResult::Error(lonelyradio_types::RequestError::WrongCoverSize);
|
||||||
|
}
|
||||||
|
if !SUPPORTED_ENCODERS.contains(&settings.encoder) {
|
||||||
|
return RequestResult::Error(lonelyradio_types::RequestError::UnsupportedEncoder);
|
||||||
|
}
|
||||||
|
RequestResult::Ok
|
||||||
|
}
|
||||||
|
|
||||||
fn is_not_hidden(entry: &DirEntry) -> bool {
|
fn is_not_hidden(entry: &DirEntry) -> bool {
|
||||||
entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false)
|
entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
@ -212,86 +299,94 @@ fn track_valid(track: &Path) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
|
struct Metadata {
|
||||||
let args = Args::parse();
|
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 {
|
||||||
|
Encoder::Opus | Encoder::Vorbis | Encoder::Aac => 48000,
|
||||||
|
Encoder::Flac => 96000,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
loop {
|
loop {
|
||||||
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
|
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
|
||||||
|
|
||||||
let mut title = String::new();
|
let Metadata {
|
||||||
let mut artist = String::new();
|
title,
|
||||||
let mut album = String::new();
|
album,
|
||||||
let mut cover = std::thread::spawn(|| None);
|
artist,
|
||||||
let mut file = std::fs::File::open(&track).unwrap();
|
cover,
|
||||||
let tagged = match lofty::read_from(&mut file) {
|
} = match get_metadata(&track, &args, &settings) {
|
||||||
Ok(f) => f,
|
Some(m) => m,
|
||||||
_ => continue,
|
_ => 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);
|
|
||||||
eprintln!(
|
|
||||||
"[{}] {} to {}:{}{} ({:?})",
|
|
||||||
Local::now().to_rfc3339(),
|
|
||||||
track_message,
|
|
||||||
s.peer_addr().unwrap().ip(),
|
|
||||||
s.peer_addr().unwrap().port(),
|
|
||||||
if args.war {
|
|
||||||
" with WAR.rs"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
settings.encoder
|
|
||||||
);
|
|
||||||
|
|
||||||
if args.public_log {
|
let track_message = format!("{} - {} - {}", &artist, &album, &title);
|
||||||
println!(
|
println!("[{}] {} ({:?})", Local::now().to_rfc3339(), track_message, settings.encoder);
|
||||||
"[{}] {} to {}{}",
|
|
||||||
Local::now().to_rfc3339(),
|
let (channels, sample_rate, time) = get_meta(track.as_path(), encoder_wants);
|
||||||
track.to_str().unwrap(),
|
let stream = decode_file_stream(track, encoder_wants);
|
||||||
s.peer_addr().unwrap().port(),
|
let id = thread_rng().gen();
|
||||||
if args.war {
|
|
||||||
" with WAR.rs"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let (channels, sample_rate, time) = get_meta(track.as_path()).await;
|
|
||||||
let stream = decode_file_stream(track);
|
|
||||||
if stream_track(
|
if stream_track(
|
||||||
stream,
|
stream,
|
||||||
args.war,
|
|
||||||
TrackMetadata {
|
TrackMetadata {
|
||||||
track_length_frac: time.frac as f32,
|
track_length_frac: time.frac as f32,
|
||||||
track_length_secs: time.seconds,
|
track_length_secs: time.seconds,
|
||||||
encoder: settings.encoder,
|
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,
|
album,
|
||||||
artist,
|
artist,
|
||||||
title,
|
title,
|
||||||
|
|