Skip to content

Commit b4a0438

Browse files
committed
Refactor Spotify service
1 parent 42ccc3e commit b4a0438

File tree

6 files changed

+104
-12
lines changed

6 files changed

+104
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ Sources/LyricsService/.DS_Store
44
Sources/.DS_Store
55
.DS_Store
66
Package.resolved
7+
.claude
8+
.build
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
public protocol AuthenticationManager: Sendable {
4+
func isAuthenticated() async -> Bool
5+
func authenticate() async throws
6+
func getCredentials() async throws -> [String: String]
7+
}
8+
9+
public enum AuthenticationError: Error {
10+
case notAuthenticated
11+
case credentialsNotFound
12+
case authenticationFailed(Error)
13+
}

Sources/LyricsService/Provider/Group.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ extension LyricsProviders {
99
self.providers = service.map { $0.create() }
1010
}
1111

12+
public init(providers: [LyricsProvider]) {
13+
self.providers = providers
14+
}
15+
1216
public func lyrics(for request: LyricsSearchRequest) -> AsyncThrowingStream<Lyrics, Error> {
1317
return AsyncThrowingStream { continuation in
1418
Task {
Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import Foundation
22

33
extension LyricsProviders {
4-
public enum Service {
4+
public enum Service: CaseIterable, Equatable, Hashable {
55
case qq
66
case netease
77
case kugou
88
case lrclib
9-
case spotify(searchAccessToken: String, lyricsAccessToken: String)
9+
case spotify
1010

1111
public var displayName: String {
1212
switch self {
@@ -18,6 +18,18 @@ extension LyricsProviders {
1818
}
1919
}
2020

21+
public var requiresAuthentication: Bool {
22+
switch self {
23+
case .spotify:
24+
return true
25+
case .qq,
26+
.netease,
27+
.kugou,
28+
.lrclib:
29+
return false
30+
}
31+
}
32+
2133
public static var noAuthenticationRequiredServices: [Service] {
2234
[
2335
.qq,
@@ -26,18 +38,40 @@ extension LyricsProviders {
2638
.lrclib,
2739
]
2840
}
41+
42+
public static var authenticationRequiredServices: [Service] {
43+
[
44+
.spotify,
45+
]
46+
}
2947
}
3048
}
3149

3250
extension LyricsProviders.Service {
33-
func create() -> LyricsProvider {
51+
public func create() -> LyricsProvider {
3452
switch self {
3553
case .netease: return LyricsProviders.NetEase()
3654
case .qq: return LyricsProviders.QQMusic()
3755
case .kugou: return LyricsProviders.Kugou()
38-
case .spotify(let searchAccessToken, let lyricsAccessToken): return LyricsProviders.Spotify(searchAccessToken: searchAccessToken, lyricsAccessToken: lyricsAccessToken)
56+
case .spotify: return LyricsProviders.Spotify()
3957
case .lrclib: return LyricsProviders.LRCLIB()
40-
// default: return LyricsProviders.Unsupported()
58+
}
59+
}
60+
61+
public func create(with authManager: AuthenticationManager?) async throws -> LyricsProvider {
62+
switch self {
63+
case .spotify:
64+
guard let authManager = authManager else {
65+
throw AuthenticationError.notAuthenticated
66+
}
67+
let provider = LyricsProviders.Spotify()
68+
provider.authenticationManager = authManager
69+
return provider
70+
case .netease,
71+
.qq,
72+
.kugou,
73+
.lrclib:
74+
return create()
4175
}
4276
}
4377
}

Sources/LyricsService/Provider/Services/Spotify.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ import LyricsCore
44

55
extension LyricsProviders {
66
public final class Spotify {
7-
let searchAccessToken: String
7+
var authenticationManager: AuthenticationManager?
88

9-
let lyricsAccessToken: String
9+
init() {}
1010

11-
init(searchAccessToken: String, lyricsAccessToken: String) {
12-
self.searchAccessToken = searchAccessToken
13-
self.lyricsAccessToken = lyricsAccessToken
11+
private func getAccessTokens() async throws -> (search: String, lyrics: String) {
12+
guard let authManager = authenticationManager else {
13+
throw AuthenticationError.notAuthenticated
14+
}
15+
16+
if !(await authManager.isAuthenticated()) {
17+
try await authManager.authenticate()
18+
}
19+
20+
let credentials = try await authManager.getCredentials()
21+
guard let searchToken = credentials["searchAccessToken"],
22+
let lyricsToken = credentials["lyricsAccessToken"] else {
23+
throw AuthenticationError.credentialsNotFound
24+
}
25+
26+
return (search: searchToken, lyrics: lyricsToken)
1427
}
1528
}
1629
}
@@ -23,6 +36,8 @@ extension LyricsProviders.Spotify: _LyricsProvider {
2336
public static let service: String = "Spotify"
2437

2538
public func search(for request: LyricsSearchRequest) async throws -> [LyricsToken] {
39+
let tokens = try await getAccessTokens()
40+
2641
let url: URL
2742
switch request.searchTerm {
2843
case .keyword(let string):
@@ -35,7 +50,7 @@ extension LyricsProviders.Spotify: _LyricsProvider {
3550

3651
var req = URLRequest(url: url)
3752
req.addValue("WebPlayer", forHTTPHeaderField: "app-platform")
38-
req.addValue("Bearer \(searchAccessToken)", forHTTPHeaderField: "Authorization")
53+
req.addValue("Bearer \(tokens.search)", forHTTPHeaderField: "Authorization")
3954

4055
do {
4156
let (data, _) = try await URLSession.shared.data(for: req)
@@ -50,14 +65,15 @@ extension LyricsProviders.Spotify: _LyricsProvider {
5065
}
5166

5267
public func fetch(with token: LyricsToken) async throws -> Lyrics {
68+
let tokens = try await getAccessTokens()
5369
let token = token.value
5470
guard let url = URL(string: "https://spclient.wg.spotify.com/color-lyrics/v2/track/\(token.id)?format=json&vocalRemoval=false&market=from_token") else {
5571
throw LyricsProviderError.invalidURL(urlString: "Spotify fetch URL")
5672
}
5773

5874
var request = URLRequest(url: url)
5975
request.addValue("WebPlayer", forHTTPHeaderField: "app-platform")
60-
request.addValue("Bearer \(lyricsAccessToken)", forHTTPHeaderField: "Authorization")
76+
request.addValue("Bearer \(tokens.lyrics)", forHTTPHeaderField: "Authorization")
6177

6278
let singleLyricsResponse: SpotifyResponseSingleLyrics
6379
do {

Sources/LyricsServiceUI/Controller/SpotifyLoginManager.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AppKit
22
import WebKit
33
import Schedule
4+
import LyricsService
45

56
private typealias Task = _Concurrency.Task
67
private typealias ScheduleTask = Schedule.Task
@@ -152,3 +153,25 @@ public final class SpotifyLoginManager: NSObject, @unchecked Sendable {
152153
}
153154
}
154155
}
156+
157+
extension SpotifyLoginManager: AuthenticationManager {
158+
public func isAuthenticated() async -> Bool {
159+
return await isAccessible
160+
}
161+
162+
public func authenticate() async throws {
163+
try await login()
164+
}
165+
166+
public func getCredentials() async throws -> [String: String] {
167+
guard let searchToken = await searchAccessTokenString,
168+
let lyricsToken = await lyricsAccessTokenString else {
169+
throw AuthenticationError.credentialsNotFound
170+
}
171+
172+
return [
173+
"searchAccessToken": searchToken,
174+
"lyricsAccessToken": lyricsToken
175+
]
176+
}
177+
}

0 commit comments

Comments
 (0)