lyrics not working bugfix

Download track feature complete support in Android
Android sudden solid color screen
inno-script updated for permission_handler
gensums updated for support for android apk
updated description of Spotube in every file
changes added to CHANGELOG
This commit is contained in:
Kingkor Roy Tirtho 2022-03-24 13:30:59 +06:00
parent 0cf5bfea50
commit 45f9d08595
24 changed files with 240 additions and 82 deletions

View File

@ -29,6 +29,7 @@ jobs:
- run: make appimage - run: make appimage
# Building Android Application # Building Android Application
- run: flutter build apk - run: flutter build apk
- run: mv build/app/outputs/apk/release/app-release.apk build/Spotube-android-arm64-v8a.apk
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
name: Spotube-Linux-Bundle name: Spotube-Linux-Bundle
@ -36,7 +37,7 @@ jobs:
build/Spotube-linux-x86_64.deb build/Spotube-linux-x86_64.deb
build/Spotube-linux-x86_64.tar.xz build/Spotube-linux-x86_64.tar.xz
build/Spotube-*-x86_64.AppImage build/Spotube-*-x86_64.AppImage
build/app/outputs/apk/release/app-release.apk build/Spotube-android-arm64-v8a.apk
build_windows: build_windows:
runs-on: windows-latest runs-on: windows-latest
steps: steps:

View File

@ -1,3 +1,30 @@
# v2.0.0
### New
- Android Support https://github.com/KRTirtho/spotube/issues/24
- Responsive UI (Mobile, Tablet)
- Anonymous/Guest Account
- Mini floating player
- Full page PlayerView for smaller devices
- Horizontal CategoryCard Scroll & pagination for quicker access to Playlists
- Bottom bar for smaller devices
- Collapsed Sidebar for medium sized devices
- Persists Volume level
- Android NavigationPanel controls (OS media controls of Android)
### Improved
- Search - now scrolls & paginates for Playlists & Albums
- Authentication - allows guest accounts making authentication optional
- Lyrics - can be fetched without requiring GeniusAccessToken. This makes geniusAccessToken optional
- UI snappiness & faster load times
- Simpler logic, faster calculations & better caching (flutter_hooks)
- shared state management - uses riverpod & hooks combination
### Bug fixes
- Can't play any song in macos https://github.com/KRTirtho/spotube/issues/23
- Downloaded tracks can't be played as they're WebAudio (.weba) instead of MP3
- delay while changing Playlist/Single tracks
# v1.2.0 # v1.2.0
### New ### New

View File

