Skip to content

Commit 358d609

Browse files
committed
Many fixes, Last FM integration, Local Files fixes, Remmber Window Size LocationSetting, Search bar in playlist
1 parent a1b7625 commit 358d609

24 files changed

Lines changed: 1133 additions & 210 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "just-another-music-client",
33
"private": true,
4-
"version": "1.2.74",
4+
"version": "1.2.80",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "just-another-music-client"
3-
version = "1.2.74"
3+
version = "1.2.80"
44
description = "A desktop music client"
55
authors = ["you"]
66
edition = "2021"

src-tauri/capabilities/default.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"core:window:allow-outer-size",
2929
"core:window:allow-current-monitor",
3030
"core:window:allow-set-position",
31+
"core:window:allow-set-size",
3132
"core:window:allow-cursor-position",
3233
"core:window:allow-set-cursor-icon",
3334
"core:window:allow-set-ignore-cursor-events"
@@ -67,7 +68,13 @@
6768
"delete_youtube_credentials",
6869
"load_youtube_music_cookie",
6970
"sign_in_youtube_music",
70-
"delete_youtube_music_cookie"
71+
"delete_youtube_music_cookie",
72+
"lastfm_auth_token",
73+
"lastfm_complete_auth",
74+
"lastfm_disconnect",
75+
"lastfm_get_session",
76+
"lastfm_scrobble",
77+
"lastfm_update_now_playing"
7178
]
7279
}
7380
}

src-tauri/src/lastfm.rs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
33
use std::collections::BTreeMap;
44

55
const LASTFM_API_URL: &str = "https://ws.audioscrobbler.com/2.0/";
6+
const LASTFM_API_KEY: &str = "802624099e59adcc599c9f6e6aa60371";
7+
const LASTFM_SHARED_SECRET: &str = "186e5d4fa717d145ece7e50aff07757c";
68
const LASTFM_KEYRING_USER: &str = "lastfm-session-v1";
79

810
#[derive(Serialize, Deserialize)]
@@ -66,20 +68,8 @@ pub struct LastFmScrobbleInput {
6668
timestamp: u64,
6769
}
6870

