Compare commits

...

14 Commits

Author SHA1 Message Date
S.B
80e16f4fa2
Merge c5a72cd44c into 464666c01a 2025-03-08 12:14:46 +01:00
Kingkor Roy Tirtho
464666c01a
Merge pull request #2410 from KRTirtho/dev
chore: update linux appdata screenshot
2025-03-07 20:22:32 +06:00
Kingkor Roy Tirtho
4db9a95a91 chore: update linux appdata screenshot 2025-03-07 20:20:56 +06:00
Kingkor Roy Tirtho
0e58cd0e99
Merge pull request #2408 from KRTirtho/dev
chore: add new images
2025-03-07 20:18:03 +06:00
Kingkor Roy Tirtho
3cce2868de website: remove playstore button 2025-03-07 20:12:05 +06:00
Kingkor Roy Tirtho
894b0d7e5e Merge branch 'dev' into website 2025-03-07 20:08:09 +06:00
Kingkor Roy Tirtho
7befbca8e5 chore: update images 2025-03-07 20:06:58 +06:00
Kingkor Roy Tirtho
ccf84c568e website: show overlay ads at bottom not blocking the view 2024-12-23 09:02:49 +06:00
Kingkor Roy Tirtho
fdb5ed8f56 website: publish from idea to impact 2024-12-22 23:12:12 +06:00
Kingkor Roy Tirtho
55871e3cdd website: change download page ad 2024-12-16 11:50:12 +06:00
Kingkor Roy Tirtho
0aa44520ac website: redirect from /other-downloads/stable-downloads/ to /downloads 2024-12-16 11:43:41 +06:00
S.B
c5a72cd44c
Update custom_player.dart 2024-10-19 14:54:05 +02:00
S.B
8b6cc11486
Update audio_player_impl.dart 2024-10-19 14:50:43 +02:00
S.B
cee65b5f2f
Update audio_player.dart
Key Improvements:

    SpotubeMediaFactory: Handles the logic of creating SpotubeMedia instances, allowing for easier scalability and reducing repetitive code.

    Dependency Injection (DI): CustomPlayer is injected into the AudioPlayerInterface, improving testability and modularity.

    Helper Methods: Functions like getNetworkAddress() and getUriForTrack() simplify and centralize repeated logic, improving maintainability.

    Playback Control Methods: Added play(), pause(), stop(), and seek() methods for better playback control with error handling.

    PlaybackStateManager: Manages the state-related properties (isPlaying, duration, etc.), keeping the AudioPlayerInterface cleaner and more focused on playback control.

Advantages:

    Separation of Concerns: The code is now better structured with clear separation between media management (SpotubeMedia), playback state management (PlaybackStateManager), and playback controls (AudioPlayerInterface).

    Extensibility: The code is more scalable with the factory pattern, making it easy to add new track types or other media sources.

    Testability: With dependency injection, you can easily mock the CustomPlayer and test the logic of AudioPlayerInterface independently.

    Clean Code: Centralized logic and helper methods reduce code duplication, improving readability and maintainability.
2024-10-19 14:48:18 +02:00
33 changed files with 260 additions and 267 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 KiB

After

Width:  |  Height:  |  Size: 1006 KiB

