added srt support maybe

This commit is contained in:
cranci1 2025-03-18 15:47:57 +01:00
parent 53e1b08956
commit 9b8a389def
6 changed files with 183 additions and 90 deletions

View file

@ -17,4 +17,8 @@ extension String {
let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil)
return attributedString?.string ?? self
}
var trimmed: String {
return self.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View file

@ -0,0 +1,168 @@
//
// VTTSubtitlesLoader.swift
// Sora
//
// Created by Francesco on 15/02/25.
//
import Combine
import Foundation
struct SubtitleCue: Identifiable {
let id = UUID()
let startTime: Double
let endTime: Double
let text: String
}
class VTTSubtitlesLoader: ObservableObject {
@Published var cues: [SubtitleCue] = []
enum SubtitleFormat {
case vtt
case srt
case unknown
}
func load(from urlString: String) {
guard let url = URL(string: urlString) else { return }
let format = determineSubtitleFormat(from: url)
URLSession.custom.dataTask(with: url) { data, _, error in
guard let data = data,
let content = String(data: data, encoding: .utf8),
error == nil else { return }
DispatchQueue.main.async {
switch format {
case .vtt:
self.cues = self.parseVTT(content: content)
case .srt:
self.cues = self.parseSRT(content: content)
case .unknown:
if content.trimmed.hasPrefix("WEBVTT") {
self.cues = self.parseVTT(content: content)
} else {
self.cues = self.parseSRT(content: content)
}
}
}
}.resume()
}
private func determineSubtitleFormat(from url: URL) -> SubtitleFormat {
let fileExtension = url.pathExtension.lowercased()
switch fileExtension {
case "vtt", "webvtt":
return .vtt
case "srt":
return .srt
default:
return .unknown
}
}
private func parseVTT(content: String) -> [SubtitleCue] {
var cues: [SubtitleCue] = []
let lines = content.components(separatedBy: .newlines)
var index = 0
while index < lines.count {
let line = lines[index].trimmingCharacters(in: .whitespaces)
if line.isEmpty || line == "WEBVTT" {
index += 1
continue
}
if !line.contains("-->") {
index += 1
if index >= lines.count { break }
}
let timeLine = lines[index]
let times = timeLine.components(separatedBy: "-->")
if times.count < 2 {
index += 1
continue
}
let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces))
let adjustedStartTime = max(startTime - 0.5, 0)
let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces))
let adjusteEndTime = max(endTime - 0.5, 0)
index += 1
var cueText = ""
while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty {
cueText += lines[index] + "\n"
index += 1
}
cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjusteEndTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines)))
}
return cues
}
private func parseSRT(content: String) -> [SubtitleCue] {
var cues: [SubtitleCue] = []
let normalizedContent = content.replacingOccurrences(of: "\r\n", with: "\n")
.replacingOccurrences(of: "\r", with: "\n")
let blocks = normalizedContent.components(separatedBy: "\n\n")
for block in blocks {
let lines = block.components(separatedBy: "\n").filter { !$0.isEmpty }
guard lines.count >= 2 else { continue }
let timeLine = lines[1]
let times = timeLine.components(separatedBy: "-->")
guard times.count >= 2 else { continue }
let startTime = parseSRTTimecode(times[0].trimmingCharacters(in: .whitespaces))
let adjustedStartTime = max(startTime - 0.5, 0)
let endTime = parseSRTTimecode(times[1].trimmingCharacters(in: .whitespaces))
let adjustedEndTime = max(endTime - 0.5, 0)
var textLines = [String]()
if lines.count > 2 {
textLines = Array(lines[2...])
}
let text = textLines.joined(separator: "\n")
cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjustedEndTime, text: text))
}
return cues
}
private func parseTimecode(_ timeString: String) -> Double {
let parts = timeString.components(separatedBy: ":")
var seconds = 0.0
if parts.count == 3,
let h = Double(parts[0]),
let m = Double(parts[1]),
let s = Double(parts[2].replacingOccurrences(of: ",", with: ".")) {
seconds = h * 3600 + m * 60 + s
} else if parts.count == 2,
let m = Double(parts[0]),
let s = Double(parts[1].replacingOccurrences(of: ",", with: ".")) {
seconds = m * 60 + s
}
return seconds
}
private func parseSRTTimecode(_ timeString: String) -> Double {
let parts = timeString.components(separatedBy: ":")
guard parts.count == 3 else { return 0 }
let secondsParts = parts[2].components(separatedBy: ",")
guard secondsParts.count == 2,
let hours = Double(parts[0]),
let minutes = Double(parts[1]),
let seconds = Double(secondsParts[0]),
let milliseconds = Double(secondsParts[1]) else {
return 0
}
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000
}
}

