diff --git a/CHDataManagement/Generator/Blocks/VideoBlock.swift b/CHDataManagement/Generator/Blocks/VideoBlock.swift
index acd88e5..8206ec4 100644
--- a/CHDataManagement/Generator/Blocks/VideoBlock.swift
+++ b/CHDataManagement/Generator/Blocks/VideoBlock.swift
@@ -188,6 +188,14 @@ extension VideoBlock {
case .webm: "video/webm"
}
}
+
+ static func h265(codec: String) -> SourceType? {
+ switch codec {
+ case "hvc1": return .h265
+ case "avc1": return .h264
+ default: return nil
+ }
+ }
}
struct Source {
diff --git a/CHDataManagement/Generator/Commands/VideoCommand.swift b/CHDataManagement/Generator/Commands/VideoCommand.swift
index aade462..f990b24 100644
--- a/CHDataManagement/Generator/Commands/VideoCommand.swift
+++ b/CHDataManagement/Generator/Commands/VideoCommand.swift
@@ -30,8 +30,8 @@ struct VideoCommand: CommandProcessor {
}
results.require(file: file)
- guard let videoType = file.type.htmlType else {
- invalid(markdown)
+ guard let videoType = file.videoType() else {
+ invalid("File \(file.identifier) has an unknown video type")
return ""
}
diff --git a/CHDataManagement/Generator/ImageGenerator.swift b/CHDataManagement/Generator/ImageGenerator.swift
index f2a8185..abf8983 100644
--- a/CHDataManagement/Generator/ImageGenerator.swift
+++ b/CHDataManagement/Generator/ImageGenerator.swift
@@ -186,6 +186,7 @@ final class ImageGenerator {
let generatedImagePath = storage.outputPath(to: version.outputPath)!.path()
let quality = Int(version.quality * 100)
+ // TODO: Run in security scope
let process = Process()
#warning("TODO: Move avifenc path to settings")
process.launchPath = "/opt/homebrew/bin/avifenc" // Adjust based on installation
diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift
index d8dd02f..19aedac 100644
--- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift
+++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift
@@ -82,7 +82,18 @@ final class PostListPageGenerator {
images.forEach(source.results.require)
media = .images(images)
} else if localized.hasVideos {
- media = .video(localized.images)
+ let videos: [PostVideo.Video] = localized.images.compactMap { file -> PostVideo.Video? in
+ guard file.type.isVideo else {
+ self.source.results.warning("File \(file.identifier) ignored due to videos present in the post")
+ return nil
+ }
+ guard let type = file.videoType() else {
+ self.source.results.warning("Video \(file.identifier) ignored due to unknown video type")
+ return nil
+ }
+ return .init(path: file.absoluteUrl, type: type)
+ }
+ media = .video(videos)
localized.images.forEach(source.results.require)
} else {
media = nil
diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift
index 23e6b8e..2d57e2b 100644
--- a/CHDataManagement/Model/FileResource.swift
+++ b/CHDataManagement/Model/FileResource.swift
@@ -343,6 +343,62 @@ final class FileResource: Item, LocalizedItem {
}
}
+ private var _videoType: String?
+
+ func videoType() -> String? {
+ if let _videoType {
+ return _videoType
+ }
+ _videoType = determineVideoType()
+ return _videoType
+ }
+
+ private func determineVideoType() -> String? {
+ #warning("TODO: Move ffmpeg path to settings")
+ switch type {
+ case .webm:
+ return "video/webm"
+ case .mp4, .m4v:
+ if isExternallyStored {
+ return "video/mp4"
+ }
+ break
+ default:
+ return nil
+ }
+
+ return content.storage.with(file: identifier) { path in
+ let process = Process()
+ let arguments = "-v error -select_streams v:0 -show_entries stream=codec_tag_string -of default=noprint_wrappers=1:nokey=1 \(path.path())"
+ .components(separatedBy: " ")
+ process.launchPath = "/opt/homebrew/bin/ffprobe"
+ process.arguments = Array(arguments)
+
+ let pipe = Pipe()
+ process.standardOutput = pipe
+ process.standardError = pipe
+
+ process.launch()
+ process.waitUntilExit()
+
+ let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
+ let outputString = String(data: outputData, encoding: .utf8) ?? ""
+
+ if process.terminationStatus != 0 {
+ print("Failed to determine video type for \(identifier)")
+ print(outputString)
+ return nil
+ }
+ let firstLine = outputString.components(separatedBy: .newlines).first!.trimmed
+ guard let type = VideoBlock.SourceType.h265(codec: firstLine) else {
+ print("Unknown codec type for \(identifier): \(firstLine)")
+ print(outputString)
+ return "video/mp4"
+ }
+ return type.mimeType
+ }
+ }
+
// MARK: Paths
func removeFileFromOutputFolder() {
diff --git a/CHDataManagement/Model/FileType.swift b/CHDataManagement/Model/FileType.swift
index e6f01af..adab248 100644
--- a/CHDataManagement/Model/FileType.swift
+++ b/CHDataManagement/Model/FileType.swift
@@ -234,15 +234,4 @@ enum FileType: String {
default: return false
}
}
-
- var htmlType: String? {
- switch self {
- case .mp4, .m4v:
- return "video/mp4"
- case .webm:
- return "video/webm"
- default:
- return nil
- }
- }
}
diff --git a/CHDataManagement/Page Elements/ContentElements/PostVideo.swift b/CHDataManagement/Page Elements/ContentElements/PostVideo.swift
index 50cf3c9..3d8bf6c 100644
--- a/CHDataManagement/Page Elements/ContentElements/PostVideo.swift
+++ b/CHDataManagement/Page Elements/ContentElements/PostVideo.swift
@@ -1,13 +1,18 @@
struct PostVideo: HtmlProducer {
- let videos: [FileResource]
+ struct Video {
+ let path: String
+ let type: String
+ }
+
+ let videos: [Video]
func populate(_ result: inout String) {
result += ""
}
diff --git a/CHDataManagement/Page Elements/FeedEntryData.swift b/CHDataManagement/Page Elements/FeedEntryData.swift
index 7a7937a..facac4d 100644
--- a/CHDataManagement/Page Elements/FeedEntryData.swift
+++ b/CHDataManagement/Page Elements/FeedEntryData.swift
@@ -46,7 +46,7 @@ struct FeedEntryData {
enum Media {
case images([ImageSet])
- case video([FileResource])
+ case video([PostVideo.Video])
}
var requiresSwiper: Bool {
diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift
index f49522d..417fdb8 100644
--- a/CHDataManagement/Storage/Storage.swift
+++ b/CHDataManagement/Storage/Storage.swift
@@ -432,6 +432,12 @@ final class Storage: ObservableObject {
return await contentScope.with(relativePath: path, perform: operation)
}
+ func with(file fileId: String, perform operation: (URL) -> T?) -> T? {
+ guard let contentScope else { return nil }
+ let path = filePath(file: fileId)
+ return contentScope.with(relativePath: path, perform: operation)
+ }
+
// MARK: Video thumbnails
func hasVideoThumbnail(for videoId: String) -> Bool {