fix: loading albums and songs

This commit is contained in:
Matyáš Caras 2024-05-24 00:10:11 +02:00
parent 139844596a
commit 2461c82e1d
Signed by: hernik
GPG key ID: 2A3175F98820C5C6
16 changed files with 241 additions and 100 deletions

View file

@ -5,11 +5,11 @@ meta {
} }
get { get {
url: {{baseUrl}}/rest/getMusicDirectory?id=1aee72dc6749f4cb906f7d740ee23b78 url: {{baseUrl}}/rest/getMusicDirectory?id=615eb32e4ca36d6ab405852a2842f6b6
body: none body: none
auth: none auth: none
} }
query { query {
id: 1aee72dc6749f4cb906f7d740ee23b78 id: 615eb32e4ca36d6ab405852a2842f6b6
} }

View file

@ -1,7 +1,10 @@
import 'package:fast_cached_network_image/fast_cached_network_image.dart';
import 'package:flutter/material.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:just_audio_background/just_audio_background.dart';
import 'package:ocarina/api/subsonic/song.dart'; import 'package:ocarina/api/subsonic/song.dart';
import 'package:ocarina/main.dart'; import 'package:ocarina/main.dart';
import 'package:ocarina/util/util.dart';
/// Service used to control the audio player /// Service used to control the audio player
class AudioPlayerService { class AudioPlayerService {
@ -15,19 +18,57 @@ class AudioPlayerService {
AudioPlayerService._internal(); AudioPlayerService._internal();
/// The [AudioPlayer] instance /// The [AudioPlayer] instance
final player = AudioPlayer(); final _player = AudioPlayer();
/// True if [AudioPlayer] instance is playing
bool get isPlaying => _player.playing;
/// Currently playing song /// Currently playing song
/// ///
/// Null if no song is loaded /// Null if no song is loaded
Song? song; Song? get song => _song;
set song(Song? s) {
_song = s;
logger.d("CHANGE song");
songNotifier.value = s;
_setColorScheme();
}
Song? _song;
/// Pauses playback
Future<void> pause() async {
await _player.pause();
logger.d("Paused");
}
/// Resumes playback
void resume() {
_player.play();
logger.d("Playing");
}
/// Sets color scheme from image
Future<void> _setColorScheme() async {
if (AudioPlayerService().song == null) {
themeNotifier.value = ColorScheme.fromSeed(seedColor: Colors.deepPurple);
}
themeNotifier.value = await ColorScheme.fromImageProvider(
provider: FastCachedImageProvider(
AudioPlayerService().song!.coverArtUrl,
),
);
logger.d(AudioPlayerService().song!.coverArtUrl);
}
/// Plays the passed [Song] as a file /// Plays the passed [Song] as a file
Future<void> playFile({Song? song}) async { Future<void> playFile({Song? song}) async {
final doCache = sp.getBool("doCache") ?? true; final doCache = sp.getBool("doCache") ?? true;
if (song == null && this.song == null) return; if (song == null && this.song == null) return;
song ??= this.song; song ??= this.song;
await player.setAudioSource( await _player.setAudioSource(
doCache doCache
? LockCachingAudioSource( ? LockCachingAudioSource(
Uri.parse(song!.streamUrl), Uri.parse(song!.streamUrl),
@ -50,7 +91,8 @@ class AudioPlayerService {
), ),
), ),
); );
await player.seek(Duration.zero); await _player.seek(Duration.zero);
await player.play(); playerKey.currentState?.update();
resume();
} }
} }

View file

