feat: tile view, in-app streaming, and self-painted title bar
Clicking an undownloaded video now plays it in the app instead of handing off to the browser. YouTube's iframe embed cannot be used — it rejects a tauri:// origin with Error 153 — so yt-dlp resolves YouTube's HLS master playlist instead, whose H.264+AAC variants AVFoundation streams natively in WKWebView, adaptive up to 1080p. Adds an optional Tiles view alongside the list, remembered between launches. The title bar is now transparent with the title hidden, and the app paints that strip itself in the page background so it matches instead of showing macOS chrome beside the traffic lights.
This commit is contained in:
@@ -165,6 +165,61 @@ pub async fn import_takeout_csv(
|
||||
db.replace_channels(&channels)
|
||||
}
|
||||
|
||||
/// Resolves a directly playable URL for a video we have NOT downloaded, so it
|
||||
/// can stream inside the app's own player.
|
||||
///
|
||||
/// YouTube's iframe embed is not an option here: it rejects a Tauri window with
|
||||
/// "Error 153" because the page origin is `tauri://localhost` rather than an
|
||||
/// http(s) origin it will accept. Instead we ask yt-dlp for YouTube's HLS master
|
||||
/// playlist, which lists H.264 + AAC variants up to 1080p with separate audio
|
||||
/// tracks — exactly the shape AVFoundation plays natively in WKWebView, with
|
||||
/// adaptive bitrate for free.
|
||||
#[tauri::command]
|
||||
pub async fn resolve_stream(video_id: String) -> Result<String, String> {
|
||||
let url = format!("https://www.youtube.com/watch?v={video_id}");
|
||||
|
||||
// The HLS master playlist. Every m3u8 format shares the same manifest_url,
|
||||
// so any one of them yields the master.
|
||||
if let Some(u) = yt_dlp_print(
|
||||
&["-f", "bv*[protocol^=m3u8]", "--print", "%(manifest_url)s", &url],
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Ok(u);
|
||||
}
|
||||
|
||||
// Rare fallback: an old-style progressive muxed MP4.
|
||||
if let Some(u) = yt_dlp_print(&[
|
||||
"-f",
|
||||
"b[ext=mp4][acodec!=none][vcodec!=none]",
|
||||
"--print",
|
||||
"%(url)s",
|
||||
&url,
|
||||
])
|
||||
.await
|
||||
{
|
||||
return Ok(u);
|
||||
}
|
||||
|
||||
Err("Could not find a playable stream for this video.".into())
|
||||
}
|
||||
|
||||
/// Runs yt-dlp and returns its first non-empty stdout line, or None.
|
||||
async fn yt_dlp_print(args: &[&str]) -> Option<String> {
|
||||
let mut cmd = tokio::process::Command::new(bin("yt-dlp"));
|
||||
cmd.args(["--no-warnings", "--no-playlist", "--simulate"]);
|
||||
cmd.args(args);
|
||||
let out = cmd.output().await.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find(|l| l.starts_with("http"))
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_channels(state: State<'_, AppState>) -> Result<Vec<ChannelWithCount>, String> {
|
||||
state.db.lock().await.list_channels()
|
||||
|
||||
@@ -25,6 +25,7 @@ pub fn run() {
|
||||
commands::preview_takeout_import,
|
||||
commands::list_channels,
|
||||
commands::list_feed,
|
||||
commands::resolve_stream,
|
||||
commands::refresh_feeds,
|
||||
commands::download_video,
|
||||
commands::cancel_download,
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
"height": 780,
|
||||
"minWidth": 900,
|
||||
"minHeight": 560,
|
||||
"center": true
|
||||
"center": true,
|
||||
"titleBarStyle": "Transparent",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
Reference in New Issue
Block a user