Play direct media links without Yattee Server by probing the URL first

Debrid and CDN download links (TorBox, Real-Debrid, presigned S3) hide the
file behind an opaque path, so the extension-based direct media check missed
them and every one was sent to Yattee Server for yt-dlp extraction, failing
outright when no server is configured.

Add DirectMediaProbe: a HEAD request classifies the response by Content-Type
and Content-Disposition, and when the type is generic or HEAD is refused, a
ranged GET sniffs the first bytes for container signatures (Matroska/WebM,
ISO BMFF, Ogg, FLAC, MP3, RIFF, FLV, ASF, MPEG-PS/TS, HLS and DASH
manifests). Anything playable goes straight to mpv with the server-reported
filename as the title; everything else falls back to extraction as before.

The probe runs ahead of the Yattee Server path in ExternalVideoView, both
OpenLinkSheet flows and the deep-link download path, and PlayerService
rebuilds direct_media streams itself so replays from history no longer
depend on a server either.
This commit is contained in:
Arkadiusz Fal
2026-09-21 20:03:06 +02:00
parent 53fda7574c
commit 6efd60a18e
7 changed files with 515 additions and 8 deletions

View File

@@ -0,0 +1,315 @@
//
// DirectMediaProbe.swift
// Yattee
//
// Decides whether an arbitrary http(s) URL serves a media file mpv can play
// as-is, so the app can skip Yattee Server extraction for debrid/CDN download
// links whose path is an opaque ID (TorBox, Real-Debrid, presigned S3, …).
//
import Foundation
/// What the probe learned about a URL that serves playable media.
struct DirectMediaProbeResult: Sendable, Equatable {
/// Filename reported by the server (Content-Disposition), if any.
let filename: String?
/// Media type the server reported, or the one sniffed from the payload.
let mimeType: String?
/// Content-Length, if the server sent one.
let contentLength: Int64?
}
enum DirectMediaProbe {
/// How long a probe may take before falling back to extraction.
static let timeout: TimeInterval = 10
/// How many leading bytes to sniff when the server's type is generic.
static let sniffLength = 512
// MARK: - Network
/// Asks the server what it serves at `url` and returns a result when it is
/// something mpv can play directly. `nil` means "not obviously media",
/// e.g. an HTML page, and the caller should try Yattee Server instead.
static func probe(_ url: URL, session: URLSession = .shared) async -> DirectMediaProbeResult? {
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
return nil
}
var headResult: HeaderVerdict?
if let response = await head(url, session: session) {
headResult = classify(response)
switch headResult {
case .media(let result):
LoggingService.shared.debug(
"[DirectMediaProbe] HEAD says media: \(result.mimeType ?? "-") \(result.filename ?? "")",
category: .player
)
return result
case .notMedia:
LoggingService.shared.debug("[DirectMediaProbe] HEAD says not media for \(url.host ?? "")", category: .player)
return nil
case .unknown, .none:
break
}
}
// HEAD failed or the type was generic: look at the first bytes.
guard let sniffed = await sniff(url, session: session) else {
return nil
}
let headers = sniffed.headers
guard let mimeType = sniffMimeType(sniffed.data) ?? mediaTypeFromFilename(headers) else {
LoggingService.shared.debug("[DirectMediaProbe] payload not recognised for \(url.host ?? "")", category: .player)
return nil
}
LoggingService.shared.debug("[DirectMediaProbe] sniffed \(mimeType) for \(url.host ?? "")", category: .player)
return DirectMediaProbeResult(
filename: headers.filename,
mimeType: mimeType,
contentLength: headers.totalLength
)
}
private static func head(_ url: URL, session: URLSession) async -> HTTPURLResponse? {
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
request.httpMethod = "HEAD"
applyUserAgent(&request)
do {
let (_, response) = try await session.data(for: request)
return response as? HTTPURLResponse
} catch {
LoggingService.shared.debug("[DirectMediaProbe] HEAD failed: \(error.localizedDescription)", category: .player)
return nil
}
}
private static func sniff(_ url: URL, session: URLSession) async -> (headers: ResponseHeaders, data: Data)? {
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
request.setValue("bytes=0-\(sniffLength - 1)", forHTTPHeaderField: "Range")
applyUserAgent(&request)
do {
let (bytes, response) = try await session.bytes(for: request)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
return nil
}
var data = Data()
data.reserveCapacity(sniffLength)
for try await byte in bytes {
data.append(byte)
if data.count >= sniffLength { break }
}
return (ResponseHeaders(http), data)
} catch {
LoggingService.shared.debug("[DirectMediaProbe] sniff failed: \(error.localizedDescription)", category: .player)
return nil
}
}
private static func applyUserAgent(_ request: inout URLRequest) {
let settings = UserAgentConfiguration.shared.current()
if let userAgent = settings.userAgent, !userAgent.isEmpty {
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
}
}
// MARK: - Header classification
enum HeaderVerdict: Equatable {
case media(DirectMediaProbeResult)
case notMedia
/// Generic type (octet-stream, missing) — needs a payload sniff.
case unknown
}
/// The headers that matter, pulled out of an `HTTPURLResponse`.
struct ResponseHeaders {
let statusCode: Int
let contentType: String?
let contentDisposition: String?
let contentLength: Int64?
let contentRange: String?
init(_ response: HTTPURLResponse) {
statusCode = response.statusCode
contentType = response.value(forHTTPHeaderField: "Content-Type")
contentDisposition = response.value(forHTTPHeaderField: "Content-Disposition")
contentLength = response.value(forHTTPHeaderField: "Content-Length").flatMap { Int64($0) }
contentRange = response.value(forHTTPHeaderField: "Content-Range")
}
init(statusCode: Int = 200, contentType: String? = nil, contentDisposition: String? = nil,
contentLength: Int64? = nil, contentRange: String? = nil) {
self.statusCode = statusCode
self.contentType = contentType
self.contentDisposition = contentDisposition
self.contentLength = contentLength
self.contentRange = contentRange
}
/// Filename from Content-Disposition (`filename*=` wins over `filename=`).
var filename: String? {
DirectMediaProbe.filename(fromContentDisposition: contentDisposition)
}
/// The full size: Content-Range's total for a 206, else Content-Length.
var totalLength: Int64? {
if let contentRange,
let slash = contentRange.lastIndex(of: "/"),
let total = Int64(contentRange[contentRange.index(after: slash)...].trimmingCharacters(in: .whitespaces)) {
return total
}
return contentLength
}
/// The media type without parameters, lowercased.
var essenceType: String? {
guard let contentType else { return nil }
let essence = contentType.split(separator: ";", maxSplits: 1).first.map(String.init) ?? contentType
let trimmed = essence.trimmingCharacters(in: .whitespaces).lowercased()
return trimmed.isEmpty ? nil : trimmed
}
}
static func classify(_ response: HTTPURLResponse) -> HeaderVerdict {
classify(ResponseHeaders(response))
}
static func classify(_ headers: ResponseHeaders) -> HeaderVerdict {
guard (200...299).contains(headers.statusCode) else {
// 403/405 on HEAD is common for CDNs that only serve GET; let the sniff decide.
return .unknown
}
let type = headers.essenceType
let filename = headers.filename
if let type, isPlayableMediaType(type) {
return .media(DirectMediaProbeResult(filename: filename, mimeType: type, contentLength: headers.totalLength))
}
// A generic type with a media filename attached is still a media file.
if let fromName = mediaTypeFromFilename(headers), type.map(isGenericType) ?? true {
return .media(DirectMediaProbeResult(filename: filename, mimeType: fromName, contentLength: headers.totalLength))
}
guard let type else { return .unknown }
if isGenericType(type) { return .unknown }
return .notMedia
}
/// Types mpv plays straight from the network.
static func isPlayableMediaType(_ type: String) -> Bool {
if type.hasPrefix("video/") || type.hasPrefix("audio/") { return true }
return [
"application/vnd.apple.mpegurl",
"application/x-mpegurl",
"application/dash+xml",
"application/mp4",
"application/x-matroska",
"application/ogg",
"application/x-flac",
"application/mpeg",
"application/vnd.rn-realmedia",
].contains(type)
}
/// Types that say nothing about the payload.
static func isGenericType(_ type: String) -> Bool {
[
"application/octet-stream",
"application/x-download",
"application/force-download",
"application/download",
"binary/octet-stream",
"application/unknown",
].contains(type)
}
/// A media type derived from the Content-Disposition filename's extension.
static func mediaTypeFromFilename(_ headers: ResponseHeaders) -> String? {
guard let filename = headers.filename else { return nil }
let ext = (filename as NSString).pathExtension.lowercased()
guard DirectMediaHelper.allExtensions.contains(ext) else { return nil }
return DirectMediaHelper.mimeType(for: ext) ?? "video/\(ext)"
}
/// Parses `filename*=UTF-8''…` or `filename="…"` out of a Content-Disposition value.
static func filename(fromContentDisposition value: String?) -> String? {
guard let value else { return nil }
var plain: String?
for part in value.split(separator: ";").dropFirst() {
let trimmed = part.trimmingCharacters(in: .whitespaces)
guard let eq = trimmed.firstIndex(of: "=") else { continue }
let key = trimmed[..<eq].trimmingCharacters(in: .whitespaces).lowercased()
var raw = trimmed[trimmed.index(after: eq)...].trimmingCharacters(in: .whitespaces)
if key == "filename*" {
// RFC 5987: charset'lang'percent-encoded
if let lastQuote = raw.lastIndex(of: "'") {
raw = String(raw[raw.index(after: lastQuote)...])
}
raw = raw.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
if let decoded = raw.removingPercentEncoding, !decoded.isEmpty {
return (decoded as NSString).lastPathComponent
}
} else if key == "filename" {
raw = raw.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
if !raw.isEmpty { plain = (raw as NSString).lastPathComponent }
}
}
return plain
}
// MARK: - Payload sniffing
/// Recognises common container signatures in the first bytes of a file.
static func sniffMimeType(_ data: Data) -> String? {
guard data.count >= 4 else { return nil }
let bytes = [UInt8](data.prefix(sniffLength))
func starts(with signature: [UInt8], at offset: Int = 0) -> Bool {
guard bytes.count >= offset + signature.count else { return false }
return Array(bytes[offset..<offset + signature.count]) == signature
}
func ascii(_ s: String) -> [UInt8] { Array(s.utf8) }
// EBML: Matroska or WebM. The DocType string follows within the header.
if starts(with: [0x1A, 0x45, 0xDF, 0xA3]) {
if let text = String(bytes: bytes.prefix(64), encoding: .isoLatin1), text.contains("webm") {
return "video/webm"
}
return "video/x-matroska"
}
// ISO BMFF: mp4/mov/m4a/3gp — "ftyp" at offset 4.
if starts(with: ascii("ftyp"), at: 4) {
let brand = String(bytes: bytes[8..<min(12, bytes.count)], encoding: .isoLatin1) ?? ""
if brand.hasPrefix("M4A") { return "audio/mp4" }
if brand.hasPrefix("qt") { return "video/quicktime" }
if brand.hasPrefix("3g") { return "video/3gpp" }
return "video/mp4"
}
if starts(with: ascii("OggS")) { return "application/ogg" }
if starts(with: ascii("fLaC")) { return "audio/flac" }
if starts(with: ascii("ID3")) { return "audio/mpeg" }
if bytes[0] == 0xFF, bytes[1] & 0xE6 == 0xE2 || bytes[1] & 0xF6 == 0xF2 { return "audio/mpeg" } // MPEG audio frame sync
if starts(with: ascii("RIFF")) {
if starts(with: ascii("AVI "), at: 8) { return "video/x-msvideo" }
if starts(with: ascii("WAVE"), at: 8) { return "audio/wav" }
return nil
}
if starts(with: ascii("FLV")) { return "video/x-flv" }
if starts(with: [0x30, 0x26, 0xB2, 0x75]) { return "video/x-ms-wmv" } // ASF
if starts(with: [0x00, 0x00, 0x01, 0xBA]) { return "video/mpeg" } // MPEG-PS
if starts(with: [0x00, 0x00, 0x01, 0xB3]) { return "video/mpeg" } // MPEG-1/2 video ES
if starts(with: ascii(".RMF")) { return "application/vnd.rn-realmedia" }
// MPEG-TS: sync byte 0x47 repeating every 188 bytes.
if bytes[0] == 0x47, bytes.count > 188, bytes[188] == 0x47 { return "video/mp2t" }
// Text-based manifests.
if let text = String(bytes: bytes, encoding: .utf8) ?? String(bytes: bytes, encoding: .isoLatin1) {
let head = text.trimmingCharacters(in: .whitespacesAndNewlines)
if head.hasPrefix("#EXTM3U") { return "application/vnd.apple.mpegurl" }
if head.hasPrefix("<") && head.contains("<MPD") { return "application/dash+xml" }
}
return nil
}
}

