import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:get/get.dart'; import 'package:lrc/lrc.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rhythm_box/providers/database.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/lyrics/model.dart'; import 'package:spotify/spotify.dart'; class SyncedLyricsProvider extends GetxController { RxInt delay = 0.obs; Future getSpotifyLyrics(Track track, String? token) async { final res = await Dio().getUri( Uri.parse( 'https://spclient.wg.spotify.com/color-lyrics/v2/track/${track.id}?format=json&market=from_token', ), options: Options( headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36', 'App-platform': 'WebPlayer', 'authorization': 'Bearer $token' }, responseType: ResponseType.json, validateStatus: (status) => true, ), ); if (res.statusCode != 200) { return SubtitleSimple( lyrics: [], name: track.name!, uri: res.realUri, rating: 0, provider: 'Spotify', ); } final linesRaw = Map.castFrom(res.data)['lyrics'] ?['lines'] as List?; final lines = linesRaw?.map((line) { return LyricSlice( time: Duration(milliseconds: int.parse(line['startTimeMs'])), text: line['words'] as String, ); }).toList() ?? []; return SubtitleSimple( lyrics: lines, name: track.name!, uri: res.realUri, rating: 100, provider: 'Spotify', ); } Future getLRCLibLyrics(Track track) async { final packageInfo = await PackageInfo.fromPlatform(); final res = await Dio().getUri( Uri( scheme: 'https', host: 'lrclib.net', path: '/api/get', queryParameters: { 'artist_name': track.artists?.first.name, 'track_name': track.name, 'album_name': track.album?.name, 'duration': track.duration?.inSeconds.toString(), }, ), options: Options( headers: {'User-Agent': 'RhythmBox/${packageInfo.version}'}, responseType: ResponseType.json, ), ); if (res.statusCode != 200) { return SubtitleSimple( lyrics: [], name: track.name!, uri: res.realUri, rating: 0, provider: 'LRCLib', ); } final json = res.data as Map; final syncedLyricsRaw = json['syncedLyrics'] as String?; final syncedLyrics = syncedLyricsRaw?.isNotEmpty == true ? Lrc.parse(syncedLyricsRaw!) .lyrics .map(LyricSlice.fromLrcLine) .toList() : null; if (syncedLyrics?.isNotEmpty == true) { return SubtitleSimple( lyrics: syncedLyrics!, name: track.name!, uri: res.realUri, rating: 100, provider: 'LRCLib', ); } final plainLyrics = (json['plainLyrics'] as String) .split('\n') .map((line) => LyricSlice(text: line, time: Duration.zero)) .toList(); return SubtitleSimple( lyrics: plainLyrics, name: track.name!, uri: res.realUri, rating: 0, provider: 'LRCLib', ); } Future fetch(Track track) async { try { final database = Get.find().database; final spotify = Get.find().api; final cachedLyrics = await (database.select(database.lyricsTable) ..where((tbl) => tbl.trackId.equals(track.id!))) .map((row) => row.data) .getSingleOrNull(); SubtitleSimple? lyrics = cachedLyrics; final token = await spotify.getCredentials(); if (lyrics == null || lyrics.lyrics.isEmpty) { lyrics = await getSpotifyLyrics(track, token.accessToken); } if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) { lyrics = await getLRCLibLyrics(track); } if (lyrics.lyrics.isEmpty) { throw Exception('Unable to find lyrics'); } if (cachedLyrics == null || cachedLyrics.lyrics.isEmpty) { await database.into(database.lyricsTable).insert( LyricsTableCompanion.insert( trackId: track.id!, data: lyrics, ), mode: InsertMode.replace, ); } return lyrics; } catch (e, stackTrace) { log('[Lyrics] Error: $e; Trace:\n$stackTrace'); rethrow; } } }