diff --git a/Sora/Info.plist b/Sora/Info.plist
index b9e2c4d..6e00c4a 100644
--- a/Sora/Info.plist
+++ b/Sora/Info.plist
@@ -25,6 +25,7 @@
infuse
vlc
nplayer-https
+ senplayer
NSAppTransportSecurity
diff --git a/Sora/Utils/ContinueWatching/DownloadManager.swift b/Sora/Utils/ContinueWatching/DownloadManager.swift
new file mode 100644
index 0000000..9c3d90d
--- /dev/null
+++ b/Sora/Utils/ContinueWatching/DownloadManager.swift
@@ -0,0 +1,95 @@
+//
+// DownloadManager.swift
+// Sulfur
+//
+// Created by Francesco on 29/04/25.
+//
+
+import SwiftUI
+import AVKit
+import AVFoundation
+
+class DownloadManager: NSObject, ObservableObject {
+ @Published var activeDownloads: [(URL, Double)] = []
+ @Published var localPlaybackURL: URL?
+
+ private var assetDownloadURLSession: AVAssetDownloadURLSession!
+ private var activeDownloadTasks: [URLSessionTask: URL] = [:]
+
+ override init() {
+ super.init()
+ initializeDownloadSession()
+ loadLocalContent()
+ }
+
+ private func initializeDownloadSession() {
+ let configuration = URLSessionConfiguration.background(withIdentifier: "hls-downloader")
+ assetDownloadURLSession = AVAssetDownloadURLSession(
+ configuration: configuration,
+ assetDownloadDelegate: self,
+ delegateQueue: .main
+ )
+ }
+
+ func downloadAsset(from url: URL) {
+ let asset = AVURLAsset(url: url)
+ let task = assetDownloadURLSession.makeAssetDownloadTask(
+ asset: asset,
+ assetTitle: "Offline Video",
+ assetArtworkData: nil,
+ options: nil
+ )
+
+ task?.resume()
+ activeDownloadTasks[task!] = url
+ }
+
+ private func loadLocalContent() {
+ guard let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
+
+ do {
+ let contents = try FileManager.default.contentsOfDirectory(
+ at: documents,
+ includingPropertiesForKeys: nil,
+ options: .skipsHiddenFiles
+ )
+
+ if let localURL = contents.first(where: { $0.pathExtension == "movpkg" }) {
+ localPlaybackURL = localURL
+ }
+ } catch {
+ print("Error loading local content: \(error)")
+ }
+ }
+}
+
+extension DownloadManager: AVAssetDownloadDelegate {
+ func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
+ activeDownloadTasks.removeValue(forKey: assetDownloadTask)
+ localPlaybackURL = location
+ }
+
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+ guard let error = error else { return }
+ print("Download error: \(error.localizedDescription)")
+ activeDownloadTasks.removeValue(forKey: task)
+ }
+
+ func urlSession(_ session: URLSession,
+ assetDownloadTask: AVAssetDownloadTask,
+ didLoad timeRange: CMTimeRange,
+ totalTimeRangesLoaded loadedTimeRanges: [NSValue],
+ timeRangeExpectedToLoad: CMTimeRange) {
+
+ guard let url = activeDownloadTasks[assetDownloadTask] else { return }
+ let progress = loadedTimeRanges
+ .map { $0.timeRangeValue.duration.seconds / timeRangeExpectedToLoad.duration.seconds }
+ .reduce(0, +)
+
+ if let index = activeDownloads.firstIndex(where: { $0.0 == url }) {
+ activeDownloads[index].1 = progress
+ } else {
+ activeDownloads.append((url, progress))
+ }
+ }
+}
diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift
index 174809d..28c10da 100644
--- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift
+++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift
@@ -2038,15 +2038,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
}
]
- let resetDelayAction = UIAction(title: "Reset Timing") { [weak self] _ in
+ let resetDelayAction = UIAction(title: "Reset Delay") { [weak self] _ in
guard let self = self else { return }
SubtitleSettingsManager.shared.update { settings in settings.subtitleDelay = 0.0 }
self.subtitleDelay = 0.0
self.loadSubtitleSettings()
- DropManager.shared.showDrop(title: "Subtitle Timing Reset", subtitle: "", duration: 0.5, icon: UIImage(systemName: "clock.arrow.circlepath"))
}
- let delayMenu = UIMenu(title: "Subtitle Timing", children: delayActions + [resetDelayAction])
+ let delayMenu = UIMenu(title: "Subtitle Delay", children: delayActions + [resetDelayAction])
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [
subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu, delayMenu
diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift
index 8717e2a..d98a28a 100644
--- a/Sora/Views/DownloadView.swift
+++ b/Sora/Views/DownloadView.swift
@@ -2,82 +2,46 @@
// DownloadView.swift
// Sulfur
//
-// Created by Francesco on 12/03/25.
+// Created by Francesco on 29/04/25.
//
import SwiftUI
-
-struct DownloadItem: Identifiable {
- let id = UUID()
- let title: String
- let episode: Int
- let type: String
- var progress: Double
- var status: String
-}
-
-class DownloadViewModel: ObservableObject {
- @Published var downloads: [DownloadItem] = []
-
- init() {
- NotificationCenter.default.addObserver(self, selector: #selector(updateStatus(_:)), name: .DownloadManagerStatusUpdate, object: nil)
- }
-
- @objc func updateStatus(_ notification: Notification) {
- guard let info = notification.userInfo,
- let title = info["title"] as? String,
- let episode = info["episode"] as? Int,
- let type = info["type"] as? String,
- let status = info["status"] as? String,
- let progress = info["progress"] as? Double else { return }
-
- if let index = downloads.firstIndex(where: { $0.title == title && $0.episode == episode }) {
- downloads[index] = DownloadItem(title: title, episode: episode, type: type, progress: progress, status: status)
- } else {
- let newDownload = DownloadItem(title: title, episode: episode, type: type, progress: progress, status: status)
- downloads.append(newDownload)
- }
- }
-}
+import AVKit
struct DownloadView: View {
- @StateObject var viewModel = DownloadViewModel()
+ @StateObject private var viewModel = DownloadManager()
+ @State private var hlsURL = "https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8"
var body: some View {
NavigationView {
- List(viewModel.downloads) { download in
- HStack(spacing: 16) {
- Image(systemName: iconName(for: download))
- .resizable()
- .frame(width: 30, height: 30)
- .foregroundColor(.accentColor)
-
- VStack(alignment: .leading, spacing: 4) {
- Text("\(download.title) - Episode \(download.episode)")
- .font(.headline)
-
- ProgressView(value: download.progress)
- .progressViewStyle(LinearProgressViewStyle(tint: .accentColor))
- .frame(height: 8)
-
- Text(download.status)
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
- Spacer()
+ VStack {
+ TextField("Enter HLS URL", text: $hlsURL)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .padding()
+
+ Button("Download Stream") {
+ viewModel.downloadAsset(from: URL(string: hlsURL)!)
}
- .padding(.vertical, 8)
+ .padding()
+
+ List(viewModel.activeDownloads, id: \.0) { (url, progress) in
+ VStack(alignment: .leading) {
+ Text(url.absoluteString)
+ ProgressView(value: progress)
+ .progressViewStyle(LinearProgressViewStyle())
+ }
+ }
+
+ NavigationLink("Play Offline Content") {
+ if let url = viewModel.localPlaybackURL {
+ VideoPlayer(player: AVPlayer(url: url))
+ } else {
+ Text("No offline content available")
+ }
+ }
+ .padding()
}
- .navigationTitle("Downloads")
- }
- .navigationViewStyle(StackNavigationViewStyle())
- }
-
- func iconName(for download: DownloadItem) -> String {
- if download.type == "hls" {
- return download.status.lowercased().contains("converting") ? "arrow.triangle.2.circlepath.circle.fill" : "checkmark.circle.fill"
- } else {
- return download.progress >= 1.0 ? "checkmark.circle.fill" : "arrow.down.circle.fill"
+ .navigationTitle("HLS Downloader")
}
}
}
diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift
index 709e3c2..6dec97e 100644
--- a/Sora/Views/MediaInfoView/MediaInfoView.swift
+++ b/Sora/Views/MediaInfoView/MediaInfoView.swift
@@ -859,6 +859,8 @@ struct MediaInfoView: View {
scheme = "outplayer://\(url)"
case "nPlayer":
scheme = "nplayer-\(url)"
+ case "SenPlayer":
+ scheme = "SenPlayer://x-callback-url/play?url=\(url)"
case "Default":
let videoPlayerViewController = VideoPlayerViewController(module: module)
videoPlayerViewController.streamUrl = url
diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift
index dd5143f..28b6d77 100644
--- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift
+++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift
@@ -19,7 +19,7 @@ struct SettingsViewPlayer: View {
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
- private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"]
+ private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "SenPlayer", "Sora"]
var body: some View {
Form {
diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj
index 89079d9..4415b7b 100644
--- a/Sulfur.xcodeproj/project.pbxproj
+++ b/Sulfur.xcodeproj/project.pbxproj
@@ -7,10 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
- 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; };
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; };
13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; };
13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8D2D58E04A000F0673 /* SkeletonCell.swift */; };
+ 131270172DC13A010093AA9C /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131270162DC13A010093AA9C /* DownloadManager.swift */; };
+ 131270192DC13A3C0093AA9C /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131270182DC13A3C0093AA9C /* DownloadView.swift */; };
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */; };
1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA62D758CEA00FC6689 /* Analytics.swift */; };
1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */; };
@@ -69,11 +70,12 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
- 130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; };
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; };
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = ""; };
13103E8A2D58E028000F0673 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; };
13103E8D2D58E04A000F0673 /* SkeletonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCell.swift; sourceTree = ""; };
+ 131270162DC13A010093AA9C /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; };
+ 131270182DC13A3C0093AA9C /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; };
131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewGeneral.swift; sourceTree = ""; };
1327FBA62D758CEA00FC6689 /* Analytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; };
1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Model.swift"; sourceTree = ""; };
@@ -171,6 +173,13 @@
path = SkeletonCells;
sourceTree = "";
};
+ 131270152DC139CD0093AA9C /* DownloadManager */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = DownloadManager;
+ sourceTree = "";
+ };
1327FBA52D758CEA00FC6689 /* Analytics */ = {
isa = PBXGroup;
children = (
@@ -226,7 +235,7 @@
1399FAD22D3AB34F00E97C31 /* SettingsView */,
133F55B92D33B53E00E08EEA /* LibraryView */,
133D7C7C2D2BE2630075467E /* SearchView.swift */,
- 130217CB2D81C55E0011EFF5 /* DownloadView.swift */,
+ 131270182DC13A3C0093AA9C /* DownloadView.swift */,
);
path = Views;
sourceTree = "";
@@ -257,6 +266,7 @@
133D7C852D2BE2640075467E /* Utils */ = {
isa = PBXGroup;
children = (
+ 131270152DC139CD0093AA9C /* DownloadManager */,
13C0E5E82D5F85DD00E7F619 /* ContinueWatching */,
13103E8C2D58E037000F0673 /* SkeletonCells */,
13DC0C442D302C6A00D0F966 /* MediaPlayer */,
@@ -364,6 +374,7 @@
children = (
13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */,
13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */,
+ 131270162DC13A010093AA9C /* DownloadManager.swift */,
);
path = ContinueWatching;
sourceTree = "";
@@ -521,6 +532,7 @@
buildActionMask = 2147483647;
files = (
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */,
+ 131270172DC13A010093AA9C /* DownloadManager.swift in Sources */,
1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */,
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */,
1359ED142D76F49900C13034 /* finTopView.swift in Sources */,
@@ -558,8 +570,8 @@
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
+ 131270192DC13A3C0093AA9C /* DownloadView.swift in Sources */,
13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */,
- 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */,
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */,
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,