mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
replaced mpv audio backed with just_audio
Player works as previous except shuffling Youtube Search is handled through youtube_explode now
This commit is contained in:
parent
f9d3d58398
commit
07b1891cb4
@ -1,15 +1,11 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mpv_dart/mpv_dart.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:spotube/provider/PlayerDI.dart';
|
||||
|
||||
class TitleBarActionButtons extends StatelessWidget {
|
||||
const TitleBarActionButtons({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
MPVPlayer player = context.watch<PlayerDI>().player;
|
||||
return Row(
|
||||
children: [
|
||||
TextButton(
|
||||
@ -32,7 +28,6 @@ class TitleBarActionButtons extends StatelessWidget {
|
||||
child: const Icon(Icons.crop_square_rounded)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
player.stop();
|
||||
appWindow.close();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
@ -1,16 +1,16 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/PlayerControls.dart';
|
||||
import 'package:spotube/helpers/artist-to-string.dart';
|
||||
import 'package:spotube/helpers/search-youtube.dart';
|
||||
import 'package:spotube/models/GlobalKeyActions.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mpv_dart/mpv_dart.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:spotube/provider/PlayerDI.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class Player extends StatefulWidget {
|
||||
@ -20,66 +20,70 @@ class Player extends StatefulWidget {
|
||||
_PlayerState createState() => _PlayerState();
|
||||
}
|
||||
|
||||
class _PlayerState extends State<Player> {
|
||||
class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
late AudioPlayer player;
|
||||
bool _isPlaying = false;
|
||||
bool _shuffled = false;
|
||||
double _duration = 0;
|
||||
Duration? _duration;
|
||||
|
||||
String? _currentPlaylistId;
|
||||
String? _currentTrackId;
|
||||
|
||||
double _volume = 0;
|
||||
|
||||
List<HotKey> _hotKeys = [];
|
||||
final List<HotKey> _hotKeys = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
player = AudioPlayer();
|
||||
WidgetsBinding.instance?.addObserver(this);
|
||||
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
|
||||
try {
|
||||
MPVPlayer player = context.read<PlayerDI>().player;
|
||||
if (!player.isRunning()) {
|
||||
await player.start();
|
||||
}
|
||||
double volume = await player.getProperty<double>("volume");
|
||||
setState(() {
|
||||
_volume = volume / 100;
|
||||
_volume = player.volume;
|
||||
});
|
||||
player.on(MPVEvents.paused, null, (ev, context) {
|
||||
player.playingStream.listen((playing) async {
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
_isPlaying = playing;
|
||||
});
|
||||
});
|
||||
|
||||
player.on(MPVEvents.resumed, null, (ev, context) {
|
||||
player.durationStream.listen((duration) async {
|
||||
if (duration != null) {
|
||||
// Actually things doesn't work all the time as they were
|
||||
// described. So instead of listening to a `playback.ready`
|
||||
// stream, it has to listen to duration stream since duration
|
||||
// is always added to the Stream sink after all icyMetadata has
|
||||
// been loaded thus indicating buffering started
|
||||
if (duration != Duration.zero && duration != _duration) {
|
||||
// this line is for prev/next or already playing playlist
|
||||
if (player.playing) await player.pause();
|
||||
await player.play();
|
||||
}
|
||||
setState(() {
|
||||
_isPlaying = true;
|
||||
_duration = duration;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
player.on(MPVEvents.status, null, (ev, _) async {
|
||||
Map data = ev.eventData as Map;
|
||||
Playback playback = context.read<Playback>();
|
||||
player.processingStateStream.listen((event) async {
|
||||
try {
|
||||
if (event == ProcessingState.completed && _currentTrackId != null) {
|
||||
_movePlaylistPositionBy(1);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[PrecessingStateStreamListener] $e");
|
||||
print(stack);
|
||||
}
|
||||
});
|
||||
|
||||
if (data["property"] == "media-title" && data["value"] != null) {
|
||||
var track = playback.currentPlaylist?.tracks.where((track) {
|
||||
String title = data["value"];
|
||||
return title.contains(track.name!) &&
|
||||
track.artists!
|
||||
.every((artist) => title.contains(artist.name!));
|
||||
});
|
||||
if (track != null && track.isNotEmpty) {
|
||||
setState(() {
|
||||
_isPlaying = true;
|
||||
});
|
||||
playback.setCurrentTrack = track.first;
|
||||
}
|
||||
}
|
||||
if (data["property"] == "duration" && data["value"] != null) {
|
||||
setState(() {
|
||||
_duration = data["value"];
|
||||
});
|
||||
}
|
||||
playOrPause(key) async {
|
||||
try {
|
||||
_isPlaying ? await player.pause() : await player.play();
|
||||
} catch (e, stack) {
|
||||
print("[PlayPauseShortcut] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
List<GlobalKeyActions> keyWithActions = [
|
||||
@ -92,17 +96,17 @@ class _PlayerState extends State<Player> {
|
||||
playOrPause,
|
||||
),
|
||||
GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async {
|
||||
await player.next();
|
||||
_movePlaylistPositionBy(1);
|
||||
}),
|
||||
GlobalKeyActions(HotKey(KeyCode.mediaTrackPrevious), (key) async {
|
||||
await player.prev();
|
||||
_movePlaylistPositionBy(-1);
|
||||
}),
|
||||
GlobalKeyActions(HotKey(KeyCode.mediaStop), (key) async {
|
||||
await player.stop();
|
||||
Playback playback = context.read<Playback>();
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
_currentPlaylistId = null;
|
||||
_duration = 0;
|
||||
_currentTrackId = null;
|
||||
_duration = null;
|
||||
_shuffled = false;
|
||||
});
|
||||
playback.reset();
|
||||
@ -117,72 +121,99 @@ class _PlayerState extends State<Player> {
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print("[PLAYER]: $e");
|
||||
}
|
||||
print("[Player.initState()]: $e");
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
MPVPlayer player = context.read<PlayerDI>().player;
|
||||
player.removeAllByEvent(MPVEvents.paused);
|
||||
player.removeAllByEvent(MPVEvents.resumed);
|
||||
player.removeAllByEvent(MPVEvents.status);
|
||||
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
|
||||
void dispose() {
|
||||
WidgetsBinding.instance?.removeObserver(this);
|
||||
player.dispose();
|
||||
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e)));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String playlistToStr(CurrentPlaylist playlist) {
|
||||
var tracks = playlist.tracks.map((track) {
|
||||
var artists = artistsToString(track.artists ?? []);
|
||||
var title = track.name?.replaceAll("-", " ");
|
||||
|
||||
return "ytdl://ytsearch:$artists - $title";
|
||||
}).toList();
|
||||
|
||||
return tracks.join("\n");
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused) {
|
||||
// Release the player's resources when not in use. We use "stop" so that
|
||||
// if the app resumes later, it will still remember what position to
|
||||
// resume from.
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Future playPlaylist(MPVPlayer player, CurrentPlaylist playlist) async {
|
||||
try {
|
||||
if (player.isRunning() && playlist.id != _currentPlaylistId) {
|
||||
var playlistPath = "/tmp/playlist-${playlist.id}.txt";
|
||||
File file = File(playlistPath);
|
||||
var newPlaylist = playlistToStr(playlist);
|
||||
void _movePlaylistPositionBy(int pos) {
|
||||
Playback playback = context.read<Playback>();
|
||||
if (playback.currentTrack != null && playback.currentPlaylist != null) {
|
||||
int index = playback.currentPlaylist!.trackIds
|
||||
.indexOf(playback.currentTrack!.id!) +
|
||||
pos;
|
||||
|
||||
if (!await file.exists()) {
|
||||
await file.create();
|
||||
}
|
||||
|
||||
await file.writeAsString(newPlaylist);
|
||||
|
||||
await player.loadPlaylist(playlistPath);
|
||||
var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1
|
||||
? 0
|
||||
: index < 0
|
||||
? playback.currentPlaylist!.trackIds.length
|
||||
: index;
|
||||
Track? track =
|
||||
playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
||||
? playback.currentPlaylist!.tracks.elementAt(safeIndex)
|
||||
: null;
|
||||
if (track != null) {
|
||||
playback.setCurrentTrack = track;
|
||||
setState(() {
|
||||
_currentPlaylistId = playlist.id;
|
||||
_shuffled = false;
|
||||
_duration = null;
|
||||
});
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print("[Player]: $e");
|
||||
print(stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future _playTrack(Track currentTrack, Playback playback) async {
|
||||
try {
|
||||
if (currentTrack.id != _currentTrackId) {
|
||||
if (currentTrack.uri != null) {
|
||||
await player
|
||||
.setAudioSource(
|
||||
AudioSource.uri(Uri.parse(currentTrack.uri!)),
|
||||
preload: true,
|
||||
)
|
||||
.then((value) async {
|
||||
setState(() {
|
||||
_currentTrackId = currentTrack.id;
|
||||
if (_duration != null) {
|
||||
_duration = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
var ytTrack = await toYoutubeTrack(currentTrack);
|
||||
if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) {
|
||||
await player
|
||||
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
|
||||
.then((value) {
|
||||
setState(() {
|
||||
_currentTrackId = currentTrack.id;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[Player._playTrack()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
MPVPlayer player = context.watch<PlayerDI>().player;
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: Consumer<Playback>(
|
||||
builder: (context, playback, widget) {
|
||||
if (playback.currentPlaylist != null) {
|
||||
playPlaylist(player, playback.currentPlaylist!);
|
||||
if (playback.currentPlaylist != null &&
|
||||
playback.currentTrack != null) {
|
||||
_playTrack(playback.currentTrack!, playback);
|
||||
}
|
||||
|
||||
String? albumArt = playback.currentTrack?.album?.images?.last.url;
|
||||
@ -223,33 +254,89 @@ class _PlayerState extends State<Player> {
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: PlayerControls(
|
||||
player: player,
|
||||
positionStream: player.positionStream,
|
||||
isPlaying: _isPlaying,
|
||||
duration: _duration,
|
||||
duration: _duration ?? Duration.zero,
|
||||
shuffled: _shuffled,
|
||||
onShuffle: () {
|
||||
onNext: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onNext()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onPrevious: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(-1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPrevious()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onPause: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPause()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onPlay: () async {
|
||||
try {
|
||||
await player.play();
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPlay()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onSeek: (value) async {
|
||||
try {
|
||||
await player.seek(Duration(seconds: value.toInt()));
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onSeek()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onShuffle: () async {
|
||||
try {
|
||||
if (!_shuffled) {
|
||||
player.shuffle().then(
|
||||
await player.setShuffleModeEnabled(true).then(
|
||||
(value) => setState(() {
|
||||
_shuffled = true;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
player.unshuffle().then(
|
||||
await player.setShuffleModeEnabled(false).then(
|
||||
(value) => setState(() {
|
||||
_shuffled = false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onShuffle()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onStop: () {
|
||||
onStop: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
_currentPlaylistId = null;
|
||||
_duration = 0;
|
||||
_currentTrackId = null;
|
||||
_duration = null;
|
||||
_shuffled = false;
|
||||
});
|
||||
playback.reset();
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onStop()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -289,12 +376,17 @@ class _PlayerState extends State<Player> {
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: Slider.adaptive(
|
||||
value: _volume,
|
||||
onChanged: (value) {
|
||||
player.volume(value * 100).then((_) {
|
||||
onChanged: (value) async {
|
||||
try {
|
||||
await player.setVolume(value).then((_) {
|
||||
setState(() {
|
||||
_volume = value;
|
||||
});
|
||||
});
|
||||
} catch (e, stack) {
|
||||
print("[VolumeSlider.onChange()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -1,22 +1,32 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mpv_dart/mpv_dart.dart';
|
||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
|
||||
class PlayerControls extends StatefulWidget {
|
||||
final MPVPlayer player;
|
||||
final Stream<Duration> positionStream;
|
||||
final bool isPlaying;
|
||||
final double duration;
|
||||
final Duration duration;
|
||||
final bool shuffled;
|
||||
final Function? onStop;
|
||||
final Function? onShuffle;
|
||||
final Function(double value)? onSeek;
|
||||
final Function? onNext;
|
||||
final Function? onPrevious;
|
||||
final Function? onPlay;
|
||||
final Function? onPause;
|
||||
const PlayerControls({
|
||||
required this.player,
|
||||
required this.positionStream,
|
||||
required this.isPlaying,
|
||||
required this.duration,
|
||||
required this.shuffled,
|
||||
this.onShuffle,
|
||||
this.onStop,
|
||||
this.onSeek,
|
||||
this.onNext,
|
||||
this.onPrevious,
|
||||
this.onPlay,
|
||||
this.onPause,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -25,56 +35,49 @@ class PlayerControls extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PlayerControlsState extends State<PlayerControls> {
|
||||
double currentPos = 0;
|
||||
StreamSubscription? _timePositionListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.player.on(MPVEvents.timeposition, null, (ev, context) {
|
||||
widget.player.getPercentPosition().then((value) {
|
||||
setState(() {
|
||||
currentPos = value / 100;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.player.removeAllByEvent(MPVEvents.timeposition);
|
||||
void dispose() async {
|
||||
await _timePositionListener?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var totalDuration = Duration(seconds: widget.duration.toInt());
|
||||
var totalMinutes = zeroPadNumStr(totalDuration.inMinutes.remainder(60));
|
||||
var totalSeconds = zeroPadNumStr(totalDuration.inSeconds.remainder(60));
|
||||
|
||||
var currentDuration =
|
||||
Duration(seconds: (widget.duration * currentPos).toInt());
|
||||
|
||||
var currentMinutes = zeroPadNumStr(currentDuration.inMinutes.remainder(60));
|
||||
var currentSeconds = zeroPadNumStr(currentDuration.inSeconds.remainder(60));
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 700),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
StreamBuilder<Duration>(
|
||||
stream: widget.positionStream,
|
||||
builder: (context, snapshot) {
|
||||
var totalMinutes =
|
||||
zeroPadNumStr(widget.duration.inMinutes.remainder(60));
|
||||
var totalSeconds =
|
||||
zeroPadNumStr(widget.duration.inSeconds.remainder(60));
|
||||
var currentMinutes = snapshot.hasData
|
||||
? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60))
|
||||
: "00";
|
||||
var currentSeconds = snapshot.hasData
|
||||
? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60))
|
||||
: "00";
|
||||
|
||||
var sliderMax = widget.duration.inSeconds;
|
||||
var sliderValue = snapshot.data?.inSeconds ?? 0;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Slider.adaptive(
|
||||
value: currentPos,
|
||||
onChanged: (value) async {
|
||||
try {
|
||||
setState(() {
|
||||
currentPos = value;
|
||||
});
|
||||
await widget.player.goToPosition(value * widget.duration);
|
||||
} catch (e) {
|
||||
print("[PlayerControls]: $e");
|
||||
}
|
||||
// cannot divide by zero
|
||||
// there's an edge case for value being bigger
|
||||
// than total duration. Keeping it resolved
|
||||
value: (sliderMax == 0 || sliderValue > sliderMax)
|
||||
? 0
|
||||
: sliderValue / sliderMax,
|
||||
onChanged: (value) {},
|
||||
onChangeEnd: (value) {
|
||||
widget.onSeek?.call(value * sliderMax);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -82,7 +85,8 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
"$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds",
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
@ -95,13 +99,8 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_previous_rounded),
|
||||
onPressed: () async {
|
||||
bool moved = await widget.player.prev();
|
||||
if (moved) {
|
||||
setState(() {
|
||||
currentPos = 0;
|
||||
});
|
||||
}
|
||||
onPressed: () {
|
||||
widget.onPrevious?.call();
|
||||
}),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
@ -109,30 +108,17 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
widget.isPlaying
|
||||
? await widget.player.pause()
|
||||
: await widget.player.play();
|
||||
? widget.onPause?.call()
|
||||
: widget.onPlay?.call();
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
onPressed: () async {
|
||||
bool moved = await widget.player.next();
|
||||
if (moved) {
|
||||
setState(() {
|
||||
currentPos = 0;
|
||||
});
|
||||
}
|
||||
}),
|
||||
onPressed: () => widget.onNext?.call()),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.stop_rounded),
|
||||
onPressed: () async {
|
||||
await widget.player.stop();
|
||||
widget.onStop?.call();
|
||||
setState(() {
|
||||
currentPos = 0;
|
||||
});
|
||||
},
|
||||
onPressed: () => widget.onStop?.call(),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -87,6 +87,7 @@ class _PlaylistCardState extends State<PlaylistCard> {
|
||||
name: widget.playlist.name!,
|
||||
thumbnail: widget.playlist.images!.first.url!,
|
||||
);
|
||||
playback.setCurrentTrack = tracks.first;
|
||||
},
|
||||
child: Icon(
|
||||
isPlaylistPlaying
|
||||
|
@ -155,6 +155,7 @@ class _PlaylistViewState extends State<PlaylistView> {
|
||||
.images![0]
|
||||
.url!,
|
||||
);
|
||||
playback.setCurrentTrack = tracks.first;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
|
24
lib/helpers/search-youtube.dart
Normal file
24
lib/helpers/search-youtube.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
YoutubeExplode youtube = YoutubeExplode();
|
||||
Future<Track> toYoutubeTrack(Track track) async {
|
||||
var artistsName = track.artists?.map((ar) => ar.name).toList() ?? [];
|
||||
String queryString =
|
||||
"${artistsName.first} - ${track.name}${artistsName.length > 1 ? " feat. ${artistsName.sublist(1).join(" ")}" : ""}";
|
||||
|
||||
SearchList videos = await youtube.search.getVideos(queryString);
|
||||
|
||||
List<Video> matchedVideos = videos.where((video) {
|
||||
return video.title.contains(track.name!) &&
|
||||
(track.artists?.every((artist) => video.title.contains(artist.name!)) ??
|
||||
false);
|
||||
}).toList();
|
||||
|
||||
Video ytVideo = matchedVideos.isNotEmpty ? matchedVideos.first : videos.first;
|
||||
|
||||
var trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
|
||||
|
||||
track.uri = trackManifest.audioOnly.first.url.toString();
|
||||
return track;
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:mpv_dart/mpv_dart.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -9,18 +8,16 @@ import 'package:spotube/components/Home.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/PlayerDI.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
|
||||
void main() async {
|
||||
// Must add this line.
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// For hot reload, `unregisterAll()` needs to be called.
|
||||
await hotKeyManager.unregisterAll();
|
||||
runApp(MyApp());
|
||||
doWhenWindowReady(() {
|
||||
appWindow.minSize = const Size(900, 700);
|
||||
appWindow.size = const Size(900, 700);
|
||||
appWindow.alignment = Alignment.center;
|
||||
appWindow.maximize();
|
||||
appWindow.show();
|
||||
@ -67,15 +64,6 @@ class MyApp extends StatelessWidget {
|
||||
);
|
||||
}),
|
||||
ChangeNotifierProvider<Playback>(create: (context) => Playback()),
|
||||
ChangeNotifierProvider<PlayerDI>(
|
||||
create: (context) => PlayerDI(MPVPlayer(
|
||||
audioOnly: true,
|
||||
mpvArgs: [
|
||||
"--ytdl-raw-options-set=format=140,http-chunk-size=300000",
|
||||
"--script-opts=ytdl_hook-ytdl_path=yt-dlp",
|
||||
],
|
||||
)),
|
||||
),
|
||||
ChangeNotifierProvider<UserPreferences>(
|
||||
create: (context) {
|
||||
return UserPreferences();
|
||||
|
@ -12,6 +12,8 @@ class CurrentPlaylist {
|
||||
required String this.name,
|
||||
required String this.thumbnail,
|
||||
});
|
||||
|
||||
List<String> get trackIds => tracks.map((e) => e.id!).toList();
|
||||
}
|
||||
|
||||
class Playback extends ChangeNotifier {
|
||||
@ -40,6 +42,22 @@ class Playback extends ChangeNotifier {
|
||||
_currentTrack = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// sets the provided id matched track's uri\
|
||||
/// Doesn't notify listeners\
|
||||
/// @returns `bool` - `true` if succeed & `false` when failed
|
||||
bool setTrackUriById(String id, String uri) {
|
||||
if (_currentPlaylist == null) return false;
|
||||
try {
|
||||
int index =
|
||||
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
|
||||
if (index == -1) return false;
|
||||
_currentPlaylist!.tracks[index].uri = uri;
|
||||
return _currentPlaylist!.tracks[index].uri == uri;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var x = Playback();
|
||||
|
@ -1,15 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:mpv_dart/mpv_dart.dart';
|
||||
|
||||
class PlayerDI extends ChangeNotifier {
|
||||
MPVPlayer _player;
|
||||
|
||||
PlayerDI(this._player);
|
||||
|
||||
get player => _player;
|
||||
|
||||
setPlayer(MPVPlayer player) {
|
||||
_player = player;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||
#include <hotkey_manager/hotkey_manager_plugin.h>
|
||||
#include <libwinmedia/libwinmedia_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) hotkey_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin");
|
||||
hotkey_manager_plugin_register_with_registrar(hotkey_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) libwinmedia_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "LibwinmediaPlugin");
|
||||
libwinmedia_plugin_register_with_registrar(libwinmedia_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_linux
|
||||
hotkey_manager
|
||||
libwinmedia
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
56
pubspec.lock
56
pubspec.lock
@ -29,6 +29,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
audio_session:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_session
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.6+1"
|
||||
bitsdojo_window:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -141,13 +148,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
eventify:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: eventify
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -275,6 +275,41 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.4.0"
|
||||
just_audio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.18"
|
||||
just_audio_libwinmedia:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio_libwinmedia
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.4"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
just_audio_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
libwinmedia:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: libwinmedia
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.7"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -296,13 +331,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
mpv_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: mpv_dart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1"
|
||||
msix:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -42,10 +42,11 @@ dependencies:
|
||||
spotify: ^0.6.0
|
||||
url_launcher: ^6.0.17
|
||||
youtube_explode_dart: ^1.10.8
|
||||
mpv_dart: ^0.0.1
|
||||
infinite_scroll_pagination: ^3.1.0
|
||||
bitsdojo_window: ^0.1.1+1
|
||||
hotkey_manager: ^0.1.6
|
||||
just_audio: ^0.9.18
|
||||
just_audio_libwinmedia: ^0.0.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <hotkey_manager/hotkey_manager_plugin.h>
|
||||
#include <libwinmedia/libwinmedia_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||
HotkeyManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("HotkeyManagerPlugin"));
|
||||
LibwinmediaPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LibwinmediaPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_windows
|
||||
hotkey_manager
|
||||
libwinmedia
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user