Compare commits

...

7 Commits

Author SHA1 Message Date
Guanciottaman
0c9a545c33
Merge ff252d6b14 into 973ca20c8e 2025-09-20 20:42:24 +06:00
Kingkor Roy Tirtho
973ca20c8e fix(playback): play next not working 2025-09-20 20:25:51 +06:00
Kingkor Roy Tirtho
7d849b1430 fix: change plugin download directory to application support 2025-09-20 18:01:42 +06:00
Kingkor Roy Tirtho
e5150515f3 chore: cache dab music match source 2025-09-20 17:54:08 +06:00
Guanciottaman
ff252d6b14
Merge branch 'dev' into patch-1 2024-06-03 16:26:24 +02:00
Guanciottaman
195cad8f39
Update app_it.arb fixing translations 2024-03-27 20:54:18 +01:00
Guanciottaman
19f525fa3c
Update app_it.arb
Made the translations more friendly
2024-03-12 21:44:24 +01:00
11 changed files with 150 additions and 47 deletions

View File

@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
@ -59,7 +60,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.delete, TrackOptionValue.delete,
playlistId, playlistId,
); );
@ -73,7 +74,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.album, TrackOptionValue.album,
playlistId, playlistId,
); );
@ -97,7 +98,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.addToQueue, TrackOptionValue.addToQueue,
playlistId, playlistId,
); );
@ -110,7 +111,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.playNext, TrackOptionValue.playNext,
playlistId, playlistId,
); );
@ -124,7 +125,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.removeFromQueue, TrackOptionValue.removeFromQueue,
playlistId, playlistId,
); );
@ -139,7 +140,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.favorite, TrackOptionValue.favorite,
playlistId, playlistId,
); );
@ -162,7 +163,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.startRadio, TrackOptionValue.startRadio,
playlistId, playlistId,
); );
@ -175,7 +176,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.addToPlaylist, TrackOptionValue.addToPlaylist,
playlistId, playlistId,
); );
@ -190,7 +191,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.removeFromPlaylist, TrackOptionValue.removeFromPlaylist,
playlistId, playlistId,
); );
@ -204,7 +205,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.download, TrackOptionValue.download,
playlistId, playlistId,
); );
@ -226,7 +227,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.blacklist, TrackOptionValue.blacklist,
playlistId, playlistId,
); );
@ -250,7 +251,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.share, TrackOptionValue.share,
playlistId, playlistId,
); );
@ -264,7 +265,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.songlink, TrackOptionValue.songlink,
playlistId, playlistId,
); );
@ -282,7 +283,7 @@ class TrackOptions extends HookConsumerWidget {
style: ButtonVariance.menu, style: ButtonVariance.menu,
onPressed: () async { onPressed: () async {
await trackOptionActions.action( await trackOptionActions.action(
context, rootNavigatorKey.currentContext!,
TrackOptionValue.details, TrackOptionValue.details,
playlistId, playlistId,
); );

View File

@ -137,16 +137,16 @@
"pre_download_play_description": "Anzi che effettuare lo stream dell'audio, scarica invece i byte e li riproduce (raccomandato per gli utenti con banda più alta)", "pre_download_play_description": "Anzi che effettuare lo stream dell'audio, scarica invece i byte e li riproduce (raccomandato per gli utenti con banda più alta)",
"skip_non_music": "Salta i segmenti non di musica (SponsorBlock)", "skip_non_music": "Salta i segmenti non di musica (SponsorBlock)",
"blacklist_description": "Tracce e artisti in blacklist", "blacklist_description": "Tracce e artisti in blacklist",
"wait_for_download_to_finish": "Prego attendere che lo scaricamento corrente finisca", "wait_for_download_to_finish": "Prego attendere che il download corrente finisca",
"desktop": "Desktop", "desktop": "Desktop",
"close_behavior": "Comportamento Chiusura", "close_behavior": "Comportamento Chiusura",
"close": "Chiudi", "close": "Chiudi",
"minimize_to_tray": "Minimizza in tray", "minimize_to_tray": "Minimizza in tray",
"show_tray_icon": "Mostra icona in tray di sistema", "show_tray_icon": "Mostra icona in tray di sistema",
"about": "A proposito di", "about": "Informazioni su",
"u_love_spotube": "Sappiamo che ami Spotube", "u_love_spotube": "Sappiamo che ami Spotube",
"check_for_updates": "Controlla aggiornamenti", "check_for_updates": "Controlla aggiornamenti",
"about_spotube": "A proposito di Spotube", "about_spotube": "Informazioni su Spotube",
"blacklist": "Blacklist", "blacklist": "Blacklist",
"please_sponsor": "Per favore sponsorizza/dona", "please_sponsor": "Per favore sponsorizza/dona",
"spotube_description": "Spotube, un client spotify gratis per tutti, multipiattaforma e leggero", "spotube_description": "Spotube, un client spotify gratis per tutti, multipiattaforma e leggero",
@ -187,7 +187,7 @@
"generate_playlist": "Genera Playlist", "generate_playlist": "Genera Playlist",
"track_exists": "La traccia {track} esiste già", "track_exists": "La traccia {track} esiste già",
"replace_downloaded_tracks": "Sostituisci tutte le tracce scaricate", "replace_downloaded_tracks": "Sostituisci tutte le tracce scaricate",
"skip_download_tracks": "Salta lo scaricamento di tutte le tracce scaricate", "skip_download_tracks": "Salta il download di tutte le tracce scaricate",
"do_you_want_to_replace": "Vuoi sovrascrivere la traccia esistente??", "do_you_want_to_replace": "Vuoi sovrascrivere la traccia esistente??",
"replace": "Sovrascrivi", "replace": "Sovrascrivi",
"skip": "Salta", "skip": "Salta",
@ -256,7 +256,7 @@
"querying_info": "Richiesta informazioni...", "querying_info": "Richiesta informazioni...",
"piped_api_down": "Le Piped API non funzionano", "piped_api_down": "Le Piped API non funzionano",
"piped_down_error_instructions": "L'istanza di Piped {pipedInstance} è correntemente offline\n\nCambia istanza o cambia 'Tipo API' alle API ufficiali YouTube\n\nAssicurati di riavviare l'app dopo il cambio", "piped_down_error_instructions": "L'istanza di Piped {pipedInstance} è correntemente offline\n\nCambia istanza o cambia 'Tipo API' alle API ufficiali YouTube\n\nAssicurati di riavviare l'app dopo il cambio",
"you_are_offline": "Sei correntemente offline", "you_are_offline": "Al momento sei offline",
"connection_restored": "Connessione ad internet ripristinata", "connection_restored": "Connessione ad internet ripristinata",
"use_system_title_bar": "Usa la barra del titolo di sistema", "use_system_title_bar": "Usa la barra del titolo di sistema",
"crunching_results": "Elaborazione risultati...", "crunching_results": "Elaborazione risultati...",
@ -267,15 +267,15 @@
"change_cover": "Cambia copertina", "change_cover": "Cambia copertina",
"add_cover": "Aggiungi copertina", "add_cover": "Aggiungi copertina",
"restore_defaults": "Ripristina default", "restore_defaults": "Ripristina default",
"download_music_codec": "Codec musicale scaricamento", "download_music_codec": "Codec download musica",
"streaming_music_codec": "Codec musicale streaming", "streaming_music_codec": "Codec streaming musica",
"login_with_lastfm": "Accesso a Last.fm", "login_with_lastfm": "Accedi con Last.fm",
"connect": "Connetti", "connect": "Connettiti",
"disconnect_lastfm": "Disconnetti Last.fm", "disconnect_lastfm": "Disconnettiti da Last.fm",
"disconnect": "Disconnetti", "disconnect": "Disconnetti",
"username": "Nome utente", "username": "Nome utente",
"password": "Password", "password": "Password",
"login": "Accesso", "login": "Accedi",
"login_with_your_lastfm": "Accedi con il tuo account Last.fm", "login_with_your_lastfm": "Accedi con il tuo account Last.fm",
"scrobble_to_lastfm": "Invia a Last.fm", "scrobble_to_lastfm": "Invia a Last.fm",
"audio_source": "Fonte audio", "audio_source": "Fonte audio",
@ -299,7 +299,7 @@
"song_link": "Link della Canzone", "song_link": "Link della Canzone",
"skip_this_nonsense": "Salta questa sciocchezza", "skip_this_nonsense": "Salta questa sciocchezza",
"freedom_of_music": "“Libertà della Musica”", "freedom_of_music": "“Libertà della Musica”",
"freedom_of_music_palm": "“Libertà della Musica nel palmo della tua mano”", "freedom_of_music_palm": "“Libertà della Musica nelle tue mani”",
"get_started": "Cominciamo", "get_started": "Cominciamo",
"youtube_source_description": "Consigliato e funziona meglio.", "youtube_source_description": "Consigliato e funziona meglio.",
"piped_source_description": "Ti senti libero? Come YouTube ma molto più gratuito.", "piped_source_description": "Ti senti libero? Come YouTube ma molto più gratuito.",

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' show Badge;
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';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
@ -83,14 +84,20 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
runSpacing: 6, runSpacing: 6,
children: [ children: [
for (final source in AudioSource.values) for (final source in AudioSource.values)
RadioCard( Badge(
value: source, isLabelVisible: source == AudioSource.dabMusic,
child: Column( label: const Text("NEW"),
mainAxisSize: MainAxisSize.min, backgroundColor: Colors.lime[300],
children: [ textColor: Colors.black,
audioSourceToIconMap[source]!, child: RadioCard(
Text(source.label), value: source,
], child: Column(
mainAxisSize: MainAxisSize.min,
children: [
audioSourceToIconMap[source]!,
Text(source.label),
],
),
), ),
), ),
], ],

View File

@ -259,11 +259,14 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
return addTracks(tracks); return addTracks(tracks);
} }
final addableTracks = _blacklist.filter(tracks).where( final addableTracks = _blacklist
.filter(tracks)
.where(
(track) => (track) =>
allowDuplicates || allowDuplicates ||
!state.tracks.any((element) => _compareTracks(element, track)), !state.tracks.any((element) => _compareTracks(element, track)),
); )
.toList();
state = state.copyWith( state = state.copyWith(
tracks: [...addableTracks, ...state.tracks], tracks: [...addableTracks, ...state.tracks],
@ -371,13 +374,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
} }
bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) { bool _compareTracks(SpotubeTrackObject a, SpotubeTrackObject b) {
if ((a is SpotubeLocalTrackObject && b is! SpotubeLocalTrackObject) || if (a.runtimeType != b.runtimeType) {
(a is! SpotubeLocalTrackObject && b is SpotubeLocalTrackObject)) {
return false; return false;
} }
return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject return a is SpotubeLocalTrackObject && b is SpotubeLocalTrackObject
? (a).path == (b).path ? a.path == b.path
: a.id == b.id; : a.id == b.id;
} }