@ -27,7 +27,9 @@ Spotube is a [Flutter](https://flutter.dev) based lightweight spotify client. It
Following are the features that currently spotube offers: Following are the features that currently spotube offers:
- Open Source - Open Source
- No telementry, diagnostics or user data collection - Anonymous/Guest Login
- Cross platform
- No telemetry, diagnostics or user data collection
- Lightweight & resource friendly - Lightweight & resource friendly
- Native performance (Thanks to Flutter+Skia) - Native performance (Thanks to Flutter+Skia)
- Playback control is on user's machine instead of server based - Playback control is on user's machine instead of server based
@ -36,7 +38,7 @@ Following are the features that currently spotube offers:
- Lyrics - Lyrics
- Downloadable track - Downloadable track
<a href="https://www.producthunt.com/posts/spotube?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-spotube" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=327965&theme=dark" alt="Spotube - A lightweight+free Spotify desktop-client made with flutter | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/spotube?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-spotube" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=327965&theme=dark" alt="Spotube - A lightweight+free Spotify crossplatform-client made with flutter | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# Installation # Installation
@ -44,6 +46,10 @@ I'm always releasing newer versions of binary of the software each 2-3 month wit
All the binaries are located in the [releases](https://github.com/krtirtho/spotube/releases), just download All the binaries are located in the [releases](https://github.com/krtirtho/spotube/releases), just download
## Android
Download the [Android app](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-android-arm64-v8a.apk) & install it on your phone/tablet
## Windows ## Windows
Download the [setup file](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-windows-x86_64-setup.exe) & follow along the installer Download the [setup file](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-windows-x86_64-setup.exe) & follow along the installer
@ -98,9 +104,10 @@ Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube
**I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat/chocolatey/homebrew stores or software centers or repositories** **I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat/chocolatey/homebrew stores or software centers or repositories**
# Configuration
There are some configurations that needs to be done to start using this software ## Optional Configurations
<details>
<summary>Login with <b>Spotify</b></summary>
You need a spotify account & a developer app for You need a spotify account & a developer app for
@ -120,8 +127,10 @@ You need a spotify account & a developer app for
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields - Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields
![step-4](https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png) ![step-4](https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png)
</details>
**Setup Genius Lyrics (Optional)** <details>
<summary>Setup <b>Genius Lyrics</b></summary>
- Signup/Login into [genius](https://genius.com/signup) for **lyrics** - Signup/Login into [genius](https://genius.com/signup) for **lyrics**
- Go To [Genius Developer Portal](https://genius.com/api-clients/new) for creating an API client - Go To [Genius Developer Portal](https://genius.com/api-clients/new) for creating an API client
@ -132,6 +141,7 @@ You need a spotify account & a developer app for
![Step 4](https://user-images.githubusercontent.com/61944859/158823984-17f08534-5c92-41bc-918a-23194aad00f5.png) ![Step 4](https://user-images.githubusercontent.com/61944859/158823984-17f08534-5c92-41bc-918a-23194aad00f5.png)
> **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself > **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
</details>
# TODO: # TODO:
@ -140,6 +150,7 @@ You need a spotify account & a developer app for
- [x] Track download - [x] Track download
- [ ] Support for playing/streaming podcasts/shows - [ ] Support for playing/streaming podcasts/shows
- [x] Artist, User & Album pages - [x] Artist, User & Album pages
- [x] Android Support
# Building from source # Building from source
@ -179,6 +190,16 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour
- [bitsdojo_window](https://github.com/bitsdojo/bitsdojo_window) - A Flutter package that makes it easy to customize and work with your Flutter desktop app window on Windows, macOS and Linux - [bitsdojo_window](https://github.com/bitsdojo/bitsdojo_window) - A Flutter package that makes it easy to customize and work with your Flutter desktop app window on Windows, macOS and Linux
- [hotkey_manager](https://github.com/leanflutter/hotkey_manager) - A flutter plugin that allow Flutter desktop apps to defines system/inapp wide hotkey - [hotkey_manager](https://github.com/leanflutter/hotkey_manager) - A flutter plugin that allow Flutter desktop apps to defines system/inapp wide hotkey
- [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan - [Inno Setup](https://jrsoftware.org/isinfo.php) - Inno Setup is a free installer for Windows programs by Jordan Russell and Martijn Laan
- [collection](https://github.com/dart-lang/collection) - The collection package for Dart contains a number of separate libraries with utility functions and classes that makes working with collections easier
- [flutter_riverpod](https://riverpod.dev/) - A Reactive Caching and Data-binding Framework
- [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - React hooks for Flutter. Hooks are a new kind of object that manages a Widget life-cycles. They are used to increase code sharing between widgets and as a complete replacement for StatefulWidget
- [hooks_riverpod](https://riverpod.dev/) - Riverpod with hooks
- [go_router](https://github.com/flutter/packages/tree/main/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
- [palette_generator](https://github.com/flutter/packages/tree/main/packages/palette_generator) - Flutter package for generating palette colors from a source image.
- [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour.
- [logger](https://github.com/leisim/logger) - Small, easy to use and extensible logger which prints beautiful logs
- [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
- [permission_handler](https://github.com/baseflow/flutter-permission-handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
# Social handlers # Social handlers

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion 31
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8

View File

@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<queries> <queries>
<!-- If your app opens https URLs --> <!-- If your app opens https URLs -->
@ -12,7 +14,7 @@
</intent> </intent>
</queries> </queries>
<application android:label="Spotube" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true"> <application android:label="Spotube" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true">
<activity android:name="com.ryanheise.audioservice.AudioServiceActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> <activity android:name="com.ryanheise.audioservice.AudioServiceActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user

View File

@ -1,5 +1,5 @@
pkgbase = spotube-bin pkgbase = spotube-bin
pkgdesc = A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed pkgdesc = A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed
pkgver = 1.2.0 pkgver = 1.2.0
pkgrel = 2 pkgrel = 2
url = https://github.com/KRTirtho/spotube/ url = https://github.com/KRTirtho/spotube/

View File

@ -3,7 +3,7 @@ pkgname=spotube-bin
pkgver=1.2.0 pkgver=1.2.0
pkgrel=2 pkgrel=2
epoch= epoch=
pkgdesc="A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed" pkgdesc="A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed"
arch=(x86_64) arch=(x86_64)
url="https://github.com/KRTirtho/spotube/" url="https://github.com/KRTirtho/spotube/"
license=('BSD-4-Clause') license=('BSD-4-Clause')

View File

@ -22,7 +22,7 @@
<docsUrl>https://github.com/KRTirtho/spotube#readme</docsUrl> <docsUrl>https://github.com/KRTirtho/spotube#readme</docsUrl>
<bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl> <bugTrackerUrl>https://github.com/KRTirtho/spotube/issues/new</bugTrackerUrl>
<tags>spotube music audio spotify youtube flutter</tags> <tags>spotube music audio spotify youtube flutter</tags>
<summary>A lightweight free Spotify 🎧 desktop-client 🖥 which handles playback manually, streams music using Youtube &amp; no Spotify premium account is needed 😱</summary> <summary>A lightweight free Spotify 🎧 crossplatform-client 🖥📱 which handles playback manually, streams music using Youtube &amp; no Spotify premium account is needed 😱</summary>
<description> <description>
Spotube is a Flutter based lightweight spotify client. It utilizes the power Spotube is a Flutter based lightweight spotify client. It utilizes the power
of Spotify &amp; Youtube's public API &amp; creates a hazardless, performant &amp; resource of Spotify &amp; Youtube's public API &amp; creates a hazardless, performant &amp; resource

View File

@ -7,5 +7,5 @@ Essential: no
Installed-Size: 24400 Installed-Size: 24400
Depends: libkeybinder-3.0-0 (>= 0.3.2) Depends: libkeybinder-3.0-0 (>= 0.3.2)
Maintainer: Kingkor Roy Tirtho <krtirtho@gmail.com> Maintainer: Kingkor Roy Tirtho <krtirtho@gmail.com>
Description: A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed Description: A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed
Homepage: https://github.com/KRTirtho/spotube Homepage: https://github.com/KRTirtho/spotube

View File

@ -15,12 +15,11 @@ class Lyrics extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
UserPreferences userPreferences = ref.watch(userPreferencesProvider); UserPreferences userPreferences = ref.watch(userPreferencesProvider);
var lyrics = useState({}); final lyrics = useState({});
bool hasToken = userPreferences.geniusAccessToken.isNotEmpty; final lyricsFuture = useMemoized(() {
var lyricsFuture = useMemoized(() {
if (playback.currentTrack == null || if (playback.currentTrack == null ||
!hasToken || userPreferences.geniusAccessToken.isEmpty ||
(playback.currentTrack?.id != null && (playback.currentTrack?.id != null &&
playback.currentTrack?.id == lyrics.value["id"])) { playback.currentTrack?.id == lyrics.value["id"])) {
return null; return null;
@ -31,9 +30,9 @@ class Lyrics extends HookConsumerWidget {
apiKey: userPreferences.geniusAccessToken, apiKey: userPreferences.geniusAccessToken,
optimizeQuery: true, optimizeQuery: true,
); );
}, [playback.currentTrack]); }, [playback.currentTrack, userPreferences.geniusAccessToken]);
var lyricsSnapshot = useFuture(lyricsFuture); final lyricsSnapshot = useFuture(lyricsFuture);
useEffect(() { useEffect(() {
if (lyricsSnapshot.hasData && if (lyricsSnapshot.hasData &&

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -40,9 +41,16 @@ class Player extends HookConsumerWidget {
/// [disposeAllPlayers] method which is throwing /// [disposeAllPlayers] method which is throwing
/// [UnimplementedException] in the [PlatformInterface] /// [UnimplementedException] in the [PlatformInterface]
/// implementation /// implementation
if (Platform.isAndroid || Platform.isIOS) {
playback.audioSession playback.audioSession
?.setActive(true) ?.setActive(true)
.then((_) => player.setAsset("assets/warmer.mp3")); .then((_) => player.setAsset("assets/warmer.mp3"))
.catchError((e) {
logger.e("useEffect", e, StackTrace.current);
});
} else {
player.setAsset("assets/warmer.mp3");
}
return null; return null;
}, []); }, []);
@ -64,7 +72,7 @@ class Player extends HookConsumerWidget {
final entryRef = useRef<OverlayEntry?>(null); final entryRef = useRef<OverlayEntry?>(null);
disposeOverlay() { void disposeOverlay() {
try { try {
entryRef.value?.remove(); entryRef.value?.remove();
entryRef.value = null; entryRef.value = null;
@ -76,25 +84,37 @@ class Player extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
// clearing the overlay-entry as passing the already available
// entry will result in splashing while resizing the window
if (entryRef.value != null) disposeOverlay();
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) {
entryRef.value = OverlayEntry(
opaque: false,
builder: (context) => PlayerOverlay(albumArt: albumArt),
);
// I can't believe useEffect doesn't run Post Frame aka // I can't believe useEffect doesn't run Post Frame aka
// after rendering/painting the UI // after rendering/painting the UI
// `My disappointment is immeasurable and my day is ruined` XD // `My disappointment is immeasurable and my day is ruined` XD
WidgetsBinding.instance?.addPostFrameCallback((time) { WidgetsBinding.instance?.addPostFrameCallback((time) {
// clearing the overlay-entry as passing the already available
// entry will result in splashing while resizing the window
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
entryRef.value == null &&
playback.currentTrack != null) {
entryRef.value = OverlayEntry(
opaque: false,
builder: (context) => PlayerOverlay(albumArt: albumArt),
);
try {
Overlay.of(context)?.insert(entryRef.value!);
} catch (e) {
if (e is AssertionError &&
e.message ==
'The specified entry is already present in the Overlay.') {
disposeOverlay();
Overlay.of(context)?.insert(entryRef.value!); Overlay.of(context)?.insert(entryRef.value!);
});
} }
}
} else {
disposeOverlay();
}
});
return () { return () {
disposeOverlay(); disposeOverlay();
}; };
}, [breakpoint]); }, [breakpoint, playback.currentTrack]);
// returning an empty non spacious Container as the overlay will take // returning an empty non spacious Container as the overlay will take
// place in the global overlay stack aka [_entries] // place in the global overlay stack aka [_entries]

View File

@ -66,14 +66,17 @@ class Settings extends HookConsumerWidget {
? () async { ? () async {
SharedPreferences localStorage = SharedPreferences localStorage =
await SharedPreferences.getInstance(); await SharedPreferences.getInstance();
if (geniusAccessToken.value != null &&
geniusAccessToken.value!.isNotEmpty) {
preferences.setGeniusAccessToken( preferences.setGeniusAccessToken(
geniusAccessToken.value ?? ""); geniusAccessToken.value!,
);
localStorage.setString( localStorage.setString(
LocalStorageKeys.geniusAccessToken, LocalStorageKeys.geniusAccessToken,
geniusAccessToken.value ?? ""); geniusAccessToken.value!);
}
geniusAccessToken.value = null; geniusAccessToken.value = null;
textEditingController.text = ""; textEditingController.text = "";
} }
: null, : null,
@ -141,12 +144,12 @@ class Settings extends HookConsumerWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
if (auth.isAnonymous) if (auth.isAnonymous)
Wrap( Wrap(
spacing: 20,
runSpacing: 20,
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
const Text("Login with your Spotify account"), const Text("Login with your Spotify account"),
const SizedBox(width: 20),
ElevatedButton( ElevatedButton(
child: Text("Connect with Spotify".toUpperCase()), child: Text("Connect with Spotify".toUpperCase()),
onPressed: () { onPressed: () {

View File

@ -1,10 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class AnonymousFallback extends StatelessWidget { class AnonymousFallback extends StatelessWidget {
const AnonymousFallback({Key? key}) : super(key: key); const AnonymousFallback({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Center(child: Text("You're not logged in")); return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("You're not logged in"),
const SizedBox(height: 10),
ElevatedButton(
child: const Text("Login with Spotify"),
onPressed: () => GoRouter.of(context).push("/settings"),
)
],
),
);
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:spotube/helpers/artist-to-string.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:path_provider/path_provider.dart' as path_provider; import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
enum TrackStatus { downloading, idle, done } enum TrackStatus { downloading, idle, done }
@ -21,16 +22,31 @@ class DownloadTrackButton extends HookWidget {
var _downloadTrack = useCallback(() async { var _downloadTrack = useCallback(() async {
if (track == null) return; if (track == null) return;
if ((Platform.isAndroid || Platform.isIOS) &&
!await Permission.storage.isGranted &&
!await Permission.storage.isPermanentlyDenied) {
final status = await Permission.storage.request();
if (!status.isGranted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Couldn't download track. Not enough permissions"),
),
);
return;
}
}
StreamManifest manifest = StreamManifest manifest =
await yt.videos.streamsClient.getManifest(track?.href); await yt.videos.streamsClient.getManifest(track?.href);
var audioStream = yt.videos.streamsClient.get( final audioStream = yt.videos.streamsClient
.get(
manifest.audioOnly manifest.audioOnly
.where((audio) => audio.codec.mimeType == "audio/mp4") .where((audio) => audio.codec.mimeType == "audio/mp4")
.withHighestBitrate(), .withHighestBitrate(),
); )
.asBroadcastStream();
var statusCb = audioStream.listen( final statusCb = audioStream.listen(
(event) { (event) {
if (status.value != TrackStatus.downloading) { if (status.value != TrackStatus.downloading) {
status.value = TrackStatus.downloading; status.value = TrackStatus.downloading;
@ -50,7 +66,10 @@ class DownloadTrackButton extends HookWidget {
); );
String downloadFolder = path.join( String downloadFolder = path.join(
(await path_provider.getDownloadsDirectory())!.path, "Spotube"); Platform.isAndroid
? "/storage/emulated/0/Download"
: (await path_provider.getDownloadsDirectory())!.path,
"Spotube");
String fileName = String fileName =
"${track?.name} - ${artistsToString<Artist>(track?.artists ?? [])}.mp3"; "${track?.name} - ${artistsToString<Artist>(track?.artists ?? [])}.mp3";
File outputFile = File(path.join(downloadFolder, fileName)); File outputFile = File(path.join(downloadFolder, fileName));

View File

@ -88,7 +88,7 @@ Future<List?> searchSong(
Future<String?> getLyrics( Future<String?> getLyrics(
String title, String title,
String artist, { String artist, {
String apiKey = "", required String apiKey,
bool optimizeQuery = false, bool optimizeQuery = false,
bool authHeader = false, bool authHeader = false,
}) async { }) async {

View File

@ -107,8 +107,15 @@ class Playback extends ChangeNotifier {
_processingStateStreamListener = _processingStateStreamListener =
player.processingStateStream.listen((event) async { player.processingStateStream.listen((event) async {
try { try {
if (event == ProcessingState.completed && _currentTrack?.id != null) { if (event != ProcessingState.completed) return;
if (_currentTrack?.id != null) {
movePlaylistPositionBy(1); movePlaylistPositionBy(1);
} else {
await audioSession?.setActive(false);
_isPlaying = false;
_duration = null;
_callAllDurationListeners(null);
notifyListeners();
} }
} catch (e, stack) { } catch (e, stack) {
_logger.e("PrecessingStateStreamListener", e, stack); _logger.e("PrecessingStateStreamListener", e, stack);
@ -167,6 +174,7 @@ class Playback extends ChangeNotifier {
_callAllDurationListeners(null); _callAllDurationListeners(null);
_currentPlaylist = null; _currentPlaylist = null;
_currentTrack = null; _currentTrack = null;
_audioSession?.setActive(false);
notifyListeners(); notifyListeners();
} }
@ -196,7 +204,7 @@ class Playback extends ChangeNotifier {
super.dispose(); super.dispose();
} }
movePlaylistPositionBy(int pos) { void movePlaylistPositionBy(int pos) {
if (_currentTrack != null && _currentPlaylist != null) { if (_currentTrack != null && _currentPlaylist != null) {
int index = _currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos; int index = _currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos;

View File

@ -4,8 +4,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/helpers/get-random-element.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/models/generated_secrets.dart';
class UserPreferences extends ChangeNotifier { class UserPreferences extends ChangeNotifier {
String geniusAccessToken; String geniusAccessToken;
@ -33,15 +35,17 @@ class UserPreferences extends ChangeNotifier {
} }
return HotKey.fromJson(json); return HotKey.fromJson(json);
} }
return null;
} }
onInit() async { Future<void> onInit() async {
try { try {
SharedPreferences localStorage = await SharedPreferences.getInstance(); SharedPreferences localStorage = await SharedPreferences.getInstance();
String? accessToken = String? accessToken =
localStorage.getString(LocalStorageKeys.geniusAccessToken); localStorage.getString(LocalStorageKeys.geniusAccessToken);
geniusAccessToken = accessToken != null && accessToken.isNotEmpty
if (accessToken != null) geniusAccessToken = accessToken; ? accessToken
: getRandomElement(lyricsSecrets);
nextTrackHotKey ??= (await _getHotKeyFromLocalStorage( nextTrackHotKey ??= (await _getHotKeyFromLocalStorage(
localStorage, localStorage,
@ -75,12 +79,12 @@ class UserPreferences extends ChangeNotifier {
} }
} }
setGeniusAccessToken(String token) { void setGeniusAccessToken(String token) {
geniusAccessToken = token; geniusAccessToken = token;
notifyListeners(); notifyListeners();
} }
setNextTrackHotKey(HotKey? value) { void setNextTrackHotKey(HotKey? value) {
nextTrackHotKey = value; nextTrackHotKey = value;
SharedPreferences.getInstance().then((preferences) { SharedPreferences.getInstance().then((preferences) {
preferences.setString( preferences.setString(
@ -91,7 +95,7 @@ class UserPreferences extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
setPrevTrackHotKey(HotKey? value) { void setPrevTrackHotKey(HotKey? value) {
prevTrackHotKey = value; prevTrackHotKey = value;
SharedPreferences.getInstance().then((preferences) { SharedPreferences.getInstance().then((preferences) {
preferences.setString( preferences.setString(
@ -102,7 +106,7 @@ class UserPreferences extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
setPlayPauseHotKey(HotKey? value) { void setPlayPauseHotKey(HotKey? value) {
playPauseHotKey = value; playPauseHotKey = value;
SharedPreferences.getInstance().then((preferences) { SharedPreferences.getInstance().then((preferences) {
preferences.setString( preferences.setString(
@ -114,5 +118,5 @@ class UserPreferences extends ChangeNotifier {
} }
} }
var userPreferencesProvider = final userPreferencesProvider =
ChangeNotifierProvider((_) => UserPreferences(geniusAccessToken: "")); ChangeNotifierProvider((_) => UserPreferences(geniusAccessToken: ""));

View File

@ -3,7 +3,7 @@
<id>com.github.KRTirtho.Spotube</id> <id>com.github.KRTirtho.Spotube</id>
<name>Spotube</name> <name>Spotube</name>
<summary> <summary>
A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube &amp; no Spotify premium account is needed A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube &amp; no Spotify premium account is needed
</summary> </summary>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>BSD-4-Clause</project_license> <project_license>BSD-4-Clause</project_license>

View File

@ -506,6 +506,41 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.11.1" version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "9.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.2+1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.3"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.7.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:

View File

@ -1,5 +1,5 @@
name: spotube name: spotube
description: A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed description: A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
@ -56,6 +56,7 @@ dependencies:
audio_session: ^0.1.6+1 audio_session: ^0.1.6+1
just_audio_background: ^0.0.1-beta.5 just_audio_background: ^0.0.1-beta.5
logger: ^1.1.0 logger: ^1.1.0
permission_handler: ^9.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -1,4 +1,4 @@
#!/bin/env bash #!/bin/env bash
md5sum build/**/*.{AppImage,deb,tar.xz,dmg,exe,nupkg} >build/RELEASE.md5sum md5sum build/**/*.{AppImage,deb,tar.xz,dmg,exe,nupkg,apk} >build/RELEASE.md5sum
sha256sum build/**/*.{AppImage,deb,tar.xz,dmg,exe,nupkg} >build/RELEASE.sha256sum sha256sum build/**/*.{AppImage,deb,tar.xz,dmg,exe,nupkg,apk} >build/RELEASE.sha256sum
sed -i 's|build/Spotube-.*-Bundle/||' build/RELEASE.sha256sum build/RELEASE.md5sum sed -i 's|build/Spotube-.*-Bundle/||' build/RELEASE.sha256sum build/RELEASE.md5sum

View File

@ -10,7 +10,7 @@
[Setup] [Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{80B901C8-D6FE-494E-8AF7-A2BD440E8644} AppId={80B901C8-D6FE-494E-8AF7-A2BD440E8644}
AppName={#MyAppName} AppName={#MyAppName}
AppVersion={#MyAppVersion} AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion} ;AppVerName={#MyAppName} {#MyAppVersion}
@ -43,6 +43,7 @@ Source: "..\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}";
Source: "..\build\windows\runner\Release\hotkey_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "..\build\windows\runner\Release\hotkey_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\build\windows\runner\Release\libwinmedia.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "..\build\windows\runner\Release\libwinmedia.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\build\windows\runner\Release\libwinmedia_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "..\build\windows\runner\Release\libwinmedia_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\build\windows\runner\Release\permission_handler_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\build\windows\runner\Release\spotube.exp"; DestDir: "{app}"; Flags: ignoreversion Source: "..\build\windows\runner\Release\spotube.exp"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\build\windows\runner\Release\spotube.lib"; DestDir: "{app}"; Flags: ignoreversion Source: "..\build\windows\runner\Release\spotube.lib"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "..\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion

View File

@ -9,6 +9,7 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <hotkey_manager/hotkey_manager_plugin.h> #include <hotkey_manager/hotkey_manager_plugin.h>
#include <libwinmedia/libwinmedia_plugin.h> #include <libwinmedia/libwinmedia_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("HotkeyManagerPlugin")); registry->GetRegistrarForPlugin("HotkeyManagerPlugin"));
LibwinmediaPluginRegisterWithRegistrar( LibwinmediaPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LibwinmediaPlugin")); registry->GetRegistrarForPlugin("LibwinmediaPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows bitsdojo_window_windows
hotkey_manager hotkey_manager
libwinmedia libwinmedia
permission_handler_windows
url_launcher_windows url_launcher_windows
) )