@@ -15,6 +15,7 @@ import javax.net.ssl.HostnameVerifier
1515import javax.net.ssl.SSLContext
1616import javax.net.ssl.TrustManager
1717import javax.net.ssl.X509TrustManager
18+ import kotlinx.coroutines.CancellationException
1819import kotlinx.coroutines.Dispatchers
1920import kotlinx.coroutines.currentCoroutineContext
2021import kotlinx.coroutines.ensureActive
@@ -39,27 +40,33 @@ import top.iwesley.lyn.music.core.model.parseSubsonicCompatibleSongLocator
3940import top.iwesley.lyn.music.core.model.parseSambaLocator
4041import top.iwesley.lyn.music.core.model.parseWebDavLocator
4142import top.iwesley.lyn.music.data.db.LynMusicDatabase
42- import top.iwesley.lyn.music.domain.resolveEmbyDownloadUrl
43- import top.iwesley.lyn.music.domain.resolveNavidromeDownloadUrl
44- import top.iwesley.lyn.music.domain.resolveNavidromeStreamUrl
43+ import top.iwesley.lyn.music.domain.RemoteSourceResolvedUrl
44+ import top.iwesley.lyn.music.domain.RemoteSourceAddressSelector
45+ import top.iwesley.lyn.music.domain.isRemoteSourceAddressFallbackAllowed
46+ import top.iwesley.lyn.music.domain.resolveEmbyDownloadUrlCandidates
47+ import top.iwesley.lyn.music.domain.resolveNavidromeDownloadUrlCandidates
48+ import top.iwesley.lyn.music.domain.resolveNavidromeStreamUrlCandidates
4549
4650fun createAndroidOfflineDownloadGateway (
4751 context : Context ,
4852 database : LynMusicDatabase ,
4953 secureCredentialStore : SecureCredentialStore ,
5054 logger : DiagnosticLogger ,
55+ addressSelector : RemoteSourceAddressSelector = RemoteSourceAddressSelector (),
5156): OfflineDownloadGateway = AndroidOfflineDownloadGateway (
5257 context = context.applicationContext,
5358 database = database,
5459 secureCredentialStore = secureCredentialStore,
5560 logger = logger,
61+ addressSelector = addressSelector,
5662)
5763
5864private class AndroidOfflineDownloadGateway (
5965 context : Context ,
6066 private val database : LynMusicDatabase ,
6167 private val secureCredentialStore : SecureCredentialStore ,
6268 private val logger : DiagnosticLogger ,
69+ private val addressSelector : RemoteSourceAddressSelector ,
6370) : OfflineDownloadGateway {
6471 private val rootDirectory = File (context.filesDir, " offline" )
6572 private val defaultClient = OkHttpClient ()
@@ -82,13 +89,13 @@ private class AndroidOfflineDownloadGateway(
8289 try {
8390 val totalBytes = when {
8491 parseSubsonicCompatibleSongLocator(track.mediaLocator) != null -> {
85- val requestUrl = if (quality == NavidromeAudioQuality .Original ) {
86- resolveNavidromeDownloadUrl (database, secureCredentialStore, track.mediaLocator)
92+ val requestUrls = if (quality == NavidromeAudioQuality .Original ) {
93+ resolveNavidromeDownloadUrlCandidates (database, secureCredentialStore, track.mediaLocator, addressSelector )
8794 } else {
88- resolveNavidromeStreamUrl (database, secureCredentialStore, track.mediaLocator, quality)
95+ resolveNavidromeStreamUrlCandidates (database, secureCredentialStore, track.mediaLocator, quality, addressSelector )
8996 } ? : error(" Subsonic-compatible 来源不可用。" )
90- downloadHttpFile (
91- requestUrl = requestUrl ,
97+ downloadHttpFileWithAddressFallback (
98+ requestUrls = requestUrls ,
9299 authorizationHeader = null ,
93100 allowInsecureTls = false ,
94101 target = partFile,
@@ -97,10 +104,10 @@ private class AndroidOfflineDownloadGateway(
97104 }
98105
99106 parseEmbySongLocator(track.mediaLocator) != null -> {
100- val requestUrl = resolveEmbyDownloadUrl (database, secureCredentialStore, track.mediaLocator)
107+ val requestUrls = resolveEmbyDownloadUrlCandidates (database, secureCredentialStore, track.mediaLocator, addressSelector )
101108 ? : error(" Emby 来源不可用。" )
102- downloadHttpFile (
103- requestUrl = requestUrl ,
109+ downloadHttpFileWithAddressFallback (
110+ requestUrls = requestUrls ,
104111 authorizationHeader = null ,
105112 allowInsecureTls = false ,
106113 target = partFile,
@@ -167,6 +174,41 @@ private class AndroidOfflineDownloadGateway(
167174 }
168175 }
169176
177+ private suspend fun downloadHttpFileWithAddressFallback (
178+ requestUrls : List <RemoteSourceResolvedUrl >,
179+ authorizationHeader : String? ,
180+ allowInsecureTls : Boolean ,
181+ target : File ,
182+ onProgress : suspend (OfflineDownloadProgress ) -> Unit ,
183+ ): Long? {
184+ var lastFailure: Throwable ? = null
185+ requestUrls.forEachIndexed { index, requestUrl ->
186+ try {
187+ target.delete()
188+ val totalBytes = downloadHttpFile(
189+ requestUrl = requestUrl.value,
190+ authorizationHeader = authorizationHeader,
191+ allowInsecureTls = allowInsecureTls,
192+ target = target,
193+ onProgress = onProgress,
194+ )
195+ if (requestUrl.sourceId.isNotBlank()) {
196+ addressSelector.markSuccess(requestUrl.sourceId, requestUrl.kind)
197+ }
198+ return totalBytes
199+ } catch (throwable: Throwable ) {
200+ if (throwable is CancellationException ) throw throwable
201+ target.delete()
202+ lastFailure = throwable
203+ val hasFallback = index < requestUrls.lastIndex
204+ if (! hasFallback || ! isRemoteSourceAddressFallbackAllowed(throwable)) {
205+ throw throwable
206+ }
207+ }
208+ }
209+ throw lastFailure ? : IllegalStateException (" 远程来源缺少可用下载地址。" )
210+ }
211+
170212 private suspend fun downloadWebDav (
171213 track : Track ,
172214 target : File ,
0 commit comments