69-
fn configured_credentials() -> Result<(&'static str, &'static str), CommandError> {
70-
let api_key = option_env!("LASTFM_API_KEY")
71-
.map(str::trim)
72-
.filter(|value| !value.is_empty());
73-
let shared_secret = option_env!("LASTFM_SHARED_SECRET")
74-
.map(str::trim)
75-
.filter(|value| !value.is_empty());
76-
77-
match (api_key, shared_secret) {
78-
(Some(api_key), Some(shared_secret)) => Ok((api_key, shared_secret)),
79-
_ => Err(CommandError {
80-
message: "Last.fm API credentials are not configured for this build.".to_string(),
81-
}),
82-
}
71+
fn configured_credentials() -> (&'static str, &'static str) {
72+
(LASTFM_API_KEY, LASTFM_SHARED_SECRET)
8373
}
8474

8575
fn lastfm_keyring_entry() -> Result<keyring::Entry, CommandError> {
@@ -135,7 +125,7 @@ fn api_signature(params: &BTreeMap<String, String>, shared_secret: &str) -> Stri
135125
async fn signed_lastfm_post<T: for<'de> Deserialize<'de>>(
136126
mut params: BTreeMap<String, String>,
137127
) -> Result<T, CommandError> {
138-
let (api_key, shared_secret) = configured_credentials()?;
128+
let (api_key, shared_secret) = configured_credentials();
139129
params.insert("api_key".to_string(), api_key.to_string());
140130
let signature = api_signature(&params, shared_secret);
141131
params.insert("api_sig".to_string(), signature);
@@ -194,7 +184,7 @@ fn clean_metadata(value: String, label: &str) -> Result<String, CommandError> {
194184

195185
#[tauri::command]
196186
pub async fn lastfm_auth_token() -> Result<LastFmAuthStart, CommandError> {
197-
let (api_key, _) = configured_credentials()?;
187+
let (api_key, _) = configured_credentials();
198188
let mut params = BTreeMap::new();
199189
params.insert("method".to_string(), "auth.getToken".to_string());
200190
let response = signed_lastfm_post::<LastFmTokenResponse>(params).await?;

src-tauri/src/lib.rs

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::fs::{self, File, OpenOptions};
99
use std::io::Write;
1010
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
1111
use std::path::{Path, PathBuf};
12-
use std::sync::{Arc, Mutex, OnceLock};
12+
use std::sync::{Mutex, OnceLock};
1313
use std::thread;
1414
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
1515

@@ -62,9 +62,6 @@ const YOUTUBE_COOKIE_CHUNK_SIZE: usize = 900;
6262
const YOUTUBE_COOKIE_MAX_CHUNKS: usize = 16;
6363
const DEFAULT_CACHE_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
6464
const CURRENT_LOG_FILE_NAME: &str = "current.log";
65-
const MAIN_WEBVIEW_SLEEP_RECOVERY_DELAY_MS: u64 = 900;
66-
const MAIN_WEBVIEW_RECOVERY_RELOAD_GRACE_MS: u64 = 150;
67-
const MAIN_WEBVIEW_FOCUS_RECOVERY_IDLE_MS: u64 = 60_000;
6865

6966
static APP_LOG_FILE: OnceLock<Mutex<Option<File>>> = OnceLock::new();
7067

@@ -688,34 +685,6 @@ fn quit_app(app: tauri::AppHandle) {
688685
app.exit(0);
689686
}
690687

691-
fn schedule_main_webview_recovery(app: tauri::AppHandle, reason: &'static str) {
692-
thread::spawn(move || {
693-
thread::sleep(Duration::from_millis(MAIN_WEBVIEW_SLEEP_RECOVERY_DELAY_MS));
694-
695-
let Some(main) = app.get_webview_window("main") else {
696-
eprintln!(
697-
"[internal][tauri][warn] main webview recovery skipped reason={} cause=missing_window",
698-
reason
699-
);
700-
return;
701-
};
702-
703-
eprintln!(
704-
"[internal][tauri][info] main webview recovery reload reason={}",
705-
reason
706-
);
707-
let _ = app.emit("main-window-recovery-reload", reason);
708-
thread::sleep(Duration::from_millis(MAIN_WEBVIEW_RECOVERY_RELOAD_GRACE_MS));
709-
710-
if let Err(error) = main.reload() {
711-
eprintln!(
712-
"[internal][tauri][warn] main webview recovery reload failed reason={} error={}",
713-
reason, error
714-
);
715-
}
716-
});
717-
}
718-
719688
#[tauri::command]
720689
fn frontend_log(level: String, context: String, payload: String) {
721690
eprintln!("[internal][frontend][{}] {} {}", level, context, payload);
@@ -2049,9 +2018,6 @@ pub fn run() {
20492018
#[cfg(target_os = "macos")]
20502019
let builder = builder.manage(macos_media::MacosMediaSession::new());
20512020

2052-
let last_main_focus_lost_at = Arc::new(Mutex::new(None::<Instant>));
2053-
let window_event_last_main_focus_lost_at = Arc::clone(&last_main_focus_lost_at);
2054-
20552021
builder
20562022
.setup(|app| {
20572023
if let Err(error) = initialize_app_log(app.handle()) {
@@ -2072,11 +2038,6 @@ pub fn run() {
20722038
}
20732039
tauri::WindowEvent::Focused(false) => {
20742040
if window.label() == "main" {
2075-
if let Ok(mut last_focus_lost_at) = window_event_last_main_focus_lost_at.lock()
2076-
{
2077-
*last_focus_lost_at = Some(Instant::now());
2078-
}
2079-
20802041
let app = window.app_handle().clone();
20812042
thread::spawn(move || {
20822043
thread::sleep(Duration::from_millis(100));
@@ -2099,21 +2060,7 @@ pub fn run() {
20992060
}
21002061
tauri::WindowEvent::Focused(true) => {
21012062
if window.label() == "main" {
2102-
let app = window.app_handle().clone();
2103-
let _ = app.emit("window-focused", ());
2104-
2105-
let was_idle_long_enough = window_event_last_main_focus_lost_at
2106-
.lock()
2107-
.ok()
2108-
.and_then(|mut last_focus_lost_at| last_focus_lost_at.take())
2109-
.is_some_and(|lost_at| {
2110-
lost_at.elapsed()
2111-
>= Duration::from_millis(MAIN_WEBVIEW_FOCUS_RECOVERY_IDLE_MS)
2112-
});
2113-
2114-
if was_idle_long_enough {
2115-
schedule_main_webview_recovery(app, "main-focus-return-after-idle");
2116-
}
2063+
let _ = window.app_handle().emit("window-focused", ());
21172064
}
21182065
}
21192066
_ => {}

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Just Another Music Client",
4-
"version": "1.2.74",
4+
"version": "1.2.80",
55
"identifier": "com.justanothermusicclient.desktop",
66
"build": {
77
"beforeDevCommand": "npm run dev",

src/datasource/youtube/YouTubeMusicDataSource.ts

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ const ARTIST_CACHE_VERSION = "v3";
211211
const ARTIST_SUBSCRIPTION_OVERRIDE_MS = 60_000;
212212
const PLAYLIST_PAGE_SESSION_TTL_MS = 10 * 60_000;
213213
const PLAYLIST_TRACK_CACHE_VERSION = "v4";
214+
const PLAYLIST_EMPTY_RETRY_DELAYS_MS = [0, 600, 1_500];
214215

215216
class YouTubeMusicAuthError extends Error {
216217
constructor(message: string) {
@@ -1191,6 +1192,25 @@ export class YouTubeMusicDataSource extends DataSource {
11911192
});
11921193
}
11931194

1195+
private async updateCachedAlbumSaved(album: Album, saved: boolean): Promise<void> {
1196+
const cachedLibrary = await getCachedJson<LibrarySnapshot>(LIBRARY_CACHE_KEY);
1197+
if (!cachedLibrary) return;
1198+
1199+
const sameAlbum = (item: Album) =>
1200+
item.id === album.id
1201+
|| Boolean(album.playlistId && item.playlistId === album.playlistId)
1202+
|| Boolean(album.playlistId && item.id === album.playlistId)
1203+
|| Boolean(item.playlistId && item.playlistId === album.id);
1204+
const albums = saved
1205+
? [album, ...cachedLibrary.albums.filter((item) => !sameAlbum(item))]
1206+
: cachedLibrary.albums.filter((item) => !sameAlbum(item));
1207+
1208+
await setCachedJson(LIBRARY_CACHE_KEY, {
1209+
...cachedLibrary,
1210+
albums,
1211+
});
1212+
}
1213+
11941214
private getActionableServiceEndpoint(endpoint: RawServiceEndpoint): RawServiceEndpoint {
11951215
const commands = endpoint.commandExecutorCommand?.commands;
11961216
if (!commands?.length) return endpoint;
@@ -1493,6 +1513,39 @@ export class YouTubeMusicDataSource extends DataSource {
14931513
return tracks;
14941514
}
14951515

1516+
private async waitForPlaylistEmptyRetry(attempt: number): Promise<void> {
1517+
const delayMs = PLAYLIST_EMPTY_RETRY_DELAYS_MS[attempt] ?? 0;
1518+
if (delayMs <= 0) return;
1519+
await new Promise<void>((resolve) => globalThis.setTimeout(resolve, delayMs));
1520+
}
1521+
1522+
private async collectPlaylistTracksWithEmptyRetries(
1523+
client: Innertube,
1524+
playlistId: string,
1525+
source: string,
1526+
): Promise<Track[]> {
1527+
for (let attempt = 0; attempt < PLAYLIST_EMPTY_RETRY_DELAYS_MS.length; attempt += 1) {
1528+
await this.waitForPlaylistEmptyRetry(attempt);
1529+
1530+
const tracks = await this.collectPlaylistTracks(client, playlistId);
1531+
if (tracks.length > 0) return tracks;
1532+
1533+
const browseId = playlistId.startsWith("VL") ? playlistId : `VL${playlistId}`;
1534+
logInternalWarn("YouTubeMusicDataSource.collectPlaylistTracksWithEmptyRetries retrying empty response", {
1535+
playlistId,
1536+
browseId,
1537+
source,
1538+
attempt: attempt + 1,
1539+
});
1540+
1541+
const response = await this.executeMusicBrowse(client, { browseId });
1542+
const fallbackTracks = await this.collectAllTracks(client, response);
1543+
if (fallbackTracks.length > 0) return fallbackTracks;
1544+
}
1545+
1546+
return [];
1547+
}
1548+
14961549
private createPlaylistPageKey(playlistId: string): string {
14971550
return `${playlistId}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
14981551
}
@@ -2171,6 +2224,7 @@ export class YouTubeMusicDataSource extends DataSource {
21712224
albumPlaylistId,
21722225
saved,
21732226
});
2227+
await this.updateCachedAlbumSaved(album, saved);
21742228
return;
21752229
} catch (directError) {
21762230
logInternalWarn("YouTubeMusicDataSource.setAlbumSaved direct like command failed", {
@@ -2185,7 +2239,10 @@ export class YouTubeMusicDataSource extends DataSource {
21852239
const rawToggle = this.findRawLibraryToggle(albumResponse);
21862240

21872241
if (rawToggle) {
2188-
if (rawToggle.isToggled === saved) return;
2242+
if (rawToggle.isToggled === saved) {
2243+
await this.updateCachedAlbumSaved(album, saved);
2244+
return;
2245+
}
21892246
const endpoint = saved
21902247
? rawToggle.defaultServiceEndpoint
21912248
: rawToggle.toggledServiceEndpoint;
@@ -2205,12 +2262,16 @@ export class YouTubeMusicDataSource extends DataSource {
22052262
if (response.success === false) {
22062263
throw new Error(`Album library update returned HTTP ${response.status_code}.`);
22072264
}
2265+
await this.updateCachedAlbumSaved(album, saved);
22082266
return;
22092267
}
22102268

22112269
const rawMenuToggle = this.findRawLibraryMenuToggle(albumResponse);
22122270
if (rawMenuToggle) {
2213-
if (rawMenuToggle.isToggled === saved) return;
2271+
if (rawMenuToggle.isToggled === saved) {
2272+
await this.updateCachedAlbumSaved(album, saved);
2273+
return;
2274+
}
22142275
const endpoint = saved
22152276
? rawMenuToggle.defaultServiceEndpoint
22162277
: rawMenuToggle.toggledServiceEndpoint;
@@ -2231,6 +2292,7 @@ export class YouTubeMusicDataSource extends DataSource {
22312292
if (response.success === false) {
22322293
throw new Error(`Album library menu update returned HTTP ${response.status_code}.`);
22332294
}
2295+
await this.updateCachedAlbumSaved(album, saved);
22342296
return;
22352297
}
22362298

@@ -2239,7 +2301,10 @@ export class YouTubeMusicDataSource extends DataSource {
22392301
if (!toggle) {
22402302
throw new Error("YouTube Music did not return a library command for this album.");
22412303
}
2242-
if (toggle.isToggled === saved) return;
2304+
if (toggle.isToggled === saved) {
2305+
await this.updateCachedAlbumSaved(album, saved);
2306+
return;
2307+
}
22432308

22442309
const endpoint = saved ? toggle.endpoint : toggle.toggledEndpoint;
22452310
if (!endpoint) {
@@ -2258,6 +2323,7 @@ export class YouTubeMusicDataSource extends DataSource {
22582323
if (response.success === false) {
22592324
throw new Error(`Album library update returned HTTP ${response.status_code}.`);
22602325
}
2326+
await this.updateCachedAlbumSaved(album, saved);
22612327
} catch (error) {
22622328
logInternalError("YouTubeMusicDataSource.setAlbumSaved failed", error, {
22632329
albumId: album.id,
@@ -2728,7 +2794,11 @@ export class YouTubeMusicDataSource extends DataSource {
27282794
logInternalWarn("YouTubeMusicDataSource.getPlaylistTrackPage verifying empty first page", {
27292795
playlistId: playlist.id,
27302796
});
2731-
const fallbackTracks = await this.collectPlaylistTracks(client, playlist.id);
2797+
const fallbackTracks = await this.collectPlaylistTracksWithEmptyRetries(
2798+
client,
2799+
playlist.id,
2800+
"paged-first-load",
2801+
);
27322802
if (fallbackTracks.length > 0) {
27332803
const cachedFallbackTracks = await this.cachePlaylistTracks(playlist.id, fallbackTracks);
27342804
return { tracks: cachedFallbackTracks, hasMore: false };
@@ -2787,16 +2857,7 @@ export class YouTubeMusicDataSource extends DataSource {
27872857

27882858
private async fetchPlaylistTracksFresh(playlist: Playlist): Promise<Track[]> {
27892859
const client = await this.getMusicClient();
2790-
const tracks = await this.collectPlaylistTracks(client, playlist.id);
2791-
if (tracks.length > 0) return tracks;
2792-
2793-
const browseId = playlist.id.startsWith("VL") ? playlist.id : `VL${playlist.id}`;
2794-
logInternalWarn("YouTubeMusicDataSource.fetchPlaylistTracksFresh retrying empty playlist response", {
2795-
playlistId: playlist.id,
2796-
browseId,
2797-
});
2798-
const response = await this.executeMusicBrowse(client, { browseId });
2799-
return this.collectAllTracks(client, response);
2860+
return this.collectPlaylistTracksWithEmptyRetries(client, playlist.id, "fresh-load");
28002861
}
28012862

28022863
async addTrackToPlaylist(

0 commit comments

Comments
 (0)