mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Genius Lyrics works without access tokens
android background playback configuration added
This commit is contained in:
parent
f483c59915
commit
bbfb8f8522
3
.github/workflows/flutter-build.yml
vendored
3
.github/workflows/flutter-build.yml
vendored
@ -27,6 +27,8 @@ jobs:
|
|||||||
- run: chmod +x /usr/local/bin/appimagetool
|
- run: chmod +x /usr/local/bin/appimagetool
|
||||||
- run: pip3 install appimage-builder
|
- run: pip3 install appimage-builder
|
||||||
- run: make appimage
|
- run: make appimage
|
||||||
|
# Building Android Application
|
||||||
|
- run: flutter build apk
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Spotube-Linux-Bundle
|
name: Spotube-Linux-Bundle
|
||||||
@ -34,6 +36,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_windows:
|
build_windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
|
10
README.md
10
README.md
@ -121,9 +121,15 @@ 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
|
||||||

|

|
||||||
|
|
||||||
Also, you need a [genius](https://genius.com) account for **lyrics** & a API Client for
|
**Setup Genius Lyrics (Optional)**
|
||||||
|
|
||||||
- accessToken
|
- 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
|
||||||
|

|
||||||
|
- Generate & copy access token
|
||||||
|

|
||||||
|
- Paste the copied access token in Spotube's Settings
|
||||||
|

|
||||||
|
|
||||||
> **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
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="oss.krtirtho.spotube">
|
||||||
|
|
||||||
<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.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<!-- If your app opens https URLs -->
|
<!-- If your app opens https URLs -->
|
||||||
@ -11,7 +13,7 @@
|
|||||||
</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">
|
||||||
<activity android:name=".MainActivity" 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
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
@ -23,6 +25,19 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service android:name="com.ryanheise.audioservice.AudioService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
|
19
bin/create-secrets.dart
Normal file
19
bin/create-secrets.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:dotenv/dotenv.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
load();
|
||||||
|
final bool hasKey = env.containsKey("SECRET");
|
||||||
|
final val = hasKey ? jsonDecode(env["SECRET"]!) : null;
|
||||||
|
if (!hasKey || (hasKey && val is! List)) {
|
||||||
|
throw Exception(
|
||||||
|
"'SECRET' Environmental Variable isn't configured properly");
|
||||||
|
}
|
||||||
|
|
||||||
|
await File(path.join(
|
||||||
|
Directory.current.path, "lib/models/generated_secrets.dart"))
|
||||||
|
.writeAsString("final List<String> secrets = ${env["SECRET"]};");
|
||||||
|
}
|
@ -3,8 +3,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Settings.dart';
|
|
||||||
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
|
|
||||||
import 'package:spotube/helpers/artist-to-string.dart';
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
import 'package:spotube/helpers/getLyrics.dart';
|
import 'package:spotube/helpers/getLyrics.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
@ -39,7 +37,9 @@ class Lyrics extends HookConsumerWidget {
|
|||||||
var lyricsSnapshot = useFuture(lyricsFuture);
|
var lyricsSnapshot = useFuture(lyricsFuture);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (lyricsSnapshot.hasData && lyricsSnapshot.data != null) {
|
if (lyricsSnapshot.hasData &&
|
||||||
|
lyricsSnapshot.data != null &&
|
||||||
|
playback.currentTrack != null) {
|
||||||
lyrics.value = {
|
lyrics.value = {
|
||||||
"lyrics": lyricsSnapshot.data,
|
"lyrics": lyricsSnapshot.data,
|
||||||
"id": playback.currentTrack!.id!
|
"id": playback.currentTrack!.id!
|
||||||
|
6
lib/helpers/get-random-element.dart
Normal file
6
lib/helpers/get-random-element.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
final Random _random = Random();
|
||||||
|
T getRandomElement<T>(List<T> list) {
|
||||||
|
return list[_random.nextInt(list.length)];
|
||||||
|
}
|
@ -2,6 +2,8 @@ import 'dart:convert';
|
|||||||
import 'package:html/parser.dart' as parser;
|
import 'package:html/parser.dart' as parser;
|
||||||
import 'package:html/dom.dart';
|
import 'package:html/dom.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:spotube/helpers/get-random-element.dart';
|
||||||
|
import 'package:spotube/models/generated_secrets.dart';
|
||||||
|
|
||||||
String getTitle(String title, String artist) {
|
String getTitle(String title, String artist) {
|
||||||
return "$title $artist"
|
return "$title $artist"
|
||||||
@ -47,16 +49,17 @@ Future<String?> extractLyrics(Uri url) async {
|
|||||||
Future<List?> searchSong(
|
Future<List?> searchSong(
|
||||||
String title,
|
String title,
|
||||||
String artist, {
|
String artist, {
|
||||||
String apiKey = "",
|
String? apiKey,
|
||||||
bool optimizeQuery = false,
|
bool optimizeQuery = false,
|
||||||
bool authHeader = false,
|
bool authHeader = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
if (apiKey == "" || apiKey == null) apiKey = getRandomElement(secrets);
|
||||||
const searchUrl = 'https://api.genius.com/search?q=';
|
const searchUrl = 'https://api.genius.com/search?q=';
|
||||||
String song = optimizeQuery ? getTitle(title, artist) : "$title $artist";
|
String song = optimizeQuery ? getTitle(title, artist) : "$title $artist";
|
||||||
|
|
||||||
String reqUrl = "$searchUrl${Uri.encodeComponent(song)}";
|
String reqUrl = "$searchUrl${Uri.encodeComponent(song)}";
|
||||||
Map<String, String> headers = {"Authorization": 'Bearer ' + apiKey};
|
Map<String, String> headers = {"Authorization": 'Bearer $apiKey'};
|
||||||
var response = await http.get(
|
var response = await http.get(
|
||||||
Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"),
|
Uri.parse(authHeader ? reqUrl : "$reqUrl&access_token=$apiKey"),
|
||||||
headers: authHeader ? headers : null,
|
headers: authHeader ? headers : null,
|
||||||
@ -100,5 +103,6 @@ Future<String?> getLyrics(
|
|||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
print("[getLyrics] $e");
|
print("[getLyrics] $e");
|
||||||
print(stack);
|
print(stack);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,9 +12,16 @@ import 'package:spotube/models/LocalStorageKeys.dart';
|
|||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
import 'package:spotube/provider/ThemeProvider.dart';
|
import 'package:spotube/provider/ThemeProvider.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
|
import 'package:just_audio_background/just_audio_background.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
if (Platform.isAndroid || Platform.isIOS) {
|
||||||
|
await JustAudioBackground.init(
|
||||||
|
androidNotificationChannelId: 'oss.krtirtho.Spotube',
|
||||||
|
androidNotificationChannelName: 'Spotube',
|
||||||
|
androidNotificationOngoing: true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await hotKeyManager.unregisterAll();
|
await hotKeyManager.unregisterAll();
|
||||||
doWhenWindowReady(() {
|
doWhenWindowReady(() {
|
||||||
|
@ -4,7 +4,10 @@ import 'package:audio_session/audio_session.dart';
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
import 'package:just_audio_background/just_audio_background.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/search-youtube.dart';
|
import 'package:spotube/helpers/search-youtube.dart';
|
||||||
import 'package:spotube/provider/AudioPlayer.dart';
|
import 'package:spotube/provider/AudioPlayer.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
@ -221,10 +224,17 @@ class Playback extends ChangeNotifier {
|
|||||||
track ??= _currentTrack;
|
track ??= _currentTrack;
|
||||||
if (track != null && await _audioSession?.setActive(true) == true) {
|
if (track != null && await _audioSession?.setActive(true) == true) {
|
||||||
Uri? parsedUri = Uri.tryParse(track.uri ?? "");
|
Uri? parsedUri = Uri.tryParse(track.uri ?? "");
|
||||||
|
final tag = MediaItem(
|
||||||
|
id: track.id!,
|
||||||
|
title: track.name!,
|
||||||
|
album: track.album?.name,
|
||||||
|
artist: artistsToString(track.artists ?? <ArtistSimple>[]),
|
||||||
|
artUri: Uri.parse(imageToUrlString(track.album?.images)),
|
||||||
|
);
|
||||||
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
||||||
await player
|
await player
|
||||||
.setAudioSource(
|
.setAudioSource(
|
||||||
AudioSource.uri(parsedUri),
|
AudioSource.uri(parsedUri, tag: tag),
|
||||||
preload: true,
|
preload: true,
|
||||||
)
|
)
|
||||||
.then((value) async {
|
.then((value) async {
|
||||||
@ -238,7 +248,7 @@ class Playback extends ChangeNotifier {
|
|||||||
if (setTrackUriById(track.id!, ytTrack.uri!)) {
|
if (setTrackUriById(track.id!, ytTrack.uri!)) {
|
||||||
await player
|
await player
|
||||||
.setAudioSource(
|
.setAudioSource(
|
||||||
AudioSource.uri(Uri.parse(ytTrack.uri!)),
|
AudioSource.uri(Uri.parse(ytTrack.uri!), tag: tag),
|
||||||
preload: true,
|
preload: true,
|
||||||
)
|
)
|
||||||
.then((value) {
|
.then((value) {
|
||||||
|
35
pubspec.lock
35
pubspec.lock
@ -29,6 +29,27 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.2"
|
version: "2.8.2"
|
||||||
|
audio_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audio_service
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.18.4"
|
||||||
|
audio_service_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audio_service_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
|
audio_service_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: audio_service_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
audio_session:
|
audio_session:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -148,6 +169,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
dotenv:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: dotenv
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -310,6 +338,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.20"
|
version: "0.9.20"
|
||||||
|
just_audio_background:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: just_audio_background
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1-beta.5"
|
||||||
just_audio_libwinmedia:
|
just_audio_libwinmedia:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -55,6 +55,7 @@ dependencies:
|
|||||||
go_router: ^3.0.4
|
go_router: ^3.0.4
|
||||||
palette_generator: ^0.3.3
|
palette_generator: ^0.3.3
|
||||||
audio_session: ^0.1.6+1
|
audio_session: ^0.1.6+1
|
||||||
|
just_audio_background: ^0.0.1-beta.5
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -68,6 +69,7 @@ dev_dependencies:
|
|||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^1.0.0
|
flutter_lints: ^1.0.0
|
||||||
flutter_launcher_icons: ^0.9.2
|
flutter_launcher_icons: ^0.9.2
|
||||||
|
dotenv: ^3.0.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
Loading…
Reference in New Issue
Block a user