react-native-nitro-player 0.3.0-alpha.9 → 0.4.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +444 -4
- package/android/build.gradle +4 -1
- package/android/src/main/AndroidManifest.xml +16 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +944 -213
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
- package/ios/HybridDownloadManager.swift +226 -0
- package/ios/HybridEqualizer.swift +111 -0
- package/ios/HybridTrackPlayer.swift +36 -8
- package/ios/core/TrackPlayerCore.swift +996 -288
- package/ios/download/DownloadDatabase.swift +493 -0
- package/ios/download/DownloadFileManager.swift +241 -0
- package/ios/download/DownloadManagerCore.swift +923 -0
- package/ios/equalizer/EqualizerCore.swift +685 -0
- package/ios/media/MediaSessionManager.swift +40 -28
- package/ios/playlist/PlaylistManager.swift +40 -9
- package/ios/queue/HybridPlayerQueue.swift +33 -13
- package/lib/hooks/callbackManager.d.ts +18 -0
- package/lib/hooks/callbackManager.js +66 -0
- package/lib/hooks/downloadCallbackManager.d.ts +36 -0
- package/lib/hooks/downloadCallbackManager.js +108 -0
- package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
- package/lib/hooks/equalizerCallbackManager.js +109 -0
- package/lib/hooks/index.d.ts +16 -0
- package/lib/hooks/index.js +10 -0
- package/lib/hooks/useActualQueue.d.ts +48 -0
- package/lib/hooks/useActualQueue.js +98 -0
- package/lib/hooks/useDownloadActions.d.ts +26 -0
- package/lib/hooks/useDownloadActions.js +117 -0
- package/lib/hooks/useDownloadProgress.d.ts +25 -0
- package/lib/hooks/useDownloadProgress.js +79 -0
- package/lib/hooks/useDownloadStorage.d.ts +19 -0
- package/lib/hooks/useDownloadStorage.js +60 -0
- package/lib/hooks/useDownloadedTracks.d.ts +25 -0
- package/lib/hooks/useDownloadedTracks.js +69 -0
- package/lib/hooks/useEqualizer.d.ts +25 -0
- package/lib/hooks/useEqualizer.js +124 -0
- package/lib/hooks/useEqualizerPresets.d.ts +22 -0
- package/lib/hooks/useEqualizerPresets.js +96 -0
- package/lib/hooks/useNowPlaying.js +32 -19
- package/lib/hooks/useOnChangeTrack.js +15 -12
- package/lib/hooks/useOnPlaybackProgressChange.js +2 -2
- package/lib/hooks/useOnPlaybackStateChange.js +16 -13
- package/lib/hooks/usePlaylist.d.ts +48 -0
- package/lib/hooks/usePlaylist.js +136 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/specs/DownloadManager.nitro.d.ts +152 -0
- package/lib/specs/DownloadManager.nitro.js +1 -0
- package/lib/specs/Equalizer.nitro.d.ts +43 -0
- package/lib/specs/Equalizer.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
- package/lib/types/DownloadTypes.d.ts +110 -0
- package/lib/types/DownloadTypes.js +1 -0
- package/lib/types/EqualizerTypes.d.ts +52 -0
- package/lib/types/EqualizerTypes.js +1 -0
- package/lib/types/PlayerQueue.d.ts +4 -0
- package/nitro.json +8 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
- package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
- package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
- package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
- package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
- package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
- package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
- package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
- package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
- package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
- package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
- package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
- package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
- package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
- package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
- package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
- package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
- package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
- package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
- package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
- package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
- package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
- package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
- package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
- package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
- package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
- package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
- package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
- package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
- package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
- package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
- package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
- package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
- package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
- package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
- package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
- package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
- package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
- package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
- package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
- package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
- package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
- package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
- package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
- package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
- package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
- package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
- package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
- package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
- package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
- package/package.json +1 -1
- package/src/hooks/callbackManager.ts +87 -0
- package/src/hooks/downloadCallbackManager.ts +149 -0
- package/src/hooks/equalizerCallbackManager.ts +138 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/useActualQueue.ts +116 -0
- package/src/hooks/useDownloadActions.ts +179 -0
- package/src/hooks/useDownloadProgress.ts +126 -0
- package/src/hooks/useDownloadStorage.ts +84 -0
- package/src/hooks/useDownloadedTracks.ts +138 -0
- package/src/hooks/useEqualizer.ts +173 -0
- package/src/hooks/useEqualizerPresets.ts +140 -0
- package/src/hooks/useNowPlaying.ts +33 -20
- package/src/hooks/useOnChangeTrack.ts +15 -11
- package/src/hooks/useOnPlaybackProgressChange.ts +2 -2
- package/src/hooks/useOnPlaybackStateChange.ts +19 -15
- package/src/hooks/usePlaylist.ts +161 -0
- package/src/index.ts +12 -0
- package/src/specs/DownloadManager.nitro.ts +203 -0
- package/src/specs/Equalizer.nitro.ts +69 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -2
- package/src/types/DownloadTypes.ts +135 -0
- package/src/types/EqualizerTypes.ts +72 -0
- package/src/types/PlayerQueue.ts +9 -0
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DownloadManagerCore.swift
|
|
3
|
+
// NitroPlayer
|
|
4
|
+
//
|
|
5
|
+
// Created by Ritesh Shukla on 2026-01-23..
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import NitroModules
|
|
10
|
+
|
|
11
|
+
/// Core download manager using URLSession background transfers
|
|
12
|
+
final class DownloadManagerCore: NSObject {
|
|
13
|
+
|
|
14
|
+
// MARK: - Singleton
|
|
15
|
+
|
|
16
|
+
static let shared = DownloadManagerCore()
|
|
17
|
+
|
|
18
|
+
// MARK: - Constants
|
|
19
|
+
|
|
20
|
+
private static let backgroundSessionIdentifier = "com.nitroplayer.backgroundDownloads"
|
|
21
|
+
private static let trackMetadataKey = "NitroPlayerTrackMetadata"
|
|
22
|
+
private static let playlistAssociationsKey = "NitroPlayerPlaylistAssociations"
|
|
23
|
+
|
|
24
|
+
// MARK: - Properties
|
|
25
|
+
|
|
26
|
+
private var config: DownloadConfig = DownloadConfig(
|
|
27
|
+
storageLocation: .private,
|
|
28
|
+
maxConcurrentDownloads: 3,
|
|
29
|
+
autoRetry: true,
|
|
30
|
+
maxRetryAttempts: 3,
|
|
31
|
+
backgroundDownloadsEnabled: true,
|
|
32
|
+
downloadArtwork: true,
|
|
33
|
+
customDownloadPath: nil,
|
|
34
|
+
wifiOnlyDownloads: false
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
private var playbackSourcePreference: PlaybackSource = .auto
|
|
38
|
+
|
|
39
|
+
private lazy var backgroundSession: URLSession = {
|
|
40
|
+
let configuration = URLSessionConfiguration.background(
|
|
41
|
+
withIdentifier: Self.backgroundSessionIdentifier)
|
|
42
|
+
configuration.isDiscretionary = false
|
|
43
|
+
configuration.sessionSendsLaunchEvents = true
|
|
44
|
+
configuration.allowsCellularAccess = !config.wifiOnlyDownloads!
|
|
45
|
+
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
|
|
46
|
+
}()
|
|
47
|
+
|
|
48
|
+
/// Active download tasks mapped by downloadId
|
|
49
|
+
private var activeTasks: [String: URLSessionDownloadTask] = [:]
|
|
50
|
+
|
|
51
|
+
/// Download task metadata mapped by downloadId
|
|
52
|
+
private var taskMetadata: [String: DownloadTaskMetadata] = [:]
|
|
53
|
+
|
|
54
|
+
/// Track metadata for downloads (trackId -> TrackItem)
|
|
55
|
+
private var trackMetadata: [String: TrackItem] = [:]
|
|
56
|
+
|
|
57
|
+
/// Playlist associations (downloadId -> playlistId)
|
|
58
|
+
private var playlistAssociations: [String: String] = [:]
|
|
59
|
+
|
|
60
|
+
/// Background completion handler from AppDelegate
|
|
61
|
+
var backgroundCompletionHandler: (() -> Void)?
|
|
62
|
+
|
|
63
|
+
// MARK: - Callbacks
|
|
64
|
+
|
|
65
|
+
private var progressCallbacks: [(DownloadProgress) -> Void] = []
|
|
66
|
+
private var stateChangeCallbacks: [(String, String, DownloadState, DownloadError?) -> Void] = []
|
|
67
|
+
private var completeCallbacks: [(DownloadedTrack) -> Void] = []
|
|
68
|
+
|
|
69
|
+
// MARK: - Thread Safety
|
|
70
|
+
|
|
71
|
+
private let queue = DispatchQueue(
|
|
72
|
+
label: "com.nitroplayer.downloadManager", attributes: .concurrent)
|
|
73
|
+
|
|
74
|
+
// MARK: - Initialization
|
|
75
|
+
|
|
76
|
+
private override init() {
|
|
77
|
+
super.init()
|
|
78
|
+
// Load persisted metadata first (before restoring downloads)
|
|
79
|
+
loadPersistedMetadata()
|
|
80
|
+
// Restore any pending downloads
|
|
81
|
+
restorePendingDownloads()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - Configuration
|
|
85
|
+
|
|
86
|
+
func configure(_ config: DownloadConfig) {
|
|
87
|
+
queue.async(flags: .barrier) {
|
|
88
|
+
self.config = config
|
|
89
|
+
|
|
90
|
+
// Update session configuration if needed
|
|
91
|
+
if let wifiOnly = config.wifiOnlyDownloads {
|
|
92
|
+
// Note: We can't change session config after creation
|
|
93
|
+
// User needs to restart app for this to take effect
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func getConfig() -> DownloadConfig {
|
|
99
|
+
return queue.sync { config }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: - Download Operations
|
|
103
|
+
|
|
104
|
+
func downloadTrack(track: TrackItem, playlistId: String?) -> String {
|
|
105
|
+
let downloadId = UUID().uuidString
|
|
106
|
+
|
|
107
|
+
queue.async(flags: .barrier) {
|
|
108
|
+
// Store track metadata
|
|
109
|
+
self.trackMetadata[track.id] = track
|
|
110
|
+
|
|
111
|
+
// Store playlist association if provided
|
|
112
|
+
if let playlistId = playlistId {
|
|
113
|
+
self.playlistAssociations[downloadId] = playlistId
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Persist metadata (survives app restart)
|
|
117
|
+
self.savePersistedMetadata()
|
|
118
|
+
|
|
119
|
+
// Create download task
|
|
120
|
+
guard let url = URL(string: track.url) else {
|
|
121
|
+
self.notifyStateChange(
|
|
122
|
+
downloadId: downloadId, trackId: track.id, state: .failed,
|
|
123
|
+
error: DownloadError(
|
|
124
|
+
code: "INVALID_URL",
|
|
125
|
+
message: "Invalid track URL: \(track.url)",
|
|
126
|
+
reason: .invalidUrl,
|
|
127
|
+
isRetryable: false
|
|
128
|
+
))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let task = self.backgroundSession.downloadTask(with: url)
|
|
133
|
+
task.taskDescription = "\(downloadId)|\(track.id)"
|
|
134
|
+
|
|
135
|
+
self.activeTasks[downloadId] = task
|
|
136
|
+
self.taskMetadata[downloadId] = DownloadTaskMetadata(
|
|
137
|
+
downloadId: downloadId,
|
|
138
|
+
trackId: track.id,
|
|
139
|
+
playlistId: playlistId,
|
|
140
|
+
state: .pending,
|
|
141
|
+
createdAt: Date().timeIntervalSince1970,
|
|
142
|
+
retryCount: 0
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Respect max concurrent downloads
|
|
146
|
+
let activeCount = self.activeTasks.values.filter { $0.state == .running }.count
|
|
147
|
+
if activeCount < Int(self.config.maxConcurrentDownloads ?? 3) {
|
|
148
|
+
task.resume()
|
|
149
|
+
self.taskMetadata[downloadId]?.state = .downloading
|
|
150
|
+
self.taskMetadata[downloadId]?.startedAt = Date().timeIntervalSince1970
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
self.notifyStateChange(
|
|
154
|
+
downloadId: downloadId, trackId: track.id,
|
|
155
|
+
state: self.taskMetadata[downloadId]?.state ?? .pending, error: nil)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return downloadId
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func downloadPlaylist(playlistId: String, tracks: [TrackItem]) -> [String] {
|
|
162
|
+
var downloadIds: [String] = []
|
|
163
|
+
|
|
164
|
+
for track in tracks {
|
|
165
|
+
let downloadId = downloadTrack(track: track, playlistId: playlistId)
|
|
166
|
+
downloadIds.append(downloadId)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return downloadIds
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - Download Control
|
|
173
|
+
|
|
174
|
+
func pauseDownload(downloadId: String) {
|
|
175
|
+
queue.async(flags: .barrier) {
|
|
176
|
+
guard let task = self.activeTasks[downloadId] else { return }
|
|
177
|
+
|
|
178
|
+
task.cancel(byProducingResumeData: { resumeData in
|
|
179
|
+
// Store resume data for later
|
|
180
|
+
self.taskMetadata[downloadId]?.resumeData = resumeData
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
self.taskMetadata[downloadId]?.state = .paused
|
|
184
|
+
|
|
185
|
+
if let trackId = self.taskMetadata[downloadId]?.trackId {
|
|
186
|
+
self.notifyStateChange(downloadId: downloadId, trackId: trackId, state: .paused, error: nil)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func resumeDownload(downloadId: String) {
|
|
192
|
+
queue.async(flags: .barrier) {
|
|
193
|
+
guard let metadata = self.taskMetadata[downloadId] else { return }
|
|
194
|
+
|
|
195
|
+
var task: URLSessionDownloadTask
|
|
196
|
+
|
|
197
|
+
if let resumeData = metadata.resumeData {
|
|
198
|
+
task = self.backgroundSession.downloadTask(withResumeData: resumeData)
|
|
199
|
+
} else if let track = self.trackMetadata[metadata.trackId],
|
|
200
|
+
let url = URL(string: track.url)
|
|
201
|
+
{
|
|
202
|
+
task = self.backgroundSession.downloadTask(with: url)
|
|
203
|
+
} else {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
task.taskDescription = "\(downloadId)|\(metadata.trackId)"
|
|
208
|
+
self.activeTasks[downloadId] = task
|
|
209
|
+
self.taskMetadata[downloadId]?.state = .downloading
|
|
210
|
+
self.taskMetadata[downloadId]?.resumeData = nil
|
|
211
|
+
|
|
212
|
+
task.resume()
|
|
213
|
+
|
|
214
|
+
self.notifyStateChange(
|
|
215
|
+
downloadId: downloadId, trackId: metadata.trackId, state: .downloading, error: nil)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func cancelDownload(downloadId: String) {
|
|
220
|
+
queue.async(flags: .barrier) {
|
|
221
|
+
guard let task = self.activeTasks[downloadId] else { return }
|
|
222
|
+
|
|
223
|
+
task.cancel()
|
|
224
|
+
|
|
225
|
+
if let trackId = self.taskMetadata[downloadId]?.trackId {
|
|
226
|
+
self.taskMetadata[downloadId]?.state = .cancelled
|
|
227
|
+
self.notifyStateChange(
|
|
228
|
+
downloadId: downloadId, trackId: trackId, state: .cancelled, error: nil)
|
|
229
|
+
// Clean up persisted metadata
|
|
230
|
+
self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
self.activeTasks.removeValue(forKey: downloadId)
|
|
234
|
+
self.taskMetadata.removeValue(forKey: downloadId)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func retryDownload(downloadId: String) {
|
|
239
|
+
queue.async(flags: .barrier) {
|
|
240
|
+
guard let metadata = self.taskMetadata[downloadId],
|
|
241
|
+
let track = self.trackMetadata[metadata.trackId],
|
|
242
|
+
let url = URL(string: track.url)
|
|
243
|
+
else { return }
|
|
244
|
+
|
|
245
|
+
let task = self.backgroundSession.downloadTask(with: url)
|
|
246
|
+
task.taskDescription = "\(downloadId)|\(metadata.trackId)"
|
|
247
|
+
|
|
248
|
+
self.activeTasks[downloadId] = task
|
|
249
|
+
self.taskMetadata[downloadId]?.state = .downloading
|
|
250
|
+
self.taskMetadata[downloadId]?.retryCount += 1
|
|
251
|
+
self.taskMetadata[downloadId]?.error = nil
|
|
252
|
+
|
|
253
|
+
task.resume()
|
|
254
|
+
|
|
255
|
+
self.notifyStateChange(
|
|
256
|
+
downloadId: downloadId, trackId: metadata.trackId, state: .downloading, error: nil)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
func pauseAllDownloads() {
|
|
261
|
+
queue.async(flags: .barrier) {
|
|
262
|
+
for downloadId in self.activeTasks.keys {
|
|
263
|
+
self.pauseDownload(downloadId: downloadId)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func resumeAllDownloads() {
|
|
269
|
+
queue.async(flags: .barrier) {
|
|
270
|
+
for downloadId in self.taskMetadata.keys where self.taskMetadata[downloadId]?.state == .paused
|
|
271
|
+
{
|
|
272
|
+
self.resumeDownload(downloadId: downloadId)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func cancelAllDownloads() {
|
|
278
|
+
queue.async(flags: .barrier) {
|
|
279
|
+
for downloadId in self.activeTasks.keys {
|
|
280
|
+
self.cancelDownload(downloadId: downloadId)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// MARK: - Download Status
|
|
286
|
+
|
|
287
|
+
func getDownloadTask(downloadId: String) -> DownloadTask? {
|
|
288
|
+
return queue.sync {
|
|
289
|
+
guard let metadata = taskMetadata[downloadId] else { return nil }
|
|
290
|
+
return metadata.toDownloadTask()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
func getActiveDownloads() -> [DownloadTask] {
|
|
295
|
+
return queue.sync {
|
|
296
|
+
return taskMetadata.values
|
|
297
|
+
.filter { $0.state == .downloading || $0.state == .pending || $0.state == .paused }
|
|
298
|
+
.map { $0.toDownloadTask() }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func getQueueStatus() -> DownloadQueueStatus {
|
|
303
|
+
return queue.sync {
|
|
304
|
+
let metadata = Array(taskMetadata.values)
|
|
305
|
+
|
|
306
|
+
let pendingCount = metadata.filter { $0.state == .pending }.count
|
|
307
|
+
let activeCount = metadata.filter { $0.state == .downloading }.count
|
|
308
|
+
let completedCount = DownloadDatabase.shared.getAllDownloadedTracks().count
|
|
309
|
+
let failedCount = metadata.filter { $0.state == .failed }.count
|
|
310
|
+
|
|
311
|
+
let totalBytes = metadata.reduce(0.0) { $0 + ($1.totalBytes ?? 0) }
|
|
312
|
+
let downloadedBytes = metadata.reduce(0.0) { $0 + $1.bytesDownloaded }
|
|
313
|
+
|
|
314
|
+
return DownloadQueueStatus(
|
|
315
|
+
pendingCount: Double(pendingCount),
|
|
316
|
+
activeCount: Double(activeCount),
|
|
317
|
+
completedCount: Double(completedCount),
|
|
318
|
+
failedCount: Double(failedCount),
|
|
319
|
+
totalBytesToDownload: totalBytes,
|
|
320
|
+
totalBytesDownloaded: downloadedBytes,
|
|
321
|
+
overallProgress: totalBytes > 0 ? downloadedBytes / totalBytes : 0
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
func isDownloading(trackId: String) -> Bool {
|
|
327
|
+
return queue.sync {
|
|
328
|
+
return taskMetadata.values.contains { $0.trackId == trackId && $0.state == .downloading }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func getDownloadState(trackId: String) -> DownloadState? {
|
|
333
|
+
return queue.sync {
|
|
334
|
+
if let metadata = taskMetadata.values.first(where: { $0.trackId == trackId }) {
|
|
335
|
+
return metadata.state
|
|
336
|
+
}
|
|
337
|
+
if DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) != nil {
|
|
338
|
+
return .completed
|
|
339
|
+
}
|
|
340
|
+
return nil
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// MARK: - Downloaded Content Queries
|
|
345
|
+
|
|
346
|
+
func isTrackDownloaded(trackId: String) -> Bool {
|
|
347
|
+
return DownloadDatabase.shared.isTrackDownloaded(trackId: trackId)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func isPlaylistDownloaded(playlistId: String) -> Bool {
|
|
351
|
+
return DownloadDatabase.shared.isPlaylistDownloaded(playlistId: playlistId)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func isPlaylistPartiallyDownloaded(playlistId: String) -> Bool {
|
|
355
|
+
return DownloadDatabase.shared.isPlaylistPartiallyDownloaded(playlistId: playlistId)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func getDownloadedTrack(trackId: String) -> DownloadedTrack? {
|
|
359
|
+
return DownloadDatabase.shared.getDownloadedTrack(trackId: trackId)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func getAllDownloadedTracks() -> [DownloadedTrack] {
|
|
363
|
+
return DownloadDatabase.shared.getAllDownloadedTracks()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func getDownloadedPlaylist(playlistId: String) -> DownloadedPlaylist? {
|
|
367
|
+
return DownloadDatabase.shared.getDownloadedPlaylist(playlistId: playlistId)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
func getAllDownloadedPlaylists() -> [DownloadedPlaylist] {
|
|
371
|
+
return DownloadDatabase.shared.getAllDownloadedPlaylists()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
func getLocalPath(trackId: String) -> String? {
|
|
375
|
+
print("🔍 DownloadManagerCore.getLocalPath() called for trackId: \(trackId)")
|
|
376
|
+
if let downloadedTrack = DownloadDatabase.shared.getDownloadedTrack(trackId: trackId) {
|
|
377
|
+
print(" ✅ Found downloaded track, localPath: \(downloadedTrack.localPath)")
|
|
378
|
+
return downloadedTrack.localPath
|
|
379
|
+
} else {
|
|
380
|
+
print(" ❌ No downloaded track found for trackId: \(trackId)")
|
|
381
|
+
return nil
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// MARK: - Deletion
|
|
386
|
+
|
|
387
|
+
func deleteDownloadedTrack(trackId: String) {
|
|
388
|
+
DownloadDatabase.shared.deleteDownloadedTrack(trackId: trackId)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
func deleteDownloadedPlaylist(playlistId: String) {
|
|
392
|
+
DownloadDatabase.shared.deleteDownloadedPlaylist(playlistId: playlistId)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func deleteAllDownloads() {
|
|
396
|
+
DownloadDatabase.shared.deleteAllDownloads()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// MARK: - Storage
|
|
400
|
+
|
|
401
|
+
func getStorageInfo() -> DownloadStorageInfo {
|
|
402
|
+
return DownloadFileManager.shared.getStorageInfo()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// Validates all downloads and cleans up orphaned records (files that were manually deleted)
|
|
406
|
+
func syncDownloads() -> Int {
|
|
407
|
+
let removedFromDb = DownloadDatabase.shared.syncDownloads()
|
|
408
|
+
let bytesFreed = DownloadFileManager.shared.cleanupOrphanedFiles()
|
|
409
|
+
print(
|
|
410
|
+
"🔄 DownloadManagerCore: syncDownloads completed - removed \(removedFromDb) orphaned records, freed \(bytesFreed) bytes"
|
|
411
|
+
)
|
|
412
|
+
return removedFromDb
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// MARK: - Playback Source Preference
|
|
416
|
+
|
|
417
|
+
func setPlaybackSourcePreference(_ preference: PlaybackSource) {
|
|
418
|
+
queue.async(flags: .barrier) {
|
|
419
|
+
self.playbackSourcePreference = preference
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
func getPlaybackSourcePreference() -> PlaybackSource {
|
|
424
|
+
return queue.sync { playbackSourcePreference }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
func getEffectiveUrl(track: TrackItem) -> String {
|
|
428
|
+
let preference = getPlaybackSourcePreference()
|
|
429
|
+
print("🔍 DownloadManagerCore.getEffectiveUrl() for track: \(track.id)")
|
|
430
|
+
print(" Playback preference: \(preference)")
|
|
431
|
+
|
|
432
|
+
switch preference {
|
|
433
|
+
case .network:
|
|
434
|
+
print(" → Using network URL (preference=network)")
|
|
435
|
+
return track.url
|
|
436
|
+
case .download:
|
|
437
|
+
if let localPath = getLocalPath(trackId: track.id) {
|
|
438
|
+
print(" → Using local path: \(localPath)")
|
|
439
|
+
return localPath
|
|
440
|
+
} else {
|
|
441
|
+
print(" → Local path not found, falling back to network URL")
|
|
442
|
+
return track.url
|
|
443
|
+
}
|
|
444
|
+
case .auto:
|
|
445
|
+
if let localPath = getLocalPath(trackId: track.id) {
|
|
446
|
+
print(" → Using local path: \(localPath)")
|
|
447
|
+
return localPath
|
|
448
|
+
} else {
|
|
449
|
+
print(" → Local path not found, using network URL")
|
|
450
|
+
return track.url
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// MARK: - Callbacks
|
|
456
|
+
|
|
457
|
+
func addProgressCallback(_ callback: @escaping (DownloadProgress) -> Void) {
|
|
458
|
+
queue.async(flags: .barrier) {
|
|
459
|
+
self.progressCallbacks.append(callback)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
func addStateChangeCallback(
|
|
464
|
+
_ callback: @escaping (String, String, DownloadState, DownloadError?) -> Void
|
|
465
|
+
) {
|
|
466
|
+
queue.async(flags: .barrier) {
|
|
467
|
+
self.stateChangeCallbacks.append(callback)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
func addCompleteCallback(_ callback: @escaping (DownloadedTrack) -> Void) {
|
|
472
|
+
queue.async(flags: .barrier) {
|
|
473
|
+
self.completeCallbacks.append(callback)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// MARK: - Private Helpers
|
|
478
|
+
|
|
479
|
+
private func restorePendingDownloads() {
|
|
480
|
+
backgroundSession.getTasksWithCompletionHandler { [weak self] _, _, downloadTasks in
|
|
481
|
+
for task in downloadTasks {
|
|
482
|
+
guard let description = task.taskDescription else { continue }
|
|
483
|
+
let parts = description.split(separator: "|")
|
|
484
|
+
guard parts.count == 2 else { continue }
|
|
485
|
+
|
|
486
|
+
let downloadId = String(parts[0])
|
|
487
|
+
let trackId = String(parts[1])
|
|
488
|
+
|
|
489
|
+
self?.queue.async(flags: .barrier) {
|
|
490
|
+
self?.activeTasks[downloadId] = task
|
|
491
|
+
if self?.taskMetadata[downloadId] == nil {
|
|
492
|
+
self?.taskMetadata[downloadId] = DownloadTaskMetadata(
|
|
493
|
+
downloadId: downloadId,
|
|
494
|
+
trackId: trackId,
|
|
495
|
+
playlistId: nil,
|
|
496
|
+
state: task.state == .running ? .downloading : .paused,
|
|
497
|
+
createdAt: Date().timeIntervalSince1970,
|
|
498
|
+
retryCount: 0
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// MARK: - Metadata Persistence
|
|
507
|
+
|
|
508
|
+
/// Load persisted track metadata and playlist associations (survives app restart)
|
|
509
|
+
private func loadPersistedMetadata() {
|
|
510
|
+
print("📦 DownloadManagerCore: Loading persisted metadata...")
|
|
511
|
+
|
|
512
|
+
// Load track metadata
|
|
513
|
+
if let data = UserDefaults.standard.data(forKey: Self.trackMetadataKey) {
|
|
514
|
+
do {
|
|
515
|
+
let records = try JSONDecoder().decode([String: TrackItemRecord].self, from: data)
|
|
516
|
+
for (trackId, record) in records {
|
|
517
|
+
trackMetadata[trackId] = recordToTrackItem(record)
|
|
518
|
+
}
|
|
519
|
+
print(" ✅ Loaded \(trackMetadata.count) track metadata entries")
|
|
520
|
+
} catch {
|
|
521
|
+
print(" ❌ Failed to load track metadata: \(error)")
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
print(" ⚠️ No persisted track metadata found")
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Load playlist associations
|
|
528
|
+
if let data = UserDefaults.standard.data(forKey: Self.playlistAssociationsKey) {
|
|
529
|
+
do {
|
|
530
|
+
playlistAssociations = try JSONDecoder().decode([String: String].self, from: data)
|
|
531
|
+
print(" ✅ Loaded \(playlistAssociations.count) playlist associations")
|
|
532
|
+
} catch {
|
|
533
|
+
print(" ❌ Failed to load playlist associations: \(error)")
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
print(" ⚠️ No persisted playlist associations found")
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/// Persist track metadata and playlist associations to disk
|
|
541
|
+
private func savePersistedMetadata() {
|
|
542
|
+
// Convert TrackItem to TrackItemRecord for encoding
|
|
543
|
+
var records: [String: TrackItemRecord] = [:]
|
|
544
|
+
for (trackId, track) in trackMetadata {
|
|
545
|
+
records[trackId] = trackItemToRecord(track)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
do {
|
|
549
|
+
let trackData = try JSONEncoder().encode(records)
|
|
550
|
+
UserDefaults.standard.set(trackData, forKey: Self.trackMetadataKey)
|
|
551
|
+
|
|
552
|
+
let playlistData = try JSONEncoder().encode(playlistAssociations)
|
|
553
|
+
UserDefaults.standard.set(playlistData, forKey: Self.playlistAssociationsKey)
|
|
554
|
+
} catch {
|
|
555
|
+
print("❌ DownloadManagerCore: Failed to save metadata: \(error)")
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/// Clean up persisted metadata for completed/cancelled downloads
|
|
560
|
+
private func cleanupPersistedMetadata(trackId: String, downloadId: String) {
|
|
561
|
+
trackMetadata.removeValue(forKey: trackId)
|
|
562
|
+
playlistAssociations.removeValue(forKey: downloadId)
|
|
563
|
+
savePersistedMetadata()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// MARK: - TrackItem Serialization
|
|
567
|
+
|
|
568
|
+
private func trackItemToRecord(_ track: TrackItem) -> TrackItemRecord {
|
|
569
|
+
var artworkString: String? = nil
|
|
570
|
+
if let artwork = track.artwork {
|
|
571
|
+
switch artwork {
|
|
572
|
+
case .first(_):
|
|
573
|
+
artworkString = nil
|
|
574
|
+
case .second(let value):
|
|
575
|
+
artworkString = value
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return TrackItemRecord(
|
|
579
|
+
id: track.id,
|
|
580
|
+
title: track.title,
|
|
581
|
+
artist: track.artist,
|
|
582
|
+
album: track.album,
|
|
583
|
+
duration: track.duration,
|
|
584
|
+
url: track.url,
|
|
585
|
+
artwork: artworkString
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private func recordToTrackItem(_ record: TrackItemRecord) -> TrackItem {
|
|
590
|
+
let artwork: Variant_NullType_String? = record.artwork.map { .second($0) }
|
|
591
|
+
return TrackItem(
|
|
592
|
+
id: record.id,
|
|
593
|
+
title: record.title,
|
|
594
|
+
artist: record.artist,
|
|
595
|
+
album: record.album,
|
|
596
|
+
duration: record.duration,
|
|
597
|
+
url: record.url,
|
|
598
|
+
artwork: artwork,
|
|
599
|
+
extraPayload: nil
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private func notifyProgress(_ progress: DownloadProgress) {
|
|
604
|
+
DispatchQueue.main.async {
|
|
605
|
+
for callback in self.progressCallbacks {
|
|
606
|
+
callback(progress)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private func notifyStateChange(
|
|
612
|
+
downloadId: String, trackId: String, state: DownloadState, error: DownloadError?
|
|
613
|
+
) {
|
|
614
|
+
DispatchQueue.main.async {
|
|
615
|
+
for callback in self.stateChangeCallbacks {
|
|
616
|
+
callback(downloadId, trackId, state, error)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private func notifyComplete(_ downloadedTrack: DownloadedTrack) {
|
|
622
|
+
DispatchQueue.main.async {
|
|
623
|
+
for callback in self.completeCallbacks {
|
|
624
|
+
callback(downloadedTrack)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private func startNextPendingDownload() {
|
|
630
|
+
queue.async(flags: .barrier) {
|
|
631
|
+
let activeCount = self.activeTasks.values.filter { $0.state == .running }.count
|
|
632
|
+
let maxConcurrent = Int(self.config.maxConcurrentDownloads ?? 3)
|
|
633
|
+
|
|
634
|
+
if activeCount >= maxConcurrent { return }
|
|
635
|
+
|
|
636
|
+
if let pendingId = self.taskMetadata.first(where: { $0.value.state == .pending })?.key,
|
|
637
|
+
let task = self.activeTasks[pendingId]
|
|
638
|
+
{
|
|
639
|
+
task.resume()
|
|
640
|
+
self.taskMetadata[pendingId]?.state = .downloading
|
|
641
|
+
self.taskMetadata[pendingId]?.startedAt = Date().timeIntervalSince1970
|
|
642
|
+
|
|
643
|
+
if let trackId = self.taskMetadata[pendingId]?.trackId {
|
|
644
|
+
self.notifyStateChange(
|
|
645
|
+
downloadId: pendingId, trackId: trackId, state: .downloading, error: nil)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// MARK: - URLSessionDownloadDelegate
|
|
653
|
+
|
|
654
|
+
extension DownloadManagerCore: URLSessionDownloadDelegate {
|
|
655
|
+
|
|
656
|
+
func urlSession(
|
|
657
|
+
_ session: URLSession, downloadTask: URLSessionDownloadTask,
|
|
658
|
+
didFinishDownloadingTo location: URL
|
|
659
|
+
) {
|
|
660
|
+
print("🎯 DownloadManagerCore: didFinishDownloadingTo called")
|
|
661
|
+
|
|
662
|
+
guard let description = downloadTask.taskDescription else {
|
|
663
|
+
print("❌ DownloadManagerCore: No task description")
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
let parts = description.split(separator: "|")
|
|
667
|
+
guard parts.count == 2 else {
|
|
668
|
+
print("❌ DownloadManagerCore: Invalid task description format: \(description)")
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
let downloadId = String(parts[0])
|
|
673
|
+
let trackId = String(parts[1])
|
|
674
|
+
|
|
675
|
+
print(
|
|
676
|
+
"🎯 DownloadManagerCore: Processing completion for downloadId=\(downloadId), trackId=\(trackId)"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
// IMPORTANT: Move file SYNCHRONOUSLY - the temp file is deleted after this method returns!
|
|
680
|
+
// Get storage location and original URL from track metadata
|
|
681
|
+
let (storageLocation, originalURL) = queue.sync {
|
|
682
|
+
(self.config.storageLocation ?? .private, self.trackMetadata[trackId]?.url)
|
|
683
|
+
}
|
|
684
|
+
let destinationPath = DownloadFileManager.shared.saveDownloadedFile(
|
|
685
|
+
from: location,
|
|
686
|
+
trackId: trackId,
|
|
687
|
+
storageLocation: storageLocation,
|
|
688
|
+
originalURL: originalURL
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
// Now handle the rest asynchronously
|
|
692
|
+
queue.async(flags: .barrier) {
|
|
693
|
+
guard let destinationPath = destinationPath else {
|
|
694
|
+
print("❌ DownloadManagerCore: Failed to save file for trackId=\(trackId)")
|
|
695
|
+
self.taskMetadata[downloadId]?.state = .failed
|
|
696
|
+
self.taskMetadata[downloadId]?.error = DownloadError(
|
|
697
|
+
code: "FILE_MOVE_FAILED",
|
|
698
|
+
message: "Failed to save downloaded file",
|
|
699
|
+
reason: .unknown,
|
|
700
|
+
isRetryable: true
|
|
701
|
+
)
|
|
702
|
+
self.notifyStateChange(
|
|
703
|
+
downloadId: downloadId, trackId: trackId, state: .failed,
|
|
704
|
+
error: self.taskMetadata[downloadId]?.error)
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
print("✅ DownloadManagerCore: File saved to \(destinationPath)")
|
|
709
|
+
|
|
710
|
+
guard let track = self.trackMetadata[trackId] else {
|
|
711
|
+
print("❌ DownloadManagerCore: No track metadata for trackId=\(trackId)")
|
|
712
|
+
print(" Available trackIds: \(Array(self.trackMetadata.keys))")
|
|
713
|
+
|
|
714
|
+
// Still mark as completed even if we don't have metadata
|
|
715
|
+
self.taskMetadata[downloadId]?.state = .completed
|
|
716
|
+
self.taskMetadata[downloadId]?.completedAt = Date().timeIntervalSince1970
|
|
717
|
+
self.activeTasks.removeValue(forKey: downloadId)
|
|
718
|
+
self.notifyStateChange(
|
|
719
|
+
downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
|
|
720
|
+
self.startNextPendingDownload()
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let playlistId = self.playlistAssociations[downloadId]
|
|
725
|
+
|
|
726
|
+
// Get file size
|
|
727
|
+
let fileSize = DownloadFileManager.shared.getFileSize(at: destinationPath)
|
|
728
|
+
|
|
729
|
+
// Create downloaded track record
|
|
730
|
+
let downloadedTrack = DownloadedTrack(
|
|
731
|
+
trackId: trackId,
|
|
732
|
+
originalTrack: track,
|
|
733
|
+
localPath: destinationPath,
|
|
734
|
+
localArtworkPath: nil,
|
|
735
|
+
downloadedAt: Date().timeIntervalSince1970,
|
|
736
|
+
fileSize: Double(fileSize),
|
|
737
|
+
storageLocation: storageLocation
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
// Save to database
|
|
741
|
+
DownloadDatabase.shared.saveDownloadedTrack(downloadedTrack, playlistId: playlistId)
|
|
742
|
+
|
|
743
|
+
print("✅ DownloadManagerCore: Track saved to database")
|
|
744
|
+
|
|
745
|
+
// Clean up persisted metadata (no longer needed after completion)
|
|
746
|
+
self.cleanupPersistedMetadata(trackId: trackId, downloadId: downloadId)
|
|
747
|
+
|
|
748
|
+
// Update state
|
|
749
|
+
self.taskMetadata[downloadId]?.state = .completed
|
|
750
|
+
self.taskMetadata[downloadId]?.completedAt = Date().timeIntervalSince1970
|
|
751
|
+
|
|
752
|
+
// Clean up active task but keep metadata for state queries
|
|
753
|
+
self.activeTasks.removeValue(forKey: downloadId)
|
|
754
|
+
|
|
755
|
+
// Notify
|
|
756
|
+
print("✅ DownloadManagerCore: Notifying completion for trackId=\(trackId)")
|
|
757
|
+
self.notifyStateChange(
|
|
758
|
+
downloadId: downloadId, trackId: trackId, state: .completed, error: nil)
|
|
759
|
+
self.notifyComplete(downloadedTrack)
|
|
760
|
+
|
|
761
|
+
// Start next download
|
|
762
|
+
self.startNextPendingDownload()
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
func urlSession(
|
|
767
|
+
_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64,
|
|
768
|
+
totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64
|
|
769
|
+
) {
|
|
770
|
+
guard let description = downloadTask.taskDescription else { return }
|
|
771
|
+
let parts = description.split(separator: "|")
|
|
772
|
+
guard parts.count == 2 else { return }
|
|
773
|
+
|
|
774
|
+
let downloadId = String(parts[0])
|
|
775
|
+
let trackId = String(parts[1])
|
|
776
|
+
|
|
777
|
+
queue.async(flags: .barrier) {
|
|
778
|
+
self.taskMetadata[downloadId]?.bytesDownloaded = Double(totalBytesWritten)
|
|
779
|
+
self.taskMetadata[downloadId]?.totalBytes =
|
|
780
|
+
totalBytesExpectedToWrite > 0 ? Double(totalBytesExpectedToWrite) : nil
|
|
781
|
+
|
|
782
|
+
let progress = DownloadProgress(
|
|
783
|
+
trackId: trackId,
|
|
784
|
+
downloadId: downloadId,
|
|
785
|
+
bytesDownloaded: Double(totalBytesWritten),
|
|
786
|
+
totalBytes: Double(totalBytesExpectedToWrite),
|
|
787
|
+
progress: totalBytesExpectedToWrite > 0
|
|
788
|
+
? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) : 0,
|
|
789
|
+
state: .downloading
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
self.notifyProgress(progress)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
797
|
+
guard let downloadTask = task as? URLSessionDownloadTask,
|
|
798
|
+
let description = downloadTask.taskDescription
|
|
799
|
+
else { return }
|
|
800
|
+
|
|
801
|
+
let parts = description.split(separator: "|")
|
|
802
|
+
guard parts.count == 2 else { return }
|
|
803
|
+
|
|
804
|
+
let downloadId = String(parts[0])
|
|
805
|
+
let trackId = String(parts[1])
|
|
806
|
+
|
|
807
|
+
guard let error = error else { return } // Success case handled in didFinishDownloadingTo
|
|
808
|
+
|
|
809
|
+
queue.async(flags: .barrier) {
|
|
810
|
+
let nsError = error as NSError
|
|
811
|
+
|
|
812
|
+
// Check if this is a cancellation
|
|
813
|
+
if nsError.code == NSURLErrorCancelled {
|
|
814
|
+
// Check if we have resume data (pause)
|
|
815
|
+
if self.taskMetadata[downloadId]?.resumeData != nil {
|
|
816
|
+
return // Already handled in pauseDownload
|
|
817
|
+
}
|
|
818
|
+
// Otherwise it's a cancellation
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Determine error reason
|
|
823
|
+
let errorReason: DownloadErrorReason
|
|
824
|
+
switch nsError.code {
|
|
825
|
+
case NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost:
|
|
826
|
+
errorReason = .networkError
|
|
827
|
+
case NSURLErrorTimedOut:
|
|
828
|
+
errorReason = .timeout
|
|
829
|
+
case NSURLErrorFileDoesNotExist:
|
|
830
|
+
errorReason = .fileNotFound
|
|
831
|
+
default:
|
|
832
|
+
errorReason = .unknown
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let downloadError = DownloadError(
|
|
836
|
+
code: String(nsError.code),
|
|
837
|
+
message: error.localizedDescription,
|
|
838
|
+
reason: errorReason,
|
|
839
|
+
isRetryable: errorReason == .networkError || errorReason == .timeout
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
self.taskMetadata[downloadId]?.state = .failed
|
|
843
|
+
self.taskMetadata[downloadId]?.error = downloadError
|
|
844
|
+
|
|
845
|
+
// Auto-retry if enabled
|
|
846
|
+
if let autoRetry = self.config.autoRetry, autoRetry,
|
|
847
|
+
downloadError.isRetryable,
|
|
848
|
+
let retryCount = self.taskMetadata[downloadId]?.retryCount,
|
|
849
|
+
retryCount < Int(self.config.maxRetryAttempts ?? 3)
|
|
850
|
+
{
|
|
851
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
852
|
+
self.retryDownload(downloadId: downloadId)
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
self.notifyStateChange(
|
|
856
|
+
downloadId: downloadId, trackId: trackId, state: .failed, error: downloadError)
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Start next download
|
|
860
|
+
self.startNextPendingDownload()
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
|
|
865
|
+
DispatchQueue.main.async {
|
|
866
|
+
self.backgroundCompletionHandler?()
|
|
867
|
+
self.backgroundCompletionHandler = nil
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// MARK: - Download Task Metadata
|
|
873
|
+
|
|
874
|
+
private struct DownloadTaskMetadata {
|
|
875
|
+
let downloadId: String
|
|
876
|
+
let trackId: String
|
|
877
|
+
let playlistId: String?
|
|
878
|
+
var state: DownloadState
|
|
879
|
+
let createdAt: Double
|
|
880
|
+
var startedAt: Double?
|
|
881
|
+
var completedAt: Double?
|
|
882
|
+
var retryCount: Int
|
|
883
|
+
var resumeData: Data?
|
|
884
|
+
var bytesDownloaded: Double = 0
|
|
885
|
+
var totalBytes: Double?
|
|
886
|
+
var error: DownloadError?
|
|
887
|
+
|
|
888
|
+
func toDownloadTask() -> DownloadTask {
|
|
889
|
+
let progress = DownloadProgress(
|
|
890
|
+
trackId: trackId,
|
|
891
|
+
downloadId: downloadId,
|
|
892
|
+
bytesDownloaded: bytesDownloaded,
|
|
893
|
+
totalBytes: totalBytes ?? 0,
|
|
894
|
+
progress: totalBytes != nil && totalBytes! > 0 ? bytesDownloaded / totalBytes! : 0,
|
|
895
|
+
state: state
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
return DownloadTask(
|
|
899
|
+
downloadId: downloadId,
|
|
900
|
+
trackId: trackId,
|
|
901
|
+
playlistId: playlistId.map { Variant_NullType_String.second($0) },
|
|
902
|
+
state: state,
|
|
903
|
+
progress: progress,
|
|
904
|
+
createdAt: createdAt,
|
|
905
|
+
startedAt: startedAt.map { Variant_NullType_Double.second($0) },
|
|
906
|
+
completedAt: completedAt.map { Variant_NullType_Double.second($0) },
|
|
907
|
+
error: error.map { Variant_NullType_DownloadError.second($0) },
|
|
908
|
+
retryCount: Double(retryCount)
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// MARK: - Track Item Record (for persistence)
|
|
914
|
+
|
|
915
|
+
private struct TrackItemRecord: Codable {
|
|
916
|
+
let id: String
|
|
917
|
+
let title: String
|
|
918
|
+
let artist: String
|
|
919
|
+
let album: String
|
|
920
|
+
let duration: Double
|
|
921
|
+
let url: String
|
|
922
|
+
let artwork: String?
|
|
923
|
+
}
|