···190190 title: self.nowPlaying.songName ?? "",
191191 artist: self.nowPlaying.artistName ?? "",
192192 playbackDuration: self.totalDuration)
193193+194194+ FloooService.shared.scrobbleToBuiltInEndpoint(
195195+ submission: false, songId: self.nowPlaying.id ?? ""
196196+ ) { result in
197197+ // TODO: handle when this fail, maybe also move to viewModel? what if user have no account linked
198198+ }
193199 }
194200195201 private func addPeriodicTimeObserver() {
···210216 if !self.isLocallySaved && self.progress >= 0.5 {
211217 Task {
212218 FloooService.shared.saveListeningHistory(payload: self.nowPlaying)
219219+ // TODO: refactor to group with saveListeningHistory?
220220+ FloooService.shared.scrobbleToBuiltInEndpoint(
221221+ submission: true, songId: self.nowPlaying.id ?? ""
222222+ ) { result in
223223+ // TODO: handle when this fail, maybe also move to viewModel? what if user have no account linked
224224+ }
213225214226 self.isLocallySaved = true
215227 }
+121
flo/Shared/Services/FloooService.swift
···11+import Alamofire
12//
23// FloooService.swift
34// flo
···56// Created by rizaldy on 22/11/24.
67//
78import Foundation
99+1010+struct AccountStatusResponse: Decodable {
1111+ let status: Bool
1212+}
1313+1414+struct AccountLinkStatus: Decodable {
1515+ let listenBrainz: Bool
1616+ let lastFM: Bool
1717+}
1818+1919+struct SubsonicResponse: Codable {
2020+ let status: String
2121+ let version: String
2222+ let type: String
2323+ let serverVersion: String
2424+ let openSubsonic: Bool
2525+}
2626+2727+enum CodingKeys: String, CodingKey {
2828+ // FIXME: constants?
2929+ case subsonicResponse = "subsonic-response"
3030+}
831932class FloooService {
1033 static let shared: FloooService = FloooService()
···57805881 return stats
5982 }.value
8383+ }
8484+8585+ func getAccountLinkStatuses(completion: @escaping (Result<AccountLinkStatus, Error>) -> Void) {
8686+ let group = DispatchGroup()
8787+8888+ var listenBrainzStatus: Bool?
8989+ var lastFMStatus: Bool?
9090+ var error: Error?
9191+9292+ group.enter()
9393+9494+ checkListenBrainzAccountStatus { result in
9595+ switch result {
9696+ case .success(let status):
9797+ listenBrainzStatus = status
9898+ case .failure(let err):
9999+ error = err
100100+ }
101101+102102+ group.leave()
103103+ }
104104+105105+ group.enter()
106106+107107+ checkLastFMAccountStatus { result in
108108+ switch result {
109109+ case .success(let status):
110110+ lastFMStatus = status
111111+ case .failure(let err):
112112+ error = err
113113+ }
114114+115115+ group.leave()
116116+ }
117117+118118+ group.notify(queue: .main) {
119119+ if let error = error {
120120+ completion(.failure(error))
121121+122122+ return
123123+ }
124124+125125+ guard let listenBrainz = listenBrainzStatus, let lastFm = lastFMStatus else {
126126+ completion(.failure(NSError(domain: "", code: -1)))
127127+128128+ return
129129+ }
130130+131131+ completion(.success(AccountLinkStatus(listenBrainz: listenBrainz, lastFM: lastFm)))
132132+ }
133133+ }
134134+135135+ func checkListenBrainzAccountStatus(completion: @escaping (Result<Bool, Error>) -> Void) {
136136+ APIManager.shared.NDEndpointRequest(endpoint: API.NDEndpoint.listenBrainzLink, parameters: [:])
137137+ {
138138+ (response: DataResponse<AccountStatusResponse, AFError>) in
139139+ switch response.result {
140140+ case .success(let status):
141141+ completion(.success(status.status))
142142+ case .failure(let error):
143143+ completion(.failure(error))
144144+ }
145145+ }
146146+ }
147147+148148+ func checkLastFMAccountStatus(completion: @escaping (Result<Bool, Error>) -> Void) {
149149+ APIManager.shared.NDEndpointRequest(endpoint: API.NDEndpoint.lastFMLink, parameters: [:]) {
150150+ (response: DataResponse<AccountStatusResponse, AFError>) in
151151+ switch response.result {
152152+ case .success(let status):
153153+ completion(.success(status.status))
154154+ case .failure(let error):
155155+ completion(.failure(error))
156156+ }
157157+ }
158158+ }
159159+160160+ func scrobbleToBuiltInEndpoint(
161161+ submission: Bool, songId: String,
162162+ completion: @escaping (Result<SubsonicResponse, Error>) -> Void
163163+ ) {
164164+ var params: [String: Any] = ["submission": String(submission), "id": songId]
165165+166166+ if submission {
167167+ params["time"] = Int(Date().timeIntervalSince1970 * 1000)
168168+ }
169169+170170+ APIManager.shared.SubsonicEndpointRequest(
171171+ endpoint: API.SubsonicEndpoint.scrobble, parameters: params
172172+ ) {
173173+ (response: DataResponse<SubsonicResponse, AFError>) in
174174+ switch response.result {
175175+ case .success(let response):
176176+ completion(.success(response))
177177+ case .failure(let error):
178178+ completion(.failure(error))
179179+ }
180180+ }
60181 }
61182}
+3
flo/Shared/Utils/Constants.swift
···1717 static let getPlaylists = "/api/playlist"
1818 static let getSong = "/api/song"
1919 static let shareAlbum = "/api/share"
2020+ static let listenBrainzLink = "/api/listenbrainz/link"
2121+ static let lastFMLink = "/api/lastfm/link"
2022 }
21232224 struct SubsonicEndpoint {
···2527 static let albuminfo = "/rest/getAlbumInfo"
2628 static let scanStatus = "/rest/getScanStatus"
2729 static let download = "/rest/download"
3030+ static let scrobble = "/rest/scrobble"
2831 }
2932}
3033