View file

@ -1,87 +0,0 @@
//
// VTTSubtitlesLoader.swift
// Sora
//
// Created by Francesco on 15/02/25.
//
import Combine
import Foundation
struct SubtitleCue: Identifiable {
let id = UUID()
let startTime: Double
let endTime: Double
let text: String
}
class VTTSubtitlesLoader: ObservableObject {
@Published var cues: [SubtitleCue] = []
func load(from urlString: String) {
guard let url = URL(string: urlString) else { return }
URLSession.custom.dataTask(with: url) { data, _, error in
guard let data = data,
let vttContent = String(data: data, encoding: .utf8),
error == nil else { return }
DispatchQueue.main.async {
self.cues = self.parseVTT(content: vttContent)
}
}.resume()
}
private func parseVTT(content: String) -> [SubtitleCue] {
var cues: [SubtitleCue] = []
let lines = content.components(separatedBy: .newlines)
var index = 0
while index < lines.count {
let line = lines[index].trimmingCharacters(in: .whitespaces)
if line.isEmpty || line == "WEBVTT" {
index += 1
continue
}
if !line.contains("-->") {
index += 1
if index >= lines.count { break }
}
let timeLine = lines[index]
let times = timeLine.components(separatedBy: "-->")
if times.count < 2 {
index += 1
continue
}
let startTime = parseTimecode(times[0].trimmingCharacters(in: .whitespaces))
let adjustedStartTime = max(startTime - 0.5, 0)
let endTime = parseTimecode(times[1].trimmingCharacters(in: .whitespaces))
let adjusteEndTime = max(endTime - 0.5, 0)
index += 1
var cueText = ""
while index < lines.count && !lines[index].trimmingCharacters(in: .whitespaces).isEmpty {
cueText += lines[index] + "\n"
index += 1
}
cues.append(SubtitleCue(startTime: adjustedStartTime, endTime: adjusteEndTime, text: cueText.trimmingCharacters(in: .whitespacesAndNewlines)))
}
return cues
}
private func parseTimecode(_ timeString: String) -> Double {
let parts = timeString.components(separatedBy: ":")
var seconds = 0.0
if parts.count == 3,
let h = Double(parts[0]),
let m = Double(parts[1]),
let s = Double(parts[2].replacingOccurrences(of: ",", with: ".")) {
seconds = h * 3600 + m * 60 + s
} else if parts.count == 2,
let m = Double(parts[0]),
let s = Double(parts[1].replacingOccurrences(of: ",", with: ".")) {
seconds = m * 60 + s
}
return seconds
}
}

View file

@ -45,7 +45,7 @@ class LibraryManager: ObservableObject {
private func loadBookmarks() {
guard let data = UserDefaults.standard.data(forKey: bookmarksKey) else {
Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Error")
Logger.shared.log("No bookmarks data found in UserDefaults.", type: "Debug")
return
}

View file

@ -361,6 +361,15 @@
path = DetailsView;
sourceTree = "<group>";
};
1384DCDF2D89BE870094797A /* Helpers */ = {
isa = PBXGroup;
children = (
13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */,
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = {
isa = PBXGroup;
children = (
@ -425,10 +434,9 @@
13EA2BD02D32D97400C1EBD7 /* CustomPlayer */ = {
isa = PBXGroup;
children = (
1384DCDF2D89BE870094797A /* Helpers */,
13EA2BD22D32D97400C1EBD7 /* Components */,
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */,
13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */,
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */,
);
path = CustomPlayer;
sourceTree = "<group>";