Skip to content

Commit

Permalink
feat: add songlink based track matching for youtube and open song lin…
Browse files Browse the repository at this point in the history
…k button

songlink.com will provide accurate match verified by community for most spotify tracks improving overall match accuracy for Youtube audio source
  • Loading branch information
KRTirtho committed Feb 25, 2024
1 parent 6f71e52 commit 9095a8c
Show file tree
Hide file tree
Showing 14 changed files with 565 additions and 19 deletions.
Binary file added assets/logos/songlink-transparent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logos/songlink.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions lib/collections/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions lib/components/player/player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:url_launcher/url_launcher_string.dart';

class PlayerView extends HookConsumerWidget {
final PanelController panelController;
Expand Down Expand Up @@ -137,6 +138,21 @@ class PlayerView extends HookConsumerWidget {
onPressed: panelController.close,
),
actions: [
IconButton(
icon: Assets.logos.songlink.image(
width: 20,
height: 20,
),
tooltip: context.l10n.song_link,
onPressed: currentTrack == null
? null
: () {
final url =
"https://song.link/s/${currentTrack.id}";

launchUrlString(url);
},
),
IconButton(
icon: const Icon(SpotubeIcons.info, size: 18),
tooltip: context.l10n.details,
Expand Down
17 changes: 17 additions & 0 deletions lib/components/shared/track_tile/track_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/library/user_local_tracks.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
Expand All @@ -26,10 +27,12 @@ import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/search.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:url_launcher/url_launcher_string.dart';

enum TrackOptionValue {
album,
share,
songlink,
addToPlaylist,
addToQueue,
removeFromPlaylist,
Expand Down Expand Up @@ -165,6 +168,7 @@ class TrackOptions extends HookConsumerWidget {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context);
final router = GoRouter.of(context);
final ThemeData(:colorScheme) = Theme.of(context);

final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
Expand Down Expand Up @@ -276,6 +280,10 @@ class TrackOptions extends HookConsumerWidget {
case TrackOptionValue.share:
actionShare(context, track);
break;
case TrackOptionValue.songlink:
final url = "https://song.link/s/${track.id}";
await launchUrlString(url);
break;
case TrackOptionValue.details:
showDialog(
context: context,
Expand Down Expand Up @@ -418,6 +426,15 @@ class TrackOptions extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.share),
title: Text(context.l10n.share),
),
PopSheetEntry(
value: TrackOptionValue.songlink,
leading: Assets.logos.songlinkTransparent.image(
width: 22,
height: 22,
color: colorScheme.onSurface.withOpacity(0.5),
),
title: Text(context.l10n.song_link),
),
PopSheetEntry(
value: TrackOptionValue.details,
leading: const Icon(SpotubeIcons.info),
Expand Down
3 changes: 2 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -294,5 +294,6 @@
"endless_playback": "Endless Playback",
"delete_playlist": "Delete Playlist",
"delete_playlist_confirmation": "Are you sure you want to delete this playlist?",
"local_tracks": "Local Tracks"
"local_tracks": "Local Tracks",
"song_link": "Song Link"
}
19 changes: 19 additions & 0 deletions lib/services/song_link/model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
part of './song_link.dart';

@freezed
class SongLink with _$SongLink {
const factory SongLink({
required String displayName,
required String linkId,
required String platform,
required bool show,
required String? uniqueId,
required String? country,
required String? url,
required String? nativeAppUriMobile,
required String? nativeAppUriDesktop,
}) = _SongLink;

factory SongLink.fromJson(Map<String, dynamic> json) =>
_$SongLinkFromJson(json);
}
47 changes: 47 additions & 0 deletions lib/services/song_link/song_link.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
library song_link;

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:html/parser.dart';

part 'model.dart';

part 'song_link.freezed.dart';
part 'song_link.g.dart';

abstract class SongLinkService {
static Future<List<SongLink>> links(String spotifyId) async {
final dio = Dio();

final res = await dio.get(
"https://song.link/s/$spotifyId",
options: Options(
headers: {
"Accept":
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
},
responseType: ResponseType.plain,
),
);

final document = parse(res.data);

final script = document.getElementById("__NEXT_DATA__")?.text;

if (script == null) {
return <SongLink>[];
}

final pageProps = jsonDecode(script) as Map<String, dynamic>;
final songLinks =
pageProps["props"]["pageProps"]["pageData"]["sections"].firstWhere(
(section) => section["sectionId"] == "section|auto|links|listen",
)["links"] as List;

return songLinks.map((link) => SongLink.fromJson(link)).toList();
}
}
Loading

0 comments on commit 9095a8c

Please sign in to comment.