View File

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -7,9 +6,7 @@ import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/services/audio_player/custom_player.dart'; import 'package:spotube/services/audio_player/custom_player.dart';
import 'dart:async'; import 'dart:async';
import 'package:media_kit/media_kit.dart' as mk; import 'package:media_kit/media_kit.dart' as mk;
import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
@ -17,40 +14,40 @@ import 'package:spotube/utils/platform.dart';
part 'audio_players_streams_mixin.dart'; part 'audio_players_streams_mixin.dart';
part 'audio_player_impl.dart'; part 'audio_player_impl.dart';
// Constants class for shared constants like port and addresses
class Constants {
static const defaultServerPort = 8080;
static const defaultLocalHost = "localhost";
}
// Helper to get network address based on the platform
String getNetworkAddress() {
return kIsWindows ? Constants.defaultLocalHost : InternetAddress.anyIPv4.address;
}
// Helper to get URI for a given track
String getUriForTrack(Track track, int serverPort) {
return track is LocalTrack
? track.path
: "http://${getNetworkAddress()}:$serverPort/stream/${track.id}";
}
// SpotubeMedia class handling media creation logic
class SpotubeMedia extends mk.Media { class SpotubeMedia extends mk.Media {
final Track track; final Track track;
static int serverPort = Constants.defaultServerPort;
static int serverPort = 0; SpotubeMedia(this.track, {Map<String, dynamic>? extras, super.httpHeaders})
: super(
SpotubeMedia( getUriForTrack(track, serverPort),
this.track, {
Map<String, dynamic>? extras,
super.httpHeaders,
}) : super(
track is LocalTrack
? track.path
: "http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}",
extras: { extras: {
...?extras, ...?extras,
"track": switch (track) { "track": track.toJson(),
LocalTrack() => track.toJson(),
SourcedTrack() => track.toJson(),
_ => track.toJson(),
},
}, },
); );
@override @override
String get uri { String get uri => getUriForTrack(track, serverPort);
return switch (track) {
/// [super.uri] must be used instead of [track.path] to prevent wrong
/// path format exceptions in Windows causing [extras] to be null
LocalTrack() => super.uri,
_ =>
"http://${kIsWindows ? "localhost" : InternetAddress.anyIPv4.address}:"
"$serverPort/stream/${track.id}",
};
}
factory SpotubeMedia.fromMedia(mk.Media media) { factory SpotubeMedia.fromMedia(mk.Media media) {
final track = media.uri.startsWith("http") final track = media.uri.startsWith("http")
@ -62,102 +59,100 @@ class SpotubeMedia extends mk.Media {
httpHeaders: media.httpHeaders, httpHeaders: media.httpHeaders,
); );
} }
// @override
// operator ==(Object other) {
// if (other is! SpotubeMedia) return false;
// final isLocal = track is LocalTrack && other.track is LocalTrack;
// return isLocal
// ? (other.track as LocalTrack).path == (track as LocalTrack).path
// : other.track.id == track.id;
// }
// @override
// int get hashCode => track is LocalTrack
// ? (track as LocalTrack).path.hashCode
// : track.id.hashCode;
} }
abstract class AudioPlayerInterface { // Factory class to create SpotubeMedia instances
final CustomPlayer _mkPlayer; class SpotubeMediaFactory {
static SpotubeMedia create(Track track, {Map<String, dynamic>? extras, Map<String, String>? headers}) {
return SpotubeMedia(track, extras: extras, httpHeaders: headers);
}
}
AudioPlayerInterface() // Playback state management class
: _mkPlayer = CustomPlayer( class PlaybackStateManager {
configuration: const mk.PlayerConfiguration( final CustomPlayer player;
title: "Spotube",
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, PlaybackStateManager(this.player);
),
) { bool get isPlaying => player.state.playing;
_mkPlayer.stream.error.listen((event) { bool get isPaused => !player.state.playing;
bool get isStopped => player.state.playlist.medias.isEmpty;
Duration get duration => player.state.duration;
Duration get position => player.state.position;
Duration get bufferedPosition => player.state.buffer;
bool get isShuffled => player.shuffled;
double get volume => player.state.volume / 100;
Future<List<mk.AudioDevice>> get devices async => player.state.audioDevices;
Future<mk.AudioDevice> get selectedDevice async => player.state.audioDevice;
PlaylistMode get loopMode => player.state.playlistMode;
}
// Main AudioPlayerInterface class with DI and error handling
abstract class AudioPlayerInterface {
final CustomPlayer player;
final PlaybackStateManager stateManager;
AudioPlayerInterface(this.player)
: stateManager = PlaybackStateManager(player) {
player.stream.error.listen((event) {
AppLogger.reportError(event, StackTrace.current); AppLogger.reportError(event, StackTrace.current);
// Retry or fallback mechanism can be added here
}); });
} }
/// Whether the current platform supports the audioplayers plugin // High-level control methods for playback
static const bool _mkSupportedPlatform = true; Future<void> play() async {
try {
bool get mkSupportedPlatform => _mkSupportedPlatform; await player.play();
} catch (e) {
Duration get duration { AppLogger.reportError(e, StackTrace.current);
return _mkPlayer.state.duration; }
} }
Playlist get playlist { Future<void> pause() async {
return _mkPlayer.state.playlist; try {
await player.pause();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
} }
Duration get position { Future<void> stop() async {
return _mkPlayer.state.position; try {
await player.stop();
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
} }
Duration get bufferedPosition { Future<void> seek(Duration position) async {
return _mkPlayer.state.buffer; try {
await player.seek(position);
} catch (e) {
AppLogger.reportError(e, StackTrace.current);
}
} }
Future<mk.AudioDevice> get selectedDevice async { // Access state information through the state manager
return _mkPlayer.state.audioDevice; bool get isPlaying => stateManager.isPlaying;
} bool get isPaused => stateManager.isPaused;
bool get isStopped => stateManager.isStopped;
Future<List<mk.AudioDevice>> get devices async { Duration get duration => stateManager.duration;
return _mkPlayer.state.audioDevices; Duration get position => stateManager.position;
} Duration get bufferedPosition => stateManager.bufferedPosition;
bool get isShuffled => stateManager.isShuffled;
bool get hasSource { double get volume => stateManager.volume;
return _mkPlayer.state.playlist.medias.isNotEmpty; Future<List<mk.AudioDevice>> get devices => stateManager.devices;
} Future<mk.AudioDevice> get selectedDevice => stateManager.selectedDevice;
PlaylistMode get loopMode => stateManager.loopMode;
// states }
bool get isPlaying {
return _mkPlayer.state.playing; // Example implementation for a specific platform/player
} class MyAudioPlayer extends AudioPlayerInterface {
MyAudioPlayer(CustomPlayer player) : super(player);
bool get isPaused {
return !_mkPlayer.state.playing; // Additional functionality can be added here if necessary
}
bool get isStopped {
return !hasSource;
}
Future<bool> get isCompleted async {
return _mkPlayer.state.completed;
}
bool get isShuffled {
return _mkPlayer.shuffled;
}
PlaylistMode get loopMode {
return _mkPlayer.state.playlistMode;
}
/// Returns the current volume of the player, between 0 and 1
double get volume {
return _mkPlayer.state.volume / 100;
}
bool get isBuffering {
return _mkPlayer.state.buffering;
}
} }

View File

@ -1,45 +1,28 @@
part of 'audio_player.dart';
final audioPlayer = SpotubeAudioPlayer(); final audioPlayer = SpotubeAudioPlayer();
class SpotubeAudioPlayer extends AudioPlayerInterface class SpotubeAudioPlayer extends AudioPlayerInterface with SpotubeAudioPlayersStreams {
with SpotubeAudioPlayersStreams { // Playback control methods
Future<void> pause() async { Future<void> pause() async => await player.pause();
await _mkPlayer.pause();
}
Future<void> resume() async { Future<void> resume() async => await player.play();
await _mkPlayer.play();
}
Future<void> stop() async { Future<void> stop() async => await player.stop();
await _mkPlayer.stop();
}
Future<void> seek(Duration position) async { Future<void> seek(Duration position) async => await player.seek(position);
await _mkPlayer.seek(position);
}
/// Volume is between 0 and 1 /// Set volume between 0 and 1
Future<void> setVolume(double volume) async { Future<void> setVolume(double volume) async {
assert(volume >= 0 && volume <= 1); assert(volume >= 0 && volume <= 1);
await _mkPlayer.setVolume(volume * 100); await player.setVolume(volume * 100);
} }
Future<void> setSpeed(double speed) async { Future<void> setSpeed(double speed) async => await player.setRate(speed);
await _mkPlayer.setRate(speed);
}
Future<void> setAudioDevice(mk.AudioDevice device) async { Future<void> setAudioDevice(mk.AudioDevice device) async => await player.setAudioDevice(device);
await _mkPlayer.setAudioDevice(device);
}
Future<void> dispose() async { Future<void> dispose() async => await player.dispose();
await _mkPlayer.dispose();
}
// Playlist related
// Playlist control methods
Future<void> openPlaylist( Future<void> openPlaylist(
List<mk.Media> tracks, { List<mk.Media> tracks, {
bool autoPlay = true, bool autoPlay = true,
@ -47,88 +30,59 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
}) async { }) async {
assert(tracks.isNotEmpty); assert(tracks.isNotEmpty);
assert(initialIndex <= tracks.length - 1); assert(initialIndex <= tracks.length - 1);
await _mkPlayer.open(
await player.open(
mk.Playlist(tracks, index: initialIndex), mk.Playlist(tracks, index: initialIndex),
play: autoPlay, play: autoPlay,
); );
} }
List<String> get sources { // Helper methods for playlist sources
return _mkPlayer.state.playlist.medias.map((e) => e.uri).toList(); List<String> get sources => player.state.playlist.medias.map((e) => e.uri).toList();
}
String? get currentSource { String? get currentSource {
if (_mkPlayer.state.playlist.index == -1) return null; final index = player.state.playlist.index;
return _mkPlayer.state.playlist.medias if (index == -1) return null;
.elementAtOrNull(_mkPlayer.state.playlist.index) return player.state.playlist.medias.elementAtOrNull(index)?.uri;
?.uri;
} }
String? get nextSource { String? get nextSource {
if (loopMode == PlaylistMode.loop && final isLastTrack = player.state.playlist.index == player.state.playlist.medias.length - 1;
_mkPlayer.state.playlist.index == if (loopMode == PlaylistMode.loop && isLastTrack) return sources.first;
_mkPlayer.state.playlist.medias.length - 1) {
return sources.first;
}
return _mkPlayer.state.playlist.medias return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index + 1)?.uri;
.elementAtOrNull(_mkPlayer.state.playlist.index + 1)
?.uri;
} }
String? get previousSource { String? get previousSource {
if (loopMode == PlaylistMode.loop && _mkPlayer.state.playlist.index == 0) { if (loopMode == PlaylistMode.loop && player.state.playlist.index == 0) return sources.last;
return sources.last;
}
return _mkPlayer.state.playlist.medias return player.state.playlist.medias.elementAtOrNull(player.state.playlist.index - 1)?.uri;
.elementAtOrNull(_mkPlayer.state.playlist.index - 1)
?.uri;
} }
int get currentIndex => _mkPlayer.state.playlist.index; int get currentIndex => player.state.playlist.index;
Future<void> skipToNext() async { // Playlist navigation methods
await _mkPlayer.next(); Future<void> skipToNext() async => await player.next();
}
Future<void> skipToPrevious() async { Future<void> skipToPrevious() async => await player.previous();
await _mkPlayer.previous();
}
Future<void> jumpTo(int index) async { Future<void> jumpTo(int index) async => await player.jump(index);
await _mkPlayer.jump(index);
}
Future<void> addTrack(mk.Media media) async { // Playlist management methods
await _mkPlayer.add(media); Future<void> addTrack(mk.Media media) async => await player.add(media);
}
Future<void> addTrackAt(mk.Media media, int index) async { Future<void> addTrackAt(mk.Media media, int index) async => await player.insert(index, media);
await _mkPlayer.insert(index, media);
}
Future<void> removeTrack(int index) async { Future<void> removeTrack(int index) async => await player.remove(index);
await _mkPlayer.remove(index);
}
Future<void> moveTrack(int from, int to) async { Future<void> moveTrack(int from, int to) async => await player.move(from, to);
await _mkPlayer.move(from, to);
}
Future<void> clearPlaylist() async { Future<void> clearPlaylist() async => await player.stop();
_mkPlayer.stop();
}
Future<void> setShuffle(bool shuffle) async { // Shuffle and loop mode control
await _mkPlayer.setShuffle(shuffle); Future<void> setShuffle(bool shuffle) async => await player.setShuffle(shuffle);
}
Future<void> setLoopMode(PlaylistMode loop) async { Future<void> setLoopMode(PlaylistMode loop) async => await player.setPlaylistMode(loop);
await _mkPlayer.setPlaylistMode(loop);
}
Future<void> setAudioNormalization(bool normalize) async { Future<void> setAudioNormalization(bool normalize) async => await player.setAudioNormalization(normalize);
await _mkPlayer.setAudioNormalization(normalize);
}
} }

View File

@ -4,43 +4,52 @@ import 'package:media_kit/media_kit.dart';
import 'package:flutter_broadcasts/flutter_broadcasts.dart'; import 'package:flutter_broadcasts/flutter_broadcasts.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:audio_session/audio_session.dart'; import 'package:audio_session/audio_session.dart';
// ignore: implementation_imports
import 'package:spotube/services/audio_player/playback_state.dart'; import 'package:spotube/services/audio_player/playback_state.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
/// MediaKit [Player] by default doesn't have a state stream.
/// This class adds a state stream to the [Player] class.
class CustomPlayer extends Player { class CustomPlayer extends Player {
final StreamController<AudioPlaybackState> _playerStateStream; final StreamController<AudioPlaybackState> _playerStateStream = StreamController.broadcast();
final StreamController<bool> _shuffleStream; final StreamController<bool> _shuffleStream = StreamController.broadcast();
late final List<StreamSubscription> _subscriptions; late final List<StreamSubscription> _subscriptions;
bool _shuffled = false;
bool _shuffled;
int _androidAudioSessionId = 0; int _androidAudioSessionId = 0;
String _packageName = ""; String _packageName = "";
AndroidAudioManager? _androidAudioManager; AndroidAudioManager? _androidAudioManager;
CustomPlayer({super.configuration}) CustomPlayer({super.configuration}) {
: _playerStateStream = StreamController.broadcast(),
_shuffleStream = StreamController.broadcast(),
_shuffled = false {
nativePlayer.setProperty("network-timeout", "120"); nativePlayer.setProperty("network-timeout", "120");
_initPlatformSpecificSetup();
_listenToPlayerEvents();
}
Future<void> _initPlatformSpecificSetup() async {
final packageInfo = await PackageInfo.fromPlatform();
_packageName = packageInfo.packageName;
if (kIsAndroid) {
_androidAudioManager = AndroidAudioManager();
_androidAudioSessionId = await _androidAudioManager!.generateAudioSessionId();
notifyAudioSessionUpdate(true);
await _setAndroidAudioSession();
}
}
Future<void> _setAndroidAudioSession() async {
await nativePlayer.setProperty("audiotrack-session-id", _androidAudioSessionId.toString());
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
}
void _listenToPlayerEvents() {
_subscriptions = [ _subscriptions = [
stream.buffering.listen((event) { stream.buffering.listen((_) => _playerStateStream.add(AudioPlaybackState.buffering)),
_playerStateStream.add(AudioPlaybackState.buffering);
}),
stream.playing.listen((playing) { stream.playing.listen((playing) {
if (playing) { _playerStateStream.add(playing ? AudioPlaybackState.playing : AudioPlaybackState.paused);
_playerStateStream.add(AudioPlaybackState.playing);
} else {
_playerStateStream.add(AudioPlaybackState.paused);
}
}), }),
stream.completed.listen((isCompleted) async { stream.completed.listen((isCompleted) {
if (!isCompleted) return; if (isCompleted) {
_playerStateStream.add(AudioPlaybackState.completed); _playerStateStream.add(AudioPlaybackState.completed);
}
}), }),
stream.playlist.listen((event) { stream.playlist.listen((event) {
if (event.medias.isEmpty) { if (event.medias.isEmpty) {
@ -51,23 +60,6 @@ class CustomPlayer extends Player {
AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current); AppLogger.reportError('[MediaKitError] \n$event', StackTrace.current);
}), }),
]; ];
PackageInfo.fromPlatform().then((packageInfo) {
_packageName = packageInfo.packageName;
});
if (kIsAndroid) {
_androidAudioManager = AndroidAudioManager();
AudioSession.instance.then((s) async {
_androidAudioSessionId =
await _androidAudioManager!.generateAudioSessionId();
notifyAudioSessionUpdate(true);
await nativePlayer.setProperty(
"audiotrack-session-id",
_androidAudioSessionId.toString(),
);
await nativePlayer.setProperty("ao", "audiotrack,opensles,");
});
}
} }
Future<void> notifyAudioSessionUpdate(bool active) async { Future<void> notifyAudioSessionUpdate(bool active) async {
@ -79,7 +71,7 @@ class CustomPlayer extends Player {
: "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION", : "android.media.action.CLOSE_AUDIO_EFFECT_CONTROL_SESSION",
data: { data: {
"android.media.extra.AUDIO_SESSION": _androidAudioSessionId, "android.media.extra.AUDIO_SESSION": _androidAudioSessionId,
"android.media.extra.PACKAGE_NAME": _packageName "android.media.extra.PACKAGE_NAME": _packageName,
}, },
), ),
); );
@ -90,6 +82,7 @@ class CustomPlayer extends Player {
Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream; Stream<AudioPlaybackState> get playerStateStream => _playerStateStream.stream;
Stream<bool> get shuffleStream => _shuffleStream.stream; Stream<bool> get shuffleStream => _shuffleStream.stream;
Stream<int> get indexChangeStream { Stream<int> get indexChangeStream {
int oldIndex = state.playlist.index; int oldIndex = state.playlist.index;
return stream.playlist.map((event) => event.index).where((newIndex) { return stream.playlist.map((event) => event.index).where((newIndex) {
@ -106,6 +99,8 @@ class CustomPlayer extends Player {
_shuffled = shuffle; _shuffled = shuffle;
await super.setShuffle(shuffle); await super.setShuffle(shuffle);
_shuffleStream.add(shuffle); _shuffleStream.add(shuffle);
// Ensure delay before rearranging playlist
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
if (shuffle) { if (shuffle) {
await move(state.playlist.index, 0); await move(state.playlist.index, 0);
@ -115,7 +110,6 @@ class CustomPlayer extends Player {
@override @override
Future<void> stop() async { Future<void> stop() async {
await super.stop(); await super.stop();
_shuffled = false; _shuffled = false;
_playerStateStream.add(AudioPlaybackState.stopped); _playerStateStream.add(AudioPlaybackState.stopped);
_shuffleStream.add(false); _shuffleStream.add(false);
@ -123,10 +117,10 @@ class CustomPlayer extends Player {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
for (var element in _subscriptions) { await Future.wait(_subscriptions.map((sub) => sub.cancel()));
element.cancel();
}
await notifyAudioSessionUpdate(false); await notifyAudioSessionUpdate(false);
await _playerStateStream.close();
await _shuffleStream.close();
return super.dispose(); return super.dispose();
} }
@ -138,10 +132,9 @@ class CustomPlayer extends Player {
} }
Future<void> setAudioNormalization(bool normalize) async { Future<void> setAudioNormalization(bool normalize) async {
if (normalize) { await nativePlayer.setProperty(
await nativePlayer.setProperty('af', 'dynaudnorm=g=5:f=250:r=0.9:p=0.5'); 'af',
} else { normalize ? 'dynaudnorm=g=5:f=250:r=0.9:p=0.5' : ''
await nativePlayer.setProperty('af', ''); );
}
} }
} }

View File

@ -36,7 +36,7 @@
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image type="source"> <image type="source">
https://rawcdn.githack.com/KRTirtho/spotube/62055018feade0b895663a0bfc5f85f265ae2154/assets/spotube-screenshot.png https://rawcdn.githack.com/KRTirtho/spotube/refs/heads/master/assets/spotube-screenshot.png
</image> </image>
<caption>Spotube screenshot</caption> <caption>Spotube screenshot</caption>
</screenshot> </screenshot>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -60,5 +60,15 @@
"remark-reading-time": "^1.0.1", "remark-reading-time": "^1.0.1",
"svelte-fa": "^4.0.2", "svelte-fa": "^4.0.2",
"svelte-markdown": "^0.4.1" "svelte-markdown": "^0.4.1"
},
"packageManager": "pnpm@10.4.0+sha512.6b849d0787d97f8f4e1f03a9b8ff8f038e79e153d6f11ae539ae7c435ff9e796df6a862c991502695c7f9e8fac8aeafc1ac5a8dab47e36148d183832d886dd52",
"pnpm": {
"onlyBuiltDependencies": [
"@fortawesome/fontawesome-common-types",
"@fortawesome/free-brands-svg-icons",
"@sveltejs/kit",
"esbuild",
"svelte-preprocess"
]
} }
} }

View File

@ -0,0 +1,36 @@
---
title: From Idea to Impact
author: Prottoy Roy
date: 2024-12-22
published: true
cover_img: /images/from-idea-to-impact/cover.jpg
---
> An school magazine article by the beloved brother of the founder of the Spotube app
In the vibrant city of Narayanganj, Dhaka, Bangladesh, a young man named Kingkor Roy Tirtho was carving out his path in the world of technology. Currently a second-year Computer Science and Engineering (CSE) student at East West University, Kingkor had always been captivated by the magic of coding. From a young age, he spent countless hours tinkering with computers, teaching himself programming languages and exploring the digital realm.
Kingkor's passion wasn't just about writing code; it was about solving problems and creating innovative solutions. Inspired by the way technology could enhance everyday life, he dreamed of building apps that would bring joy and convenience to users. His dedication was evident; he often participated in hackathons and coding competitions, where he showcased his talent and creativity.
The turning point in his journey came when he envisioned an app that would revolutionize music streaming. With millions of people seeking accessible music, he wanted to create a platform that could bridge gaps and provide a seamless experience. Thus, Spotube was born.
Initially, Kingkor faced numerous challenges. Balancing his academic responsibilities with app development was no easy feat. There were nights filled with coding, debugging, and sleepless hours fueled by caffeine and determination. Despite setbacks and moments of self-doubt, Kingkor remained resilient. He sought feedback, learned from criticisms, and continually iterated on his project.
As Spotube gained traction, it garnered attention for its user-friendly interface and innovative features. Kingkors ability to blend technical skills with an understanding of user needs made the app a hit among music lovers. He received positive reviews, not just for the functionality, but for the passion evident in his work.
Kingkors story is one of perseverance and innovation. He embodies the spirit of a new generation of tech enthusiasts who believe that with dedication, anything is possible. His journey serves as an inspiration to his peers at East West University and beyond, reminding them that the intersection of creativity and technology can lead to remarkable achievements.
Today, Kingkor continues to evolve as a developer, always looking for ways to improve Spotube and explore new ideas. His story illustrates that genius isn't just about raw talent; it's about hard work, resilience, and the willingness to dream big. Kingkor Roy Tirtho is a shining example of what can be achieved when passion meets perseverance, and he is just getting international attentions.
Here is some key features of Spotube:
1. **Seamless Music Streaming**: Spotube offers a smooth streaming experience with a vast library of tracks, allowing users to easily find and play their favorite songs.
1. **Offline Listening**: Users can download their favorite tracks for offline playback, making it convenient to enjoy music anytime, anywhere, without relying on an internet connection.
1. **User-Friendly Interface**: The app is designed with an intuitive interface, making navigation easy for users of all ages. Its clean layout ensures a pleasant user experience.
1. **Cross-Platform Compatibility**: Spotube is accessible on multiple devices, enabling users to enjoy their music on smartphones, tablets, and desktops seamlessly.
1. **Personalized Playlists**: Users can create and manage their playlists, helping them curate their listening experience based on their mood and preferences.
1. **Social Sharing Features**: The app allows users to share their favorite tracks and playlists with friends and family, fostering a community of music lovers.
1. **Regular Updates**: Spotube is continually updated with new features and improvements based on user feedback, reflecting Kingkor's commitment to enhancing the app's performance and user satisfaction.
1. **Global Reach**: With its growing popularity, Spotube is gaining attention worldwide, attracting users from various countries and cultures, showcasing Kingkors vision of accessible music for everyone. He's recently got mentioned in a Spanish well known magazine for his invention.
As Spotube continues to evolve, Kingkor Roy Tirtho's innovative approach is positioning him and his app as significant players in the music streaming landscape, capturing the attention of users and industry experts alike.

View File

@ -14,7 +14,7 @@
<!-- Android Chrome --> <!-- Android Chrome -->
<link rel="manifest" href="%sveltekit.assets%/manifest.json" /> <link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6419300932495863" <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6419300932495863"
crossorigin="anonymous"></script> data-overlays="bottom" crossorigin="anonymous"></script>
%sveltekit.head% %sveltekit.head%
</head> </head>

View File

@ -9,3 +9,15 @@
src: url('/fonts/AbrilFatface.ttf'); src: url('/fonts/AbrilFatface.ttf');
font-display: swap; font-display: swap;
} }
.text-stroke {
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000,
-1px 0 0 #000,
1px 0 0 #000,
0 -1px 0 #000,
0 1px 0 #000;
}

View File

@ -97,7 +97,7 @@ export const extendedNightlyDownloadLinks: Record<
export const ADS_SLOTS = Object.freeze({ export const ADS_SLOTS = Object.freeze({
rootPageDisplay: 5979549631, rootPageDisplay: 5979549631,
blogPageInFeed: 3386010031, blogPageInFeed: 3386010031,
downloadPageDisplay: 9521642154, downloadPageDisplay: 9928443050,
packagePageArticle: 9119323068, packagePageArticle: 9119323068,
// This is being used for rehype-auto-ads in svelte.config.js // This is being used for rehype-auto-ads in svelte.config.js
blogArticlePageArticle: 6788673194, blogArticlePageArticle: 6788673194,

View File

@ -53,17 +53,9 @@
built with Electron (web technologies) built with Electron (web technologies)
</p> </p>
<br /> <br />
<div class="flex items-center"> <div class="flex items-center gap-3">
<a <a href="https://news.ycombinator.com/item?id=39066136" target="_blank">
href="https://play.google.com/store/apps/details?id=oss.krtirtho.spotube" <img src="https://hackerbadge.vercel.app/api?id=39066136" alt="HackerNews" />
target="_blank"
>
<img
class="-m-2"
src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
alt="Google PlayStore"
width="200"
/>
</a> </a>
<a href="https://flathub.org/apps/com.github.KRTirtho.Spotube" target="_blank"> <a href="https://flathub.org/apps/com.github.KRTirtho.Spotube" target="_blank">
<img <img
@ -73,12 +65,7 @@
/> />
</a> </a>
</div> </div>
<br />
<a href="https://news.ycombinator.com/item?id=39066136" target="_blank">
<img src="https://hackerbadge.vercel.app/api?id=39066136" alt="HackerNews" />
</a>
</div> </div>
<br class="hidden md:block" />
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/downloads" class="flex gap-2 btn variant-filled-primary"> <a href="/downloads" class="flex gap-2 btn variant-filled-primary">
Download Download

View File

@ -5,7 +5,7 @@
export let data: PageData; export let data: PageData;
const { const {
Content, Content,
meta: { date, title, readingTime, cover_img } meta: { date, title, readingTime, cover_img, author }
} = data as Required<PageData>; } = data as Required<PageData>;
</script> </script>
@ -20,7 +20,8 @@
: null} : null}
style={cover_img ? `background-image: url(/posts/${cover_img});` : ''} style={cover_img ? `background-image: url(/posts/${cover_img});` : ''}
> >
<h1 class={`h1 ${cover_img ? 'text-white' : ''}`}>{title}</h1> <h1 class={`h1 text-stroke ${cover_img ? 'text-white' : ''}`}>{title}</h1>
<h4 class={`h4 text-stroke text-gray-400`}>By {author}</h4>
<br /> <br />
<p class={cover_img ? 'text-gray-400' : ''}>{new Date(date).toDateString()}</p> <p class={cover_img ? 'text-gray-400' : ''}>{new Date(date).toDateString()}</p>
<p class={`mb-16 ${cover_img ? 'text-gray-400' : ''}`}>{readingTime?.text ?? ''}</p> <p class={`mb-16 ${cover_img ? 'text-gray-400' : ''}`}>{readingTime?.text ?? ''}</p>

View File

@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export function load(){
redirect(301, "/downloads");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB