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 {
url: {{baseUrl}}/rest/getMusicDirectory?id=1aee72dc6749f4cb906f7d740ee23b78
url: {{baseUrl}}/rest/getMusicDirectory?id=615eb32e4ca36d6ab405852a2842f6b6
body: none
auth: none
}
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_background/just_audio_background.dart';
import 'package:ocarina/api/subsonic/song.dart';
import 'package:ocarina/main.dart';
import 'package:ocarina/util/util.dart';
/// Service used to control the audio player
class AudioPlayerService {
@ -15,19 +18,57 @@ class AudioPlayerService {
AudioPlayerService._internal();
/// The [AudioPlayer] instance
final player = AudioPlayer();
final _player = AudioPlayer();
/// True if [AudioPlayer] instance is playing
bool get isPlaying => _player.playing;
/// Currently playing song
///
/// 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
Future<void> playFile({Song? song}) async {
final doCache = sp.getBool("doCache") ?? true;
if (song == null && this.song == null) return;
song ??= this.song;
await player.setAudioSource(
await _player.setAudioSource(
doCache
? LockCachingAudioSource(
Uri.parse(song!.streamUrl),
@ -50,7 +91,8 @@ class AudioPlayerService {
),
),
);
await player.seek(Duration.zero);
await player.play();
await _player.seek(Duration.zero);
playerKey.currentState?.update();
resume();
}
}

View file

@ -7,7 +7,7 @@ class Album {
Album({
required this.id,
required this.name,
required String coverArtId,
required this.coverArtId,
required this.playCount,
required this.artistName,
required this.artistId,
@ -15,12 +15,13 @@ class Album {
required this.songCount,
required this.genres,
required this.duration,
}) : _coverArtId = coverArtId;
});
final String id;
final String name;
@JsonKey(name: "coverArt")
final String _coverArtId;
final String coverArtId;
@JsonKey(defaultValue: 0)
final int playCount;
@JsonKey(name: "artist")
final String artistName;
@ -30,7 +31,7 @@ class Album {
final List<String> genres;
final int duration;
String get coverArtUrl => SubsonicApiService().getCoverArtUrl(_coverArtId);
String get coverArtUrl => SubsonicApiService().getCoverArtUrl(coverArtId);
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,
name: json['name'] 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,
artistId: json['artistId'] as String,
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>{
'id': instance.id,
'name': instance.name,
'coverArt': instance._coverArtId,
'coverArt': instance.coverArtId,
'playCount': instance.playCount,
'artist': instance.artistName,
'artistId': instance.artistId,

View file

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

View file

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

View file

@ -105,7 +105,10 @@ class SubsonicApiService {
try {
albums.add(Album.fromJson(albumData));
} catch (e) {
logger.e(e);
logger
..e("Error for $artistId")
..e(e)
..e(albumData);
continue;
}
}
@ -129,7 +132,10 @@ class SubsonicApiService {
try {
songs.add(Song.fromJson(songData));
} catch (e) {
logger.e(e);
logger
..e("Error for $albumId")
..e(e)
..e(songData);
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:just_audio_background/just_audio_background.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/widgets/player.dart';
import 'package:path_provider/path_provider.dart';
@ -22,6 +23,7 @@ void main() async {
clearCacheAfter: const Duration(days: 31),
subDir: (await getApplicationCacheDirectory()).path,
);
sp = await SharedPreferences.getInstance();
runApp(const MyApp());
}
@ -32,18 +34,29 @@ final playerKey = GlobalKey<PlayerState>();
/// Instance of [SharedPreferences] used to get shared preferences
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 {
/// Main app class
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return ResponsiveSizer(
builder: (context, orientation, screenType) {
return ValueListenableBuilder<ColorScheme>(
valueListenable: themeNotifier,
builder: (BuildContext context, ColorScheme value, Widget? child) {
return MaterialApp(
title: 'Ocarina',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
colorScheme: value,
useMaterial3: true,
),
home: const HomeView(),
@ -60,5 +73,7 @@ class MyApp extends StatelessWidget {
);
},
);
},
);
}
}

View file

@ -1,10 +1,16 @@
import 'dart:convert';
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:flutter/material.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:responsive_sizer/responsive_sizer.dart';
import 'package:shimmer/shimmer.dart';
import 'package:text_scroll/text_scroll.dart';
/// The player widget
///
@ -22,7 +28,7 @@ class Player extends StatefulWidget {
/// State of [Player]
class PlayerState extends State<Player> {
void update() {
logger.d(AudioPlayerService().song?.title);
logger.d(AudioPlayerService().song?.coverArtUrl);
setState(() {});
}
@ -69,7 +75,10 @@ class PlayerState extends State<Player> {
width: 100.w,
child: Padding(
padding: const EdgeInsets.all(8),
child: Row(
child: ValueListenableBuilder<Song?>(
valueListenable: songNotifier,
builder: (c, t, w) {
return Row(
children: [
SizedBox(
height: 10.h,
@ -77,7 +86,7 @@ class PlayerState extends State<Player> {
child: Padding(
padding: const EdgeInsets.all(8),
child: ClipRRect(
child: (AudioPlayerService().song == null)
child: (t == null)
? ColoredBox(
color: Theme.of(context)
.colorScheme
@ -91,10 +100,17 @@ class PlayerState extends State<Player> {
),
),
)
: (FastCachedImage(
url: AudioPlayerService()
.song!
.coverArtUrl,
: FastCachedImage(
key: Key(
md5
.convert(
utf8.encode(
t.coverArtUrl,
),
)
.toString(),
),
url: t.coverArtUrl,
loadingBuilder: (c, d) =>
Shimmer.fromColors(
baseColor: Colors.grey.shade300,
@ -122,14 +138,15 @@ class PlayerState extends State<Player> {
),
);
},
)),
),
),
),
),
const SizedBox(
width: 5,
),
AutoSizeText(
Expanded(
child: AutoSizeText(
AudioPlayerService().song == null
? "Nothing"
: AudioPlayerService().song!.title,
@ -140,10 +157,50 @@ class PlayerState extends State<Player> {
fontSize: 14,
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 <dynamic_color/dynamic_color_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View file

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

View file

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

View file

@ -233,6 +233,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View file

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

View file

@ -6,10 +6,13 @@
#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 <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(

View file

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