mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: add songlink based track matching for youtube and open song link button
songlink.com will provide accurate match verified by community for most spotify tracks improving overall match accuracy for Youtube audio source
This commit is contained in:
parent
6f71e52ea8
commit
9095a8c8f8
BIN
assets/logos/songlink-transparent.png
Normal file
BIN
assets/logos/songlink-transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
assets/logos/songlink.png
Normal file
BIN
assets/logos/songlink.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
@ -9,6 +9,21 @@
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class $AssetsLogosGen {
|
||||||
|
const $AssetsLogosGen();
|
||||||
|
|
||||||
|
/// File path: assets/logos/songlink-transparent.png
|
||||||
|
AssetGenImage get songlinkTransparent =>
|
||||||
|
const AssetGenImage('assets/logos/songlink-transparent.png');
|
||||||
|
|
||||||
|
/// File path: assets/logos/songlink.png
|
||||||
|
AssetGenImage get songlink =>
|
||||||
|
const AssetGenImage('assets/logos/songlink.png');
|
||||||
|
|
||||||
|
/// List of all assets
|
||||||
|
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
||||||
|
}
|
||||||
|
|
||||||
class $AssetsTutorialGen {
|
class $AssetsTutorialGen {
|
||||||
const $AssetsTutorialGen();
|
const $AssetsTutorialGen();
|
||||||
|
|
||||||
@ -37,6 +52,7 @@ class Assets {
|
|||||||
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
||||||
static const AssetGenImage likedTracks =
|
static const AssetGenImage likedTracks =
|
||||||
AssetGenImage('assets/liked-tracks.jpg');
|
AssetGenImage('assets/liked-tracks.jpg');
|
||||||
|
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||||
static const AssetGenImage placeholder =
|
static const AssetGenImage placeholder =
|
||||||
AssetGenImage('assets/placeholder.png');
|
AssetGenImage('assets/placeholder.png');
|
||||||
static const AssetGenImage spotubeHeroBanner =
|
static const AssetGenImage spotubeHeroBanner =
|
||||||
|
@ -25,6 +25,7 @@ import 'package:spotube/pages/lyrics/lyrics.dart';
|
|||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class PlayerView extends HookConsumerWidget {
|
class PlayerView extends HookConsumerWidget {
|
||||||
final PanelController panelController;
|
final PanelController panelController;
|
||||||
@ -137,6 +138,21 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
onPressed: panelController.close,
|
onPressed: panelController.close,
|
||||||
),
|
),
|
||||||
actions: [
|
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(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||||
tooltip: context.l10n.details,
|
tooltip: context.l10n.details,
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/library/user_local_tracks.dart';
|
import 'package:spotube/components/library/user_local_tracks.dart';
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
@ -26,10 +27,12 @@ import 'package:spotube/provider/spotify_provider.dart';
|
|||||||
import 'package:spotube/services/mutations/mutations.dart';
|
import 'package:spotube/services/mutations/mutations.dart';
|
||||||
import 'package:spotube/services/queries/search.dart';
|
import 'package:spotube/services/queries/search.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
enum TrackOptionValue {
|
enum TrackOptionValue {
|
||||||
album,
|
album,
|
||||||
share,
|
share,
|
||||||
|
songlink,
|
||||||
addToPlaylist,
|
addToPlaylist,
|
||||||
addToQueue,
|
addToQueue,
|
||||||
removeFromPlaylist,
|
removeFromPlaylist,
|
||||||
@ -165,6 +168,7 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
|
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||||
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playback = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
@ -276,6 +280,10 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
case TrackOptionValue.share:
|
case TrackOptionValue.share:
|
||||||
actionShare(context, track);
|
actionShare(context, track);
|
||||||
break;
|
break;
|
||||||
|
case TrackOptionValue.songlink:
|
||||||
|
final url = "https://song.link/s/${track.id}";
|
||||||
|
await launchUrlString(url);
|
||||||
|
break;
|
||||||
case TrackOptionValue.details:
|
case TrackOptionValue.details:
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -418,6 +426,15 @@ class TrackOptions extends HookConsumerWidget {
|
|||||||
leading: const Icon(SpotubeIcons.share),
|
leading: const Icon(SpotubeIcons.share),
|
||||||
title: Text(context.l10n.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(
|
PopSheetEntry(
|
||||||
value: TrackOptionValue.details,
|
value: TrackOptionValue.details,
|
||||||
leading: const Icon(SpotubeIcons.info),
|
leading: const Icon(SpotubeIcons.info),
|
||||||
|
@ -294,5 +294,6 @@
|
|||||||
"endless_playback": "Endless Playback",
|
"endless_playback": "Endless Playback",
|
||||||
"delete_playlist": "Delete Playlist",
|
"delete_playlist": "Delete Playlist",
|
||||||
"delete_playlist_confirmation": "Are you sure you want to delete this 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
lib/services/song_link/model.dart
Normal file
19
lib/services/song_link/model.dart
Normal file
@ -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
lib/services/song_link/song_link.dart
Normal file
47
lib/services/song_link/song_link.dart
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
320
lib/services/song_link/song_link.freezed.dart
Normal file
320
lib/services/song_link/song_link.freezed.dart
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
// coverage:ignore-file
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'song_link.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
final _privateConstructorUsedError = UnsupportedError(
|
||||||
|
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||||
|
|
||||||
|
SongLink _$SongLinkFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SongLink.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SongLink {
|
||||||
|
String get displayName => throw _privateConstructorUsedError;
|
||||||
|
String get linkId => throw _privateConstructorUsedError;
|
||||||
|
String get platform => throw _privateConstructorUsedError;
|
||||||
|
bool get show => throw _privateConstructorUsedError;
|
||||||
|
String? get uniqueId => throw _privateConstructorUsedError;
|
||||||
|
String? get country => throw _privateConstructorUsedError;
|
||||||
|
String? get url => throw _privateConstructorUsedError;
|
||||||
|
String? get nativeAppUriMobile => throw _privateConstructorUsedError;
|
||||||
|
String? get nativeAppUriDesktop => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
$SongLinkCopyWith<SongLink> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SongLinkCopyWith<$Res> {
|
||||||
|
factory $SongLinkCopyWith(SongLink value, $Res Function(SongLink) then) =
|
||||||
|
_$SongLinkCopyWithImpl<$Res, SongLink>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String displayName,
|
||||||
|
String linkId,
|
||||||
|
String platform,
|
||||||
|
bool show,
|
||||||
|
String? uniqueId,
|
||||||
|
String? country,
|
||||||
|
String? url,
|
||||||
|
String? nativeAppUriMobile,
|
||||||
|
String? nativeAppUriDesktop});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SongLinkCopyWithImpl<$Res, $Val extends SongLink>
|
||||||
|
implements $SongLinkCopyWith<$Res> {
|
||||||
|
_$SongLinkCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? displayName = null,
|
||||||
|
Object? linkId = null,
|
||||||
|
Object? platform = null,
|
||||||
|
Object? show = null,
|
||||||
|
Object? uniqueId = freezed,
|
||||||
|
Object? country = freezed,
|
||||||
|
Object? url = freezed,
|
||||||
|
Object? nativeAppUriMobile = freezed,
|
||||||
|
Object? nativeAppUriDesktop = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
displayName: null == displayName
|
||||||
|
? _value.displayName
|
||||||
|
: displayName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
linkId: null == linkId
|
||||||
|
? _value.linkId
|
||||||
|
: linkId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
platform: null == platform
|
||||||
|
? _value.platform
|
||||||
|
: platform // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
show: null == show
|
||||||
|
? _value.show
|
||||||
|
: show // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
uniqueId: freezed == uniqueId
|
||||||
|
? _value.uniqueId
|
||||||
|
: uniqueId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
country: freezed == country
|
||||||
|
? _value.country
|
||||||
|
: country // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
url: freezed == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
nativeAppUriMobile: freezed == nativeAppUriMobile
|
||||||
|
? _value.nativeAppUriMobile
|
||||||
|
: nativeAppUriMobile // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
nativeAppUriDesktop: freezed == nativeAppUriDesktop
|
||||||
|
? _value.nativeAppUriDesktop
|
||||||
|
: nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SongLinkImplCopyWith<$Res>
|
||||||
|
implements $SongLinkCopyWith<$Res> {
|
||||||
|
factory _$$SongLinkImplCopyWith(
|
||||||
|
_$SongLinkImpl value, $Res Function(_$SongLinkImpl) then) =
|
||||||
|
__$$SongLinkImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{String displayName,
|
||||||
|
String linkId,
|
||||||
|
String platform,
|
||||||
|
bool show,
|
||||||
|
String? uniqueId,
|
||||||
|
String? country,
|
||||||
|
String? url,
|
||||||
|
String? nativeAppUriMobile,
|
||||||
|
String? nativeAppUriDesktop});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SongLinkImplCopyWithImpl<$Res>
|
||||||
|
extends _$SongLinkCopyWithImpl<$Res, _$SongLinkImpl>
|
||||||
|
implements _$$SongLinkImplCopyWith<$Res> {
|
||||||
|
__$$SongLinkImplCopyWithImpl(
|
||||||
|
_$SongLinkImpl _value, $Res Function(_$SongLinkImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? displayName = null,
|
||||||
|
Object? linkId = null,
|
||||||
|
Object? platform = null,
|
||||||
|
Object? show = null,
|
||||||
|
Object? uniqueId = freezed,
|
||||||
|
Object? country = freezed,
|
||||||
|
Object? url = freezed,
|
||||||
|
Object? nativeAppUriMobile = freezed,
|
||||||
|
Object? nativeAppUriDesktop = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$SongLinkImpl(
|
||||||
|
displayName: null == displayName
|
||||||
|
? _value.displayName
|
||||||
|
: displayName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
linkId: null == linkId
|
||||||
|
? _value.linkId
|
||||||
|
: linkId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
platform: null == platform
|
||||||
|
? _value.platform
|
||||||
|
: platform // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
show: null == show
|
||||||
|
? _value.show
|
||||||
|
: show // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
|
uniqueId: freezed == uniqueId
|
||||||
|
? _value.uniqueId
|
||||||
|
: uniqueId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
country: freezed == country
|
||||||
|
? _value.country
|
||||||
|
: country // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
url: freezed == url
|
||||||
|
? _value.url
|
||||||
|
: url // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
nativeAppUriMobile: freezed == nativeAppUriMobile
|
||||||
|
? _value.nativeAppUriMobile
|
||||||
|
: nativeAppUriMobile // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
nativeAppUriDesktop: freezed == nativeAppUriDesktop
|
||||||
|
? _value.nativeAppUriDesktop
|
||||||
|
: nativeAppUriDesktop // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SongLinkImpl implements _SongLink {
|
||||||
|
const _$SongLinkImpl(
|
||||||
|
{required this.displayName,
|
||||||
|
required this.linkId,
|
||||||
|
required this.platform,
|
||||||
|
required this.show,
|
||||||
|
required this.uniqueId,
|
||||||
|
required this.country,
|
||||||
|
required this.url,
|
||||||
|
required this.nativeAppUriMobile,
|
||||||
|
required this.nativeAppUriDesktop});
|
||||||
|
|
||||||
|
factory _$SongLinkImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SongLinkImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String displayName;
|
||||||
|
@override
|
||||||
|
final String linkId;
|
||||||
|
@override
|
||||||
|
final String platform;
|
||||||
|
@override
|
||||||
|
final bool show;
|
||||||
|
@override
|
||||||
|
final String? uniqueId;
|
||||||
|
@override
|
||||||
|
final String? country;
|
||||||
|
@override
|
||||||
|
final String? url;
|
||||||
|
@override
|
||||||
|
final String? nativeAppUriMobile;
|
||||||
|
@override
|
||||||
|
final String? nativeAppUriDesktop;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SongLink(displayName: $displayName, linkId: $linkId, platform: $platform, show: $show, uniqueId: $uniqueId, country: $country, url: $url, nativeAppUriMobile: $nativeAppUriMobile, nativeAppUriDesktop: $nativeAppUriDesktop)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SongLinkImpl &&
|
||||||
|
(identical(other.displayName, displayName) ||
|
||||||
|
other.displayName == displayName) &&
|
||||||
|
(identical(other.linkId, linkId) || other.linkId == linkId) &&
|
||||||
|
(identical(other.platform, platform) ||
|
||||||
|
other.platform == platform) &&
|
||||||
|
(identical(other.show, show) || other.show == show) &&
|
||||||
|
(identical(other.uniqueId, uniqueId) ||
|
||||||
|
other.uniqueId == uniqueId) &&
|
||||||
|
(identical(other.country, country) || other.country == country) &&
|
||||||
|
(identical(other.url, url) || other.url == url) &&
|
||||||
|
(identical(other.nativeAppUriMobile, nativeAppUriMobile) ||
|
||||||
|
other.nativeAppUriMobile == nativeAppUriMobile) &&
|
||||||
|
(identical(other.nativeAppUriDesktop, nativeAppUriDesktop) ||
|
||||||
|
other.nativeAppUriDesktop == nativeAppUriDesktop));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
||||||
|
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
|
||||||
|
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||||
|
__$$SongLinkImplCopyWithImpl<_$SongLinkImpl>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SongLinkImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SongLink implements SongLink {
|
||||||
|
const factory _SongLink(
|
||||||
|
{required final String displayName,
|
||||||
|
required final String linkId,
|
||||||
|
required final String platform,
|
||||||
|
required final bool show,
|
||||||
|
required final String? uniqueId,
|
||||||
|
required final String? country,
|
||||||
|
required final String? url,
|
||||||
|
required final String? nativeAppUriMobile,
|
||||||
|
required final String? nativeAppUriDesktop}) = _$SongLinkImpl;
|
||||||
|
|
||||||
|
factory _SongLink.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SongLinkImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get displayName;
|
||||||
|
@override
|
||||||
|
String get linkId;
|
||||||
|
@override
|
||||||
|
String get platform;
|
||||||
|
@override
|
||||||
|
bool get show;
|
||||||
|
@override
|
||||||
|
String? get uniqueId;
|
||||||
|
@override
|
||||||
|
String? get country;
|
||||||
|
@override
|
||||||
|
String? get url;
|
||||||
|
@override
|
||||||
|
String? get nativeAppUriMobile;
|
||||||
|
@override
|
||||||
|
String? get nativeAppUriDesktop;
|
||||||
|
@override
|
||||||
|
@JsonKey(ignore: true)
|
||||||
|
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
33
lib/services/song_link/song_link.g.dart
Normal file
33
lib/services/song_link/song_link.g.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'song_link.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_$SongLinkImpl _$$SongLinkImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SongLinkImpl(
|
||||||
|
displayName: json['displayName'] as String,
|
||||||
|
linkId: json['linkId'] as String,
|
||||||
|
platform: json['platform'] as String,
|
||||||
|
show: json['show'] as bool,
|
||||||
|
uniqueId: json['uniqueId'] as String?,
|
||||||
|
country: json['country'] as String?,
|
||||||
|
url: json['url'] as String?,
|
||||||
|
nativeAppUriMobile: json['nativeAppUriMobile'] as String?,
|
||||||
|
nativeAppUriDesktop: json['nativeAppUriDesktop'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SongLinkImplToJson(_$SongLinkImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'displayName': instance.displayName,
|
||||||
|
'linkId': instance.linkId,
|
||||||
|
'platform': instance.platform,
|
||||||
|
'show': instance.show,
|
||||||
|
'uniqueId': instance.uniqueId,
|
||||||
|
'country': instance.country,
|
||||||
|
'url': instance.url,
|
||||||
|
'nativeAppUriMobile': instance.nativeAppUriMobile,
|
||||||
|
'nativeAppUriDesktop': instance.nativeAppUriDesktop,
|
||||||
|
};
|
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/source_match.dart';
|
import 'package:spotube/models/source_match.dart';
|
||||||
|
import 'package:spotube/services/song_link/song_link.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
@ -216,6 +217,20 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
|
final links = await SongLinkService.links(track.id!);
|
||||||
|
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
||||||
|
|
||||||
|
if (ytLink?.url != null) {
|
||||||
|
return [
|
||||||
|
await toSiblingType(
|
||||||
|
0,
|
||||||
|
YoutubeVideoInfo.fromVideo(
|
||||||
|
await youtubeClient.videos.get(ytLink!.url!),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
final searchResults = await youtubeClient.search.search(
|
final searchResults = await youtubeClient.search.search(
|
||||||
|
43
lib/songlink.dart
Normal file
43
lib/songlink.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
|
||||||
|
void main(List<String> args) async {
|
||||||
|
final dio = Dio();
|
||||||
|
|
||||||
|
final spotifyId = args[0];
|
||||||
|
|
||||||
|
print("Fetching song link for $spotifyId");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw Exception("Could not find __NEXT_DATA__ script tag.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final pageProps = jsonDecode(script) as Map<String, dynamic>;
|
||||||
|
final songLinks =
|
||||||
|
pageProps["props"]["pageProps"]["pageData"]["sections"].firstWhere(
|
||||||
|
(section) => section["sectionId"] == "section|auto|links|listen",
|
||||||
|
)["links"];
|
||||||
|
|
||||||
|
for (final link in songLinks) {
|
||||||
|
print("${link["platform"]} - ${link["url"]}");
|
||||||
|
}
|
||||||
|
}
|
@ -153,6 +153,7 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/
|
- assets/
|
||||||
- assets/tutorial/
|
- assets/tutorial/
|
||||||
|
- assets/logos/
|
||||||
- LICENSE
|
- LICENSE
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"bn": [
|
"bn": [
|
||||||
@ -18,7 +19,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ca": [
|
"ca": [
|
||||||
@ -29,7 +31,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
@ -40,7 +43,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
@ -51,7 +55,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fa": [
|
"fa": [
|
||||||
@ -62,7 +67,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
@ -73,7 +79,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hi": [
|
"hi": [
|
||||||
@ -84,7 +91,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
@ -95,7 +103,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
@ -106,7 +115,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ne": [
|
"ne": [
|
||||||
@ -117,7 +127,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"nl": [
|
"nl": [
|
||||||
@ -129,7 +140,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
@ -140,7 +152,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
@ -151,7 +164,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
@ -162,7 +176,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
@ -173,7 +188,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"uk": [
|
"uk": [
|
||||||
@ -184,7 +200,8 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
@ -195,6 +212,7 @@
|
|||||||
"endless_playback",
|
"endless_playback",
|
||||||
"delete_playlist",
|
"delete_playlist",
|
||||||
"delete_playlist_confirmation",
|
"delete_playlist_confirmation",
|
||||||
"local_tracks"
|
"local_tracks",
|
||||||
|
"song_link"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user