View File

@ -214,7 +214,7 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
/// Root directory where all metadata plugins are stored. /// Root directory where all metadata plugins are stored.
Future<Directory> _getPluginRootDir() async => Directory( Future<Directory> _getPluginRootDir() async => Directory(
join( join(
(await getApplicationCacheDirectory()).path, (await getApplicationSupportDirectory()).path,
"metadata-plugins", "metadata-plugins",
), ),
); );

View File

@ -166,7 +166,7 @@ class TrackOptionsActions {
} }
break; break;
case TrackOptionValue.playNext: case TrackOptionValue.playNext:
playback.addTracksAtFirst([track]); await playback.addTracksAtFirst([track]);
if (context.mounted) { if (context.mounted) {
showToast( showToast(

View File

@ -57,6 +57,7 @@ abstract class AudioPlayerInterface {
title: "Spotube", title: "Spotube",
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error, logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
bufferSize: 4 * 1024 * 1024, // 4MB buffer bufferSize: 4 * 1024 * 1024, // 4MB buffer
async: true,
), ),
) { ) {
_mkPlayer.stream.error.listen((event) { _mkPlayer.stream.error.listen((event) {

View File

@ -121,9 +121,23 @@ class CustomPlayer extends Player {
NativePlayer get nativePlayer => platform as NativePlayer; NativePlayer get nativePlayer => platform as NativePlayer;
Future<void> insert(int index, Media media) async { Future<void> insert(int index, Media media) async {
await add(media); final addedMediaCompleter = Completer<int>();
await Future.delayed(const Duration(milliseconds: 100)); final playlistStream = stream.playlist.listen(
await move(state.playlist.medias.length - 1, index); (event) {
final mediaAddedIndex =
event.medias.indexWhere((m) => m.uri == media.uri);
if (mediaAddedIndex != -1 && !addedMediaCompleter.isCompleted) {
addedMediaCompleter.complete(mediaAddedIndex);
}
},
);
try {
await add(media);
final mediaAddedIndex = await addedMediaCompleter.future;
await move(mediaAddedIndex, index);
} finally {
playlistStream.cancel();
}
} }
Future<void> setAudioNormalization(bool normalize) async { Future<void> setAudioNormalization(bool normalize) async {

View File

@ -1,8 +1,12 @@
import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/enums.dart';
@ -32,11 +36,68 @@ class DABMusicSourcedTrack extends SourcedTrack {
required Ref ref, required Ref ref,
}) async { }) async {
try { try {
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(query.id))
..limit(1)
..orderBy([
(s) => OrderingTerm(
expression: s.createdAt,
mode: OrderingMode.desc,
),
]))
.get()
.then((s) => s.firstOrNull);
if (cachedSource != null &&
cachedSource.sourceType == SourceType.dabMusic) {
final json = jsonDecode(cachedSource.sourceId);
final info = TrackSourceInfo.fromJson(json["info"]);
final source = (json["sources"] as List?)
?.map((s) => TrackSource.fromJson(s))
.toList();
final [updatedSource] = await fetchSources(
info.id,
ref.read(userPreferencesProvider).audioQuality,
const AudioQuality(
isHiRes: true,
maximumBitDepth: 16,
maximumSamplingRate: 44.1,
),
);
return DABMusicSourcedTrack(
ref: ref,
source: AudioSource.dabMusic,
siblings: [],
info: info,
query: query,
sources: [
source!.first.copyWith(url: updatedSource.url),
],
);
}
final siblings = await fetchSiblings(ref: ref, query: query); final siblings = await fetchSiblings(ref: ref, query: query);
if (siblings.isEmpty) { if (siblings.isEmpty) {
throw TrackNotFoundError(query); throw TrackNotFoundError(query);
} }
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: jsonEncode({
"info": siblings.first.info.toJson(),
"sources": (siblings.first.source ?? [])
.map((s) => s.toJson())
.toList(),
}),
sourceType: const Value(SourceType.dabMusic),
),
);
return DABMusicSourcedTrack( return DABMusicSourcedTrack(
ref: ref, ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(), siblings: siblings.map((s) => s.info).skip(1).toList(),
@ -207,6 +268,23 @@ class DABMusicSourcedTrack extends SourcedTrack {
), ),
); );
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: jsonEncode({
"info": newSourceInfo.toJson(),
"sources": source.map((s) => s.toJson()).toList(),
}),
sourceType: const Value(SourceType.dabMusic),
// Because we're sorting by createdAt in the query
// we have to update it to indicate priority
createdAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
return DABMusicSourcedTrack( return DABMusicSourcedTrack(
ref: ref, ref: ref,
siblings: newSiblings, siblings: newSiblings,

View File

@ -1412,10 +1412,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: invidious name: invidious
sha256: "27ef3a001df875665de15535dbc9099f44d12a59480018fb1e17377d4af0308d" sha256: "0da8ebc4c4110057f03302bbd54514b10642154d7be569e7994172f2202dcfe8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.1" version: "0.1.2"
io: io:
dependency: "direct dev" dependency: "direct dev"
description: description:

View File

@ -81,7 +81,7 @@ dependencies:
http: ^1.2.1 http: ^1.2.1
image_picker: ^1.1.0 image_picker: ^1.1.0
intl: any intl: any
invidious: ^0.1.1 invidious: ^0.1.2
jiosaavn: ^0.1.0 jiosaavn: ^0.1.0
json_annotation: ^4.8.1 json_annotation: ^4.8.1
local_notifier: ^0.1.6 local_notifier: ^0.1.6