Merge 5ca38939bd
into release
This commit is contained in:
commit
bb11c80ebd
14 changed files with 700 additions and 435 deletions
|
@ -1,6 +1,9 @@
|
||||||
coverage:
|
coverage:
|
||||||
range: "0...100"
|
range: "0...100"
|
||||||
status:
|
status:
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
informational: true
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
informational: true
|
informational: true
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,5 +1,17 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Polaris 0.14.1
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
- Fixed compilation issue when using musl toolchains
|
||||||
|
- Log messages that DDNS is not setup have been downgraded to debug level
|
||||||
|
|
||||||
|
### Web client
|
||||||
|
|
||||||
|
- Fixed a bug where non-ASCII files or directories were not always alphabetically sorted (thanks @dechamps)
|
||||||
|
- Fixed a bug where after linking a last.fm account, clicking the account name would not link to the expected page
|
||||||
|
|
||||||
## Polaris 0.14.0
|
## Polaris 0.14.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
1038
Cargo.lock
generated
1038
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
26
Cargo.toml
26
Cargo.toml
|
@ -14,17 +14,20 @@ ui = ["native-windows-gui", "native-windows-derive"]
|
||||||
actix-files = { version = "0.6" }
|
actix-files = { version = "0.6" }
|
||||||
actix-web = { version = "4" }
|
actix-web = { version = "4" }
|
||||||
actix-web-httpauth = { version = "0.8" }
|
actix-web-httpauth = { version = "0.8" }
|
||||||
ape = "0.4.0"
|
ape = "0.5"
|
||||||
base64 = "0.13"
|
base64 = "0.21"
|
||||||
branca = "0.10.1"
|
branca = "0.10.1"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
diesel_migrations = { version = "2.0", features = ["sqlite"] }
|
diesel_migrations = { version = "2.0", features = ["sqlite"] }
|
||||||
futures-util = { version = "0.3" }
|
futures-util = { version = "0.3" }
|
||||||
getopts = "0.2.21"
|
getopts = "0.2.21"
|
||||||
http = "0.2.8"
|
http = "0.2.8"
|
||||||
id3 = { git = "https://github.com/polyfloyd/rust-id3.git", rev = "f3b5e3a" } # TODO update after 1.5.0 is released
|
id3 = "1.7.0"
|
||||||
lewton = "0.10.2"
|
lewton = "0.10.2"
|
||||||
libsqlite3-sys = { version = "0.25", features = ["bundled", "bundled-windows"], optional = true }
|
libsqlite3-sys = { version = "0.26", features = [
|
||||||
|
"bundled",
|
||||||
|
"bundled-windows",
|
||||||
|
], optional = true }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
metaflac = "0.2.5"
|
metaflac = "0.2.5"
|
||||||
mp3-duration = "0.1.10"
|
mp3-duration = "0.1.10"
|
||||||
|
@ -42,9 +45,8 @@ serde_derive = "1.0.147"
|
||||||
serde_json = "1.0.87"
|
serde_json = "1.0.87"
|
||||||
simplelog = "0.12.0"
|
simplelog = "0.12.0"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
tokio = "1.21"
|
toml = "0.7"
|
||||||
toml = "0.5"
|
ureq = "2.7"
|
||||||
ureq = "1.5.5"
|
|
||||||
url = "2.3"
|
url = "2.3"
|
||||||
|
|
||||||
[dependencies.diesel]
|
[dependencies.diesel]
|
||||||
|
@ -58,11 +60,17 @@ default_features = false
|
||||||
features = ["bmp", "gif", "jpeg", "png"]
|
features = ["bmp", "gif", "jpeg", "png"]
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
native-windows-gui = {version = "1.0.13", default-features = false, features = ["cursor", "image-decoder", "message-window", "menu", "tray-notification"], optional = true }
|
native-windows-gui = { version = "1.0.13", default-features = false, features = [
|
||||||
|
"cursor",
|
||||||
|
"image-decoder",
|
||||||
|
"message-window",
|
||||||
|
"menu",
|
||||||
|
"tray-notification",
|
||||||
|
], optional = true }
|
||||||
native-windows-derive = { version = "1.0.5", optional = true }
|
native-windows-derive = { version = "1.0.5", optional = true }
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
daemonize = "0.4.1"
|
daemonize = "0.5"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
|
|
|
@ -21,7 +21,9 @@ Password: `demo_password`
|
||||||
- Support for `flac`, `mp3`, `mp4`, `mpc`, `ogg`, `opus`, `ape`, `wav` and `aiff` files
|
- Support for `flac`, `mp3`, `mp4`, `mpc`, `ogg`, `opus`, `ape`, `wav` and `aiff` files
|
||||||
- Easy to setup and administer, no configuration files needed
|
- Easy to setup and administer, no configuration files needed
|
||||||
- Dark mode and customizable color themes
|
- Dark mode and customizable color themes
|
||||||
- Listen to your music on the go with [Polaris Android](https://github.com/agersant/polaris-android)
|
- Listen to your music on the go:
|
||||||
|
- Polaris Android ([Google Play Store](https://play.google.com/store/apps/details?id=agersant.polaris) · [F-Droid](https://f-droid.org/packages/agersant.polaris/) · [Repository](https://github.com/agersant/polaris-android))
|
||||||
|
- Polarios ([App Store](https://apps.apple.com/app/polarios/id1662366309) · [Repository](https://gitlab.com/elise/Polarios))
|
||||||
- [Last.fm](https://www.last.fm) scrobbling
|
- [Last.fm](https://www.last.fm) scrobbling
|
||||||
|
|
||||||
## Tutorials
|
## Tutorials
|
||||||
|
|
BIN
res/branding/logo/social_media_preview.afdesign
Normal file
BIN
res/branding/logo/social_media_preview.afdesign
Normal file
Binary file not shown.
|
@ -1,5 +1,6 @@
|
||||||
|
use base64::prelude::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use log::{error, info};
|
use log::{debug, error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time;
|
use std::time;
|
||||||
|
@ -12,6 +13,8 @@ const DDNS_UPDATE_URL: &str = "https://ydns.io/api/v1/update/";
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("DDNS update query failed with HTTP status code `{0}`")]
|
#[error("DDNS update query failed with HTTP status code `{0}`")]
|
||||||
UpdateQueryFailed(u16),
|
UpdateQueryFailed(u16),
|
||||||
|
#[error("DDNS update query failed due to a transport error")]
|
||||||
|
UpdateQueryTransport,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
DatabaseConnection(#[from] db::Error),
|
DatabaseConnection(#[from] db::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -39,19 +42,23 @@ impl Manager {
|
||||||
fn update_my_ip(&self) -> Result<(), Error> {
|
fn update_my_ip(&self) -> Result<(), Error> {
|
||||||
let config = self.config()?;
|
let config = self.config()?;
|
||||||
if config.host.is_empty() || config.username.is_empty() {
|
if config.host.is_empty() || config.username.is_empty() {
|
||||||
info!("Skipping DDNS update because credentials are missing");
|
debug!("Skipping DDNS update because credentials are missing");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
|
let full_url = format!("{}?host={}", DDNS_UPDATE_URL, &config.host);
|
||||||
|
let credentials = format!("{}:{}", &config.username, &config.password);
|
||||||
let response = ureq::get(full_url.as_str())
|
let response = ureq::get(full_url.as_str())
|
||||||
.auth(&config.username, &config.password)
|
.set(
|
||||||
|
"Authorization",
|
||||||
|
&format!("Basic {}", BASE64_STANDARD_NO_PAD.encode(credentials)),
|
||||||
|
)
|
||||||
.call();
|
.call();
|
||||||
|
|
||||||
if response.ok() {
|
match response {
|
||||||
Ok(())
|
Ok(_) => Ok(()),
|
||||||
} else {
|
Err(ureq::Error::Status(code, _)) => Err(Error::UpdateQueryFailed(code)),
|
||||||
Err(Error::UpdateQueryFailed(response.status()))
|
Err(ureq::Error::Transport(_)) => Err(Error::UpdateQueryTransport),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,6 @@ impl From<id3::Tag> for SongTags {
|
||||||
let track_number = tag.track();
|
let track_number = tag.track();
|
||||||
let year = tag
|
let year = tag
|
||||||
.year()
|
.year()
|
||||||
.map(|y| y as i32)
|
|
||||||
.or_else(|| tag.date_released().map(|d| d.year))
|
.or_else(|| tag.date_released().map(|d| d.year))
|
||||||
.or_else(|| tag.original_date_released().map(|d| d.year))
|
.or_else(|| tag.original_date_released().map(|d| d.year))
|
||||||
.or_else(|| tag.date_recorded().map(|d| d.year));
|
.or_else(|| tag.date_recorded().map(|d| d.year));
|
||||||
|
@ -316,9 +315,7 @@ fn read_flac(path: &Path) -> Result<SongTags, Error> {
|
||||||
let year = vorbis.get("DATE").and_then(|d| d[0].parse::<i32>().ok());
|
let year = vorbis.get("DATE").and_then(|d| d[0].parse::<i32>().ok());
|
||||||
let mut streaminfo = tag.get_blocks(metaflac::BlockType::StreamInfo);
|
let mut streaminfo = tag.get_blocks(metaflac::BlockType::StreamInfo);
|
||||||
let duration = match streaminfo.next() {
|
let duration = match streaminfo.next() {
|
||||||
Some(&metaflac::Block::StreamInfo(ref s)) => {
|
Some(metaflac::Block::StreamInfo(s)) => Some(s.total_samples as u32 / s.sample_rate),
|
||||||
Some((s.total_samples as u32 / s.sample_rate) as u32)
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let has_artwork = tag.pictures().count() > 0;
|
let has_artwork = tag.pictures().count() > 0;
|
||||||
|
|
|
@ -34,7 +34,7 @@ fn update_removes_missing_content() {
|
||||||
|
|
||||||
let copy_options = fs_extra::dir::CopyOptions::new();
|
let copy_options = fs_extra::dir::CopyOptions::new();
|
||||||
fs_extra::dir::copy(
|
fs_extra::dir::copy(
|
||||||
&original_collection_dir,
|
original_collection_dir,
|
||||||
&builder.test_directory,
|
&builder.test_directory,
|
||||||
©_options,
|
©_options,
|
||||||
)
|
)
|
||||||
|
@ -55,7 +55,7 @@ fn update_removes_missing_content() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let khemmis_directory = test_collection_dir.join("Khemmis");
|
let khemmis_directory = test_collection_dir.join("Khemmis");
|
||||||
std::fs::remove_dir_all(&khemmis_directory).unwrap();
|
std::fs::remove_dir_all(khemmis_directory).unwrap();
|
||||||
ctx.index.update().unwrap();
|
ctx.index.update().unwrap();
|
||||||
{
|
{
|
||||||
let mut connection = ctx.db.connect().unwrap();
|
let mut connection = ctx.db.connect().unwrap();
|
||||||
|
@ -124,7 +124,7 @@ fn can_flatten_directory() {
|
||||||
.build();
|
.build();
|
||||||
ctx.index.update().unwrap();
|
ctx.index.update().unwrap();
|
||||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
|
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao"].iter().collect();
|
||||||
let songs = ctx.index.flatten(&path).unwrap();
|
let songs = ctx.index.flatten(path).unwrap();
|
||||||
assert_eq!(songs.len(), 8);
|
assert_eq!(songs.len(), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +135,7 @@ fn can_flatten_directory_with_shared_prefix() {
|
||||||
.build();
|
.build();
|
||||||
ctx.index.update().unwrap();
|
ctx.index.update().unwrap();
|
||||||
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); // Prefix of '(Picnic Remixes)'
|
let path: PathBuf = [TEST_MOUNT_NAME, "Tobokegao", "Picnic"].iter().collect(); // Prefix of '(Picnic Remixes)'
|
||||||
let songs = ctx.index.flatten(&path).unwrap();
|
let songs = ctx.index.flatten(path).unwrap();
|
||||||
assert_eq!(songs.len(), 7);
|
assert_eq!(songs.len(), 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ impl Manager {
|
||||||
|
|
||||||
if let Some(sleep_duration) = new_settings.reindex_every_n_seconds {
|
if let Some(sleep_duration) = new_settings.reindex_every_n_seconds {
|
||||||
diesel::update(misc_settings::table)
|
diesel::update(misc_settings::table)
|
||||||
.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration as i32))
|
.set(misc_settings::index_sleep_duration_seconds.eq(sleep_duration))
|
||||||
.execute(&mut connection)?;
|
.execute(&mut connection)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub struct ContextBuilder {
|
||||||
impl ContextBuilder {
|
impl ContextBuilder {
|
||||||
pub fn new(test_name: String) -> Self {
|
pub fn new(test_name: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
test_directory: prepare_test_directory(&test_name),
|
test_directory: prepare_test_directory(test_name),
|
||||||
config: config::Config::default(),
|
config: config::Config::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ pub enum Error {
|
||||||
CliArgsParsing(getopts::Fail),
|
CliArgsParsing(getopts::Fail),
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[error("Failed to turn polaris process into a daemon:\n\n{0}")]
|
#[error("Failed to turn polaris process into a daemon:\n\n{0}")]
|
||||||
Daemonize(daemonize::DaemonizeError),
|
Daemonize(daemonize::Error),
|
||||||
#[error("Could not create log directory `{0}`:\n\n{1}")]
|
#[error("Could not create log directory `{0}`:\n\n{1}")]
|
||||||
LogDirectoryCreationError(PathBuf, std::io::Error),
|
LogDirectoryCreationError(PathBuf, std::io::Error),
|
||||||
#[error("Could not create log file `{0}`:\n\n{1}")]
|
#[error("Could not create log file `{0}`:\n\n{1}")]
|
||||||
|
|
|
@ -12,6 +12,7 @@ use actix_web::{
|
||||||
FromRequest, HttpRequest, HttpResponse, Responder, ResponseError,
|
FromRequest, HttpRequest, HttpResponse, Responder, ResponseError,
|
||||||
};
|
};
|
||||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
|
use base64::prelude::*;
|
||||||
use futures_util::future::err;
|
use futures_util::future::err;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
@ -531,7 +532,7 @@ async fn get_audio(
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let named_file = NamedFile::open(&audio_path).map_err(|_| APIError::AudioFileIOError)?;
|
let named_file = NamedFile::open(audio_path).map_err(|_| APIError::AudioFileIOError)?;
|
||||||
Ok(MediaFile::new(named_file))
|
Ok(MediaFile::new(named_file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,8 +556,7 @@ async fn get_thumbnail(
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let named_file =
|
let named_file = NamedFile::open(thumbnail_path).map_err(|_| APIError::ThumbnailFileIOError)?;
|
||||||
NamedFile::open(&thumbnail_path).map_err(|_| APIError::ThumbnailFileIOError)?;
|
|
||||||
|
|
||||||
Ok(MediaFile::new(named_file))
|
Ok(MediaFile::new(named_file))
|
||||||
}
|
}
|
||||||
|
@ -671,7 +671,8 @@ async fn lastfm_link(
|
||||||
let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
|
let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
|
||||||
|
|
||||||
// Base64 decode
|
// Base64 decode
|
||||||
let popup_content = base64::decode(base64_content.as_bytes())
|
let popup_content = BASE64_STANDARD_NO_PAD
|
||||||
|
.decode(base64_content.as_bytes())
|
||||||
.map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?;
|
.map_err(|_| APIError::LastFMLinkContentBase64DecodeError)?;
|
||||||
|
|
||||||
// UTF-8 decode
|
// UTF-8 decode
|
||||||
|
|
|
@ -162,6 +162,7 @@ impl From<ddns::Error> for APIError {
|
||||||
ddns::Error::Database(e) => APIError::Database(e),
|
ddns::Error::Database(e) => APIError::Database(e),
|
||||||
ddns::Error::DatabaseConnection(e) => e.into(),
|
ddns::Error::DatabaseConnection(e) => e.into(),
|
||||||
ddns::Error::UpdateQueryFailed(s) => APIError::DdnsUpdateQueryFailed(s),
|
ddns::Error::UpdateQueryFailed(s) => APIError::DdnsUpdateQueryFailed(s),
|
||||||
|
ddns::Error::UpdateQueryTransport => APIError::DdnsUpdateQueryFailed(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue