Skip to content

Commit

Permalink
Add PetitLyric Source (#247)
Browse files Browse the repository at this point in the history
* Petit Lyric Source

* Fix Error

* Better Description

* Use App API (Add support for Time Synced)

* Remove Debug statments

* Update Description
  • Loading branch information
yodaluca23 authored Jul 16, 2024
1 parent 828f8d3 commit d6c5654
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 2 deletions.
1 change: 1 addition & 0 deletions Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func getCurrentTrackLyricsData(originalLyrics: Lyrics? = nil) throws -> Data {
case .genius: GeniusLyricsRepository()
case .lrclib: LrcLibLyricsRepository()
case .musixmatch: MusixmatchLyricsRepository.shared
case .petitLyrics: PetitLyricsRepository()
}

let lyricsDto: LyricsDto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ enum LyricsSource : Int, CustomStringConvertible {
case genius
case lrclib
case musixmatch
case petitLyrics

var description : String {
switch self {
case .genius: "Genius"
case .lrclib: "LRCLIB"
case .musixmatch: "Musixmatch"
case .petitLyrics: "Petit Lyrics"
}
}
}
}
208 changes: 208 additions & 0 deletions Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import Foundation

class XMLDictionaryParser: NSObject, XMLParserDelegate {
private var dictionaryStack: [[String: Any]] = []
private var textInProgress: String = ""

func parse(data: Data) -> [String: Any]? {
let parser = XMLParser(data: data)
parser.delegate = self
guard parser.parse() else {
NSLog("[EeveeSpotify] Failed to parse XML")
return nil
}
return dictionaryStack.first
}

// MARK: - XMLParserDelegate

func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
var dict: [String: Any] = [:]
for (key, value) in attributeDict {
if let intValue = Int(value) {
dict[key] = intValue
} else {
dict[key] = value
}
}
dictionaryStack.append(dict)
textInProgress = ""
}

func parser(_ parser: XMLParser, foundCharacters string: String) {
textInProgress += string
}

func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
var dict = dictionaryStack.popLast()!
if !textInProgress.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if let intValue = Int(textInProgress.trimmingCharacters(in: .whitespacesAndNewlines)) {
dict[elementName] = intValue
} else {
dict[elementName] = textInProgress.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

if var top = dictionaryStack.last {
if let existingValue = top[elementName] {
if var array = existingValue as? [[String: Any]] {
array.append(dict)
top[elementName] = array
} else {
top[elementName] = [existingValue, dict]
}
} else if dict.count == 1, let key = dict.keys.first, let value = dict[key] {
top[elementName] = value
} else {
top[elementName] = dict
}
dictionaryStack[dictionaryStack.count - 1] = top
} else {
dictionaryStack.append(dict)
}
textInProgress = ""
}
}

struct PetitLyricsRepository: LyricsRepository {
private let apiUrl = "https://p1.petitlyrics.com/api/GetPetitLyricsData.php"
private let session: URLSession

init() {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "EeveeSpotify v\(EeveeSpotify.version) https://github.com/whoeevee/EeveeSpotify"
]

session = URLSession(configuration: configuration)
}

private func perform(
_ query: [String: Any]
) throws -> Data {
var request = URLRequest(url: URL(string: apiUrl)!)
request.httpMethod = "POST"

let queryString = query.queryString
request.httpBody = queryString.data(using: .utf8)

let semaphore = DispatchSemaphore(value: 0)
var data: Data?
var error: Error?

let task = session.dataTask(with: request) { response, _, err in
error = err
data = response
semaphore.signal()
}

task.resume()
semaphore.wait()

if let error = error {
throw error
}

return data!
}

private func decodeBase64(_ base64String: String) throws -> Data {
guard let data = Data(base64Encoded: base64String) else {
throw LyricsError.DecodingError
}
return data
}

private func mapTimeSyncedLyrics(_ xmlData: Data) throws -> [LyricsLineDto] {
guard let parsedDictionary = XMLDictionaryParser().parse(data: xmlData),
let lines = parsedDictionary["line"] as? [[String: Any]] else {
throw LyricsError.DecodingError
}

var lyricsLines: [LyricsLineDto] = []
for line in lines {
guard let lineString = line["linestring"] as? String,
let words = line["word"] as? [[String: Any]],
let firstWord = words.first,
let startTimeInt = firstWord["starttime"] as? Int else {
continue // Skip lines that don't have necessary data
}

let lyricsLineDto = LyricsLineDto(content: lineString, offsetMs: startTimeInt)
lyricsLines.append(lyricsLineDto)
}

return lyricsLines
}

func getLyrics(_ query: LyricsSearchQuery, options: LyricsOptions) throws -> LyricsDto {
var petitLyricsQuery = [
"maxCount": "1",
"key_title": query.title,
"key_artist": query.primaryArtist,
"terminalType": "10",
"clientAppId": "p1232089",
"lyricsType": "3"
]

let response = try perform(petitLyricsQuery)
let parser = XMLDictionaryParser()
let parsedDictionary = parser.parse(data: response)

guard let parsedDict = parsedDictionary,
let songs = parsedDict["songs"] as? [String: Any],
let song = songs["song"] as? [String: Any] else {
throw LyricsError.NoSuchSong
}


guard let lyricsDataBase64 = song["lyricsData"] as? String,
let lyricsType = song["lyricsType"] as? Int else {
throw LyricsError.DecodingError
}


let lyricsData = try decodeBase64(lyricsDataBase64)

if lyricsType == 2 {
petitLyricsQuery["lyricsType"] = "1"
let responsetype1 = try perform(petitLyricsQuery)
let parser = XMLDictionaryParser()
guard let parsedDictionary1 = parser.parse(data: responsetype1) else {
throw LyricsError.DecodingError
}

guard let songs = parsedDictionary1["songs"] as? [String: Any],
let song = songs["song"] as? [String: Any],
let lyricsDataBase64 = song["lyricsData"] as? String else {
throw LyricsError.DecodingError
}

let lyricsData = try decodeBase64(lyricsDataBase64)
let lines = String(data: lyricsData, encoding: .utf8)?
.components(separatedBy: "\n")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.map { LyricsLineDto(content: $0) } ?? []
return LyricsDto(lines: lines, timeSynced: false)
}

if lyricsType == 1 {
let lines = String(data: lyricsData, encoding: .utf8)?
.components(separatedBy: "\n")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.map { LyricsLineDto(content: $0) } ?? []
return LyricsDto(lines: lines, timeSynced: false)

}

if lyricsType == 3 {
let lines = try mapTimeSyncedLyrics(lyricsData)
return LyricsDto(lines: lines, timeSynced: true)
}

throw LyricsError.DecodingError
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ LRCLIB: The most open service, offering time-synced lyrics. However, it lacks ly
Musixmatch: The service Spotify uses. Provides time-synced lyrics for many songs, but you'll need a user token to use this source.
Petit Lyrics: Has a large database of time-synced Japanese and international songs. Lacks several Western songs.
If the tweak is unable to find a song or process the lyrics, you'll see a "Couldn't load the lyrics for this song" message. The lyrics might be wrong for some songs when using Genius due to how the tweak searches songs. I've made it work in most cases.
""")) {
Picker(
Expand All @@ -21,6 +23,7 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
Text("Genius").tag(LyricsSource.genius)
Text("LRCLIB").tag(LyricsSource.lrclib)
Text("Musixmatch").tag(LyricsSource.musixmatch)
Text("Petit Lyrics").tag(LyricsSource.petitLyrics)
}

if lyricsSource == .musixmatch {
Expand Down Expand Up @@ -60,4 +63,3 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
}
}
}

0 comments on commit d6c5654

Please sign in to comment.