@ -7,7 +7,7 @@ class Album {
Album({ Album({
required this.id, required this.id,
required this.name, required this.name,
required String coverArtId, required this.coverArtId,
required this.playCount, required this.playCount,
required this.artistName, required this.artistName,
required this.artistId, required this.artistId,
@ -15,12 +15,13 @@ class Album {
required this.songCount, required this.songCount,
required this.genres, required this.genres,
required this.duration, required this.duration,
}) : _coverArtId = coverArtId; });
final String id; final String id;
final String name; final String name;
@JsonKey(name: "coverArt") @JsonKey(name: "coverArt")
final String _coverArtId; final String coverArtId;
@JsonKey(defaultValue: 0)
final int playCount; final int playCount;
@JsonKey(name: "artist") @JsonKey(name: "artist")
final String artistName; final String artistName;
@ -30,7 +31,7 @@ class Album {
final List<String> genres; final List<String> genres;
final int duration; final int duration;
String get coverArtUrl => SubsonicApiService().getCoverArtUrl(_coverArtId); String get coverArtUrl => SubsonicApiService().getCoverArtUrl(coverArtId);
factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json); factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json);

View file

@ -10,7 +10,7 @@ Album _$AlbumFromJson(Map<String, dynamic> json) => Album(
id: json['id'] as String, id: json['id'] as String,
name: json['name'] as String, name: json['name'] as String,
coverArtId: json['coverArt'] as String, coverArtId: json['coverArt'] as String,
playCount: (json['playCount'] as num).toInt(), playCount: (json['playCount'] as num?)?.toInt() ?? 0,
artistName: json['artist'] as String, artistName: json['artist'] as String,
artistId: json['artistId'] as String, artistId: json['artistId'] as String,
year: (json['year'] as num).toInt(), year: (json['year'] as num).toInt(),
@ -23,7 +23,7 @@ Album _$AlbumFromJson(Map<String, dynamic> json) => Album(
Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{ Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'coverArt': instance._coverArtId, 'coverArt': instance.coverArtId,
'playCount': instance.playCount, 'playCount': instance.playCount,
'artist': instance.artistName, 'artist': instance.artistName,
'artistId': instance.artistId, 'artistId': instance.artistId,

View file

@ -15,9 +15,9 @@ class Song {
required this.bitRate, required this.bitRate,
required this.contentType, required this.contentType,
required this.fileType, required this.fileType,
required String coverArtId, required this.coverArtId,
this.trackNumber, this.trackNumber,
}) : _coverArtId = coverArtId; });
factory Song.fromJson(Map<String, dynamic> json) => _$SongFromJson(json); factory Song.fromJson(Map<String, dynamic> json) => _$SongFromJson(json);
@ -25,7 +25,7 @@ class Song {
final String id; final String id;
@JsonKey(name: "artist") @JsonKey(name: "artist")
final String artistName; final String artistName;
final String artistId; final String? artistId;
final String albumId; final String albumId;
final String title; final String title;
@JsonKey(name: "album") @JsonKey(name: "album")
@ -38,8 +38,8 @@ class Song {
@JsonKey(name: "suffix") @JsonKey(name: "suffix")
final String fileType; final String fileType;
@JsonKey(name: "coverArt") @JsonKey(name: "coverArt")
final String _coverArtId; final String coverArtId;
String get coverArtUrl => SubsonicApiService().getCoverArtUrl(_coverArtId); String get coverArtUrl => SubsonicApiService().getCoverArtUrl(coverArtId);
String get streamUrl => SubsonicApiService().getStreamUrl(id); String get streamUrl => SubsonicApiService().getStreamUrl(id);
} }

View file

@ -12,7 +12,7 @@ Song _$SongFromJson(Map<String, dynamic> json) => Song(
title: json['title'] as String, title: json['title'] as String,
albumName: json['album'] as String, albumName: json['album'] as String,
albumId: json['albumId'] as String, albumId: json['albumId'] as String,
artistId: json['artistId'] as String, artistId: json['artistId'] as String?,
duration: (json['duration'] as num).toInt(), duration: (json['duration'] as num).toInt(),
bitRate: (json['bitRate'] as num).toInt(), bitRate: (json['bitRate'] as num).toInt(),
contentType: json['contentType'] as String, contentType: json['contentType'] as String,
@ -33,5 +33,5 @@ Map<String, dynamic> _$SongToJson(Song instance) => <String, dynamic>{
'bitRate': instance.bitRate, 'bitRate': instance.bitRate,
'contentType': instance.contentType, 'contentType': instance.contentType,
'suffix': instance.fileType, 'suffix': instance.fileType,
'coverArt': instance._coverArtId, 'coverArt': instance.coverArtId,
}; };

View file

@ -105,7 +105,10 @@ class SubsonicApiService {
try { try {
albums.add(Album.fromJson(albumData)); albums.add(Album.fromJson(albumData));
} catch (e) { } catch (e) {
logger.e(e); logger
..e("Error for $artistId")
..e(e)
..e(albumData);
continue; continue;
} }
} }
@ -129,7 +132,10 @@ class SubsonicApiService {
try { try {
songs.add(Song.fromJson(songData)); songs.add(Song.fromJson(songData));
} catch (e) { } catch (e) {
logger.e(e); logger
..e("Error for $albumId")
..e(e)
..e(songData);
continue; continue;
} }
} }

