addon: switch play action to resolve_play (yt-dlp combined format)
Realized during M6 packaging that the rustypipe path returns separate audio + video DASH streams (Opus 251 + AV1 401 on the smoke video). Kodi can't sync those without an inputstream.adaptive DASH manifest, which would need server-side manifest generation — M3+ territory. Stopgap for shippable M3: new sidecar op resolve_play that asks yt-dlp for -f best[ext=mp4]/best — one combined audio+video URL Kodi plays as plain HTTP. ~3-5s overhead vs rustypipe but reliable sync. main.py _play() now calls resolve_play. resolve still exists for metadata + browse paths (M4 will use it). Rebuilt aarch64-musl binary, repackaged plugin.video.torttube-0.0.1.zip (38.7MB, md5 f2c08aed130b1c1bd231a9b6cbfac93c). Live at: smb://lucy/downloads/torttube/plugin.video.torttube-0.0.1.zip
This commit is contained in:
parent
f4ceae3b70
commit
283693525c
3 changed files with 76 additions and 33 deletions
|
|
@ -24,7 +24,14 @@ mod sponsor;
|
|||
#[serde(tag = "op", rename_all = "snake_case")]
|
||||
enum Request {
|
||||
Ping,
|
||||
/// Metadata-rich resolve (rustypipe → yt-dlp). May return separate
|
||||
/// audio + video streams; the addon must mux or use a DASH wrapper.
|
||||
Resolve { id: String },
|
||||
/// Playback-ready resolve: returns ONE combined audio+video URL via
|
||||
/// yt-dlp `-f best[ext=mp4]/best`. Kodi plays it as a plain HTTP URL,
|
||||
/// no inputstream.adaptive needed. Slower than `resolve` but always
|
||||
/// gives a working stream.
|
||||
ResolvePlay { id: String },
|
||||
Rip { id: String, dest_dir: String },
|
||||
Sponsorblock {
|
||||
id: String,
|
||||
|
|
@ -120,6 +127,10 @@ async fn handle_line(line: &str) -> Response {
|
|||
Ok(v) => Response::ok(v),
|
||||
Err(e) => e.into(),
|
||||
},
|
||||
Request::ResolvePlay { id } => match resolve::resolve_play(&id).await {
|
||||
Ok(v) => Response::ok(v),
|
||||
Err(e) => e.into(),
|
||||
},
|
||||
Request::Rip { id, dest_dir } => match rip::rip(&id, &dest_dir).await {
|
||||
Ok(v) => Response::ok(v),
|
||||
Err(e) => e.into(),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,62 @@ use serde_json::Value;
|
|||
|
||||
use crate::{run_yt_dlp, HandlerError};
|
||||
|
||||
/// Top-level resolve. Tries Tier 1 (rustypipe), falls back to Tier 2 (yt-dlp -j).
|
||||
/// Playback-ready single-URL resolve. Asks yt-dlp for `best[ext=mp4]/best` —
|
||||
/// a combined audio+video format that Kodi can play as a plain HTTP URL.
|
||||
/// Slower than `resolve()` (~3-5s) but guarantees a working stream.
|
||||
pub(crate) async fn resolve_play(id: &str) -> Result<Value, HandlerError> {
|
||||
let url = format!("https://www.youtube.com/watch?v={id}");
|
||||
// -f best[ext=mp4]/best — prefer mp4 progressive, else any best combined.
|
||||
// -g prints just the URL. We use -j to also get title/duration for the
|
||||
// ListItem; the URL is then "url" at the top level.
|
||||
let stdout = run_yt_dlp(&[
|
||||
"-j", "--no-warnings", "--no-playlist",
|
||||
"-f", "best[ext=mp4]/best",
|
||||
&url,
|
||||
])
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = e.to_string().to_lowercase();
|
||||
if msg.contains("age") {
|
||||
HandlerError::AgeRestricted
|
||||
} else if msg.contains("private") {
|
||||
HandlerError::PrivateVideo
|
||||
} else if msg.contains("not available") || msg.contains("does not exist") {
|
||||
HandlerError::NotFound
|
||||
} else if msg.contains("geo") || msg.contains("region") {
|
||||
HandlerError::RegionBlocked
|
||||
} else {
|
||||
HandlerError::Extractor(msg)
|
||||
}
|
||||
})?;
|
||||
|
||||
let dump: Value = serde_json::from_slice(&stdout)
|
||||
.map_err(|e| HandlerError::Extractor(format!("yt-dlp json parse: {e}")))?;
|
||||
|
||||
let stream_url = dump
|
||||
.get("url")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| HandlerError::Extractor("yt-dlp: no top-level url".into()))?
|
||||
.to_string();
|
||||
|
||||
tracing::info!(id, "resolve_play ok via yt-dlp combined");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"source": "yt-dlp",
|
||||
"stream_url": stream_url,
|
||||
"title": dump.get("title"),
|
||||
"duration_s": dump.get("duration"),
|
||||
"channel_name": dump.get("channel"),
|
||||
"channel_id": dump.get("channel_id"),
|
||||
"thumbnail": dump.get("thumbnail"),
|
||||
"format_id": dump.get("format_id"),
|
||||
"ext": dump.get("ext"),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Metadata-rich resolve. Tries Tier 1 (rustypipe), falls back to Tier 2 (yt-dlp -j).
|
||||
/// Returns the full extractor response including separate audio + video streams.
|
||||
/// Use `resolve_play` instead for direct playback (returns a single combined URL).
|
||||
pub(crate) async fn resolve(id: &str) -> Result<Value, HandlerError> {
|
||||
match tier1_rustypipe(id).await {
|
||||
Ok(v) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue