Skip to content

Commit 73192df

Browse files
committed
Support LRCLIB
1 parent 89d9462 commit 73192df

File tree

6 files changed

+102
-97
lines changed

6 files changed

+102
-97
lines changed

Package.swift

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,88 @@
1-
// swift-tools-version:5.2
1+
// swift-tools-version:5.10
22

33
import PackageDescription
44

55
let package = Package(
66
name: "LyricsKit",
77
platforms: [
88
.macOS(.v10_15),
9-
.iOS(.minimalToolChainSupported),
10-
.tvOS(.v9),
11-
.watchOS(.v2),
9+
.iOS(.v13),
10+
.tvOS(.v13),
11+
.watchOS(.v6),
1212
],
1313
products: [
1414
.library(
1515
name: "LyricsKit",
16-
targets: ["LyricsCore", "LyricsService", "LyricsServiceUI"]),
16+
targets: ["LyricsCore", "LyricsService", "LyricsServiceUI"]
17+
),
1718
],
1819
dependencies: [
19-
.package(url: "https://github.com/MxIris-LyricsX-Project/CXShim", .branchItem("master")),
20-
.package(url: "https://github.com/MxIris-LyricsX-Project/CXExtensions", .branchItem("master")),
20+
.package(url: "https://github.com/MxIris-LyricsX-Project/CXShim", branch: "master"),
21+
.package(url: "https://github.com/MxIris-LyricsX-Project/CXExtensions", branch: "master"),
2122
.package(url: "https://github.com/ddddxxx/Regex", from: "1.0.1"),
22-
.package(url: "https://github.com/MxIris-Library-Forks/SwiftCF", .branchItem("master")),
23-
.package(name: "Gzip", url: "https://github.com/1024jp/GzipSwift", from: "5.0.0"),
23+
.package(url: "https://github.com/MxIris-Library-Forks/SwiftCF", branch: "master"),
24+
.package(url: "https://github.com/1024jp/GzipSwift", from: "5.0.0"),
2425
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", .upToNextMajor(from: "4.0.0")),
26+
.package(url: "https://github.com/MxIris-Library-Forks/Schedule", branch: "master"),
2527
],
2628
targets: [
2729
.target(
2830
name: "LyricsCore",
29-
dependencies: ["Regex", "SwiftCF"]),
31+
dependencies: [
32+
.product(name: "Regex", package: "Regex"),
33+
.product(name: "SwiftCF", package: "SwiftCF"),
34+
]
35+
),
3036
.target(
3137
name: "LyricsService",
3238
dependencies: [
33-
"LyricsCore", "CXShim", "CXExtensions", "Regex", "Gzip",
39+
"LyricsCore",
40+
.product(name: "CXShim", package: "CXShim"),
41+
.product(name: "CXExtensions", package: "CXExtensions"),
42+
.product(name: "Regex", package: "Regex"),
43+
.product(name: "Gzip", package: "GzipSwift"),
3444
]
3545
),
3646
.target(
3747
name: "LyricsServiceUI",
3848
dependencies: [
3949
"LyricsCore",
4050
"LyricsService",
41-
"KeychainAccess",
51+
.product(name: "KeychainAccess", package: "KeychainAccess"),
52+
.product(name: "Schedule", package: "Schedule"),
4253
]
4354
),
4455
.testTarget(
4556
name: "LyricsKitTests",
46-
dependencies: ["LyricsCore", "LyricsService"]),
57+
dependencies: [
58+
"LyricsCore",
59+
"LyricsService",
60+
]
61+
),
4762
]
4863
)
4964

50-
extension SupportedPlatform.IOSVersion {
51-
#if compiler(>=5.3)
52-
static var minimalToolChainSupported = SupportedPlatform.IOSVersion.v9
53-
#else
54-
static var minimalToolChainSupported = SupportedPlatform.IOSVersion.v8
55-
#endif
56-
}
57-
5865
enum CombineImplementation {
59-
6066
case combine
6167
case combineX
6268
case openCombine
63-
69+
6470
static var `default`: CombineImplementation {
6571
return .combineX
6672
}
67-
73+
6874
init?(_ description: String) {
6975
let desc = description.lowercased().filter(\.isLetter)
7076
switch desc {
71-
case "combine": self = .combine
72-
case "combinex": self = .combineX
77+
case "combine": self = .combine
78+
case "combinex": self = .combineX
7379
case "opencombine": self = .openCombine
74-
default: return nil
80+
default: return nil
7581
}
7682
}
7783
}
7884

7985
extension ProcessInfo {
80-
8186
var combineImplementation: CombineImplementation {
8287
return environment["CX_COMBINE_IMPLEMENTATION"].flatMap(CombineImplementation.init) ?? .default
8388
}

Sources/LyricsService/Provider/LRCLIB.swift

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,27 @@ extension LyricsProviders {
1616
}
1717

1818
public struct LRCLIBResponse: Codable {
19-
let id: Int
20-
let name: String
21-
let trackName: String
22-
let artistName: String
23-
let albumName: String
24-
let duration: Double
25-
let instrumental: Bool
26-
let plainLyrics: String?
27-
let syncedLyrics: String?
19+
let id: Int
20+
let name: String
21+
let trackName: String
22+
let artistName: String
23+
let albumName: String
24+
let duration: Double
25+
let instrumental: Bool
26+
let plainLyrics: String?
27+
let syncedLyrics: String?
2828
}
2929

3030
extension LyricsProviders.LRCLIB: _LyricsProvider {
31-
3231
public typealias LyricsToken = LRCLIBResponse
33-
32+
3433
public static let service: String? = "LRCLIB"
35-
34+
3635
public func lyricsSearchPublisher(request: LyricsSearchRequest) -> AnyPublisher<LyricsToken, Never> {
3736
let url = switch request.searchTerm {
38-
case .keyword(let string):
37+
case let .keyword(string):
3938
URL(string: "https://lrclib.net/api/search?q=\(string)")!
40-
case .info(let title, let artist):
39+
case let .info(title, artist):
4140
URL(string: "https://lrclib.net/api/search?track_name=\(title)&artist_name=\(artist)")!
4241
}
4342
var req = URLRequest(url: url)
@@ -50,43 +49,30 @@ extension LyricsProviders.LRCLIB: _LyricsProvider {
5049
.map { $0 as LyricsToken }
5150
.eraseToAnyPublisher()
5251
}
53-
52+
53+
private func parseLyrics(for token: LyricsToken) -> Lyrics? {
54+
guard let syncedLyrics = token.syncedLyrics, let lyrics = Lyrics(syncedLyrics) else { return nil }
55+
lyrics.idTags[.title] = token.trackName
56+
lyrics.idTags[.artist] = token.artistName
57+
lyrics.idTags[.album] = token.albumName
58+
lyrics.length = Double(token.duration) / 1000
59+
lyrics.metadata.serviceToken = "\(token.id)"
60+
return lyrics
61+
}
62+
5463
public func lyricsFetchPublisher(token: LyricsToken) -> AnyPublisher<Lyrics, Never> {
55-
guard let syncedLyrics = token.syncedLyrics, let lyrics = Lyrics(syncedLyrics) else { return Empty<Lyrics, Never>().eraseToAnyPublisher() }
56-
57-
58-
return Just(lyrics).eraseToAnyPublisher()
59-
// let url = URL(string: netEaseLyricsBaseURLString + parameter.stringFromHttpParameters)!
60-
// return sharedURLSession.cx.dataTaskPublisher(for: url)
61-
// .map(\.data)
62-
// .decode(type: NetEaseResponseSingleLyrics.self, decoder: JSONDecoder().cx)
63-
// .compactMap {
64-
// let lyrics: Lyrics
65-
// let transLrc = ($0.tlyric?.fixedLyric).flatMap(Lyrics.init(_:))
66-
// if let kLrc = ($0.klyric?.fixedLyric).flatMap(Lyrics.init(netEaseKLyricContent:)) {
67-
// transLrc.map(kLrc.forceMerge)
68-
// lyrics = kLrc
69-
// } else if let lrc = ($0.lrc?.fixedLyric).flatMap(Lyrics.init(_:)) {
70-
// transLrc.map(lrc.merge)
71-
// lyrics = lrc
72-
// } else {
73-
// return nil
74-
// }
75-
//
76-
// // FIXME: merge inline time tags back to lyrics
77-
// // if let taggedLrc = (model.klyric?.lyric).flatMap(Lyrics.init(netEaseKLyricContent:))
78-
//
79-
// lyrics.idTags[.title] = token.value.name
80-
// lyrics.idTags[.artist] = token.value.artists.first?.name
81-
// lyrics.idTags[.album] = token.value.album.name
82-
// lyrics.idTags[.lrcBy] = $0.lyricUser?.nickname
83-
//
84-
// lyrics.length = Double(token.value.duration) / 1000
85-
// lyrics.metadata.artworkURL = token.value.album.picUrl
86-
// lyrics.metadata.serviceToken = "\(token.value.id)"
87-
//
88-
// return lyrics
89-
// }.ignoreError()
90-
// .eraseToAnyPublisher()
64+
if let lyrics = parseLyrics(for: token) {
65+
return Just(lyrics).eraseToAnyPublisher()
66+
} else {
67+
return sharedURLSession.cx.dataTaskPublisher(for: .init(string: "https://lrclib.net/api/get/\(token.id)")!)
68+
.map(\.data)
69+
.decode(type: LRCLIBResponse.self, decoder: JSONDecoder().cx)
70+
.compactMap { [weak self] in
71+
guard let self else { return nil }
72+
return parseLyrics(for: $0)
73+
}
74+
.ignoreError()
75+
.eraseToAnyPublisher()
76+
}
9177
}
9278
}

Sources/LyricsService/Provider/Service.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ extension LyricsProviders.Service {
5252
case .kugou: return LyricsProviders.Kugou()
5353
case .gecimi: return LyricsProviders.Gecimi()
5454
case .spotify(let accessToken): return LyricsProviders.Spotify(accessToken: accessToken)
55+
case .lrclib: return LyricsProviders.LRCLIB()
5556
#if canImport(Darwin)
5657
case .syair: return LyricsProviders.Syair()
5758
#endif

Sources/LyricsService/Provider/Spotify.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,7 @@ extension LyricsProviders.Spotify {
266266
lyrics.metadata.serviceToken = token.id
267267
return lyrics
268268
}
269-
// .ignoreError()
270-
.catch { error in
271-
print(error)
272-
return Empty<Lyrics, Never>()
273-
}
269+
.ignoreError()
274270
.eraseToAnyPublisher()
275271
}
276272
}

Sources/LyricsServiceUI/Controller/SpotifyLoginManager.swift

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import AppKit
22
import WebKit
33
import KeychainAccess
4+
import Schedule
5+
6+
private typealias Task = _Concurrency.Task
47

58
struct SpotifyAccessToken: Codable {
69
let accessToken: String
710
let accessTokenExpirationTimestampMs: TimeInterval
811
let isAnonymous: Bool
912

13+
var expirationDate: Date {
14+
return Date(timeIntervalSince1970: accessTokenExpirationTimestampMs / 1000)
15+
}
16+
1017
static func accessToken(forCookie cookie: String) async throws -> Self {
1118
let url = URL(string: "https://open.spotify.com/get_access_token?reason=transport&productType=web_player")!
1219
var request = URLRequest(url: url)
@@ -92,8 +99,8 @@ public final class SpotifyLoginManager: NSObject, @unchecked Sendable {
9299
}
93100

94101
private let secureStorage = SecureStorage()
95-
96-
private var activeTask: Task<Void, Never>?
102+
103+
private var refreshTask: Schedule.Task?
97104

98105
public var isLogin: Bool {
99106
get async {
@@ -116,23 +123,33 @@ public final class SpotifyLoginManager: NSObject, @unchecked Sendable {
116123
private override init() {
117124
super.init()
118125

119-
self.activeTask = Task {
120-
if let accessToken = await secureStorage.getAccessToken(),
121-
accessToken.accessTokenExpirationTimestampMs <= Date().timeIntervalSince1970 * 1000,
122-
await secureStorage.getCookie() != nil {
123-
try? await self.requestAccessToken()
126+
Task {
127+
if let accessToken = await secureStorage.getAccessToken() {
128+
if accessToken.expirationDate <= Date(), await secureStorage.getCookie() != nil {
129+
try await self.requestAccessToken()
130+
} else {
131+
self.scheduleAccessTokenRefresh(accessToken)
132+
}
124133
}
125134
}
126135
}
127136

128-
deinit {
129-
activeTask?.cancel()
137+
138+
private func scheduleAccessTokenRefresh(_ accessToken: SpotifyAccessToken) {
139+
refreshTask?.cancel()
140+
refreshTask = Plan.at(accessToken.expirationDate).do(queue: .global()) { [weak self] in
141+
guard let self else { return }
142+
Task {
143+
try await self.requestAccessToken()
144+
}
145+
}
130146
}
131147

132148
public func requestAccessToken() async throws {
133149
guard let cookie = await secureStorage.getCookie() else { return }
134150
let accessToken = try await SpotifyAccessToken.accessToken(forCookie: cookie)
135151
await secureStorage.setAccessToken(accessToken)
152+
scheduleAccessTokenRefresh(accessToken)
136153
}
137154

138155
public func login() async throws {

Tests/LyricsKitTests/LyricsKitTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ let duration = 305.0
77
let searchReq = LyricsSearchRequest(searchTerm: .info(title: testSong, artist: testArtist), duration: duration)
88

99
final class LyricsKitTests: XCTestCase {
10-
1110
private func test(provider: LyricsProvider) {
1211
var searchResultEx: XCTestExpectation? = expectation(description: "Search result: \(provider)")
1312
let token = provider.lyricsPublisher(request: searchReq).sink { lrc in
@@ -18,11 +17,12 @@ final class LyricsKitTests: XCTestCase {
1817
waitForExpectations(timeout: 10)
1918
token.cancel()
2019
}
21-
20+
2221
func testQQMusicProvider() {
2322
test(provider: LyricsProviders.QQMusic())
2423
}
2524

26-
27-
25+
func testLRCLIBProvider() {
26+
test(provider: LyricsProviders.LRCLIB())
27+
}
2828
}

0 commit comments

Comments
 (0)