diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt index 69acea31..11ed6606 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/CoreBackend.kt @@ -198,7 +198,6 @@ internal interface CoreBackend { fun checkHiResAuthenticity(path: String, optionsJson: String): String fun readAudioMetadata(path: String, hint: String, cacheKey: String): String fun setLibraryCoverCacheDirectory(path: String) - fun scanLibraryFolder(folder: String): String fun scanLibraryFolderToNdjsonFile(folder: String, output: String): Long fun scanLibraryFolderIncremental(folder: String, existing: String): String fun scanLibraryFolderIncrementalFromSnapshot(folder: String, snapshot: String): String diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index bb010daf..bb9b4baf 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -93,11 +93,9 @@ class MainActivity: FlutterFragmentActivity() { "editFileMetadata", "reEnrichFile", "setLibraryCoverCacheDir", - "scanLibraryFolder", "scanLibraryFolderToNDJSONFile", "scanLibraryFolderIncremental", "scanLibraryFolderIncrementalFromSnapshot", - "scanSafTree", "scanSafTreeToNDJSONFile", "scanSafTreeIncremental", "scanSafTreeIncrementalFromSnapshot", @@ -1904,14 +1902,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(null) } - "scanLibraryFolder" -> { - val folderPath = call.argument("folder_path") ?: "" - val response = withContext(Dispatchers.IO) { - safScanActive = false - bridgeJsonResult(coreBackend.scanLibraryFolder(folderPath)) - } - result.success(response) - } "scanLibraryFolderToNDJSONFile" -> { val folderPath = call.argument("folder_path") ?: "" val outputPath = call.argument("output_path") ?: "" @@ -1955,13 +1945,6 @@ class MainActivity: FlutterFragmentActivity() { } result.success(response) } - "scanSafTree" -> { - val treeUri = call.argument("tree_uri") ?: "" - val response = withContext(Dispatchers.IO) { - scanSafTree(treeUri) - } - result.success(response) - } "scanSafTreeToNDJSONFile" -> { val treeUri = call.argument("tree_uri") ?: "" val outputPath = call.argument("output_path") ?: "" diff --git a/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt b/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt index d422387f..0c8cd1ee 100644 --- a/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt +++ b/android/app/src/rust/kotlin/com/zarz/spotiflac/SelectedCoreBackend.kt @@ -297,10 +297,6 @@ internal object RustCoreBackend : CoreBackend { libraryCoverDirectory = directory } - override fun scanLibraryFolder(folder: String): String = withLibraryDirectories(listOf(folder)) { - it.scanLibraryFolder(File(folder).canonicalPath, null) - } - override fun scanLibraryFolderToNdjsonFile(folder: String, output: String): Long = withLibraryDirectories(listOf(folder)) { current -> require(File(output).isAbsolute && File(output).extension.equals("ndjson", ignoreCase = true)) { @@ -622,7 +618,7 @@ internal object RustCoreBackend : CoreBackend { null } "setLoggingEnabled", "setAllowPrivateNetwork", "setDownloadFallbackExtensionIds", - "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions", + "setNetworkCompatibilityOptions", "setLyricsProviders", "setLyricsFetchOptions" -> synchronized(this) { when (method) { "setLoggingEnabled" -> { @@ -637,7 +633,7 @@ internal object RustCoreBackend : CoreBackend { manager?.environment()?.use { it.setAllowPrivateNetwork(allowed) } allowPrivateNetwork = allowed } - "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions" -> { + "setNetworkCompatibilityOptions" -> { val allowed = args["allow_http"] as? Boolean ?: false val insecureTls = args["insecure_tls"] as? Boolean ?: false manager?.environment()?.use { it.setNetworkCompatibilityOptions(allowed, insecureTls) } @@ -678,19 +674,10 @@ internal object RustCoreBackend : CoreBackend { "getRepoExtensions" -> return repositoryOwner().extensions( args["force_refresh"] as? Boolean ?: false, ) - "searchRepoExtensions" -> return repositoryOwner().search( - string("query"), - string("category"), - ) - "getRepoCategories" -> return JSONArray(repositoryOwner().categories()).toString() "downloadRepoExtension" -> return repositoryOwner().download( string("extension_id"), string("dest_dir"), ) - "clearRepoCache" -> { - repositoryOwner().clearCache() - return null - } } val current = owner() return when (method) { @@ -708,10 +695,6 @@ internal object RustCoreBackend : CoreBackend { current.setEnabled(string("extension_id"), args["enabled"] as? Boolean ?: false) null } - "unloadExtension" -> { - current.unload(string("extension_id")) - null - } "removeExtension" -> { current.remove(string("extension_id")) null @@ -730,8 +713,6 @@ internal object RustCoreBackend : CoreBackend { "getProviderPriority", "getMetadataProviderPriority" -> { JSONObject(current.providerPriorities()).optJSONArray(if (method == "getProviderPriority") "download" else "metadata")?.toString() ?: "[]" } - "getLyricsProviders" -> current.getLyricsProvidersJson() - "getLyricsFetchOptions" -> current.getLyricsFetchOptionsJson() "getAvailableLyricsProviders" -> current.getAvailableLyricsProvidersJson() "searchTracksWithMetadataProviders" -> current.searchMetadataProviders( string("query"), @@ -766,14 +747,6 @@ internal object RustCoreBackend : CoreBackend { string("spotify_id"), null, ) - "getSpotifyIDFromDeezerTrack" -> current.getSpotifyIdFromDeezerTrack( - string("deezer_track_id"), - null, - ) - "getTidalURLFromDeezerTrack" -> current.getTidalUrlFromDeezerTrack( - string("deezer_track_id"), - null, - ) "getTrackPlatformLinks" -> current.getTrackPlatformLinksJson( string("spotify_id"), string("isrc"), @@ -810,36 +783,15 @@ internal object RustCoreBackend : CoreBackend { } "findURLHandler" -> current.findUrlHandler(string("url")) ?: "" "handleURLWithExtension" -> current.handleUrlJson(string("url")) - "enrichTrackWithExtension" -> current.enrichTrackJson(string("extension_id"), string("track", "{}")) "getExtensionPendingAuth" -> current.getExtensionPendingAuthJson(string("extension_id")).ifEmpty { null } - "setExtensionAuthCode" -> current.environment().use { it.setAuthCode(string("extension_id"), string("auth_code")); null } "completeExtensionSessionGrant" -> { completeSessionGrant(current, string("extension_id"), string("grant")) true } - "setExtensionTokens" -> current.environment().use { - it.setAuthTokens(string("extension_id"), string("access_token"), string("refresh_token"), (args["expires_in"] as? Int)?.toLong() ?: 0L) - null - } - "clearExtensionPendingAuth" -> current.environment().use { it.clearPendingAuth(string("extension_id")); null } - "isExtensionAuthenticated" -> current.environment().use { it.isAuthenticated(string("extension_id")) } - "getAllPendingAuthRequests" -> current.environment().use { it.allPendingAuth() } "getAllDownloadProgress" -> current.environment().use { environment -> environment.downloadState().use { state -> state.allProgress() } } "cleanupConnections" -> current.environment().use { it.cleanupConnections(); null } - "getPendingFFmpegCommand", "getAllPendingFFmpegCommands", "setFFmpegCommandResult" -> current.environment().use { environment -> - environment.ffmpegCommands().use { commands -> - when (method) { - "getPendingFFmpegCommand" -> commands.getCommand(string("command_id")) - "getAllPendingFFmpegCommands" -> commands.pending() - else -> { - commands.complete(string("command_id"), args["success"] as? Boolean ?: false, string("output"), string("error")) - null - } - } - } - } "clearItemProgress", "cancelDownload", "resetDownloadCancel" -> current.environment().use { environment -> environment.downloadState().use { state -> when (method) { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 771f5524..9cf44851 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -257,8 +257,8 @@ import UniformTypeIdentifiers private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) { let osMethods: Set = ["getBackendImplementations", "startWebAuthSession", "beginBackgroundDownloadTask", "endBackgroundDownloadTask", - "pickIosDirectory", "createIosBookmarkFromPath", "resolveIosBookmark", "startAccessingIosBookmark", "stopAccessingIosBookmark", "downloadCoverToFile", "releaseMemory", "releaseMemoryUnderPressure", - "setLibraryCoverCacheDir", "scanLibraryFolder", "scanLibraryFolderToNDJSONFile", "scanLibraryFolderIncremental", + "pickIosDirectory", "startAccessingIosBookmark", "stopAccessingIosBookmark", "downloadCoverToFile", "releaseMemory", "releaseMemoryUnderPressure", + "setLibraryCoverCacheDir", "scanLibraryFolderToNDJSONFile", "scanLibraryFolderIncremental", "getLibraryScanProgress", "cancelLibraryScan", "parseCueSheet", "extractCoverToFile", "rewriteSplitArtistTags", "writeM4AFreeformTags", "ensureAC4Config", "writeAC4Metadata", "reEnrichFile", "checkHiResAuthenticity"] @@ -546,11 +546,6 @@ import UniformTypeIdentifiers try coreBackend.setLibraryCoverCacheDirectory(path: cacheDir) return nil - case "scanLibraryFolder": - let args = call.arguments as! [String: Any] - let folderPath = args["folder_path"] as! String - return bridgeJsonResult(try coreBackend.scanLibraryFolder(folder: folderPath)) - case "scanLibraryFolderToNDJSONFile": guard let args = call.arguments as? [String: Any], @@ -582,11 +577,6 @@ import UniformTypeIdentifiers return nil - case "resolveIosBookmark": - let args = call.arguments as! [String: Any] - let bookmarkBase64 = args["bookmark"] as! String - return try resolveIosBookmark(bookmarkBase64) - case "startAccessingIosBookmark": guard let args = call.arguments as? [String: Any], @@ -608,11 +598,6 @@ import UniformTypeIdentifiers stopAccessingIosBookmark(token: token) return nil - case "createIosBookmarkFromPath": - let args = call.arguments as! [String: Any] - let path = args["path"] as! String - return try createIosBookmarkFromPath(path) - case "parseCueSheet": let args = call.arguments as! [String: Any] @@ -663,68 +648,6 @@ import UniformTypeIdentifiers // MARK: - iOS Security-Scoped Bookmark Helpers - /// Create a security-scoped bookmark from a filesystem path (e.g. from FilePicker). - /// The path must currently be accessible (within the same picker session). - /// Returns base64-encoded bookmark data. - private func createIosBookmarkFromPath(_ path: String) throws -> String { - let url = URL(fileURLWithPath: path) - do { - #if os(macOS) - let options: URL.BookmarkCreationOptions = .withSecurityScope - #else - let options: URL.BookmarkCreationOptions = [] - #endif - let bookmarkData = try url.bookmarkData( - options: options, - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - return bookmarkData.base64EncodedString() - } catch { - throw NSError( - domain: "SpotiFLAC", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Failed to create bookmark for path \(path): \(error.localizedDescription)"] - ) - } - } - - /// Resolve a base64-encoded security-scoped bookmark and return the resolved path. - /// Does NOT start accessing the resource. - private func resolveIosBookmark(_ bookmarkBase64: String) throws -> String { - guard let bookmarkData = Data(base64Encoded: bookmarkBase64) else { - throw NSError( - domain: "SpotiFLAC", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Invalid base64 bookmark data"] - ) - } - - var isStale = false - let url: URL - do { - #if os(macOS) - let options: URL.BookmarkResolutionOptions = .withSecurityScope - #else - let options: URL.BookmarkResolutionOptions = [] - #endif - url = try URL( - resolvingBookmarkData: bookmarkData, - options: options, - relativeTo: nil, - bookmarkDataIsStale: &isStale - ) - } catch { - throw NSError( - domain: "SpotiFLAC", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "Failed to resolve bookmark: \(error.localizedDescription)"] - ) - } - - return url.path - } - private func invalidArgumentsError(_ method: String) -> NSError { return NSError( domain: "SpotiFLAC", diff --git a/ios/Runner/CoreBackend.swift b/ios/Runner/CoreBackend.swift index ef070d7a..d96dccb4 100644 --- a/ios/Runner/CoreBackend.swift +++ b/ios/Runner/CoreBackend.swift @@ -173,7 +173,6 @@ protocol CoreBackend { func ensureAc4Config(path: String, reference: String) throws -> String func writeAc4Metadata(path: String, metadataJson: String, coverPath: String) throws -> String func setLibraryCoverCacheDirectory(path: String) throws - func scanLibraryFolder(folder: String) throws -> String func scanLibraryFolderToNdjsonFile(folder: String, output: String) throws -> Int func scanLibraryFolderIncremental(folder: String, existing: String) throws -> String func getLibraryScanProgress() throws -> String @@ -340,12 +339,6 @@ final class RustCoreBackend: CoreBackend { libraryCoverScope = next } - func scanLibraryFolder(folder: String) throws -> String { - try withLibraryDirectories([folder]) { - try $0.scanLibraryFolder(folder: URL(fileURLWithPath: folder).resolvingSymlinksInPath().standardizedFileURL.path, lease: nil) - } - } - func scanLibraryFolderToNdjsonFile(folder: String, output: String) throws -> Int { guard output.hasPrefix("/"), URL(fileURLWithPath: output).pathExtension.lowercased() == "ndjson" else { throw failure("Library scan output must be an absolute NDJSON path") @@ -632,11 +625,7 @@ final class RustCoreBackend: CoreBackend { case "setRepoRegistryUrl": try repositoryOwner().setRegistryUrl(url: string("registry_url")); return nil case "clearRepoRegistryUrl": try repositoryOwner().clearRegistryUrl(); return nil case "getRepoExtensions": return try repositoryOwner().extensions(forceRefresh: args["force_refresh"] as? Bool ?? false) - case "searchRepoExtensions": return try repositoryOwner().search(query: string("query"), category: string("category")) - case "getRepoCategories": - return String(decoding: try JSONSerialization.data(withJSONObject: repositoryOwner().categories()), as: UTF8.self) case "downloadRepoExtension": return try repositoryOwner().download(extensionId: string("extension_id"), destinationDirectory: string("dest_dir")) - case "clearRepoCache": try repositoryOwner().clearCache(); return nil case "cleanupExtensions": shutdownOwner(); return nil case "buildFilename": return try buildFilename(template: string("template"), metadataJson: string("metadata", "{}")) case "sanitizeFilename": return sanitizeFilename(filename: string("filename")) @@ -651,7 +640,7 @@ final class RustCoreBackend: CoreBackend { defer { ownerLock.unlock() } try manager?.environment().logBuffer().clear() return nil - case "setLoggingEnabled", "setAllowPrivateNetwork", "setDownloadFallbackExtensionIds", "setLyricsProviders", "setLyricsFetchOptions", "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions": + case "setLoggingEnabled", "setAllowPrivateNetwork", "setDownloadFallbackExtensionIds", "setLyricsProviders", "setLyricsFetchOptions", "setNetworkCompatibilityOptions": ownerLock.lock() defer { ownerLock.unlock() } switch method { @@ -663,7 +652,7 @@ final class RustCoreBackend: CoreBackend { let allowed = args["allowed"] as? Bool ?? false try manager?.environment().setAllowPrivateNetwork(allow: allowed) allowPrivateNetwork = allowed - case "setNetworkCompatibilityOptions", "setSongLinkNetworkOptions": + case "setNetworkCompatibilityOptions": let allowed = args["allow_http"] as? Bool ?? false let insecureTLS = args["insecure_tls"] as? Bool ?? false try manager?.environment().setNetworkCompatibilityOptions(allowHttp: allowed, insecureTls: insecureTLS) @@ -702,7 +691,6 @@ final class RustCoreBackend: CoreBackend { case "checkExtensionUpgrade": return try current.checkUpgrade(packagePath: string("file_path")) case "getInstalledExtensions": return try current.installed() case "setExtensionEnabled": try current.setEnabled(extensionId: string("extension_id"), enabled: args["enabled"] as? Bool ?? false); return nil - case "unloadExtension": try current.unload(extensionId: string("extension_id")); return nil case "removeExtension": try current.remove(extensionId: string("extension_id")); return nil case "getExtensionSettings": return try current.environment().settings(extensionId: string("extension_id")) case "setExtensionSettings": try current.updateSettings(extensionId: string("extension_id"), settingsJson: string("settings", "{}")); return nil @@ -716,14 +704,11 @@ final class RustCoreBackend: CoreBackend { return try current.getProviderMetadataJson(providerId: string("provider_id"), resourceType: string("resource_type"), resourceId: string("resource_id"), lease: nil) case "findCollectionAcrossExtensions": return try current.findCollectionAcrossExtensionsJson(requestJson: arguments as? String ?? "{}", lease: nil) - case "enrichTrackWithExtension": return try current.enrichTrackJson(extensionId: string("extension_id"), trackJson: string("track", "{}")) case "handleURLWithExtension": return try current.handleUrlJson(url: string("url")) case "findURLHandler": return try current.findUrlHandler(url: string("url")) ?? "" case "searchDeezerByISRC": return try current.searchDeezerByIsrcForItemId(isrc: string("isrc"), itemId: string("item_id"), lease: nil) case "getDeezerExtendedMetadata": return try current.getDeezerExtendedMetadata(trackId: string("track_id"), lease: nil) case "convertSpotifyToDeezer": return try current.convertSpotifyToDeezer(resourceType: string("resource_type"), spotifyId: string("spotify_id"), lease: nil) - case "getSpotifyIDFromDeezerTrack": return try current.getSpotifyIdFromDeezerTrack(trackId: string("deezer_track_id"), lease: nil) - case "getTidalURLFromDeezerTrack": return try current.getTidalUrlFromDeezerTrack(trackId: string("deezer_track_id"), lease: nil) case "getTrackPlatformLinks": return try current.getTrackPlatformLinksJson(spotifyId: string("spotify_id"), isrc: string("isrc"), lease: nil) case "fetchMusicBrainzTags": let genre = (try? current.fetchMusicBrainzGenreByIsrc(isrc: string("isrc"), lease: nil)) ?? "" @@ -737,12 +722,11 @@ final class RustCoreBackend: CoreBackend { } case "clearTrackCache": try current.clearTrackIdCache(); return nil case "setMetadataLanguage": try current.setMetadataLanguage(tag: string("tag")); return nil - case "downloadByStrategy", "downloadWithExtensions": + case "downloadByStrategy": guard let request = arguments as? String else { throw NSError(domain: "CoreBackend", code: 1, userInfo: [NSLocalizedDescriptionKey: "Download request must be a JSON string"]) } return try withFFmpegCommands(current) { - if method == "downloadWithExtensions" { return try current.downloadWithExtensionsJson(requestJson: request) } return try current.downloadByStrategy(requestJson: request) } case "getAllDownloadProgress": return try current.environment().downloadState().allProgress() @@ -751,16 +735,9 @@ final class RustCoreBackend: CoreBackend { case "cancelDownload": try current.environment().downloadState().cancelDownload(itemId: string("item_id")); return nil case "resetDownloadCancel": try current.environment().downloadState().resetDownloadCancel(itemId: string("item_id")); return nil case "getExtensionPendingAuth": return try current.getExtensionPendingAuthJson(extensionId: string("extension_id")) - case "setExtensionAuthCode": try current.environment().setAuthCode(extensionId: string("extension_id"), code: string("auth_code")); return nil case "completeExtensionSessionGrant": try completeSessionGrant(current, id: string("extension_id"), grant: string("grant")) return true - case "setExtensionTokens": - try current.environment().setAuthTokens(extensionId: string("extension_id"), accessToken: string("access_token"), refreshToken: string("refresh_token"), expiresIn: (args["expires_in"] as? NSNumber)?.int64Value ?? 0) - return nil - case "clearExtensionPendingAuth": try current.environment().clearPendingAuth(extensionId: string("extension_id")); return nil - case "isExtensionAuthenticated": return try current.environment().isAuthenticated(extensionId: string("extension_id")) - case "getAllPendingAuthRequests": return try current.environment().allPendingAuth() case "getLyricsLRC", "getLyricsLRCWithSource", "fetchAndSaveLyrics": let request = LyricsRequest( spotifyId: string("spotify_id"), @@ -773,18 +750,6 @@ final class RustCoreBackend: CoreBackend { if method == "getLyricsLRCWithSource" { return try current.getLyricsLrcWithSource(request: request, lease: nil) } try current.fetchAndSaveLyrics(request: request, outputPath: string("output_path"), lease: nil) return "{\"success\":true}" - case "getPendingFFmpegCommand": - return try current.environment().ffmpegCommands().getCommand(commandId: string("command_id")) - case "getAllPendingFFmpegCommands": - return try current.environment().ffmpegCommands().pending() - case "setFFmpegCommandResult": - _ = try current.environment().ffmpegCommands().complete( - commandId: string("command_id"), - success: args["success"] as? Bool ?? false, - output: string("output"), - error: string("error") - ) - return nil case "runPostProcessingV2": return try withFFmpegCommands(current) { try current.runPostProcessing(inputJson: string("input"), metadataJson: string("metadata"), timeoutMs: 120_000) @@ -798,8 +763,6 @@ final class RustCoreBackend: CoreBackend { let priorities = try JSONSerialization.jsonObject(with: Data(current.providerPriorities().utf8)) as! [String: Any] let value = priorities[method == "getProviderPriority" ? "download" : "metadata"] as? [String] ?? [] return String(decoding: try JSONSerialization.data(withJSONObject: value), as: UTF8.self) - case "getLyricsProviders": return try current.getLyricsProvidersJson() - case "getLyricsFetchOptions": return try current.getLyricsFetchOptionsJson() case "getAvailableLyricsProviders": return try current.getAvailableLyricsProvidersJson() default: throw failure("Rust application method is not connected yet: \(method)") } diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 50a86d96..88017ad7 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1123,11 +1123,6 @@ class PlatformBridge { }); } - static Future> getLyricsProviders() async { - final result = await _channel.invokeMethod('getLyricsProviders'); - return _decodeStringListResult(result, 'getLyricsProviders'); - } - static Future>> getAvailableLyricsProviders() async { final result = await _channel.invokeMethod('getAvailableLyricsProviders'); @@ -1144,10 +1139,6 @@ class PlatformBridge { }); } - static Future> getLyricsFetchOptions() { - return _invokeMap('getLyricsFetchOptions'); - } - static Future> reEnrichFile( Map request, ) { @@ -1608,11 +1599,6 @@ class PlatformBridge { } catch (_) {} } - static Future> getGoRuntimeMetrics() async { - final result = await _channel.invokeMethod('getGoRuntimeMetrics'); - return _decodeRequiredMapResult(result, 'getGoRuntimeMetrics'); - } - /// Tells the backend the app's display language so metadata providers /// localize by it instead of IP geolocation. Best-effort. static Future setMetadataLanguage(String tag) async { @@ -1657,14 +1643,6 @@ class PlatformBridge { return _invokeMap('loadExtensionFromPath', {'file_path': filePath}); } - static Future unloadExtension(String extensionId) async { - _log.d('unloadExtension: $extensionId'); - await _clearLookupCaches(); - await _channel.invokeMethod('unloadExtension', { - 'extension_id': extensionId, - }); - } - static Future removeExtension(String extensionId) async { _log.d('removeExtension: $extensionId'); await _clearLookupCaches(); @@ -1835,17 +1813,6 @@ class PlatformBridge { return _decodeNullableMapResult(result, 'getExtensionPendingAuth'); } - static Future setExtensionAuthCode( - String extensionId, - String authCode, - ) async { - _log.d('setExtensionAuthCode: $extensionId'); - await _channel.invokeMethod('setExtensionAuthCode', { - 'extension_id': extensionId, - 'auth_code': authCode, - }); - } - static final Map> _extensionSessionGrantCompletions = >{}; @@ -1890,68 +1857,6 @@ class PlatformBridge { return success; } - static Future setExtensionTokens( - String extensionId, { - required String accessToken, - String? refreshToken, - int? expiresIn, - }) async { - _log.d('setExtensionTokens: $extensionId'); - await _channel.invokeMethod('setExtensionTokens', { - 'extension_id': extensionId, - 'access_token': accessToken, - 'refresh_token': refreshToken ?? '', - 'expires_in': expiresIn ?? 0, - }); - } - - static Future clearExtensionPendingAuth(String extensionId) async { - await _channel.invokeMethod('clearExtensionPendingAuth', { - 'extension_id': extensionId, - }); - } - - static Future isExtensionAuthenticated(String extensionId) async { - final result = await _channel.invokeMethod('isExtensionAuthenticated', { - 'extension_id': extensionId, - }); - return result as bool; - } - - static Future>> getAllPendingAuthRequests() async { - final result = await _channel.invokeMethod('getAllPendingAuthRequests'); - return _decodeMapListResult(result, 'getAllPendingAuthRequests'); - } - - static Future?> getPendingFFmpegCommand( - String commandId, - ) async { - final result = await _channel.invokeMethod('getPendingFFmpegCommand', { - 'command_id': commandId, - }); - return _decodeNullableMapResult(result, 'getPendingFFmpegCommand'); - } - - static Future setFFmpegCommandResult( - String commandId, { - required bool success, - String output = '', - String error = '', - }) async { - await _channel.invokeMethod('setFFmpegCommandResult', { - 'command_id': commandId, - 'success': success, - 'output': output, - 'error': error, - }); - } - - static Future>> - getAllPendingFFmpegCommands() async { - final result = await _channel.invokeMethod('getAllPendingFFmpegCommands'); - return _decodeMapListResult(result, 'setFFmpegCommandResult'); - } - static Future>> customSearchWithExtension( String extensionId, String query, { @@ -2218,15 +2123,6 @@ class PlatformBridge { }); } - static Future>> scanLibraryFolder( - String folderPath, - ) async { - final result = await _channel.invokeMethod('scanLibraryFolder', { - 'folder_path': folderPath, - }); - return _decodeMapListResultAsync(result, 'scanLibraryFolder'); - } - static Future scanLibraryFolderToNDJSONFile( String folderPath, { bool forceFullScan = false, @@ -2268,13 +2164,6 @@ class PlatformBridge { ); } - static Future>> scanSafTree(String treeUri) async { - final result = await _channel.invokeMethod('scanSafTree', { - 'tree_uri': treeUri, - }); - return _decodeMapListResultAsync(result, 'scanSafTree'); - } - static Future scanSafTreeToNDJSONFile( String treeUri, { bool forceFullScan = false, @@ -2592,21 +2481,6 @@ class PlatformBridge { return IosPickedDirectory(path: path, bookmark: bookmark); } - /// Create a security-scoped bookmark from a filesystem path picked by - /// FilePicker on iOS. Must be called while the picker session is still active. - /// Returns base64-encoded bookmark data, or null on failure. - static Future createIosBookmarkFromPath(String path) async { - try { - final result = await _channel.invokeMethod('createIosBookmarkFromPath', { - 'path': path, - }); - return result as String?; - } catch (e) { - _log.w('Failed to create iOS bookmark from path: $e'); - return null; - } - } - /// Resolve a base64-encoded iOS security-scoped bookmark and start accessing /// the resource. The returned lease must be passed to /// [stopAccessingIosBookmark] by the operation that acquired it. @@ -2730,23 +2604,6 @@ class PlatformBridge { return _decodeMapListResult(result, 'getRepoExtensions'); } - static Future>> searchRepoExtensions( - String query, { - String? category, - }) async { - _log.d('searchRepoExtensions: "$query" (category: $category)'); - final result = await _channel.invokeMethod('searchRepoExtensions', { - 'query': query, - 'category': category ?? '', - }); - return _decodeMapListResult(result, 'searchRepoExtensions'); - } - - static Future> getRepoCategories() async { - final result = await _channel.invokeMethod('getRepoCategories'); - return _decodeStringListResult(result, 'getRepoCategories'); - } - static Future downloadRepoExtension( String extensionId, String destDir, @@ -2759,11 +2616,6 @@ class PlatformBridge { return result as String; } - static Future clearRepoCache() async { - _log.d('clearRepoCache'); - await _channel.invokeMethod('clearRepoCache'); - } - static Future> parseCueSheet( String cuePath, { String audioDir = '', diff --git a/rust_backend/crates/core/src/cover.rs b/rust_backend/crates/core/src/cover.rs index 804f9617..62cd5fd5 100644 --- a/rust_backend/crates/core/src/cover.rs +++ b/rust_backend/crates/core/src/cover.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use std::io::Cursor; mod library; -pub use library::{clear_library_thumbnail_cache, library_thumbnail}; +pub use library::library_thumbnail; pub const MAX_DOWNLOAD_BYTES: usize = 24 << 20; pub const LIBRARY_MAX_DIMENSION: i64 = 800; diff --git a/rust_backend/crates/core/src/cover/library.rs b/rust_backend/crates/core/src/cover/library.rs index 4e3d5f9e..4f2dc8be 100644 --- a/rust_backend/crates/core/src/cover/library.rs +++ b/rust_backend/crates/core/src/cover/library.rs @@ -90,14 +90,6 @@ pub fn library_thumbnail( Ok(thumbnail) } -pub fn clear_library_thumbnail_cache() { - if let Some(mutex) = CACHE.get() - && let Ok(mut cache) = mutex.try_lock() - { - *cache = Cache::default(); - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/rust_backend/crates/core/src/isrc/cache.rs b/rust_backend/crates/core/src/isrc/cache.rs index f6bd71eb..8afb492c 100644 --- a/rust_backend/crates/core/src/isrc/cache.rs +++ b/rust_backend/crates/core/src/isrc/cache.rs @@ -1,5 +1,4 @@ use crate::matching::uppercase; -use serde::{Deserialize, Deserializer, Serialize}; use std::collections::BTreeMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, MutexGuard, TryLockError, Weak}; @@ -246,47 +245,6 @@ impl IndexCache { pub fn clear(&self) { self.indexes.lock().expect("ISRC cache lock").clear(); } - - pub fn prebuild( - &self, - directory: &str, - files: &dyn IndexFiles, - check: Check<'_>, - ) -> Result<(), String> { - if directory.is_empty() { - return Err("output directory is required".into()); - } - self.index(directory, files, true, check).map(|_| ()) - } - - pub fn check_batch( - &self, - directory: &str, - tracks: &[TrackQuery], - files: &dyn IndexFiles, - check: Check<'_>, - ) -> Result, String> { - let index = self.index(directory, files, false, check)?; - let index = index.lock().expect("ISRC index lock"); - tracks - .iter() - .map(|track| { - check()?; - // Go's batch API deliberately trusts the cache without stat calls. - let path = index - .entries - .get(&uppercase(&track.isrc)) - .filter(|_| !track.isrc.is_empty()); - Ok(TrackExistence { - isrc: track.isrc.clone(), - exists: path.is_some(), - file_path: path.cloned().unwrap_or_default(), - track_name: track.track_name.clone(), - artist_name: track.artist_name.clone(), - }) - }) - .collect() - } } fn wait_for_builder<'a>( @@ -302,84 +260,3 @@ fn wait_for_builder<'a>( } } } - -#[derive(Clone, Debug, Default)] -pub struct TrackQuery { - pub isrc: String, - pub track_name: String, - pub artist_name: String, -} - -impl<'de> Deserialize<'de> for TrackQuery { - fn deserialize>(deserializer: D) -> Result { - struct Visitor; - impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = TrackQuery; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("a track object") - } - fn visit_map>( - self, - mut map: M, - ) -> Result { - let mut track = TrackQuery::default(); - while let Some(key) = map.next_key::()? { - let field = if equal_field(&key, "isrc") { - Some(&mut track.isrc) - } else if equal_field(&key, "track_name") { - Some(&mut track.track_name) - } else if equal_field(&key, "artist_name") { - Some(&mut track.artist_name) - } else { - None - }; - if let Some(field) = field { - if let Some(value) = map.next_value::>()? { - *field = value; - } - } else { - map.next_value::()?; - } - } - Ok(track) - } - } - deserializer.deserialize_map(Visitor) - } -} - -pub fn parse_tracks(json: &str) -> Result, String> { - // Go matches the snake_case JSON tags, folds field-name case, and ignores - // null string values without overwriting an earlier duplicate field. - serde_json::from_str::>>>(&crate::text::json_surrogates(json)) - .map(|tracks| { - tracks - .unwrap_or_default() - .into_iter() - .map(Option::unwrap_or_default) - .collect() - }) - .map_err(|error| format!("failed to parse tracks JSON: {error}")) -} - -fn equal_field(value: &str, target: &str) -> bool { - value.eq_ignore_ascii_case(target) - || (value.chars().count() == target.len() - && value.chars().zip(target.chars()).all(|(actual, expected)| { - actual.eq_ignore_ascii_case(&expected) - || (actual == 'ſ' && expected == 's') - || (actual == 'K' && expected == 'k') - })) -} - -#[derive(Debug, Serialize)] -pub struct TrackExistence { - pub isrc: String, - pub exists: bool, - #[serde(skip_serializing_if = "String::is_empty")] - pub file_path: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub track_name: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub artist_name: String, -} diff --git a/rust_backend/crates/core/src/isrc/mod.rs b/rust_backend/crates/core/src/isrc/mod.rs index d63281ba..6aad0130 100644 --- a/rust_backend/crates/core/src/isrc/mod.rs +++ b/rust_backend/crates/core/src/isrc/mod.rs @@ -2,7 +2,7 @@ mod cache; mod native_files; -pub use cache::{FileStamp, IndexCache, IndexFiles, TrackExistence, TrackQuery, parse_tracks}; +pub use cache::{FileStamp, IndexCache, IndexFiles}; pub use native_files::NativeFiles; use std::io::{Read, Seek, SeekFrom}; diff --git a/rust_backend/crates/core/src/metadata/musicbrainz/case_data.rs b/rust_backend/crates/core/src/metadata/musicbrainz/case_data.rs index d31e399a..e64e31ae 100644 --- a/rust_backend/crates/core/src/metadata/musicbrainz/case_data.rs +++ b/rust_backend/crates/core/src/metadata/musicbrainz/case_data.rs @@ -1,4 +1,4 @@ -// Generated by rust_backend/tools/generate_case_data.go. DO NOT EDIT. +// Generated by docs/internal/legacy-go/rust_backend/tools/generate_case_data.go. DO NOT EDIT. // Unicode 15.0.0, golang.org/x/text v0.41.0; see NOTICE. // Flags: cased=1, case-ignorable=2, word break=4, mid punctuation=8. #[rustfmt::skip] diff --git a/rust_backend/crates/core/src/progress/mod.rs b/rust_backend/crates/core/src/progress/mod.rs index 0b3b7359..3ff44a16 100644 --- a/rust_backend/crates/core/src/progress/mod.rs +++ b/rust_backend/crates/core/src/progress/mod.rs @@ -240,10 +240,6 @@ impl ProgressRegistry { self.update(id, mark_downloading) } - pub fn set_total(&self, id: &str, total: i64) -> Result<(), ProgressError> { - self.update(id, |item| item.bytes_total = total) - } - pub fn set_received(&self, id: &str, received: i64) -> Result<(), ProgressError> { self.update(id, |item| set_received(item, received)) } @@ -322,15 +318,6 @@ impl ProgressRegistry { Ok(()) } - pub fn clear(&self) -> Result<(), ProgressError> { - let mut state = self.shared.state.lock().expect("progress state lock"); - state.check()?; - state.items.clear(); - state.removed.clear(); - state.reset = state.next(&self.shared.changed); - Ok(()) - } - pub fn item(&self, id: &str) -> Result { let state = self.shared.state.lock().expect("progress state lock"); state.check()?; @@ -361,12 +348,6 @@ impl ProgressRegistry { Ok(state.cached.clone()) } - pub fn delta(&self, since: i64) -> Result { - let state = self.shared.state.lock().expect("progress state lock"); - state.check()?; - Ok(state.delta(since)) - } - pub fn subscribe(&self) -> Result { self.shared .state diff --git a/rust_backend/crates/extensions/src/auth.rs b/rust_backend/crates/extensions/src/auth.rs index 1d32c073..fc5a184e 100644 --- a/rust_backend/crates/extensions/src/auth.rs +++ b/rust_backend/crates/extensions/src/auth.rs @@ -97,23 +97,6 @@ impl AuthRegistry { self.edit(id, |record| record.code = Zeroizing::new(code.to_owned())) } - pub fn set_tokens( - &self, - id: &str, - access: &str, - refresh: &str, - expires_in: i64, - ) -> Result<(), String> { - let expires = (expires_in > 0) - .then(|| self.now() + i128::from(expires_in.wrapping_mul(1_000_000_000))); - self.edit(id, |record| { - record.access_token = Zeroizing::new(access.to_owned()); - record.refresh_token = Zeroizing::new(refresh.to_owned()); - record.authenticated = !access.is_empty(); - record.expires_at = expires; - }) - } - pub fn code(&self, id: &str) -> Option { self.state .lock() @@ -251,16 +234,6 @@ impl AuthRegistry { }) } - pub fn all_pending(&self) -> Vec { - self.state - .lock() - .expect("auth registry lock") - .pending - .values() - .cloned() - .collect() - } - pub fn resolve_callback(&self, nonce: &str, consume: bool) -> Result { let nonce = nonce.trim(); if nonce.is_empty() { diff --git a/rust_backend/crates/extensions/src/backend.rs b/rust_backend/crates/extensions/src/backend.rs index 565fd685..7e4b25a6 100644 --- a/rust_backend/crates/extensions/src/backend.rs +++ b/rust_backend/crates/extensions/src/backend.rs @@ -184,22 +184,6 @@ impl Backend { Arc::clone(&self.availability) } - pub fn get_app_version(&self) -> Result { - let _operation = self.enter()?; - self.manager - .environment() - .get_app_version() - .map_err(|error| error.to_string()) - } - - pub fn set_app_version(&self, version: &str) -> Result<(), String> { - let _operation = self.enter()?; - self.manager - .environment() - .set_app_version(version) - .map_err(|error| error.to_string()) - } - pub fn get_extension_pending_auth_json(&self, id: &str) -> Result { let _operation = self.enter()?; let id = id.trim(); diff --git a/rust_backend/crates/extensions/src/backend/cover.rs b/rust_backend/crates/extensions/src/backend/cover.rs index b618dc41..b56cd2ff 100644 --- a/rust_backend/crates/extensions/src/backend/cover.rs +++ b/rust_backend/crates/extensions/src/backend/cover.rs @@ -391,13 +391,6 @@ impl Backend { Ok(output.display()) } - pub fn clear_cover_memory_cache(&self) -> Result<(), String> { - let _operation = self.enter()?; - self.cover.clear(); - spotiflac_core::cover::clear_library_thumbnail_cache(); - Ok(()) - } - pub fn download_cover_to_file_sized( &self, url: &str, diff --git a/rust_backend/crates/extensions/src/backend/downloads.rs b/rust_backend/crates/extensions/src/backend/downloads.rs index 571e9bb8..27b163b0 100644 --- a/rust_backend/crates/extensions/src/backend/downloads.rs +++ b/rust_backend/crates/extensions/src/backend/downloads.rs @@ -60,17 +60,6 @@ impl Backend { .unwrap_or_else(|error| native_error_response(&error).to_string())) } - pub fn download_with_extensions_json( - &self, - raw: &str, - check: &Check<'_>, - ) -> Result { - let _operation = self.enter()?; - let request = - DownloadRequest::parse(raw).map_err(|error| format!("invalid request: {error}"))?; - self.download_request(request, check) - } - fn download_request( &self, mut request: DownloadRequest, diff --git a/rust_backend/crates/extensions/src/backend/library/scan.rs b/rust_backend/crates/extensions/src/backend/library/scan.rs index 2999e162..ec447986 100644 --- a/rust_backend/crates/extensions/src/backend/library/scan.rs +++ b/rust_backend/crates/extensions/src/backend/library/scan.rs @@ -75,21 +75,6 @@ impl Backend { Ok(()) } - pub fn scan_library_folder(&self, folder: &str, check: Check<'_>) -> Result { - let mut tracks = Vec::new(); - self.scan_library( - folder, - None, - true, - &mut |value| { - tracks.push(value); - Ok(()) - }, - check, - )?; - Ok(tracks.into()) - } - pub fn scan_library_folder_incremental( &self, folder: &str, diff --git a/rust_backend/crates/extensions/src/backend/lyrics.rs b/rust_backend/crates/extensions/src/backend/lyrics.rs index 1306214e..cdfe46c1 100644 --- a/rust_backend/crates/extensions/src/backend/lyrics.rs +++ b/rust_backend/crates/extensions/src/backend/lyrics.rs @@ -118,11 +118,6 @@ impl Backend { Ok(response) } - pub fn get_lyrics_providers_json(&self) -> Result { - let _operation = self.enter()?; - serde_json::to_string(&self.lyrics.providers()).map_err(|error| error.to_string()) - } - pub fn set_lyrics_providers_json(&self, raw: &str) -> Result<(), String> { let _operation = self.enter()?; let providers = config::decode_providers(raw).map_err(|error| error.to_string())?; @@ -136,11 +131,6 @@ impl Backend { serde_json::to_string(&config::available_providers()).map_err(|error| error.to_string()) } - pub fn get_lyrics_fetch_options_json(&self) -> Result { - let _operation = self.enter()?; - serde_json::to_string(&self.lyrics.options()).map_err(|error| error.to_string()) - } - pub fn set_lyrics_fetch_options_json(&self, raw: &str) -> Result<(), String> { let _operation = self.enter()?; let _settings = self.lyrics_settings.lock().expect("lyrics settings lock"); diff --git a/rust_backend/crates/extensions/src/backend/metadata.rs b/rust_backend/crates/extensions/src/backend/metadata.rs index b150f536..d1f97a1a 100644 --- a/rust_backend/crates/extensions/src/backend/metadata.rs +++ b/rust_backend/crates/extensions/src/backend/metadata.rs @@ -2,7 +2,7 @@ use super::Backend; use serde::Serialize; use serde_json::{Value, json}; use spotiflac_core::metadata::TrackMetadata; -use spotiflac_providers::deezer::{self, MetadataLookup}; +use spotiflac_providers::deezer::MetadataLookup; use spotiflac_providers::musicbrainz::MusicBrainzOptions; use spotiflac_providers::resolver::{Check, ResolverError, ResolverOptions}; use std::time::{Duration, Instant}; @@ -97,13 +97,6 @@ impl Backend { .map_err(|error| error.to_string()) } - pub fn get_song_link_region(&self) -> Result { - let _operation = self.enter()?; - self.availability - .region() - .map_err(|error| error.to_string()) - } - // The retired Settings track-ID cache is distinct from catalog caches. pub fn get_track_cache_size(&self) -> Result { let _operation = self.enter()?; @@ -115,11 +108,6 @@ impl Backend { Ok(()) } - pub fn parse_deezer_url(&self, url: &str) -> Result<(String, String), String> { - let _operation = self.enter()?; - deezer::parse_url(url).map_err(|error| error.to_string()) - } - pub fn search_deezer( &self, query: &str, @@ -137,15 +125,6 @@ impl Backend { }) } - pub fn get_deezer_metadata( - &self, - kind: &str, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| self.deezer_metadata(kind, id, check)) - } - pub(super) fn deezer_metadata( &self, kind: &str, @@ -175,10 +154,6 @@ impl Backend { }) } - pub fn search_deezer_by_isrc(&self, isrc: &str, check: &Check<'_>) -> Result { - self.search_deezer_by_isrc_for_item_id(isrc, "", check) - } - pub fn search_deezer_by_isrc_for_item_id( &self, isrc: &str, @@ -246,69 +221,6 @@ impl Backend { }) } - pub fn check_track_availability_json( - &self, - id: &str, - isrc: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - encode(&self.availability.check_track(id, isrc, check)?) - }) - } - - pub fn check_album_availability_json( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - encode(&self.availability.check_album(id, check)?) - }) - } - - pub fn check_availability_from_deezer_json( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - encode(&self.availability.check_from_deezer(id, check)?) - }) - } - - pub fn check_availability_by_platform_json( - &self, - platform: &str, - kind: &str, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - encode( - &self - .availability - .check_by_platform(platform, kind, id, check)?, - ) - }) - } - - pub fn check_availability_from_url_json( - &self, - url: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - encode(&self.availability.check_from_url(url, check)?) - }) - } - - pub fn get_streaming_urls_json(&self, id: &str, check: &Check<'_>) -> Result { - self.metadata_operation(30, check, |check| { - encode(&self.availability.streaming_urls(id, check)?) - }) - } - pub fn get_deezer_id_from_spotify( &self, id: &str, @@ -316,66 +228,6 @@ impl Backend { ) -> Result { self.metadata_operation(30, check, |check| self.deezer_id_from_spotify(id, check)) } - - pub fn get_deezer_album_id_from_spotify( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - self.availability.deezer_album_id(id, check) - }) - } - - pub fn get_youtube_url_from_spotify( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - self.availability.youtube_url_from_spotify(id, check) - }) - } - - pub fn get_spotify_id_from_deezer_track( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - self.availability.platform_from_deezer(id, "spotify", check) - }) - } - - pub fn get_tidal_url_from_deezer_track( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - self.availability.platform_from_deezer(id, "tidal", check) - }) - } - - pub fn get_amazon_url_from_deezer_track( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - self.availability.platform_from_deezer(id, "amazon", check) - }) - } - - pub fn get_youtube_url_from_deezer_track( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - self.metadata_operation(30, check, |check| { - self.availability.platform_from_deezer(id, "youtube", check) - }) - } } #[cfg(test)] diff --git a/rust_backend/crates/extensions/src/backend/provider_metadata.rs b/rust_backend/crates/extensions/src/backend/provider_metadata.rs index 5b9d72a2..f2093fe6 100644 --- a/rust_backend/crates/extensions/src/backend/provider_metadata.rs +++ b/rust_backend/crates/extensions/src/backend/provider_metadata.rs @@ -10,13 +10,6 @@ use std::time::Duration; mod value_tests; impl Backend { - pub fn enrich_track_json(&self, id: &str, track_json: &str) -> Result { - let _operation = self.enter()?; - self.manager - .enrich_track_export(id, track_json) - .map_err(|error| error.to_string()) - } - pub fn handle_url_json(&self, url: &str) -> Result { let _operation = self.enter()?; let id = self diff --git a/rust_backend/crates/extensions/src/backend/reenrich.rs b/rust_backend/crates/extensions/src/backend/reenrich.rs index 2e43d205..50b97db7 100644 --- a/rust_backend/crates/extensions/src/backend/reenrich.rs +++ b/rust_backend/crates/extensions/src/backend/reenrich.rs @@ -11,24 +11,6 @@ use std::io::Write; use std::sync::Mutex; impl Backend { - /// Resolve proposed tags without reading audio, downloading covers or writing files. - pub fn preview_reenrich_file( - &self, - request_json: &str, - check: &Check<'_>, - ) -> Result { - let _operation = self.enter()?; - let check = || { - self.check()?; - check() - }; - check()?; - let request = self.resolve_reenrich_request(request_json, &check)?; - serde_json::to_string(&json!({"method":"preview","success":true, - "enriched_metadata":request.result_metadata()})) - .map_err(|error| error.to_string()) - } - /// Execute FLAC enrichment or return the existing native FFmpeg plan. /// Returned cover files belong to the caller, which removes them after use. pub fn reenrich_file(&self, request_json: &str, check: &Check<'_>) -> Result { diff --git a/rust_backend/crates/extensions/src/environment.rs b/rust_backend/crates/extensions/src/environment.rs index 6f0e483f..efed36d0 100644 --- a/rust_backend/crates/extensions/src/environment.rs +++ b/rust_backend/crates/extensions/src/environment.rs @@ -207,11 +207,6 @@ impl ExtensionEnvironment { Ok(self.store(id)?.set(StoreKind::Settings, key, value)?) } - pub fn remove_setting(&self, id: &str, key: &str) -> Result<(), EnvironmentError> { - let _operation = self.enter()?; - Ok(self.store(id)?.remove(StoreKind::Settings, key)?) - } - pub fn set_allow_private_network(&self, allow: bool) -> Result<(), EnvironmentError> { let _operation = self.enter()?; self.network.set_allow_private_network(allow); @@ -247,13 +242,6 @@ impl ExtensionEnvironment { Ok(self.app_version.get()) } - pub fn set_app_version(&self, version: &str) -> Result<(), EnvironmentError> { - let _operation = self.enter()?; - self.app_version - .set(version) - .map_err(|error| EnvironmentError::Gate(error.to_string())) - } - pub(crate) fn shared_app_version(&self) -> AppVersion { self.app_version.clone() } @@ -622,17 +610,6 @@ impl ExtensionEnvironment { Ok(self.auth.pending(id.trim())) } - pub fn all_pending_auth(&self) -> Result, EnvironmentError> { - let _operation = self.enter()?; - Ok(self.auth.all_pending()) - } - - pub fn clear_pending_auth(&self, id: &str) -> Result<(), EnvironmentError> { - let _operation = self.enter()?; - self.auth.clear_pending(id); - Ok(()) - } - pub fn resolve_callback_state( &self, state: &str, @@ -648,24 +625,6 @@ impl ExtensionEnvironment { let _operation = self.enter()?; self.auth.set_code(id, code).map_err(EnvironmentError::Auth) } - - pub fn set_auth_tokens( - &self, - id: &str, - access: &str, - refresh: &str, - expires_in: i64, - ) -> Result<(), EnvironmentError> { - let _operation = self.enter()?; - self.auth - .set_tokens(id, access, refresh, expires_in) - .map_err(EnvironmentError::Auth) - } - - pub fn is_authenticated(&self, id: &str) -> Result { - let _operation = self.enter()?; - Ok(self.auth.authenticated(id)) - } } fn network_timeout(value: Option<&Value>) -> Duration { diff --git a/rust_backend/crates/extensions/src/environment/index.rs b/rust_backend/crates/extensions/src/environment/index.rs index 2b4b07ba..0ac85fc0 100644 --- a/rust_backend/crates/extensions/src/environment/index.rs +++ b/rust_backend/crates/extensions/src/environment/index.rs @@ -1,5 +1,5 @@ use super::{EnvironmentError, ExtensionEnvironment}; -use spotiflac_core::isrc::{IndexCache, IndexFiles, NativeFiles, parse_tracks}; +use spotiflac_core::isrc::{IndexCache, NativeFiles}; use std::sync::atomic::Ordering; type Check<'a> = &'a (dyn Fn() -> Result<(), String> + Sync); @@ -27,27 +27,6 @@ impl ExtensionEnvironment { /// Trusted native entry points share the SDK's index, while JavaScript uses /// a scoped IndexFiles adapter and revalidates native directory grants. - pub fn prebuild_isrc_index( - &self, - directory: &str, - check: Check<'_>, - ) -> Result<(), EnvironmentError> { - self.with_index(check, |cache, files, check| { - cache.prebuild(directory, files, check) - }) - } - - pub fn check_isrc_exists( - &self, - directory: &str, - isrc: &str, - check: Check<'_>, - ) -> Result { - self.with_index(check, |cache, files, check| { - cache.check(directory, isrc, files, check) - }) - } - pub fn add_to_isrc_index( &self, directory: &str, @@ -60,33 +39,9 @@ impl ExtensionEnvironment { }) } - pub fn check_files_exist_parallel( - &self, - directory: &str, - tracks_json: &str, - check: Check<'_>, - ) -> Result { - self.with_index(check, |cache, files, check| { - if tracks_json.len() > 8 * 1024 * 1024 { - return Err("tracks JSON exceeds 8 MiB limit".into()); - } - let tracks = parse_tracks(tracks_json)?; - let results = cache.check_batch(directory, &tracks, files, check)?; - serde_json::to_string(&results).map_err(|error| error.to_string()) - }) - } - pub fn invalidate_isrc_cache(&self, directory: &str) -> Result<(), EnvironmentError> { let _operation = self.enter()?; self.isrc.invalidate(directory); Ok(()) } - - pub fn check_file_exists(&self, path: &str) -> Result { - let _operation = self.enter()?; - NativeFiles - .stat(path) - .map(|stamp| stamp.is_some_and(|stamp| !stamp.directory && stamp.size > 0)) - .map_err(EnvironmentError::Index) - } } diff --git a/rust_backend/crates/extensions/src/ffmpeg.rs b/rust_backend/crates/extensions/src/ffmpeg.rs index c26fece6..d50755ba 100644 --- a/rust_backend/crates/extensions/src/ffmpeg.rs +++ b/rust_backend/crates/extensions/src/ffmpeg.rs @@ -56,12 +56,6 @@ impl CommandRegistry { Ok(state.commands.get(id).map(|entry| entry.command.clone())) } - pub fn pending_json(&self) -> Result { - let mut state = self.state.lock().expect("FFmpeg state lock"); - state.check()?; - Ok(state.pending(false)) - } - /// Claims each command at most once, even with multiple native pumps. /// A zero/negative timeout performs a nonblocking claim. Shutdown wakes waits. pub fn wait_pending_json(&self, timeout_ms: i64) -> Result { @@ -70,7 +64,7 @@ impl CommandRegistry { let mut state = self.state.lock().expect("FFmpeg state lock"); loop { state.check()?; - let commands = state.pending(true); + let commands = state.claim_pending(); let remaining = timeout.saturating_sub(started.elapsed()); if commands != "[]" || remaining.is_zero() { return Ok(commands); @@ -185,7 +179,7 @@ impl State { } } - fn pending(&mut self, claim: bool) -> String { + fn claim_pending(&mut self) -> String { #[derive(Serialize)] struct PendingCommand<'a> { command_id: &'a str, @@ -198,9 +192,7 @@ impl State { .values_mut() .filter(|entry| !entry.claimed && entry.result.is_none()) .map(|entry| { - if claim { - entry.claimed = true; - } + entry.claimed = true; PendingCommand { command_id: &entry.command.command_id, extension_id: &entry.command.extension_id, diff --git a/rust_backend/crates/extensions/src/manager.rs b/rust_backend/crates/extensions/src/manager.rs index 2954b5e9..65445a54 100644 --- a/rust_backend/crates/extensions/src/manager.rs +++ b/rust_backend/crates/extensions/src/manager.rs @@ -590,12 +590,6 @@ impl ExtensionManager { .map_err(|e| error(e.to_string())) } - pub fn unload(&self, id: &str) -> Result<(), ManagerError> { - let _metadata = MetadataChange(&self.metadata_revision); - let _mutation = self.mutation.lock().expect("extension mutation lock"); - self.unload_locked(id) - } - fn unload_locked(&self, id: &str) -> Result<(), ManagerError> { self.check()?; let entry = self @@ -612,23 +606,6 @@ impl ExtensionManager { Ok(()) } - pub fn unload_all(&self) -> Result<(), ManagerError> { - let _metadata = MetadataChange(&self.metadata_revision); - let _mutation = self.mutation.lock().expect("extension mutation lock"); - self.check()?; - let ids: Vec<_> = self - .entries - .lock() - .expect("extension manager lock") - .keys() - .cloned() - .collect(); - for id in ids { - self.unload_locked(&id)?; - } - Ok(()) - } - pub fn remove(&self, id: &str) -> Result<(), ManagerError> { let _metadata = MetadataChange(&self.metadata_revision); let _mutation = self.mutation.lock().expect("extension mutation lock"); diff --git a/rust_backend/crates/extensions/src/manager/metadata.rs b/rust_backend/crates/extensions/src/manager/metadata.rs index b2d7b404..1b2a9472 100644 --- a/rust_backend/crates/extensions/src/manager/metadata.rs +++ b/rust_backend/crates/extensions/src/manager/metadata.rs @@ -32,16 +32,6 @@ impl ExtensionManager { .map_err(|failure| error(failure.to_string())) } - pub fn check_availability( - &self, - id: &str, - request: ProviderAvailabilityRequest, - timeout_ms: u64, - ) -> Result { - let lease = self.item_lease(&request.item_id)?; - self.check_availability_with_lease(id, request, timeout_ms, lease) - } - pub(crate) fn check_availability_with_lease( &self, id: &str, @@ -70,52 +60,6 @@ impl ExtensionManager { ) } - /// Best-effort enrichment preserves the original provider attribution. - /// Cancellation remains an error, allowing the download worker to stop. - pub fn enrich_track( - &self, - id: &str, - track_json: &str, - item_id: &str, - timeout_ms: u64, - ) -> Result { - self.enrich_track_with_lease( - id, - track_json, - item_id, - timeout_ms, - self.item_lease(item_id)?, - ) - } - - pub(crate) fn enrich_track_export( - &self, - id: &str, - track_json: &str, - ) -> Result { - self.check()?; - let Ok(entry) = self.get(id) else { - return Ok(track_json.into()); - }; - if !entry.manifest.has_type("metadata_provider") { - return Ok(track_json.into()); - } - // This export unmarshals into a value struct, unlike the nullable - // provider argument used by the internal download API. - let input = if track_json.trim() == "null" { - "{}" - } else { - track_json - }; - self.enrich_track(id, input, "", 30_000).map_err(|failure| { - if let Some(message) = failure.0.strip_prefix("invalid track: ") { - error(format!("failed to parse track: {message}")) - } else { - failure - } - }) - } - pub(crate) fn enrich_track_with_lease( &self, id: &str, diff --git a/rust_backend/crates/extensions/src/repository.rs b/rust_backend/crates/extensions/src/repository.rs index 5552cec1..b70318c5 100644 --- a/rust_backend/crates/extensions/src/repository.rs +++ b/rust_backend/crates/extensions/src/repository.rs @@ -258,12 +258,6 @@ impl ExtensionRepository { Ok(()) } - pub fn clear_cache(&self) -> Result<(), RepositoryError> { - self.check()?; - self.clear_locked(&mut self.state.lock().expect("repository state lock")); - Ok(()) - } - fn clear_locked(&self, state: &mut State) { state.generation = state.generation.wrapping_add(1); state.cache = None; @@ -420,13 +414,6 @@ impl ExtensionRepository { ) } - pub fn categories(&self) -> Result, RepositoryError> { - self.check()?; - Ok(["metadata", "download", "utility", "lyrics", "integration"] - .map(str::to_owned) - .to_vec()) - } - pub fn download(&self, id: &str, directory: &Path) -> Result { let _operation = self.enter()?; let generation = self.state.lock().expect("repository state lock").generation; diff --git a/rust_backend/crates/extensions/src/runtime.rs b/rust_backend/crates/extensions/src/runtime.rs index a8c06e13..bea8f9e0 100644 --- a/rust_backend/crates/extensions/src/runtime.rs +++ b/rust_backend/crates/extensions/src/runtime.rs @@ -478,16 +478,6 @@ impl ExtensionRuntime { /// Read typed provider fields in the owning VM. Go's provider wrappers do /// not await a returned Promise; the provider object itself is parsed. - pub fn call_provider( - &self, - method: &str, - arguments: &str, - lease: Option>, - timeout_ms: u64, - ) -> Result { - self.call_provider_operation(method, arguments, lease, timeout_ms, String::new()) - } - pub(crate) fn call_provider_operation( &self, method: &str, @@ -596,33 +586,6 @@ impl ExtensionRuntime { ) } - /// The manager starts progress before resolution and completes it after - /// finalization. This call acquires another reference to the same item and - /// binds its identity to the queued command, never to mutable runtime state. - pub fn call_download_for_item( - &self, - method: &str, - arguments_json: &str, - item_id: &str, - resolution_timeout_ms: u64, - ) -> Result { - if self.control.is_closed() { - return Err(ExtensionError::Closed); - } - let lease = Arc::new( - self.downloads - .acquire(item_id) - .map_err(ExtensionError::Cancelled)?, - ); - self.call_download_operation( - method, - arguments_json, - Some(lease), - resolution_timeout_ms, - item_id.trim().to_owned(), - ) - } - pub fn download_state(&self) -> Arc { Arc::clone(&self.downloads) } diff --git a/rust_backend/crates/mobile/src/cancellation.rs b/rust_backend/crates/mobile/src/cancellation.rs index ff0531cf..fddf948e 100644 --- a/rust_backend/crates/mobile/src/cancellation.rs +++ b/rust_backend/crates/mobile/src/cancellation.rs @@ -59,18 +59,10 @@ impl CancellationRegistry { self.inner.cancel(&id).map_err(Into::into) } - pub fn cancel_active(&self) -> Result, CancellationError> { - self.inner.cancel_active().map_err(Into::into) - } - pub fn is_cancelled(&self, id: String) -> Result { self.inner.is_cancelled(&id).map_err(Into::into) } - pub fn reset_if_idle(&self, id: String) -> Result<(), CancellationError> { - self.inner.reset_if_idle(&id).map_err(Into::into) - } - pub fn shutdown(&self) { self.inner.shutdown(); } @@ -87,14 +79,6 @@ impl RequestLease { self.inner.is_cancelled().map_err(Into::into) } - pub fn check_active(&self) -> Result<(), CancellationError> { - self.inner.check_active().map_err(Into::into) - } - - pub fn wait_cancelled(&self, timeout_ms: i64) -> Result { - self.inner.wait_cancelled(timeout_ms).map_err(Into::into) - } - pub fn release(&self) { self.inner.release(); } diff --git a/rust_backend/crates/mobile/src/extensions.rs b/rust_backend/crates/mobile/src/extensions.rs index 490663c7..92f20076 100644 --- a/rust_backend/crates/mobile/src/extensions.rs +++ b/rust_backend/crates/mobile/src/extensions.rs @@ -131,49 +131,6 @@ impl JsExtension { .map_err(Into::into) } - pub fn preflight_signed_session( - &self, - lease: Option>, - timeout_ms: u64, - ) -> Result { - self.inner - .preflight_signed_session(lease.map(|lease| Arc::clone(&lease.inner)), timeout_ms) - .map_err(Into::into) - } - - pub fn call_download( - &self, - method: String, - arguments_json: String, - lease: Option>, - resolution_timeout_ms: u64, - ) -> Result { - self.inner - .call_download( - &method, - &arguments_json, - lease.map(|lease| Arc::clone(&lease.inner)), - resolution_timeout_ms, - ) - .map_err(Into::into) - } - - pub fn take_verification_url(&self) -> String { - self.inner.take_verification_url() - } - - pub fn call_download_for_item( - &self, - method: String, - arguments_json: String, - item_id: String, - resolution_timeout_ms: u64, - ) -> Result { - self.inner - .call_download_for_item(&method, &arguments_json, &item_id, resolution_timeout_ms) - .map_err(Into::into) - } - pub fn shutdown(&self) { self.inner.shutdown(); } @@ -220,48 +177,10 @@ impl ExtensionEnvironment { }) } - pub fn load( - &self, - manifest_json: String, - source: String, - timeout_ms: u64, - ) -> Result, JsExtensionError> { - let limits = RuntimeLimits { - timeout_ms: if timeout_ms == 0 { 30_000 } else { timeout_ms }, - ..RuntimeLimits::default() - }; - Ok(Arc::new(JsExtension { - inner: self.inner.load(&manifest_json, &source, limits)?, - })) - } - pub fn settings(&self, extension_id: String) -> Result { Ok(serde_json::Value::Object(self.inner.settings(&extension_id)?).to_string()) } - pub fn set_setting( - &self, - extension_id: String, - key: String, - value_json: String, - ) -> Result<(), JsExtensionError> { - let value = serde_json::from_str(&value_json) - .map_err(|error| JsExtensionError::InvalidInput(error.to_string()))?; - self.inner - .set_setting(&extension_id, &key, value) - .map_err(Into::into) - } - - pub fn remove_setting( - &self, - extension_id: String, - key: String, - ) -> Result<(), JsExtensionError> { - self.inner - .remove_setting(&extension_id, &key) - .map_err(Into::into) - } - pub fn set_allow_private_network(&self, allow: bool) -> Result<(), JsExtensionError> { self.inner .set_allow_private_network(allow) @@ -313,14 +232,6 @@ impl ExtensionEnvironment { })) } - pub fn pending_auth(&self, extension_id: String) -> Result { - Ok(self - .inner - .pending_auth(&extension_id)? - .map(|pending| serde_json::to_string(&pending).expect("pending auth JSON")) - .unwrap_or_default()) - } - pub fn set_runtime_state(&self, raw: String) -> Result<(), JsExtensionError> { self.inner.set_runtime_state(&raw).map_err(Into::into) } @@ -336,16 +247,6 @@ impl ExtensionEnvironment { .map_err(Into::into) } - pub fn all_pending_auth(&self) -> Result { - Ok(serde_json::to_string(&self.inner.all_pending_auth()?).expect("pending auth JSON")) - } - - pub fn clear_pending_auth(&self, extension_id: String) -> Result<(), JsExtensionError> { - self.inner - .clear_pending_auth(&extension_id) - .map_err(Into::into) - } - pub fn resolve_callback_state(&self, state: String) -> Result { self.inner .resolve_callback_state(&state, false) @@ -369,26 +270,6 @@ impl ExtensionEnvironment { .map_err(Into::into) } - pub fn set_auth_tokens( - &self, - extension_id: String, - access_token: String, - refresh_token: String, - expires_in: i64, - ) -> Result<(), JsExtensionError> { - let access_token = zeroize::Zeroizing::new(access_token); - let refresh_token = zeroize::Zeroizing::new(refresh_token); - self.inner - .set_auth_tokens(&extension_id, &access_token, &refresh_token, expires_in) - .map_err(Into::into) - } - - pub fn is_authenticated(&self, extension_id: String) -> Result { - self.inner - .is_authenticated(&extension_id) - .map_err(Into::into) - } - pub fn shutdown(&self) { self.inner.shutdown(); } diff --git a/rust_backend/crates/mobile/src/ffmpeg.rs b/rust_backend/crates/mobile/src/ffmpeg.rs index 1733de20..65adc53b 100644 --- a/rust_backend/crates/mobile/src/ffmpeg.rs +++ b/rust_backend/crates/mobile/src/ffmpeg.rs @@ -22,10 +22,6 @@ pub struct FfmpegCommands { #[uniffi::export] impl FfmpegCommands { - pub fn pending(&self) -> Result { - self.inner.pending_json().map_err(Into::into) - } - pub fn wait_pending(&self, timeout_ms: i64) -> Result { self.inner.wait_pending_json(timeout_ms).map_err(Into::into) } diff --git a/rust_backend/crates/mobile/src/index.rs b/rust_backend/crates/mobile/src/index.rs deleted file mode 100644 index e25bd19d..00000000 --- a/rust_backend/crates/mobile/src/index.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::cancellation::RequestLease; -use crate::extensions::{ExtensionEnvironment, JsExtensionError}; -use std::sync::Arc; - -fn check(lease: &Option>) -> Result<(), String> { - lease.as_ref().map_or(Ok(()), |lease| { - lease - .inner - .check_active() - .map_err(|error| error.to_string()) - }) -} - -#[uniffi::export] -impl ExtensionEnvironment { - pub fn prebuild_isrc_index( - &self, - directory: String, - lease: Option>, - ) -> Result<(), JsExtensionError> { - self.inner - .prebuild_isrc_index(&directory, &|| check(&lease)) - .map_err(Into::into) - } - - pub fn check_isrc_exists( - &self, - directory: String, - isrc: String, - lease: Option>, - ) -> Result { - self.inner - .check_isrc_exists(&directory, &isrc, &|| check(&lease)) - .map_err(Into::into) - } - - pub fn add_to_isrc_index( - &self, - directory: String, - isrc: String, - path: String, - lease: Option>, - ) -> Result<(), JsExtensionError> { - self.inner - .add_to_isrc_index(&directory, &isrc, &path, &|| check(&lease)) - .map_err(Into::into) - } - - pub fn check_files_exist_parallel( - &self, - directory: String, - tracks_json: String, - lease: Option>, - ) -> Result { - self.inner - .check_files_exist_parallel(&directory, &tracks_json, &|| check(&lease)) - .map_err(Into::into) - } - - pub fn invalidate_isrc_cache(&self, directory: String) -> Result<(), JsExtensionError> { - self.inner - .invalidate_isrc_cache(&directory) - .map_err(Into::into) - } - - pub fn check_file_exists(&self, path: String) -> Result { - self.inner.check_file_exists(&path).map_err(Into::into) - } -} diff --git a/rust_backend/crates/mobile/src/lib.rs b/rust_backend/crates/mobile/src/lib.rs index 44c3ee5a..891fb92d 100644 --- a/rust_backend/crates/mobile/src/lib.rs +++ b/rust_backend/crates/mobile/src/lib.rs @@ -5,7 +5,6 @@ mod extensions; mod ffmpeg; mod filename; mod hires; -mod index; mod library_metadata; mod logging; mod lyrics; diff --git a/rust_backend/crates/mobile/src/lyrics.rs b/rust_backend/crates/mobile/src/lyrics.rs index e3260526..fd63b4d6 100644 --- a/rust_backend/crates/mobile/src/lyrics.rs +++ b/rust_backend/crates/mobile/src/lyrics.rs @@ -79,12 +79,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn get_lyrics_providers_json(&self) -> Result { - self.inner - .get_lyrics_providers_json() - .map_err(ExtensionManagerError::Operation) - } - pub fn set_lyrics_providers_json( &self, providers_json: String, @@ -100,12 +94,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn get_lyrics_fetch_options_json(&self) -> Result { - self.inner - .get_lyrics_fetch_options_json() - .map_err(ExtensionManagerError::Operation) - } - pub fn set_lyrics_fetch_options_json( &self, options_json: String, diff --git a/rust_backend/crates/mobile/src/manager.rs b/rust_backend/crates/mobile/src/manager.rs index 8d133473..7e7960a7 100644 --- a/rust_backend/crates/mobile/src/manager.rs +++ b/rust_backend/crates/mobile/src/manager.rs @@ -81,18 +81,6 @@ impl ExtensionManager { }) } - pub fn get_app_version(&self) -> Result { - self.inner - .get_app_version() - .map_err(ExtensionManagerError::Operation) - } - - pub fn set_app_version(&self, version: String) -> Result<(), ExtensionManagerError> { - self.inner - .set_app_version(&version) - .map_err(ExtensionManagerError::Operation) - } - pub fn install(&self, package_path: String) -> Result { self.inner .install(Path::new(&package_path)) @@ -168,15 +156,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn download_with_extensions_json( - &self, - request_json: String, - ) -> Result { - self.inner - .download_with_extensions_json(&request_json, &|| Ok(())) - .map_err(ExtensionManagerError::Operation) - } - pub fn check_extension_health_json( &self, extension_id: String, @@ -200,12 +179,6 @@ impl ExtensionManager { .map_err(Into::into) } - pub fn release_idle_download_runtimes(&self) -> Result { - self.inner - .release_idle_download_runtimes() - .map_err(Into::into) - } - pub fn release_memory(&self, under_pressure: bool) -> Result<(), ExtensionManagerError> { self.inner .release_memory(under_pressure) @@ -247,32 +220,6 @@ impl ExtensionManager { .map_err(Into::into) } - pub fn check_availability( - &self, - extension_id: String, - request_json: String, - timeout_ms: u64, - ) -> Result { - let request = serde_json::from_str(&request_json).map_err(|error| { - ExtensionManagerError::Operation(format!("invalid availability request: {error}")) - })?; - self.inner - .check_availability(&extension_id, request, timeout_ms) - .map_err(Into::into) - } - - pub fn enrich_track( - &self, - extension_id: String, - track_json: String, - item_id: String, - timeout_ms: u64, - ) -> Result { - self.inner - .enrich_track(&extension_id, &track_json, &item_id, timeout_ms) - .map_err(Into::into) - } - pub fn search_metadata_provider( &self, extension_id: String, @@ -333,10 +280,6 @@ impl ExtensionManager { .map_err(Into::into) } - pub fn provider_ids(&self, kind: String) -> Result, ExtensionManagerError> { - self.inner.provider_ids(&kind).map_err(Into::into) - } - pub fn provider_call( &self, extension_id: String, @@ -377,12 +320,6 @@ impl ExtensionManager { self.inner.provider_priorities().map_err(Into::into) } - pub fn fallback_allowed(&self, extension_id: String) -> Result { - self.inner - .fallback_allowed(&extension_id) - .map_err(Into::into) - } - pub fn find_url_handler(&self, url: String) -> Result, ExtensionManagerError> { self.inner.find_url_handler(&url).map_err(Into::into) } @@ -396,14 +333,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn unload(&self, extension_id: String) -> Result<(), ExtensionManagerError> { - self.inner.unload(&extension_id).map_err(Into::into) - } - - pub fn unload_all(&self) -> Result<(), ExtensionManagerError> { - self.inner.unload_all().map_err(Into::into) - } - pub fn remove(&self, extension_id: String) -> Result<(), ExtensionManagerError> { self.inner.remove(&extension_id).map_err(Into::into) } diff --git a/rust_backend/crates/mobile/src/metadata.rs b/rust_backend/crates/mobile/src/metadata.rs index 605cbaa8..0e3d6ebf 100644 --- a/rust_backend/crates/mobile/src/metadata.rs +++ b/rust_backend/crates/mobile/src/metadata.rs @@ -2,12 +2,6 @@ use crate::cancellation::RequestLease; use crate::manager::{ExtensionManager, ExtensionManagerError}; use std::sync::Arc; -#[derive(uniffi::Record)] -pub struct DeezerResource { - pub resource_type: String, - pub resource_id: String, -} - fn check(lease: &Option>) -> Result<(), String> { lease.as_ref().map_or(Ok(()), |lease| { lease @@ -21,16 +15,6 @@ fn check(lease: &Option>) -> Result<(), String> { /// Run network operations on a native background thread and retain any lease. #[uniffi::export] impl ExtensionManager { - pub fn enrich_track_json( - &self, - extension_id: String, - track_json: String, - ) -> Result { - self.inner - .enrich_track_json(&extension_id, &track_json) - .map_err(ExtensionManagerError::Operation) - } - pub fn handle_url_json(&self, url: String) -> Result { self.inner .handle_url_json(&url) @@ -118,18 +102,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn set_song_link_region(&self, region: String) -> Result<(), ExtensionManagerError> { - self.inner - .set_song_link_region(®ion) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_song_link_region(&self) -> Result { - self.inner - .get_song_link_region() - .map_err(ExtensionManagerError::Operation) - } - pub fn get_track_cache_size(&self) -> Result { self.inner .get_track_cache_size() @@ -142,47 +114,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn parse_deezer_url(&self, url: String) -> Result { - let (resource_type, resource_id) = self - .inner - .parse_deezer_url(&url) - .map_err(ExtensionManagerError::Operation)?; - Ok(DeezerResource { - resource_type, - resource_id, - }) - } - - pub fn search_deezer( - &self, - query: String, - track_limit: i64, - artist_limit: i64, - filter: String, - lease: Option>, - ) -> Result { - self.inner - .search_deezer( - &query, - track_limit as isize, - artist_limit as isize, - &filter, - &|| check(&lease), - ) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_deezer_metadata( - &self, - resource_type: String, - resource_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_deezer_metadata(&resource_type, &resource_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - pub fn get_deezer_extended_metadata( &self, track_id: String, @@ -193,16 +124,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn search_deezer_by_isrc( - &self, - isrc: String, - lease: Option>, - ) -> Result { - self.inner - .search_deezer_by_isrc(&isrc, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - pub fn search_deezer_by_isrc_for_item_id( &self, isrc: String, @@ -236,151 +157,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn check_track_availability_json( - &self, - spotify_id: String, - isrc: String, - lease: Option>, - ) -> Result { - self.inner - .check_track_availability_json(&spotify_id, &isrc, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn check_album_availability_json( - &self, - spotify_id: String, - lease: Option>, - ) -> Result { - self.inner - .check_album_availability_json(&spotify_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn check_availability_from_deezer_json( - &self, - track_id: String, - lease: Option>, - ) -> Result { - self.inner - .check_availability_from_deezer_json(&track_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn check_availability_by_platform_json( - &self, - platform: String, - resource_type: String, - resource_id: String, - lease: Option>, - ) -> Result { - self.inner - .check_availability_by_platform_json(&platform, &resource_type, &resource_id, &|| { - check(&lease) - }) - .map_err(ExtensionManagerError::Operation) - } - - pub fn check_availability_from_url_json( - &self, - url: String, - lease: Option>, - ) -> Result { - self.inner - .check_availability_from_url_json(&url, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_streaming_urls_json( - &self, - spotify_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_streaming_urls_json(&spotify_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_deezer_id_from_spotify( - &self, - spotify_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_deezer_id_from_spotify(&spotify_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_deezer_album_id_from_spotify( - &self, - spotify_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_deezer_album_id_from_spotify(&spotify_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_youtube_url_from_spotify( - &self, - spotify_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_youtube_url_from_spotify(&spotify_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_spotify_id_from_deezer_track( - &self, - track_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_spotify_id_from_deezer_track(&track_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_tidal_url_from_deezer_track( - &self, - track_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_tidal_url_from_deezer_track(&track_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_amazon_url_from_deezer_track( - &self, - track_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_amazon_url_from_deezer_track(&track_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn get_youtube_url_from_deezer_track( - &self, - track_id: String, - lease: Option>, - ) -> Result { - self.inner - .get_youtube_url_from_deezer_track(&track_id, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - - pub fn preview_reenrich_file( - &self, - request_json: String, - lease: Option>, - ) -> Result { - self.inner - .preview_reenrich_file(&request_json, &|| check(&lease)) - .map_err(ExtensionManagerError::Operation) - } - pub fn reenrich_file( &self, request_json: String, diff --git a/rust_backend/crates/mobile/src/progress.rs b/rust_backend/crates/mobile/src/progress.rs index 664303c2..9cc19b89 100644 --- a/rust_backend/crates/mobile/src/progress.rs +++ b/rust_backend/crates/mobile/src/progress.rs @@ -72,58 +72,6 @@ impl DownloadState { self.inner.progress.remove(&item_id).map_err(Into::into) } - pub fn clear_all_progress(&self) -> Result<(), DownloadProgressError> { - self.inner.progress.clear().map_err(Into::into) - } - - pub fn set_preparing( - &self, - item_id: String, - stage: String, - ) -> Result<(), DownloadProgressError> { - self.inner - .progress - .preparing(&item_id, &stage) - .map_err(Into::into) - } - - pub fn set_downloading(&self, item_id: String) -> Result<(), DownloadProgressError> { - self.inner - .progress - .downloading(&item_id) - .map_err(Into::into) - } - - pub fn set_total(&self, item_id: String, total: i64) -> Result<(), DownloadProgressError> { - self.inner - .progress - .set_total(&item_id, total) - .map_err(Into::into) - } - - pub fn set_received( - &self, - item_id: String, - received: i64, - ) -> Result<(), DownloadProgressError> { - self.inner - .progress - .set_received(&item_id, received) - .map_err(Into::into) - } - - pub fn set_received_with_speed( - &self, - item_id: String, - received: i64, - speed: f64, - ) -> Result<(), DownloadProgressError> { - self.inner - .progress - .set_received_with_speed(&item_id, received, speed) - .map_err(Into::into) - } - pub fn set_progress( &self, item_id: String, @@ -137,26 +85,10 @@ impl DownloadState { .map_err(Into::into) } - pub fn set_finalizing(&self, item_id: String) -> Result<(), DownloadProgressError> { - self.inner.progress.finalizing(&item_id).map_err(Into::into) - } - - pub fn complete_item(&self, item_id: String) -> Result<(), DownloadProgressError> { - self.inner.progress.complete(&item_id).map_err(Into::into) - } - - pub fn item_progress(&self, item_id: String) -> Result { - self.inner.progress.item(&item_id).map_err(Into::into) - } - pub fn all_progress(&self) -> Result { self.inner.progress.snapshot().map_err(Into::into) } - pub fn progress_delta(&self, since: i64) -> Result { - self.inner.progress.delta(since).map_err(Into::into) - } - pub fn wait_progress_delta( &self, since: i64, diff --git a/rust_backend/crates/mobile/src/repository.rs b/rust_backend/crates/mobile/src/repository.rs index 3a681067..2d5204c7 100644 --- a/rust_backend/crates/mobile/src/repository.rs +++ b/rust_backend/crates/mobile/src/repository.rs @@ -54,22 +54,6 @@ impl ExtensionRepository { self.search_inner(force_refresh, "", "") } - pub fn search( - &self, - query: String, - category: String, - ) -> Result { - self.search_inner(false, &query, &category) - } - - pub fn categories(&self) -> Result, ExtensionRepositoryError> { - self.inner.categories().map_err(Into::into) - } - - pub fn clear_cache(&self) -> Result<(), ExtensionRepositoryError> { - self.inner.clear_cache().map_err(Into::into) - } - pub fn download( &self, extension_id: String, diff --git a/rust_backend/crates/mobile/src/tags.rs b/rust_backend/crates/mobile/src/tags.rs index bbd2ea51..ccd5aa9a 100644 --- a/rust_backend/crates/mobile/src/tags.rs +++ b/rust_backend/crates/mobile/src/tags.rs @@ -93,17 +93,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn scan_library_folder( - &self, - folder: String, - lease: Option>, - ) -> Result { - self.inner - .scan_library_folder(&folder, &|| check_lease(lease.as_deref())) - .map(|value| value.to_string()) - .map_err(ExtensionManagerError::Operation) - } - pub fn scan_library_folder_incremental( &self, folder: String, @@ -181,25 +170,6 @@ impl ExtensionManager { .map_err(ExtensionManagerError::Operation) } - pub fn save_cover_to_cache_with_hint_and_key( - &self, - audio_path: String, - hint: String, - cache_directory: String, - cache_key: String, - lease: Option>, - ) -> Result { - self.inner - .save_cover_to_cache_with_hint_and_key( - &audio_path, - &hint, - &cache_directory, - &cache_key, - &|| check_lease(lease.as_deref()), - ) - .map_err(ExtensionManagerError::Operation) - } - pub fn download_cover_to_file_sized( &self, url: String, @@ -280,22 +250,6 @@ impl ExtensionManager { })?; Ok(serde_json::json!({"success":true,"handled":handled}).to_string()) } - - /// Atomic audio tag editing on the root owner's granted file paths. - pub fn edit_audio_tags( - &self, - path: String, - format: String, - fields_json: String, - lease: Option>, - ) -> Result<(), ExtensionManagerError> { - let check = || check_lease(lease.as_deref()); - check().map_err(ExtensionManagerError::Operation)?; - let fields = decode_fields(&fields_json).map_err(ExtensionManagerError::Operation)?; - self.inner - .edit_audio_tags(&path, &format, &fields, &check) - .map_err(ExtensionManagerError::Operation) - } } fn decode_fields(json: &str) -> Result, String> { @@ -308,23 +262,9 @@ fn decode_fields(json: &str) -> Result, String> { .collect()) } -/// Read only the tag fields. Native callers retain ownership of any platform -/// descriptor referenced by `path` until this synchronous operation returns. -#[uniffi::export] -pub fn read_audio_tags( - path: String, - format: String, - lease: Option>, -) -> Result { - let check = || check_lease(lease.as_deref()); - check()?; - let mut file = open_audio_file(&path)?; - let metadata = tags::read_audio_tags(&mut file, &format, &check)?; - serde_json::to_string(&metadata).map_err(|error| error.to_string().into()) -} - /// Complete application metadata. An empty hint is equivalent to the legacy -/// ReadFileMetadata call. Native descriptor ownership matches read_audio_tags. +/// ReadFileMetadata call. Native callers retain ownership of any platform +/// descriptor referenced by `path` until this synchronous operation returns. #[uniffi::export] pub fn read_file_metadata( path: String, diff --git a/rust_backend/crates/providers/src/deezer.rs b/rust_backend/crates/providers/src/deezer.rs index ab289eba..bdf32ebb 100644 --- a/rust_backend/crates/providers/src/deezer.rs +++ b/rust_backend/crates/providers/src/deezer.rs @@ -4,7 +4,6 @@ mod browse; mod cache; mod extended; mod search; -pub use extended::parse_url; use crate::resolver::{Check, ResolverError, http::ResolverHttp}; use serde::de::DeserializeOwned; diff --git a/rust_backend/crates/providers/src/deezer/extended.rs b/rust_backend/crates/providers/src/deezer/extended.rs index 1a2bb155..8b226dc3 100644 --- a/rust_backend/crates/providers/src/deezer/extended.rs +++ b/rust_backend/crates/providers/src/deezer/extended.rs @@ -3,24 +3,9 @@ use spotiflac_core::metadata::{ AlbumExtendedMetadata, TrackMetadata, deezer::{FullAlbum, Track}, }; -use spotiflac_network::url::UrlParts; use std::sync::Arc; -use std::time::Instant; impl DeezerClient { - pub fn get_track_isrc(&self, id: &str, check: &Check<'_>) -> Result { - check().map_err(ResolverError::Cancelled)?; - if let Some(value) = self.cache.lock().unwrap().isrc.get(id).cloned() { - return Ok(value); - } - let track: Track = - self.get_json(&format!("https://api.deezer.com/2.0/track/{id}"), check)?; - let mut cache = self.cache.lock().unwrap(); - cache.isrc.insert(id.into(), track.isrc.clone()); - cache.cleanup(Instant::now()); - Ok(track.isrc) - } - pub fn get_track_album_id(&self, id: &str, check: &Check<'_>) -> Result { match self.coalesced(&format!("track_album:{id}"), check, || { let track: Track = @@ -108,47 +93,6 @@ impl DeezerClient { } } -pub fn parse_url(input: &str) -> Result<(String, String), ResolverError> { - let input = input.trim(); - if input.is_empty() { - return Err(ResolverError::Failed("empty URL".into())); - } - let parsed = - UrlParts::parse(input).ok_or_else(|| ResolverError::Failed("invalid Deezer URL".into()))?; - if parsed.port.is_some() - || !matches!( - parsed.hostname.as_str(), - "www.deezer.com" | "deezer.com" | "deezer.page.link" - ) - { - return Err(ResolverError::Failed("not a Deezer URL".into())); - } - let mut path = parsed.path.as_slice(); - while let Some(rest) = path.strip_prefix(b"/") { - path = rest; - } - while let Some(rest) = path.strip_suffix(b"/") { - path = rest; - } - let mut parts: Vec<_> = path.split(|byte| *byte == b'/').collect(); - if parts.first().is_some_and(|part| part.len() == 2) { - parts.remove(0); - } - if parts.len() < 2 { - return Err(ResolverError::Failed("invalid Deezer URL format".into())); - } - match parts[0] { - b"track" | b"album" | b"artist" | b"playlist" => Ok(( - String::from_utf8_lossy(parts[0]).into_owned(), - String::from_utf8_lossy(parts[1]).into_owned(), - )), - kind => Err(ResolverError::Failed(format!( - "unsupported Deezer resource type: {}", - String::from_utf8_lossy(kind) - ))), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/rust_backend/crates/providers/src/lyrics/cache.rs b/rust_backend/crates/providers/src/lyrics/cache.rs index ec8a8a9c..28cf0dc6 100644 --- a/rust_backend/crates/providers/src/lyrics/cache.rs +++ b/rust_backend/crates/providers/src/lyrics/cache.rs @@ -111,13 +111,6 @@ impl LyricsCache { self.len() == 0 } - pub fn clean_expired(&self, now: SystemTime) -> usize { - let mut state = self.inner.state.lock().expect("lyrics cache lock"); - let before = state.entries.len(); - state.entries.retain(|_, entry| now <= entry.expires_at); - before - state.entries.len() - } - pub fn clear(&self) -> usize { let mut state = self.inner.state.lock().expect("lyrics cache lock"); let count = state.entries.len(); diff --git a/rust_backend/crates/providers/src/resolver/availability.rs b/rust_backend/crates/providers/src/resolver/availability.rs index 9b03e450..e69ed3fa 100644 --- a/rust_backend/crates/providers/src/resolver/availability.rs +++ b/rust_backend/crates/providers/src/resolver/availability.rs @@ -1,7 +1,7 @@ //! Application-facing track/album availability and platform-link contracts. mod cache; -use super::{Check, Metadata, PlatformResolverService, ResolverError, urls}; +use super::{Check, Metadata, PlatformResolverService, ResolverError}; pub use cache::AvailabilityService; use serde::{Deserialize, Serialize}; use spotiflac_core::metadata::TrackMetadata; @@ -204,17 +204,3 @@ fn album( deezer_url: url, }) } - -fn by_platform( - resolver: &PlatformResolverService, - platform: &str, - kind: &str, - id: &str, - check: &Check<'_>, -) -> Result { - if id.is_empty() { - return Err(ResolverError::Failed(format!("{platform} ID is empty"))); - } - let input = urls::from_id(platform, kind, id).map_err(ResolverError::Failed)?; - Ok(from_links("", &resolved_links(resolver, &input, check)?)) -} diff --git a/rust_backend/crates/providers/src/resolver/availability/cache.rs b/rust_backend/crates/providers/src/resolver/availability/cache.rs index 5ac3de11..0707b2ca 100644 --- a/rust_backend/crates/providers/src/resolver/availability/cache.rs +++ b/rust_backend/crates/providers/src/resolver/availability/cache.rs @@ -1,6 +1,6 @@ use super::{ - AlbumAvailability, TrackAvailability, album, by_platform, deezer_id_from_metadata, from_deezer, - from_links, resolved_links, + AlbumAvailability, TrackAvailability, album, deezer_id_from_metadata, from_deezer, from_links, + resolved_links, }; use crate::deezer::MetadataLookup; use crate::lyrics::{LyricsError, builtin::TrackResolver}; @@ -156,44 +156,6 @@ impl AvailabilityService { self.with_raw(check, |resolver, check| from_deezer(resolver, id, check)) } - pub fn check_by_platform( - &self, - platform: &str, - kind: &str, - id: &str, - check: &Check<'_>, - ) -> Result { - self.with_raw(check, |resolver, check| { - by_platform(resolver, platform, kind, id, check) - }) - } - - pub fn check_from_url( - &self, - url: &str, - check: &Check<'_>, - ) -> Result { - self.with_raw(check, |resolver, check| { - Ok(from_links("", &resolved_links(resolver, url, check)?)) - }) - } - - pub fn streaming_urls( - &self, - id: &str, - check: &Check<'_>, - ) -> Result, ResolverError> { - let availability = self.check_track(id, "", check)?; - Ok([ - ("tidal", availability.tidal_url), - ("amazon", availability.amazon_url), - ] - .into_iter() - .filter(|(_, value)| !value.is_empty()) - .map(|(key, value)| (key.into(), value)) - .collect()) - } - pub fn deezer_album_id(&self, id: &str, check: &Check<'_>) -> Result { let album = self.check_album(id, check)?; if album.deezer && !album.deezer_id.is_empty() { @@ -203,44 +165,6 @@ impl AvailabilityService { } } - pub fn youtube_url_from_spotify( - &self, - id: &str, - check: &Check<'_>, - ) -> Result { - let track = self.check_track(id, "", check)?; - if track.youtube && !track.youtube_url.is_empty() { - Ok(track.youtube_url) - } else { - Err(ResolverError::Failed("track not found on YouTube".into())) - } - } - - pub fn platform_from_deezer( - &self, - id: &str, - platform: &str, - check: &Check<'_>, - ) -> Result { - let track = self.check_from_deezer(id, check)?; - let (value, name) = match platform { - "spotify" => (track.spotify_id, "Spotify"), - "tidal" => (track.tidal_url, "Tidal"), - "amazon" => (track.amazon_url, "Amazon Music"), - "youtube" => (track.youtube_url, "YouTube"), - _ => { - return Err(ResolverError::Failed( - "unsupported availability platform".into(), - )); - } - }; - if value.is_empty() { - Err(ResolverError::Failed(format!("track not found on {name}"))) - } else { - Ok(value) - } - } - fn with_raw( &self, check: &Check<'_>, diff --git a/rust_backend/crates/providers/src/resolver/urls.rs b/rust_backend/crates/providers/src/resolver/urls.rs index fd606af6..53264abd 100644 --- a/rust_backend/crates/providers/src/resolver/urls.rs +++ b/rust_backend/crates/providers/src/resolver/urls.rs @@ -138,41 +138,3 @@ pub fn path_escape(value: &str) -> String { pub fn query_escape(value: &str) -> String { escape(value, b"", true) } - -pub fn from_id(provider: &str, kind: &str, id: &str) -> Result { - let provider = canonical(provider); - let id = id.trim(); - if provider.is_empty() || id.is_empty() { - return Err("invalid platform or entity ID".into()); - } - let kind = kind.trim().to_lowercase(); - let kind = if kind == "song" { "track" } else { &kind }; - if !matches!(kind, "track" | "album" | "artist") { - return Err(format!("unsupported entity type {kind:?}")); - } - let base = match provider { - "spotify" => "https://open.spotify.com", - "deezer" => "https://www.deezer.com", - "tidal" => "https://tidal.com/browse", - "qobuz" => "https://open.qobuz.com", - "amazonMusic" => { - return Ok(format!( - "https://music.amazon.com/{kind}s/{}", - path_escape(id) - )); - } - "youtube" | "youtubeMusic" => { - if kind != "track" { - return Err(format!("unsupported {provider} entity type {kind:?}")); - } - let host = if provider == "youtube" { - "www.youtube.com" - } else { - "music.youtube.com" - }; - return Ok(format!("https://{host}/watch?v={}", query_escape(id))); - } - _ => return Err(format!("cannot build a direct {provider} URL from an ID")), - }; - Ok(format!("{base}/{kind}/{}", path_escape(id))) -}