mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
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.
184 lines
6.3 KiB
Swift
184 lines
6.3 KiB
Swift
//
|
|
// DirectMediaHelper.swift
|
|
// Yattee
|
|
//
|
|
// Helper for creating Video/Stream objects from direct media URLs.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Helper for handling direct media URLs (mp4, m3u8, etc.) without extraction.
|
|
enum DirectMediaHelper {
|
|
/// Provider name for direct media URLs.
|
|
static let provider = "direct_media"
|
|
|
|
// MARK: - Supported Extensions
|
|
|
|
/// Video file extensions that can be played directly.
|
|
static let videoExtensions: Set<String> = [
|
|
"mp4", "m4v", "mov", "mkv", "avi", "webm", "wmv",
|
|
"flv", "mpg", "mpeg", "3gp", "ts", "m2ts"
|
|
]
|
|
|
|
/// Audio file extensions that can be played directly.
|
|
static let audioExtensions: Set<String> = [
|
|
"mp3", "m4a", "aac", "flac", "wav", "ogg", "opus"
|
|
]
|
|
|
|
/// Streaming format extensions (HLS, DASH).
|
|
static let streamingExtensions: Set<String> = [
|
|
"m3u8", "mpd"
|
|
]
|
|
|
|
/// All supported media extensions.
|
|
static var allExtensions: Set<String> {
|
|
videoExtensions.union(audioExtensions).union(streamingExtensions)
|
|
}
|
|
|
|
// MARK: - Detection
|
|
|
|
/// Checks if the URL points to a direct media file based on extension.
|
|
static func isDirectMediaURL(_ url: URL) -> Bool {
|
|
let pathExtension = url.pathExtension.lowercased()
|
|
return allExtensions.contains(pathExtension)
|
|
}
|
|
|
|
/// Checks if the URL is a streaming format (HLS/DASH).
|
|
static func isStreamingURL(_ url: URL) -> Bool {
|
|
let pathExtension = url.pathExtension.lowercased()
|
|
return streamingExtensions.contains(pathExtension)
|
|
}
|
|
|
|
/// Checks if the URL is an audio-only file.
|
|
static func isAudioURL(_ url: URL) -> Bool {
|
|
let pathExtension = url.pathExtension.lowercased()
|
|
return audioExtensions.contains(pathExtension)
|
|
}
|
|
|
|
// MARK: - Video/Stream Creation
|
|
|
|
/// Creates a Video model from a direct media URL.
|
|
/// - 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"
|
|
|
|
return Video(
|
|
id: VideoID(
|
|
source: .extracted(extractor: provider, originalURL: url),
|
|
videoID: url.absoluteString
|
|
),
|
|
title: title.isEmpty ? filename : title,
|
|
description: nil,
|
|
author: Author(id: provider, name: host, hasRealChannelInfo: false),
|
|
duration: 0, // Unknown until playback
|
|
publishedAt: nil,
|
|
publishedText: nil,
|
|
viewCount: nil,
|
|
likeCount: nil,
|
|
thumbnails: [],
|
|
isLive: isStreamingURL(url) || isStreamingType(probe?.mimeType), // Treat HLS/DASH as potentially live
|
|
isUpcoming: false,
|
|
scheduledStartTime: nil
|
|
)
|
|
}
|
|
|
|
/// Creates a Stream from a direct media URL.
|
|
/// - 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,
|
|
resolution: isAudio ? nil : .p720, // Default assumption for video
|
|
format: pathExtension,
|
|
videoCodec: isAudio ? nil : "unknown",
|
|
audioCodec: "unknown",
|
|
bitrate: nil,
|
|
fileSize: probe?.contentLength,
|
|
isAudioOnly: isAudio,
|
|
isLive: isStreaming,
|
|
mimeType: mimeType(for: pathExtension) ?? probedType,
|
|
audioLanguage: nil,
|
|
audioTrackName: nil,
|
|
isOriginalAudio: true,
|
|
httpHeaders: nil,
|
|
fps: nil
|
|
)
|
|
}
|
|
|
|
/// 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.
|
|
static func mimeType(for extension: String) -> String? {
|
|
switch `extension` {
|
|
case "mp4", "m4v":
|
|
return "video/mp4"
|
|
case "mov":
|
|
return "video/quicktime"
|
|
case "mkv":
|
|
return "video/x-matroska"
|
|
case "avi":
|
|
return "video/x-msvideo"
|
|
case "webm":
|
|
return "video/webm"
|
|
case "wmv":
|
|
return "video/x-ms-wmv"
|
|
case "flv":
|
|
return "video/x-flv"
|
|
case "mpg", "mpeg":
|
|
return "video/mpeg"
|
|
case "3gp":
|
|
return "video/3gpp"
|
|
case "ts", "m2ts":
|
|
return "video/mp2t"
|
|
case "m3u8":
|
|
return "application/vnd.apple.mpegurl"
|
|
case "mpd":
|
|
return "application/dash+xml"
|
|
case "mp3":
|
|
return "audio/mpeg"
|
|
case "m4a":
|
|
return "audio/mp4"
|
|
case "aac":
|
|
return "audio/aac"
|
|
case "flac":
|
|
return "audio/flac"
|
|
case "wav":
|
|
return "audio/wav"
|
|
case "ogg":
|
|
return "audio/ogg"
|
|
case "opus":
|
|
return "audio/opus"
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|