mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-03 14:46:57 +02:00
refactor(backend): retire unused native bridge entry points
This commit is contained in:
1 parent
b8e3a4c166
commit
fe88048db2
45 files changed
+20
-1832
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<String>("folder_path") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
safScanActive = false
|
||||
bridgeJsonResult(coreBackend.scanLibraryFolder(folderPath))
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"scanLibraryFolderToNDJSONFile" -> {
|
||||
val folderPath = call.argument<String>("folder_path") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
@@ -1955,13 +1945,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"scanSafTree" -> {
|
||||
val treeUri = call.argument<String>("tree_uri") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
scanSafTree(treeUri)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"scanSafTreeToNDJSONFile" -> {
|
||||
val treeUri = call.argument<String>("tree_uri") ?: ""
|
||||
val outputPath = call.argument<String>("output_path") ?: ""
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -257,8 +257,8 @@ import UniformTypeIdentifiers
|
||||
|
||||
private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let osMethods: Set<String> = ["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",
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -1123,11 +1123,6 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<List<String>> getLyricsProviders() async {
|
||||
final result = await _channel.invokeMethod('getLyricsProviders');
|
||||
return _decodeStringListResult(result, 'getLyricsProviders');
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>>
|
||||
getAvailableLyricsProviders() async {
|
||||
final result = await _channel.invokeMethod('getAvailableLyricsProviders');
|
||||
@@ -1144,10 +1139,6 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getLyricsFetchOptions() {
|
||||
return _invokeMap('getLyricsFetchOptions');
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> reEnrichFile(
|
||||
Map<String, dynamic> request,
|
||||
) {
|
||||
@@ -1608,11 +1599,6 @@ class PlatformBridge {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> 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<void> setMetadataLanguage(String tag) async {
|
||||
@@ -1657,14 +1643,6 @@ class PlatformBridge {
|
||||
return _invokeMap('loadExtensionFromPath', {'file_path': filePath});
|
||||
}
|
||||
|
||||
static Future<void> unloadExtension(String extensionId) async {
|
||||
_log.d('unloadExtension: $extensionId');
|
||||
await _clearLookupCaches();
|
||||
await _channel.invokeMethod('unloadExtension', {
|
||||
'extension_id': extensionId,
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> removeExtension(String extensionId) async {
|
||||
_log.d('removeExtension: $extensionId');
|
||||
await _clearLookupCaches();
|
||||
@@ -1835,17 +1813,6 @@ class PlatformBridge {
|
||||
return _decodeNullableMapResult(result, 'getExtensionPendingAuth');
|
||||
}
|
||||
|
||||
static Future<void> setExtensionAuthCode(
|
||||
String extensionId,
|
||||
String authCode,
|
||||
) async {
|
||||
_log.d('setExtensionAuthCode: $extensionId');
|
||||
await _channel.invokeMethod('setExtensionAuthCode', {
|
||||
'extension_id': extensionId,
|
||||
'auth_code': authCode,
|
||||
});
|
||||
}
|
||||
|
||||
static final Map<String, Future<bool>> _extensionSessionGrantCompletions =
|
||||
<String, Future<bool>>{};
|
||||
|
||||
@@ -1890,68 +1857,6 @@ class PlatformBridge {
|
||||
return success;
|
||||
}
|
||||
|
||||
static Future<void> 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<void> clearExtensionPendingAuth(String extensionId) async {
|
||||
await _channel.invokeMethod('clearExtensionPendingAuth', {
|
||||
'extension_id': extensionId,
|
||||
});
|
||||
}
|
||||
|
||||
static Future<bool> isExtensionAuthenticated(String extensionId) async {
|
||||
final result = await _channel.invokeMethod('isExtensionAuthenticated', {
|
||||
'extension_id': extensionId,
|
||||
});
|
||||
return result as bool;
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> getAllPendingAuthRequests() async {
|
||||
final result = await _channel.invokeMethod('getAllPendingAuthRequests');
|
||||
return _decodeMapListResult(result, 'getAllPendingAuthRequests');
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>?> getPendingFFmpegCommand(
|
||||
String commandId,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('getPendingFFmpegCommand', {
|
||||
'command_id': commandId,
|
||||
});
|
||||
return _decodeNullableMapResult(result, 'getPendingFFmpegCommand');
|
||||
}
|
||||
|
||||
static Future<void> 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<List<Map<String, dynamic>>>
|
||||
getAllPendingFFmpegCommands() async {
|
||||
final result = await _channel.invokeMethod('getAllPendingFFmpegCommands');
|
||||
return _decodeMapListResult(result, 'setFFmpegCommandResult');
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> customSearchWithExtension(
|
||||
String extensionId,
|
||||
String query, {
|
||||
@@ -2218,15 +2123,6 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> scanLibraryFolder(
|
||||
String folderPath,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod('scanLibraryFolder', {
|
||||
'folder_path': folderPath,
|
||||
});
|
||||
return _decodeMapListResultAsync(result, 'scanLibraryFolder');
|
||||
}
|
||||
|
||||
static Future<LibraryScanNDJSONFile> scanLibraryFolderToNDJSONFile(
|
||||
String folderPath, {
|
||||
bool forceFullScan = false,
|
||||
@@ -2268,13 +2164,6 @@ class PlatformBridge {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> scanSafTree(String treeUri) async {
|
||||
final result = await _channel.invokeMethod('scanSafTree', {
|
||||
'tree_uri': treeUri,
|
||||
});
|
||||
return _decodeMapListResultAsync(result, 'scanSafTree');
|
||||
}
|
||||
|
||||
static Future<LibraryScanNDJSONFile> 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<String?> 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<List<Map<String, dynamic>>> 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<List<String>> getRepoCategories() async {
|
||||
final result = await _channel.invokeMethod('getRepoCategories');
|
||||
return _decodeStringListResult(result, 'getRepoCategories');
|
||||
}
|
||||
|
||||
static Future<String> downloadRepoExtension(
|
||||
String extensionId,
|
||||
String destDir,
|
||||
@@ -2759,11 +2616,6 @@ class PlatformBridge {
|
||||
return result as String;
|
||||
}
|
||||
|
||||
static Future<void> clearRepoCache() async {
|
||||
_log.d('clearRepoCache');
|
||||
await _channel.invokeMethod('clearRepoCache');
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> parseCueSheet(
|
||||
String cuePath, {
|
||||
String audioDir = '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<Vec<TrackExistence>, 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<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
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<M: serde::de::MapAccess<'de>>(
|
||||
self,
|
||||
mut map: M,
|
||||
) -> Result<TrackQuery, M::Error> {
|
||||
let mut track = TrackQuery::default();
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
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::<Option<String>>()? {
|
||||
*field = value;
|
||||
}
|
||||
} else {
|
||||
map.next_value::<serde::de::IgnoredAny>()?;
|
||||
}
|
||||
}
|
||||
Ok(track)
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_tracks(json: &str) -> Result<Vec<TrackQuery>, 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::<Option<Vec<Option<TrackQuery>>>>(&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,
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<String, ProgressError> {
|
||||
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<String, ProgressError> {
|
||||
let state = self.shared.state.lock().expect("progress state lock");
|
||||
state.check()?;
|
||||
Ok(state.delta(since))
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> Result<ProgressSubscription, ProgressError> {
|
||||
self.shared
|
||||
.state
|
||||
|
||||
@@ -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<String> {
|
||||
self.state
|
||||
.lock()
|
||||
@@ -251,16 +234,6 @@ impl AuthRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all_pending(&self) -> Vec<PendingAuthRequest> {
|
||||
self.state
|
||||
.lock()
|
||||
.expect("auth registry lock")
|
||||
.pending
|
||||
.values()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn resolve_callback(&self, nonce: &str, consume: bool) -> Result<String, String> {
|
||||
let nonce = nonce.trim();
|
||||
if nonce.is_empty() {
|
||||
|
||||
@@ -184,22 +184,6 @@ impl Backend {
|
||||
Arc::clone(&self.availability)
|
||||
}
|
||||
|
||||
pub fn get_app_version(&self) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
let _operation = self.enter()?;
|
||||
let id = id.trim();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String, String> {
|
||||
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,
|
||||
|
||||
@@ -75,21 +75,6 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn scan_library_folder(&self, folder: &str, check: Check<'_>) -> Result<Value, String> {
|
||||
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,
|
||||
|
||||
@@ -118,11 +118,6 @@ impl Backend {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn get_lyrics_providers_json(&self) -> Result<String, String> {
|
||||
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<String, String> {
|
||||
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");
|
||||
|
||||
@@ -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<String, String> {
|
||||
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<u64, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
self.metadata_operation(30, check, |check| {
|
||||
self.availability.platform_from_deezer(id, "youtube", check)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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<String, String> {
|
||||
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<String, String> {
|
||||
let _operation = self.enter()?;
|
||||
let id = self
|
||||
|
||||
@@ -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<String, String> {
|
||||
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<String, String> {
|
||||
|
||||
@@ -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<Vec<PendingAuthRequest>, 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<bool, EnvironmentError> {
|
||||
let _operation = self.enter()?;
|
||||
Ok(self.auth.authenticated(id))
|
||||
}
|
||||
}
|
||||
|
||||
fn network_timeout(value: Option<&Value>) -> Duration {
|
||||
|
||||
@@ -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<String, EnvironmentError> {
|
||||
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<String, EnvironmentError> {
|
||||
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<bool, EnvironmentError> {
|
||||
let _operation = self.enter()?;
|
||||
NativeFiles
|
||||
.stat(path)
|
||||
.map(|stamp| stamp.is_some_and(|stamp| !stamp.directory && stamp.size > 0))
|
||||
.map_err(EnvironmentError::Index)
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,6 @@ impl CommandRegistry {
|
||||
Ok(state.commands.get(id).map(|entry| entry.command.clone()))
|
||||
}
|
||||
|
||||
pub fn pending_json(&self) -> Result<String, RegistryClosed> {
|
||||
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<String, RegistryClosed> {
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<String, ManagerError> {
|
||||
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<String, ManagerError> {
|
||||
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<String, ManagerError> {
|
||||
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,
|
||||
|
||||
@@ -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<Vec<String>, RepositoryError> {
|
||||
self.check()?;
|
||||
Ok(["metadata", "download", "utility", "lyrics", "integration"]
|
||||
.map(str::to_owned)
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
pub fn download(&self, id: &str, directory: &Path) -> Result<PathBuf, RepositoryError> {
|
||||
let _operation = self.enter()?;
|
||||
let generation = self.state.lock().expect("repository state lock").generation;
|
||||
|
||||
@@ -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<Arc<RequestLease>>,
|
||||
timeout_ms: u64,
|
||||
) -> Result<String, ExtensionError> {
|
||||
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<String, ExtensionError> {
|
||||
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<spotiflac_core::downloads::DownloadState> {
|
||||
Arc::clone(&self.downloads)
|
||||
}
|
||||
|
||||
@@ -59,18 +59,10 @@ impl CancellationRegistry {
|
||||
self.inner.cancel(&id).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn cancel_active(&self) -> Result<Vec<String>, CancellationError> {
|
||||
self.inner.cancel_active().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self, id: String) -> Result<bool, CancellationError> {
|
||||
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<bool, CancellationError> {
|
||||
self.inner.wait_cancelled(timeout_ms).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn release(&self) {
|
||||
self.inner.release();
|
||||
}
|
||||
|
||||
@@ -131,49 +131,6 @@ impl JsExtension {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn preflight_signed_session(
|
||||
&self,
|
||||
lease: Option<Arc<RequestLease>>,
|
||||
timeout_ms: u64,
|
||||
) -> Result<bool, JsExtensionError> {
|
||||
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<Arc<RequestLease>>,
|
||||
resolution_timeout_ms: u64,
|
||||
) -> Result<String, JsExtensionError> {
|
||||
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<String, JsExtensionError> {
|
||||
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<Arc<JsExtension>, 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<String, JsExtensionError> {
|
||||
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<String, JsExtensionError> {
|
||||
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<String, JsExtensionError> {
|
||||
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<String, JsExtensionError> {
|
||||
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<bool, JsExtensionError> {
|
||||
self.inner
|
||||
.is_authenticated(&extension_id)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.inner.shutdown();
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@ pub struct FfmpegCommands {
|
||||
|
||||
#[uniffi::export]
|
||||
impl FfmpegCommands {
|
||||
pub fn pending(&self) -> Result<String, FfmpegError> {
|
||||
self.inner.pending_json().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn wait_pending(&self, timeout_ms: i64) -> Result<String, FfmpegError> {
|
||||
self.inner.wait_pending_json(timeout_ms).map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
use crate::cancellation::RequestLease;
|
||||
use crate::extensions::{ExtensionEnvironment, JsExtensionError};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn check(lease: &Option<Arc<RequestLease>>) -> 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<Arc<RequestLease>>,
|
||||
) -> 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<Arc<RequestLease>>,
|
||||
) -> Result<String, JsExtensionError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> 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<Arc<RequestLease>>,
|
||||
) -> Result<String, JsExtensionError> {
|
||||
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<bool, JsExtensionError> {
|
||||
self.inner.check_file_exists(&path).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ mod extensions;
|
||||
mod ffmpeg;
|
||||
mod filename;
|
||||
mod hires;
|
||||
mod index;
|
||||
mod library_metadata;
|
||||
mod logging;
|
||||
mod lyrics;
|
||||
|
||||
@@ -79,12 +79,6 @@ impl ExtensionManager {
|
||||
.map_err(ExtensionManagerError::Operation)
|
||||
}
|
||||
|
||||
pub fn get_lyrics_providers_json(&self) -> Result<String, ExtensionManagerError> {
|
||||
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<String, ExtensionManagerError> {
|
||||
self.inner
|
||||
.get_lyrics_fetch_options_json()
|
||||
.map_err(ExtensionManagerError::Operation)
|
||||
}
|
||||
|
||||
pub fn set_lyrics_fetch_options_json(
|
||||
&self,
|
||||
options_json: String,
|
||||
|
||||
@@ -81,18 +81,6 @@ impl ExtensionManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_app_version(&self) -> Result<String, ExtensionManagerError> {
|
||||
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<String, ExtensionManagerError> {
|
||||
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<String, ExtensionManagerError> {
|
||||
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<u64, ExtensionManagerError> {
|
||||
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<String, ExtensionManagerError> {
|
||||
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<String, ExtensionManagerError> {
|
||||
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<Vec<String>, 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<bool, ExtensionManagerError> {
|
||||
self.inner
|
||||
.fallback_allowed(&extension_id)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn find_url_handler(&self, url: String) -> Result<Option<String>, 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)
|
||||
}
|
||||
|
||||
@@ -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<Arc<RequestLease>>) -> Result<(), String> {
|
||||
lease.as_ref().map_or(Ok(()), |lease| {
|
||||
lease
|
||||
@@ -21,16 +15,6 @@ fn check(lease: &Option<Arc<RequestLease>>) -> 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<String, ExtensionManagerError> {
|
||||
self.inner
|
||||
.enrich_track_json(&extension_id, &track_json)
|
||||
.map_err(ExtensionManagerError::Operation)
|
||||
}
|
||||
|
||||
pub fn handle_url_json(&self, url: String) -> Result<String, ExtensionManagerError> {
|
||||
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<String, ExtensionManagerError> {
|
||||
self.inner
|
||||
.get_song_link_region()
|
||||
.map_err(ExtensionManagerError::Operation)
|
||||
}
|
||||
|
||||
pub fn get_track_cache_size(&self) -> Result<u64, ExtensionManagerError> {
|
||||
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<DeezerResource, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
self.inner
|
||||
.preview_reenrich_file(&request_json, &|| check(&lease))
|
||||
.map_err(ExtensionManagerError::Operation)
|
||||
}
|
||||
|
||||
pub fn reenrich_file(
|
||||
&self,
|
||||
request_json: String,
|
||||
|
||||
@@ -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<String, DownloadProgressError> {
|
||||
self.inner.progress.item(&item_id).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn all_progress(&self) -> Result<String, DownloadProgressError> {
|
||||
self.inner.progress.snapshot().map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn progress_delta(&self, since: i64) -> Result<String, DownloadProgressError> {
|
||||
self.inner.progress.delta(since).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn wait_progress_delta(
|
||||
&self,
|
||||
since: i64,
|
||||
|
||||
@@ -54,22 +54,6 @@ impl ExtensionRepository {
|
||||
self.search_inner(force_refresh, "", "")
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
&self,
|
||||
query: String,
|
||||
category: String,
|
||||
) -> Result<String, ExtensionRepositoryError> {
|
||||
self.search_inner(false, &query, &category)
|
||||
}
|
||||
|
||||
pub fn categories(&self) -> Result<Vec<String>, 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,
|
||||
|
||||
@@ -93,17 +93,6 @@ impl ExtensionManager {
|
||||
.map_err(ExtensionManagerError::Operation)
|
||||
}
|
||||
|
||||
pub fn scan_library_folder(
|
||||
&self,
|
||||
folder: String,
|
||||
lease: Option<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> Result<String, ExtensionManagerError> {
|
||||
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<Arc<RequestLease>>,
|
||||
) -> 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<BTreeMap<String, String>, String> {
|
||||
@@ -308,23 +262,9 @@ fn decode_fields(json: &str) -> Result<BTreeMap<String, String>, 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<Arc<RequestLease>>,
|
||||
) -> Result<String, AudioTagsError> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String, ResolverError> {
|
||||
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<String, ResolverError> {
|
||||
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::*;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<TrackAvailability, ResolverError> {
|
||||
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)?))
|
||||
}
|
||||
@@ -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<TrackAvailability, ResolverError> {
|
||||
self.with_raw(check, |resolver, check| {
|
||||
by_platform(resolver, platform, kind, id, check)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_from_url(
|
||||
&self,
|
||||
url: &str,
|
||||
check: &Check<'_>,
|
||||
) -> Result<TrackAvailability, ResolverError> {
|
||||
self.with_raw(check, |resolver, check| {
|
||||
Ok(from_links("", &resolved_links(resolver, url, check)?))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn streaming_urls(
|
||||
&self,
|
||||
id: &str,
|
||||
check: &Check<'_>,
|
||||
) -> Result<BTreeMap<String, String>, 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<String, ResolverError> {
|
||||
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<String, ResolverError> {
|
||||
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<String, ResolverError> {
|
||||
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<T>(
|
||||
&self,
|
||||
check: &Check<'_>,
|
||||
|
||||
@@ -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<String, String> {
|
||||
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)))
|
||||
}
|
||||
Reference in new issue
Block a user