mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
fix: downloaded tracks are not tagged with metadata
This commit is contained in:
parent
700a69fcd1
commit
3209c75144
@ -202,7 +202,6 @@ If you are curious, you can [read the reason of choosing this license](https://d
|
||||
1. [Invidious](https://invidious.io/) - Invidious is an open source alternative front-end to YouTube.
|
||||
1. [yt-dlp](https://github.com/yt-dlp/yt-dlp) - A feature-rich command-line audio/video downloader.
|
||||
1. [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) - NewPipe's core library for extracting data from streaming sites.
|
||||
1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
|
||||
1. [LRCLib](https://lrclib.net/) - A public synced lyric API.
|
||||
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
|
||||
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.6 KiB |
@ -66,6 +66,19 @@ class $AssetsImagesGen {
|
||||
];
|
||||
}
|
||||
|
||||
class $AssetsPluginsGen {
|
||||
const $AssetsPluginsGen();
|
||||
|
||||
/// Directory path: assets/plugins/spotube-plugin-musicbrainz-listenbrainz
|
||||
$AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen
|
||||
get spotubePluginMusicbrainzListenbrainz =>
|
||||
const $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen();
|
||||
|
||||
/// Directory path: assets/plugins/spotube-plugin-youtube-audio
|
||||
$AssetsPluginsSpotubePluginYoutubeAudioGen get spotubePluginYoutubeAudio =>
|
||||
const $AssetsPluginsSpotubePluginYoutubeAudioGen();
|
||||
}
|
||||
|
||||
class $AssetsImagesLogosGen {
|
||||
const $AssetsImagesLogosGen();
|
||||
|
||||
@ -81,13 +94,30 @@ class $AssetsImagesLogosGen {
|
||||
AssetGenImage get jiosaavn =>
|
||||
const AssetGenImage('assets/images/logos/jiosaavn.png');
|
||||
|
||||
/// File path: assets/images/logos/songlink-transparent.png
|
||||
AssetGenImage get songlinkTransparent =>
|
||||
const AssetGenImage('assets/images/logos/songlink-transparent.png');
|
||||
/// List of all assets
|
||||
List<AssetGenImage> get values => [dabMusic, invidious, jiosaavn];
|
||||
}
|
||||
|
||||
class $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen {
|
||||
const $AssetsPluginsSpotubePluginMusicbrainzListenbrainzGen();
|
||||
|
||||
/// File path: assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug
|
||||
String get plugin =>
|
||||
'assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug';
|
||||
|
||||
/// List of all assets
|
||||
List<AssetGenImage> get values =>
|
||||
[dabMusic, invidious, jiosaavn, songlinkTransparent];
|
||||
List<String> get values => [plugin];
|
||||
}
|
||||
|
||||
class $AssetsPluginsSpotubePluginYoutubeAudioGen {
|
||||
const $AssetsPluginsSpotubePluginYoutubeAudioGen();
|
||||
|
||||
/// File path: assets/plugins/spotube-plugin-youtube-audio/plugin.smplug
|
||||
String get plugin =>
|
||||
'assets/plugins/spotube-plugin-youtube-audio/plugin.smplug';
|
||||
|
||||
/// List of all assets
|
||||
List<String> get values => [plugin];
|
||||
}
|
||||
|
||||
class Assets {
|
||||
@ -96,6 +126,7 @@ class Assets {
|
||||
static const String license = 'LICENSE';
|
||||
static const $AssetsBrandingGen branding = $AssetsBrandingGen();
|
||||
static const $AssetsImagesGen images = $AssetsImagesGen();
|
||||
static const $AssetsPluginsGen plugins = $AssetsPluginsGen();
|
||||
|
||||
/// List of all assets
|
||||
static List<String> get values => [license];
|
||||
|
||||
@ -4,7 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/ui/button_tile.dart';
|
||||
@ -36,7 +35,6 @@ class TrackOptions extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final ThemeData(:colorScheme) = Theme.of(context);
|
||||
|
||||
final trackOptionActions = ref.watch(trackOptionActionsProvider(track));
|
||||
final (
|
||||
@ -260,24 +258,6 @@ class TrackOptions extends HookConsumerWidget {
|
||||
leading: const Icon(SpotubeIcons.share),
|
||||
title: Text(context.l10n.share),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
onPressed: () async {
|
||||
await trackOptionActions.action(
|
||||
rootNavigatorKey.currentContext!,
|
||||
TrackOptionValue.songlink,
|
||||
playlistId,
|
||||
);
|
||||
onTapItem?.call();
|
||||
},
|
||||
leading: Assets.images.logos.songlinkTransparent.image(
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: colorScheme.foreground.withValues(alpha: 0.5),
|
||||
),
|
||||
title: Text(context.l10n.song_link),
|
||||
),
|
||||
if (!isLocalTrack)
|
||||
ButtonTile(
|
||||
style: ButtonVariance.menu,
|
||||
|
||||
@ -4143,8 +4143,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
late final $PluginsTableTable pluginsTable = $PluginsTableTable(this);
|
||||
late final Index uniqueBlacklist = Index('unique_blacklist',
|
||||
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
||||
late final Index uniqTrackMatch = Index('uniq_track_match',
|
||||
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)');
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@ -4160,8 +4158,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
historyTable,
|
||||
lyricsTable,
|
||||
pluginsTable,
|
||||
uniqueBlacklist,
|
||||
uniqTrackMatch
|
||||
uniqueBlacklist
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,8 @@ enum SpotubeMediaCompressionType {
|
||||
@Freezed(unionKey: 'type')
|
||||
class SpotubeAudioSourceContainerPreset
|
||||
with _$SpotubeAudioSourceContainerPreset {
|
||||
const SpotubeAudioSourceContainerPreset._();
|
||||
|
||||
@FreezedUnionValue("lossy")
|
||||
factory SpotubeAudioSourceContainerPreset.lossy({
|
||||
required SpotubeMediaCompressionType type,
|
||||
@ -27,6 +29,14 @@ class SpotubeAudioSourceContainerPreset
|
||||
factory SpotubeAudioSourceContainerPreset.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SpotubeAudioSourceContainerPresetFromJson(json);
|
||||
|
||||
String getFileExtension() {
|
||||
return switch (name) {
|
||||
"mp4" => "m4a",
|
||||
"webm" => "weba",
|
||||
_ => name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@ -197,12 +197,13 @@ class __$$SpotubeAudioSourceContainerPresetLossyImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SpotubeAudioSourceContainerPresetLossyImpl
|
||||
implements SpotubeAudioSourceContainerPresetLossy {
|
||||
extends SpotubeAudioSourceContainerPresetLossy {
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl(
|
||||
{required this.type,
|
||||
required this.name,
|
||||
required final List<SpotubeAudioLossyContainerQuality> qualities})
|
||||
: _qualities = qualities;
|
||||
: _qualities = qualities,
|
||||
super._();
|
||||
|
||||
factory _$SpotubeAudioSourceContainerPresetLossyImpl.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
@ -338,12 +339,13 @@ class _$SpotubeAudioSourceContainerPresetLossyImpl
|
||||
}
|
||||
|
||||
abstract class SpotubeAudioSourceContainerPresetLossy
|
||||
implements SpotubeAudioSourceContainerPreset {
|
||||
extends SpotubeAudioSourceContainerPreset {
|
||||
factory SpotubeAudioSourceContainerPresetLossy(
|
||||
{required final SpotubeMediaCompressionType type,
|
||||
required final String name,
|
||||
required final List<SpotubeAudioLossyContainerQuality> qualities}) =
|
||||
_$SpotubeAudioSourceContainerPresetLossyImpl;
|
||||
SpotubeAudioSourceContainerPresetLossy._() : super._();
|
||||
|
||||
factory SpotubeAudioSourceContainerPresetLossy.fromJson(
|
||||
Map<String, dynamic> json) =
|
||||
@ -419,12 +421,13 @@ class __$$SpotubeAudioSourceContainerPresetLosslessImplCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SpotubeAudioSourceContainerPresetLosslessImpl
|
||||
implements SpotubeAudioSourceContainerPresetLossless {
|
||||
extends SpotubeAudioSourceContainerPresetLossless {
|
||||
_$SpotubeAudioSourceContainerPresetLosslessImpl(
|
||||
{required this.type,
|
||||
required this.name,
|
||||
required final List<SpotubeAudioLosslessContainerQuality> qualities})
|
||||
: _qualities = qualities;
|
||||
: _qualities = qualities,
|
||||
super._();
|
||||
|
||||
factory _$SpotubeAudioSourceContainerPresetLosslessImpl.fromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
@ -561,12 +564,13 @@ class _$SpotubeAudioSourceContainerPresetLosslessImpl
|
||||
}
|
||||
|
||||
abstract class SpotubeAudioSourceContainerPresetLossless
|
||||
implements SpotubeAudioSourceContainerPreset {
|
||||
extends SpotubeAudioSourceContainerPreset {
|
||||
factory SpotubeAudioSourceContainerPresetLossless(
|
||||
{required final SpotubeMediaCompressionType type,
|
||||
required final String name,
|
||||
required final List<SpotubeAudioLosslessContainerQuality>
|
||||
qualities}) = _$SpotubeAudioSourceContainerPresetLosslessImpl;
|
||||
SpotubeAudioSourceContainerPresetLossless._() : super._();
|
||||
|
||||
factory SpotubeAudioSourceContainerPresetLossless.fromJson(
|
||||
Map<String, dynamic> json) =
|
||||
|
||||
@ -16,7 +16,7 @@ class UserDownloadsPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final downloadManager = ref.watch(downloadManagerProvider);
|
||||
|
||||
final history = downloadManager.$backHistory;
|
||||
final history = downloadManager.$history;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -48,7 +48,7 @@ class UserDownloadsPage extends HookConsumerWidget {
|
||||
child: ListView.builder(
|
||||
itemCount: history.length,
|
||||
itemBuilder: (context, index) {
|
||||
return DownloadItem(track: history.elementAt(index));
|
||||
return DownloadItem(track: history.elementAt(index).query);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@ -346,36 +346,41 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
controller: controller,
|
||||
child: Skeletonizer(
|
||||
enabled: trackSnapshot.isLoading,
|
||||
child: ListView.builder(
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: trackSnapshot.isLoading
|
||||
? 5
|
||||
: filteredTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (trackSnapshot.isLoading) {
|
||||
return TrackTile(
|
||||
playlist: playlist,
|
||||
track: FakeData.track,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
slivers: [
|
||||
SliverList.builder(
|
||||
itemCount: trackSnapshot.isLoading
|
||||
? 5
|
||||
: filteredTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (trackSnapshot.isLoading) {
|
||||
return TrackTile(
|
||||
playlist: playlist,
|
||||
track: FakeData.track,
|
||||
index: index,
|
||||
);
|
||||
}
|
||||
|
||||
final track = filteredTracks[index];
|
||||
return TrackTile(
|
||||
index: index,
|
||||
playlist: playlist,
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
onTap: () async {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
final track = filteredTracks[index];
|
||||
return TrackTile(
|
||||
index: index,
|
||||
playlist: playlist,
|
||||
track: track,
|
||||
userPlaylist: false,
|
||||
onTap: () async {
|
||||
await playLocalTracks(
|
||||
ref,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(200),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -398,7 +403,7 @@ class LocalLibraryPage extends HookConsumerWidget {
|
||||
error: (error, stackTrace) =>
|
||||
Text(error.toString() + stackTrace.toString()),
|
||||
);
|
||||
})
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -19,7 +19,6 @@ import 'package:spotube/utils/service_utils.dart';
|
||||
class DownloadManagerProvider extends ChangeNotifier {
|
||||
DownloadManagerProvider({required this.ref})
|
||||
: $history = <SourcedTrack>{},
|
||||
$backHistory = <SpotubeFullTrackObject>{},
|
||||
dl = DownloadManager() {
|
||||
dl.statusStream.listen((event) async {
|
||||
try {
|
||||
@ -28,14 +27,13 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
final sourcedTrack = $history.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.getUrlOfQuality(
|
||||
downloadContainer, downloadQualityIndex) ==
|
||||
downloadContainer,
|
||||
downloadQualityIndex,
|
||||
) ==
|
||||
request.url,
|
||||
);
|
||||
|
||||
if (sourcedTrack == null) return;
|
||||
final track = $backHistory.firstWhereOrNull(
|
||||
(element) => element.id == sourcedTrack.query.id,
|
||||
);
|
||||
if (track == null) return;
|
||||
|
||||
final savePath = getTrackFileUrl(sourcedTrack);
|
||||
// related to onFileExists
|
||||
@ -47,12 +45,12 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
await oldFile.exists()) {
|
||||
await oldFile.rename(savePath);
|
||||
}
|
||||
|
||||
if (status != DownloadStatus.completed ||
|
||||
//? WebA audiotagging is not supported yet
|
||||
//? Although in future by converting weba to opus & then tagging it
|
||||
//? is possible using vorbis comments
|
||||
downloadContainer.name == "weba" ||
|
||||
downloadContainer.name == "webm") {
|
||||
downloadContainer.getFileExtension() == "weba") {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -63,13 +61,13 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final imageBytes = await ServiceUtils.downloadImage(
|
||||
(track.album.images).asUrlString(
|
||||
(sourcedTrack.query.album.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
index: 1,
|
||||
),
|
||||
);
|
||||
|
||||
final metadata = track.toMetadata(
|
||||
final metadata = sourcedTrack.query.toMetadata(
|
||||
fileLength: await file.length(),
|
||||
imageBytes: imageBytes,
|
||||
);
|
||||
@ -111,17 +109,16 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
|
||||
final Set<SourcedTrack> $history;
|
||||
// these are the tracks which metadata hasn't been fetched yet
|
||||
final Set<SpotubeFullTrackObject> $backHistory;
|
||||
final DownloadManager dl;
|
||||
|
||||
String getTrackFileUrl(SourcedTrack track) {
|
||||
final name =
|
||||
"${track.query.name} - ${track.query.artists.join(", ")}.${downloadContainer.name}";
|
||||
"${track.query.name} - ${track.query.artists.map((e) => e.name).join(", ")}.${downloadContainer.getFileExtension()}";
|
||||
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
|
||||
}
|
||||
|
||||
bool isActive(SpotubeFullTrackObject track) {
|
||||
if ($backHistory.contains(track)) return true;
|
||||
if ($history.any((e) => e.query.id == track.id)) return true;
|
||||
|
||||
final sourcedTrack = $history.firstWhereOrNull(
|
||||
(element) => element.query.id == track.id,
|
||||
@ -146,9 +143,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
|
||||
/// For singular downloads
|
||||
Future<void> addToQueue(SpotubeFullTrackObject track) async {
|
||||
final sourcedTrack = await ref.read(
|
||||
sourcedTrackProvider(track).future,
|
||||
);
|
||||
final sourcedTrack = await ref.read(sourcedTrackProvider(track).future);
|
||||
|
||||
final savePath = getTrackFileUrl(sourcedTrack);
|
||||
|
||||
@ -161,35 +156,17 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
await oldFile.rename("$savePath.old");
|
||||
}
|
||||
|
||||
if (sourcedTrack.qualityPreset == downloadContainer) {
|
||||
final downloadTask = await dl.addDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!,
|
||||
savePath,
|
||||
);
|
||||
if (downloadTask != null) {
|
||||
$history.add(sourcedTrack);
|
||||
}
|
||||
} else {
|
||||
$backHistory.add(track);
|
||||
final sourcedTrack =
|
||||
await ref.read(sourcedTrackProvider(track).future).then((d) {
|
||||
$backHistory.remove(track);
|
||||
return d;
|
||||
});
|
||||
final downloadTask = await dl.addDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!,
|
||||
savePath,
|
||||
);
|
||||
if (downloadTask != null) {
|
||||
$history.add(sourcedTrack);
|
||||
}
|
||||
final downloadTask = await dl.addDownload(
|
||||
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!,
|
||||
savePath,
|
||||
);
|
||||
if (downloadTask != null) {
|
||||
$history.add(sourcedTrack);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> batchAddToQueue(List<SpotubeFullTrackObject> tracks) async {
|
||||
$backHistory.addAll(tracks);
|
||||
notifyListeners();
|
||||
for (final track in tracks) {
|
||||
try {
|
||||
|
||||
@ -48,7 +48,7 @@ class ServerPlaybackRoutes {
|
||||
return join(
|
||||
await UserPreferencesNotifier.getMusicCacheDir(),
|
||||
ServiceUtils.sanitizeFilename(
|
||||
'${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.name}',
|
||||
'${track.query.name} - ${track.query.artists.map((d) => d.name).join(",")} (${track.info.id}).${track.qualityPreset!.getFileExtension()}',
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -263,8 +263,7 @@ class ServerPlaybackRoutes {
|
||||
}
|
||||
|
||||
if (contentRange.total == fileLength &&
|
||||
track.qualityPreset!.name != "webm" ||
|
||||
track.qualityPreset!.name != "weba") {
|
||||
track.qualityPreset!.getFileExtension() != "weba") {
|
||||
final playlistTrack = playlist.tracks.firstWhereOrNull(
|
||||
(element) => element.id == track.query.id,
|
||||
);
|
||||
|
||||
@ -21,12 +21,10 @@ import 'package:spotube/provider/metadata_plugin/library/playlists.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/library/tracks.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
enum TrackOptionValue {
|
||||
album,
|
||||
share,
|
||||
songlink,
|
||||
addToPlaylist,
|
||||
addToQueue,
|
||||
removeFromPlaylist,
|
||||
@ -237,10 +235,6 @@ class TrackOptionsActions {
|
||||
case TrackOptionValue.share:
|
||||
actionShare(context);
|
||||
break;
|
||||
case TrackOptionValue.songlink:
|
||||
final url = "https://song.link/s/${track.id}";
|
||||
await launchUrlString(url);
|
||||
break;
|
||||
case TrackOptionValue.details:
|
||||
if (track is! SpotubeFullTrackObject) break;
|
||||
showDialog(
|
||||
@ -252,8 +246,8 @@ class TrackOptionsActions {
|
||||
);
|
||||
break;
|
||||
case TrackOptionValue.download:
|
||||
if (track is! SpotubeFullTrackObject) break;
|
||||
await downloadManager.addToQueue(track as SpotubeFullTrackObject);
|
||||
if (track is SpotubeLocalTrackObject) break;
|
||||
downloadManager.addToQueue(track as SpotubeFullTrackObject);
|
||||
break;
|
||||
case TrackOptionValue.startRadio:
|
||||
actionStartRadio(context);
|
||||
|
||||
@ -37,7 +37,7 @@ class SpotubeMedia extends mk.Media {
|
||||
|
||||
factory SpotubeMedia.media(Media media) {
|
||||
assert(media.extras != null, "[Media] must have extra metadata set");
|
||||
return SpotubeMedia(SpotubeFullTrackObject.fromJson(media.extras!));
|
||||
return SpotubeMedia(SpotubeTrackObject.fromJson(media.extras!));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
library song_link;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
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 final dio = Dio();
|
||||
static Future<List<SongLink>> links(String spotifyId) async {
|
||||
try {
|
||||
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() ??
|
||||
<SongLink>[];
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.reportError(e, stackTrace);
|
||||
return <SongLink>[];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,333 +0,0 @@
|
||||
// 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#adding-getters-and-methods-to-our-models');
|
||||
|
||||
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;
|
||||
|
||||
/// Serializes this SongLink to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$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;
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@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);
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@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(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, displayName, linkId, platform,
|
||||
show, uniqueId, country, url, nativeAppUriMobile, nativeAppUriDesktop);
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@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;
|
||||
|
||||
/// Create a copy of SongLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SongLinkImplCopyWith<_$SongLinkImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'song_link.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SongLinkImpl _$$SongLinkImplFromJson(Map 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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user