View File

@@ -2181,6 +2181,16 @@ final class PlayerService {
let stream = try await createStreamForMediaSource(video: video, url: originalURL, extractor: extractor)
return (video, [stream], [], [])
}
// Direct media (a file mpv plays as-is): rebuild the stream from the
// URL. A probe fills in the format/size for opaque CDN links; if it
// fails (offline, expired token) still hand mpv the URL and let it
// report the real error.
if extractor == DirectMediaHelper.provider {
let probe = await DirectMediaProbe.probe(originalURL)
let stream = DirectMediaHelper.createStream(from: originalURL, probe: probe)
return (video, [stream], [], [])
}
}
guard let instance = try await findInstance(for: video) else {

View File

@@ -58,8 +58,12 @@ enum DirectMediaHelper {
// MARK: - Video/Stream Creation
/// Creates a Video model from a direct media URL.
static func createVideo(from url: URL) -> Video {
let filename = url.lastPathComponent
/// - Parameters:
/// - url: The media URL.
/// - probe: What `DirectMediaProbe` learned about the URL, when the path
/// itself carries no filename (debrid/CDN links with an opaque ID).
static func createVideo(from url: URL, probe: DirectMediaProbeResult? = nil) -> Video {
let filename = probe?.filename ?? url.lastPathComponent
let title = (filename as NSString).deletingPathExtension
let host = url.host ?? "Direct Media"
@@ -77,17 +81,29 @@ enum DirectMediaHelper {
viewCount: nil,
likeCount: nil,
thumbnails: [],
isLive: isStreamingURL(url), // Treat HLS/DASH as potentially live
isLive: isStreamingURL(url) || isStreamingType(probe?.mimeType), // Treat HLS/DASH as potentially live
isUpcoming: false,
scheduledStartTime: nil
)
}
/// Creates a Stream from a direct media URL.
static func createStream(from url: URL) -> Stream {
let pathExtension = url.pathExtension.lowercased()
/// - Parameters:
/// - url: The media URL.
/// - probe: What `DirectMediaProbe` learned about the URL; supplies the
/// format, MIME type and size when the path has no extension.
static func createStream(from url: URL, probe: DirectMediaProbeResult? = nil) -> Stream {
let probedExtension = probe?.filename.map { ($0 as NSString).pathExtension.lowercased() } ?? ""
let pathExtension = allExtensions.contains(url.pathExtension.lowercased())
? url.pathExtension.lowercased()
: (probedExtension.isEmpty ? url.pathExtension.lowercased() : probedExtension)
let probedType = probe?.mimeType
let isAudio = isAudioURL(url)
|| audioExtensions.contains(probedExtension)
|| (probedType?.hasPrefix("audio/") ?? false)
let isStreaming = isStreamingURL(url)
|| streamingExtensions.contains(probedExtension)
|| isStreamingType(probedType)
return Stream(
url: url,
@@ -96,10 +112,10 @@ enum DirectMediaHelper {
videoCodec: isAudio ? nil : "unknown",
audioCodec: "unknown",
bitrate: nil,
fileSize: nil,
fileSize: probe?.contentLength,
isAudioOnly: isAudio,
isLive: isStreaming,
mimeType: mimeType(for: pathExtension),
mimeType: mimeType(for: pathExtension) ?? probedType,
audioLanguage: nil,
audioTrackName: nil,
isOriginalAudio: true,
@@ -108,10 +124,19 @@ enum DirectMediaHelper {
)
}
/// Whether a server-reported media type is an HLS or DASH manifest.
static func isStreamingType(_ mimeType: String?) -> Bool {
guard let mimeType else { return false }
return [
"application/vnd.apple.mpegurl", "application/x-mpegurl",
"audio/mpegurl", "audio/x-mpegurl", "application/dash+xml"
].contains(mimeType.lowercased())
}
// MARK: - MIME Type Mapping
/// Returns the MIME type for a file extension.
private static func mimeType(for extension: String) -> String? {
static func mimeType(for extension: String) -> String? {
switch `extension` {
case "mp4", "m4v":
return "video/mp4"

View File

@@ -754,6 +754,12 @@ struct OpenLinkFormView: View {
return (video, [stream])
case .externalVideo, nil:
// A file mpv plays as-is needs no extraction
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
return (video, [stream])
}
// External URL - use Yattee Server
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
throw OpenLinkError.noYatteeServer
@@ -896,6 +902,12 @@ struct OpenLinkFormView: View {
return (video, [stream], [], [])
case .externalVideo, nil:
// A file mpv plays as-is needs no extraction, no captions/storyboards
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
return (video, [stream], [], [])
}
// External URL - use Yattee Server (doesn't support storyboards)
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
throw OpenLinkError.noYatteeServer

View File

@@ -59,6 +59,18 @@ struct ExternalVideoView: View {
isLoading = true
errorMessage = nil
// A file mpv can play as-is (debrid/CDN download links with an opaque
// path) needs no extraction and no Yattee Server.
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
await MainActor.run {
shouldDismissWhenPlayerExpands = true
appEnvironment.playerService.openVideo(video, stream: stream)
}
return
}
// Find a Yattee Server instance
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
errorMessage = String(localized: "externalVideo.noYatteeServer")

View File

@@ -755,6 +755,19 @@ struct YatteeApp: App {
/// Download an external video from a deep link.
private func downloadExternalVideoFromDeepLink(url: URL) async {
// A file mpv plays as-is can be downloaded straight from its URL
if let probe = await DirectMediaProbe.probe(url) {
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
await MainActor.run {
deepLinkVideo = video
deepLinkStreams = [stream]
deepLinkCaptions = []
showingDeepLinkDownloadSheet = true
}
return
}
guard let instance = appEnvironment.instancesManager.yatteeServerInstance else {
// No Yattee Server - fall back to open
appEnvironment.navigationCoordinator.navigate(to: .externalVideo(url))

View File

@@ -0,0 +1,120 @@
//
// DirectMediaProbeTests.swift
// YatteeTests
//
import Testing
import Foundation
@testable import Yattee
@Suite("DirectMediaProbe Tests")
struct DirectMediaProbeTests {
private typealias Headers = DirectMediaProbe.ResponseHeaders
@Test("video/* Content-Type is media, filename comes from Content-Disposition")
func videoContentType() {
let headers = Headers(
contentType: "video/x-matroska",
contentDisposition: "attachment; filename=\"Ted Lasso S04E05 1080p.mkv\"",
contentLength: 3_847_537_440
)
let expected = DirectMediaProbeResult(
filename: "Ted Lasso S04E05 1080p.mkv",
mimeType: "video/x-matroska",
contentLength: 3_847_537_440
)
#expect(DirectMediaProbe.classify(headers) == .media(expected))
}
@Test("text/html is not media")
func htmlIsNotMedia() {
#expect(DirectMediaProbe.classify(Headers(contentType: "text/html; charset=utf-8")) == .notMedia)
}
@Test("octet-stream without filename needs a sniff")
func octetStreamUnknown() {
#expect(DirectMediaProbe.classify(Headers(contentType: "application/octet-stream")) == .unknown)
#expect(DirectMediaProbe.classify(Headers(contentType: nil)) == .unknown)
#expect(DirectMediaProbe.classify(Headers(statusCode: 405)) == .unknown)
}
@Test("octet-stream with a media filename is media")
func octetStreamWithFilename() {
let headers = Headers(
contentType: "application/octet-stream",
contentDisposition: "attachment; filename*=UTF-8''Episode%2001.mp4"
)
guard case .media(let result) = DirectMediaProbe.classify(headers) else {
Issue.record("Expected media"); return
}
#expect(result.filename == "Episode 01.mp4")
#expect(result.mimeType == "video/mp4")
}
@Test("HLS manifest types are media")
func hlsTypes() {
#expect(DirectMediaProbe.isPlayableMediaType("application/vnd.apple.mpegurl"))
#expect(DirectMediaProbe.isPlayableMediaType("application/dash+xml"))
#expect(!DirectMediaProbe.isPlayableMediaType("application/json"))
}
@Test("Content-Disposition parsing prefers filename* and strips paths")
func contentDisposition() {
#expect(DirectMediaProbe.filename(fromContentDisposition: "inline; filename=\"a.mkv\"; filename*=UTF-8''b%20c.mkv") == "b c.mkv")
#expect(DirectMediaProbe.filename(fromContentDisposition: "attachment; filename=../../evil.mp4") == "evil.mp4")
#expect(DirectMediaProbe.filename(fromContentDisposition: "attachment") == nil)
#expect(DirectMediaProbe.filename(fromContentDisposition: nil) == nil)
}
@Test("Content-Range total wins over Content-Length")
func contentRangeTotal() {
let headers = Headers(statusCode: 206, contentLength: 64, contentRange: "bytes 0-63/1000")
#expect(headers.totalLength == 1000)
}
@Test("Magic bytes identify containers")
func magicBytes() {
let mkv = Data([0x1A, 0x45, 0xDF, 0xA3, 0xA3, 0x42, 0x86, 0x81]) + Data("matroska".utf8)
#expect(DirectMediaProbe.sniffMimeType(mkv) == "video/x-matroska")
let webm = Data([0x1A, 0x45, 0xDF, 0xA3, 0x9F, 0x42, 0x86, 0x81, 0x01, 0x42, 0x82, 0x84]) + Data("webm".utf8)
#expect(DirectMediaProbe.sniffMimeType(webm) == "video/webm")
let mp4 = Data([0, 0, 0, 0x18]) + Data("ftypisom".utf8) + Data(repeating: 0, count: 8)
#expect(DirectMediaProbe.sniffMimeType(mp4) == "video/mp4")
let m4a = Data([0, 0, 0, 0x18]) + Data("ftypM4A ".utf8)
#expect(DirectMediaProbe.sniffMimeType(m4a) == "audio/mp4")
#expect(DirectMediaProbe.sniffMimeType(Data("ID3\u{03}".utf8)) == "audio/mpeg")
#expect(DirectMediaProbe.sniffMimeType(Data("fLaC".utf8)) == "audio/flac")
#expect(DirectMediaProbe.sniffMimeType(Data("RIFF....AVI ".utf8)) == "video/x-msvideo")
#expect(DirectMediaProbe.sniffMimeType(Data("#EXTM3U\n#EXT-X-VERSION:3".utf8)) == "application/vnd.apple.mpegurl")
var ts = Data(repeating: 0, count: 200); ts[0] = 0x47; ts[188] = 0x47
#expect(DirectMediaProbe.sniffMimeType(ts) == "video/mp2t")
#expect(DirectMediaProbe.sniffMimeType(Data("<!DOCTYPE html><html>".utf8)) == nil)
#expect(DirectMediaProbe.sniffMimeType(Data("{\"a\":1}".utf8)) == nil)
}
@Test("Probe result drives Video title and Stream metadata for opaque URLs")
func helperUsesProbe() {
let url = URL(string: "https://store-073.wnam.tb-cdn.io/dld/854b43c0-41bb?token=abc")!
let probe = DirectMediaProbeResult(filename: "Show S01E01.mkv", mimeType: "video/x-matroska", contentLength: 42)
let video = DirectMediaHelper.createVideo(from: url, probe: probe)
#expect(video.title == "Show S01E01")
#expect(!video.isLive)
let stream = DirectMediaHelper.createStream(from: url, probe: probe)
#expect(stream.format == "mkv")
#expect(stream.mimeType == "video/x-matroska")
#expect(stream.fileSize == 42)
#expect(!stream.isAudioOnly)
let audio = DirectMediaHelper.createStream(from: url, probe: DirectMediaProbeResult(filename: nil, mimeType: "audio/flac", contentLength: nil))
#expect(audio.isAudioOnly)
let hls = DirectMediaHelper.createVideo(from: url, probe: DirectMediaProbeResult(filename: nil, mimeType: "application/vnd.apple.mpegurl", contentLength: nil))
#expect(hls.isLive)
}
@Test("Without a probe the path extension still rules")
func helperWithoutProbe() {
let url = URL(string: "https://example.com/clip.mp4")!
#expect(DirectMediaHelper.createVideo(from: url).title == "clip")
#expect(DirectMediaHelper.createStream(from: url).format == "mp4")
}
}