View file

@ -2,6 +2,7 @@ import 'package:fast_cached_network_image/fast_cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:just_audio_background/just_audio_background.dart'; import 'package:just_audio_background/just_audio_background.dart';
import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:ocarina/api/subsonic/song.dart';
import 'package:ocarina/views/home_view.dart'; import 'package:ocarina/views/home_view.dart';
import 'package:ocarina/widgets/player.dart'; import 'package:ocarina/widgets/player.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -22,6 +23,7 @@ void main() async {
clearCacheAfter: const Duration(days: 31), clearCacheAfter: const Duration(days: 31),
subDir: (await getApplicationCacheDirectory()).path, subDir: (await getApplicationCacheDirectory()).path,
); );
sp = await SharedPreferences.getInstance(); sp = await SharedPreferences.getInstance();
runApp(const MyApp()); runApp(const MyApp());
} }
@ -32,29 +34,42 @@ final playerKey = GlobalKey<PlayerState>();
/// Instance of [SharedPreferences] used to get shared preferences /// Instance of [SharedPreferences] used to get shared preferences
late final SharedPreferences sp; late final SharedPreferences sp;
/// Notifier to change theme from inside the app
final ValueNotifier<ColorScheme> themeNotifier =
ValueNotifier(ColorScheme.fromSeed(seedColor: Colors.deepPurple));
/// Notifier to change theme from inside the app
final ValueNotifier<Song?> songNotifier = ValueNotifier(null);
/// Main app class
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
/// Main app class
const MyApp({super.key}); const MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ResponsiveSizer( return ResponsiveSizer(
builder: (context, orientation, screenType) { builder: (context, orientation, screenType) {
return MaterialApp( return ValueListenableBuilder<ColorScheme>(
title: 'Ocarina', valueListenable: themeNotifier,
theme: ThemeData( builder: (BuildContext context, ColorScheme value, Widget? child) {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), return MaterialApp(
useMaterial3: true, title: 'Ocarina',
), theme: ThemeData(
home: const HomeView(), colorScheme: value,
builder: (context, child) { useMaterial3: true,
return Stack( ),
children: [ home: const HomeView(),
child!, builder: (context, child) {
Player( return Stack(
key: playerKey, children: [
), child!,
], Player(
key: playerKey,
),
],
);
},
); );
}, },
); );

View file

@ -1,10 +1,16 @@
import 'dart:convert';
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:crypto/crypto.dart';
import 'package:fast_cached_network_image/fast_cached_network_image.dart'; import 'package:fast_cached_network_image/fast_cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:ocarina/api/audio/audioplayer_service.dart'; import 'package:ocarina/api/audio/audioplayer_service.dart';
import 'package:ocarina/api/subsonic/song.dart';
import 'package:ocarina/main.dart';
import 'package:ocarina/util/util.dart'; import 'package:ocarina/util/util.dart';
import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:text_scroll/text_scroll.dart';
/// The player widget /// The player widget
/// ///
@ -22,7 +28,7 @@ class Player extends StatefulWidget {
/// State of [Player] /// State of [Player]
class PlayerState extends State<Player> { class PlayerState extends State<Player> {
void update() { void update() {
logger.d(AudioPlayerService().song?.title); logger.d(AudioPlayerService().song?.coverArtUrl);
setState(() {}); setState(() {});
} }
@ -69,46 +75,19 @@ class PlayerState extends State<Player> {
width: 100.w, width: 100.w,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Row( child: ValueListenableBuilder<Song?>(
children: [ valueListenable: songNotifier,
SizedBox( builder: (c, t, w) {
height: 10.h, return Row(
width: 10.h, children: [
child: Padding( SizedBox(
padding: const EdgeInsets.all(8), height: 10.h,
child: ClipRRect( width: 10.h,
child: (AudioPlayerService().song == null) child: Padding(
? ColoredBox( padding: const EdgeInsets.all(8),
color: Theme.of(context) child: ClipRRect(
.colorScheme child: (t == null)
.primaryContainer, ? ColoredBox(
child: Center(
child: Icon(
Icons.music_note,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
)
: (FastCachedImage(
url: AudioPlayerService()
.song!
.coverArtUrl,
loadingBuilder: (c, d) =>
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor:
Colors.grey.shade100,
child: Container(
color: Colors.grey,
),
),
errorBuilder: (c, _, __) {
logger
..e(_)
..e(__);
return ColoredBox(
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.primaryContainer, .primaryContainer,
@ -120,28 +99,106 @@ class PlayerState extends State<Player> {
.onPrimaryContainer, .onPrimaryContainer,
), ),
), ),
); )
}, : FastCachedImage(
)), key: Key(
md5
.convert(
utf8.encode(
t.coverArtUrl,
),
)
.toString(),
),
url: t.coverArtUrl,
loadingBuilder: (c, d) =>
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor:
Colors.grey.shade100,
child: Container(
color: Colors.grey,
),
),
errorBuilder: (c, _, __) {
logger
..e(_)
..e(__);
return ColoredBox(
color: Theme.of(context)
.colorScheme
.primaryContainer,
child: Center(
child: Icon(
Icons.music_note,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
);
},
),
),
),
), ),
), const SizedBox(
), width: 5,
const SizedBox( ),
width: 5, Expanded(
), child: AutoSizeText(
AutoSizeText( AudioPlayerService().song == null
AudioPlayerService().song == null ? "Nothing"
? "Nothing" : AudioPlayerService().song!.title,
: AudioPlayerService().song!.title, style: TextStyle(
style: TextStyle( color: Theme.of(context)
color: Theme.of(context) .colorScheme
.colorScheme .onPrimaryContainer,
.onPrimaryContainer, fontSize: 14,
fontSize: 14, decoration: TextDecoration.none,
decoration: TextDecoration.none, ),
), overflowReplacement: TextScroll(
), AudioPlayerService().song == null
], ? "Nothing"
: AudioPlayerService().song!.title,
),
),
),
SizedBox(
width: 30.w,
child: Row(
children: [
IconButton(
onPressed: () async {
if (AudioPlayerService().song ==
null) {
return;
}
if (AudioPlayerService().isPlaying) {
await AudioPlayerService().pause();
} else {
AudioPlayerService().resume();
}
setState(() {});
},
icon: AnimatedCrossFade(
firstChild:
const Icon(Icons.play_arrow),
secondChild: const Icon(Icons.pause),
crossFadeState:
(AudioPlayerService().isPlaying)
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration:
const Duration(milliseconds: 300),
),
),
],
),
),
],
);
},
), ),
), ),
), ),

View file

@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View file

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_linux flutter_secure_storage_linux
media_kit_libs_linux media_kit_libs_linux
) )

View file

@ -7,6 +7,7 @@ import Foundation
import audio_service import audio_service
import audio_session import audio_session
import dynamic_color
import flutter_secure_storage_macos import flutter_secure_storage_macos
import just_audio import just_audio
import path_provider_foundation import path_provider_foundation
@ -16,6 +17,7 @@ import sqflite
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View file

@ -233,6 +233,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.3+1" version: "5.4.3+1"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
url: "https://pub.dev"
source: hosted
version: "1.7.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:

View file

@ -54,6 +54,7 @@ dependencies:
json_serializable: ^6.8.0 json_serializable: ^6.8.0
json_annotation: ^4.9.0 json_annotation: ^4.9.0
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
dynamic_color: ^1.7.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.9 build_runner: ^2.4.9

View file

@ -6,10 +6,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h> #include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar( FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(

View file

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color
flutter_secure_storage_windows flutter_secure_storage_windows
media_kit_libs_windows_audio media_kit_libs_windows_audio
) )