filter music by directory; ArtistDetailsPage added
This commit is contained in:
parent
26756bb0d5
commit
f0fbf8e526
11 changed files with 183 additions and 11 deletions
|
@ -8,6 +8,7 @@ import '../entities/song.dart';
|
||||||
abstract class MusicDataRepository {
|
abstract class MusicDataRepository {
|
||||||
Stream<List<Song>> get songStream;
|
Stream<List<Song>> get songStream;
|
||||||
Stream<List<Song>> getAlbumSongStream(Album album);
|
Stream<List<Song>> getAlbumSongStream(Album album);
|
||||||
|
Stream<List<Album>> getArtistAlbumStream(Artist artist);
|
||||||
|
|
||||||
Future<Either<Failure, List<Song>>> getSongs();
|
Future<Either<Failure, List<Song>>> getSongs();
|
||||||
Future<Either<Failure, List<Song>>> getSongsFromAlbum(Album album);
|
Future<Either<Failure, List<Song>>> getSongsFromAlbum(Album album);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:just_audio/just_audio.dart' as ja;
|
||||||
import 'domain/repositories/audio_repository.dart';
|
import 'domain/repositories/audio_repository.dart';
|
||||||
import 'domain/repositories/music_data_repository.dart';
|
import 'domain/repositories/music_data_repository.dart';
|
||||||
import 'domain/repositories/persistent_player_state_repository.dart';
|
import 'domain/repositories/persistent_player_state_repository.dart';
|
||||||
|
import 'domain/repositories/settings_repository.dart';
|
||||||
import 'presentation/state/audio_store.dart';
|
import 'presentation/state/audio_store.dart';
|
||||||
import 'presentation/state/music_data_store.dart';
|
import 'presentation/state/music_data_store.dart';
|
||||||
import 'presentation/state/navigation_store.dart';
|
import 'presentation/state/navigation_store.dart';
|
||||||
|
@ -21,9 +22,11 @@ import 'system/datasources/local_music_fetcher_contract.dart';
|
||||||
import 'system/datasources/moor_music_data_source.dart';
|
import 'system/datasources/moor_music_data_source.dart';
|
||||||
import 'system/datasources/music_data_source_contract.dart';
|
import 'system/datasources/music_data_source_contract.dart';
|
||||||
import 'system/datasources/player_state_data_source.dart';
|
import 'system/datasources/player_state_data_source.dart';
|
||||||
|
import 'system/datasources/settings_data_source.dart';
|
||||||
import 'system/repositories/audio_repository_impl.dart';
|
import 'system/repositories/audio_repository_impl.dart';
|
||||||
import 'system/repositories/music_data_repository_impl.dart';
|
import 'system/repositories/music_data_repository_impl.dart';
|
||||||
import 'system/repositories/persistent_player_state_repository_impl.dart';
|
import 'system/repositories/persistent_player_state_repository_impl.dart';
|
||||||
|
import 'system/repositories/settings_repository_impl.dart';
|
||||||
|
|
||||||
final GetIt getIt = GetIt.instance;
|
final GetIt getIt = GetIt.instance;
|
||||||
|
|
||||||
|
@ -35,6 +38,7 @@ Future<void> setupGetIt() async {
|
||||||
() {
|
() {
|
||||||
final musicDataStore = MusicDataStore(
|
final musicDataStore = MusicDataStore(
|
||||||
musicDataRepository: getIt(),
|
musicDataRepository: getIt(),
|
||||||
|
settingsRepository: getIt(),
|
||||||
);
|
);
|
||||||
musicDataStore.init();
|
musicDataStore.init();
|
||||||
return musicDataStore;
|
return musicDataStore;
|
||||||
|
@ -71,15 +75,18 @@ Future<void> setupGetIt() async {
|
||||||
getIt.registerLazySingleton<PlayerStateRepository>(
|
getIt.registerLazySingleton<PlayerStateRepository>(
|
||||||
() => PlayerStateRepositoryImpl(getIt()),
|
() => PlayerStateRepositoryImpl(getIt()),
|
||||||
);
|
);
|
||||||
|
getIt.registerLazySingleton<SettingsRepository>(() => SettingsRepositoryImpl(getIt()));
|
||||||
|
|
||||||
// data sources
|
// data sources
|
||||||
final MoorMusicDataSource moorMusicDataSource = MoorMusicDataSource();
|
final MoorMusicDataSource moorMusicDataSource = MoorMusicDataSource();
|
||||||
getIt.registerLazySingleton<MusicDataSource>(() => moorMusicDataSource);
|
getIt.registerLazySingleton<MusicDataSource>(() => moorMusicDataSource);
|
||||||
getIt.registerLazySingleton<PlayerStateDataSource>(() => moorMusicDataSource.playerStateDao);
|
getIt.registerLazySingleton<PlayerStateDataSource>(() => moorMusicDataSource.playerStateDao);
|
||||||
|
getIt.registerLazySingleton<SettingsDataSource>(() => moorMusicDataSource.settingsDao);
|
||||||
getIt.registerLazySingleton<LocalMusicFetcher>(
|
getIt.registerLazySingleton<LocalMusicFetcher>(
|
||||||
() => LocalMusicFetcherImpl(
|
() => LocalMusicFetcherImpl(
|
||||||
getIt(),
|
getIt(),
|
||||||
getIt(),
|
getIt(),
|
||||||
|
getIt(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
getIt.registerLazySingleton<AudioManager>(() => AudioManagerImpl(getIt()));
|
getIt.registerLazySingleton<AudioManager>(() => AudioManagerImpl(getIt()));
|
||||||
|
|
52
lib/presentation/pages/artist_details_page.dart
Normal file
52
lib/presentation/pages/artist_details_page.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../domain/entities/album.dart';
|
||||||
|
import '../../domain/entities/artist.dart';
|
||||||
|
import '../state/music_data_store.dart';
|
||||||
|
import '../widgets/album_art_list_tile.dart';
|
||||||
|
import 'album_details_page.dart';
|
||||||
|
|
||||||
|
class ArtistDetailsPage extends StatelessWidget {
|
||||||
|
const ArtistDetailsPage({Key key, @required this.artist}) : super(key: key);
|
||||||
|
|
||||||
|
final Artist artist;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final MusicDataStore musicDataStore = Provider.of<MusicDataStore>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Observer(
|
||||||
|
builder: (BuildContext context) => ListView.separated(
|
||||||
|
itemCount: musicDataStore.artistAlbumStream.value.length,
|
||||||
|
separatorBuilder: (BuildContext context, int index) => const Divider(
|
||||||
|
height: 4.0,
|
||||||
|
),
|
||||||
|
itemBuilder: (_, int index) {
|
||||||
|
final Album album = musicDataStore.artistAlbumStream.value[index];
|
||||||
|
return AlbumArtListTile(
|
||||||
|
title: album.title,
|
||||||
|
subtitle: album.pubYear.toString(),
|
||||||
|
albumArtPath: album.albumArtPath,
|
||||||
|
onTap: () {
|
||||||
|
musicDataStore.fetchSongsFromAlbum(album);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<Widget>(
|
||||||
|
builder: (BuildContext context) => AlbumDetailsPage(
|
||||||
|
album: album,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../domain/entities/artist.dart';
|
import '../../domain/entities/artist.dart';
|
||||||
import '../state/music_data_store.dart';
|
import '../state/music_data_store.dart';
|
||||||
|
import 'artist_details_page.dart';
|
||||||
|
|
||||||
class ArtistsPage extends StatefulWidget {
|
class ArtistsPage extends StatefulWidget {
|
||||||
const ArtistsPage({Key key}) : super(key: key);
|
const ArtistsPage({Key key}) : super(key: key);
|
||||||
|
@ -40,6 +41,17 @@ class _ArtistsPageState extends State<ArtistsPage>
|
||||||
final Artist artist = artists[index];
|
final Artist artist = artists[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(artist.name),
|
title: Text(artist.name),
|
||||||
|
onTap: () {
|
||||||
|
musicDataStore.fetchAlbumsFromArtist(artist);
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute<Widget>(
|
||||||
|
builder: (BuildContext context) => ArtistDetailsPage(
|
||||||
|
artist: artist,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (BuildContext context, int index) => const Divider(
|
separatorBuilder: (BuildContext context, int index) => const Divider(
|
||||||
|
|
|
@ -32,6 +32,9 @@ abstract class _MusicDataStore with Store {
|
||||||
@observable
|
@observable
|
||||||
ObservableStream<List<Song>> albumSongStream;
|
ObservableStream<List<Song>> albumSongStream;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
ObservableStream<List<Album>> artistAlbumStream;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
ObservableList<Artist> artists = <Artist>[].asObservable();
|
ObservableList<Artist> artists = <Artist>[].asObservable();
|
||||||
@observable
|
@observable
|
||||||
|
@ -127,6 +130,11 @@ abstract class _MusicDataStore with Store {
|
||||||
albumSongStream = _musicDataRepository.getAlbumSongStream(album).asObservable(initialValue: []);
|
albumSongStream = _musicDataRepository.getAlbumSongStream(album).asObservable(initialValue: []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> fetchAlbumsFromArtist(Artist artist) async {
|
||||||
|
artistAlbumStream = _musicDataRepository.getArtistAlbumStream(artist).asObservable(initialValue: []);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setSongBlocked(Song song, bool blocked) async {
|
Future<void> setSongBlocked(Song song, bool blocked) async {
|
||||||
await _musicDataRepository.setSongBlocked(song, blocked);
|
await _musicDataRepository.setSongBlocked(song, blocked);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,22 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _$artistAlbumStreamAtom =
|
||||||
|
Atom(name: '_MusicDataStore.artistAlbumStream');
|
||||||
|
|
||||||
|
@override
|
||||||
|
ObservableStream<List<Album>> get artistAlbumStream {
|
||||||
|
_$artistAlbumStreamAtom.reportRead();
|
||||||
|
return super.artistAlbumStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set artistAlbumStream(ObservableStream<List<Album>> value) {
|
||||||
|
_$artistAlbumStreamAtom.reportWrite(value, super.artistAlbumStream, () {
|
||||||
|
super.artistAlbumStream = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
final _$artistsAtom = Atom(name: '_MusicDataStore.artists');
|
final _$artistsAtom = Atom(name: '_MusicDataStore.artists');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -199,11 +215,21 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
|
||||||
.run(() => super.fetchSongsFromAlbum(album));
|
.run(() => super.fetchSongsFromAlbum(album));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _$fetchAlbumsFromArtistAsyncAction =
|
||||||
|
AsyncAction('_MusicDataStore.fetchAlbumsFromArtist');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> fetchAlbumsFromArtist(Artist artist) {
|
||||||
|
return _$fetchAlbumsFromArtistAsyncAction
|
||||||
|
.run(() => super.fetchAlbumsFromArtist(artist));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''
|
return '''
|
||||||
songStream: ${songStream},
|
songStream: ${songStream},
|
||||||
albumSongStream: ${albumSongStream},
|
albumSongStream: ${albumSongStream},
|
||||||
|
artistAlbumStream: ${artistAlbumStream},
|
||||||
artists: ${artists},
|
artists: ${artists},
|
||||||
isFetchingArtists: ${isFetchingArtists},
|
isFetchingArtists: ${isFetchingArtists},
|
||||||
albums: ${albums},
|
albums: ${albums},
|
||||||
|
|
|
@ -8,11 +8,13 @@ import '../models/album_model.dart';
|
||||||
import '../models/artist_model.dart';
|
import '../models/artist_model.dart';
|
||||||
import '../models/song_model.dart';
|
import '../models/song_model.dart';
|
||||||
import 'local_music_fetcher_contract.dart';
|
import 'local_music_fetcher_contract.dart';
|
||||||
|
import 'settings_data_source.dart';
|
||||||
|
|
||||||
class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
||||||
LocalMusicFetcherImpl(this._flutterAudioQuery, this._deviceInfo);
|
LocalMusicFetcherImpl(this._flutterAudioQuery, this._settingsDataSource, this._deviceInfo);
|
||||||
|
|
||||||
final FlutterAudioQuery _flutterAudioQuery;
|
final FlutterAudioQuery _flutterAudioQuery;
|
||||||
|
final SettingsDataSource _settingsDataSource;
|
||||||
// CODESMELL: should probably encapsulate the deviceinfoplugin
|
// CODESMELL: should probably encapsulate the deviceinfoplugin
|
||||||
final DeviceInfoPlugin _deviceInfo;
|
final DeviceInfoPlugin _deviceInfo;
|
||||||
|
|
||||||
|
@ -58,4 +60,50 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
||||||
}
|
}
|
||||||
return Uint8List(0);
|
return Uint8List(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, List>> getLocalMusic() async {
|
||||||
|
final musicDirectories = await _settingsDataSource.getLibraryFolders();
|
||||||
|
|
||||||
|
final songs = await _getFilteredSongs(musicDirectories);
|
||||||
|
final albumTitles = Set<String>.from(songs.map((song) => song.album));
|
||||||
|
final albums = await _getFilteredAlbums(albumTitles);
|
||||||
|
final artistNames = Set<String>.from(albums.map((album) => album.artist));
|
||||||
|
final artists = await _getFilteredArtists(artistNames);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'SONGS': songs,
|
||||||
|
'ALBUMS': albums,
|
||||||
|
'ARTISTS': artists,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SongModel>> _getFilteredSongs(Iterable<String> musicDirectories) async {
|
||||||
|
final List<SongInfo> songInfoList = await _flutterAudioQuery.getSongs();
|
||||||
|
return songInfoList
|
||||||
|
.where(
|
||||||
|
(songInfo) =>
|
||||||
|
songInfo.isMusic &&
|
||||||
|
musicDirectories.any((element) => songInfo.filePath.startsWith(element)),
|
||||||
|
)
|
||||||
|
.map((SongInfo songInfo) => SongModel.fromSongInfo(songInfo))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<AlbumModel>> _getFilteredAlbums(Iterable<String> albumTitles) async {
|
||||||
|
final List<AlbumInfo> albumInfoList = await _flutterAudioQuery.getAlbums();
|
||||||
|
return albumInfoList
|
||||||
|
.where((albumInfo) => albumTitles.contains(albumInfo.title))
|
||||||
|
.map((AlbumInfo albumInfo) => AlbumModel.fromAlbumInfo(albumInfo))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ArtistModel>> _getFilteredArtists(Iterable<String> artistNames) async {
|
||||||
|
final List<ArtistInfo> artistInfoList = await _flutterAudioQuery.getArtists();
|
||||||
|
return artistInfoList
|
||||||
|
.where((artistInfo) => artistNames.contains(artistInfo.name))
|
||||||
|
.map((ArtistInfo artistInfo) => ArtistModel.fromArtistInfo(artistInfo))
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import '../models/artist_model.dart';
|
||||||
import '../models/song_model.dart';
|
import '../models/song_model.dart';
|
||||||
|
|
||||||
abstract class LocalMusicFetcher {
|
abstract class LocalMusicFetcher {
|
||||||
|
Future<Map<String, List>> getLocalMusic();
|
||||||
|
|
||||||
Future<List<ArtistModel>> getArtists();
|
Future<List<ArtistModel>> getArtists();
|
||||||
Future<List<AlbumModel>> getAlbums();
|
Future<List<AlbumModel>> getAlbums();
|
||||||
Future<List<SongModel>> getSongs();
|
Future<List<SongModel>> getSongs();
|
||||||
|
|
|
@ -149,6 +149,18 @@ class MoorMusicDataSource extends _$MoorMusicDataSource implements MusicDataSour
|
||||||
moorSongList.map((moorSong) => SongModel.fromMoorSong(moorSong)).toList());
|
moorSongList.map((moorSong) => SongModel.fromMoorSong(moorSong)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<AlbumModel>> getArtistAlbumStream(ArtistModel artist) {
|
||||||
|
return (select(albums)
|
||||||
|
..where((tbl) => tbl.artist.equals(artist.name))
|
||||||
|
..orderBy([
|
||||||
|
(t) => OrderingTerm(expression: t.title),
|
||||||
|
]))
|
||||||
|
.watch()
|
||||||
|
.map((moorAlbumList) =>
|
||||||
|
moorAlbumList.map((moorAlbum) => AlbumModel.fromMoorAlbum(moorAlbum)).toList());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<SongModel>> getSongsFromAlbum(AlbumModel album) {
|
Future<List<SongModel>> getSongsFromAlbum(AlbumModel album) {
|
||||||
return (select(songs)
|
return (select(songs)
|
||||||
|
|
|
@ -7,6 +7,7 @@ abstract class MusicDataSource {
|
||||||
|
|
||||||
Stream<List<SongModel>> get songStream;
|
Stream<List<SongModel>> get songStream;
|
||||||
Stream<List<SongModel>> getAlbumSongStream(AlbumModel album);
|
Stream<List<SongModel>> getAlbumSongStream(AlbumModel album);
|
||||||
|
Stream<List<AlbumModel>> getArtistAlbumStream(ArtistModel artist);
|
||||||
|
|
||||||
/// Insert album into the database. Return the ID of the inserted album.
|
/// Insert album into the database. Return the ID of the inserted album.
|
||||||
Future<int> insertAlbum(AlbumModel albumModel);
|
Future<int> insertAlbum(AlbumModel albumModel);
|
||||||
|
|
|
@ -61,9 +61,11 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
||||||
Future<void> updateDatabase() async {
|
Future<void> updateDatabase() async {
|
||||||
_log.info('updateDatabase called');
|
_log.info('updateDatabase called');
|
||||||
|
|
||||||
await updateArtists();
|
final localMusic = await localMusicFetcher.getLocalMusic();
|
||||||
final albumIdMap = await updateAlbums();
|
|
||||||
await updateSongs(albumIdMap);
|
await updateArtists(localMusic['ARTISTS'] as List<ArtistModel>);
|
||||||
|
final albumIdMap = await updateAlbums(localMusic['ALBUMS'] as List<AlbumModel>);
|
||||||
|
await updateSongs(localMusic['SONGS'] as List<SongModel>, albumIdMap);
|
||||||
|
|
||||||
_log.info('updateDatabase finished');
|
_log.info('updateDatabase finished');
|
||||||
}
|
}
|
||||||
|
@ -73,18 +75,15 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
||||||
await musicDataSource.setSongBlocked(song as SongModel, blocked);
|
await musicDataSource.setSongBlocked(song as SongModel, blocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateArtists() async {
|
Future<void> updateArtists(List<ArtistModel> artists) async {
|
||||||
await musicDataSource.deleteAllArtists();
|
await musicDataSource.deleteAllArtists();
|
||||||
final List<ArtistModel> artists = await localMusicFetcher.getArtists();
|
|
||||||
|
|
||||||
for (final ArtistModel artist in artists) {
|
for (final ArtistModel artist in artists) {
|
||||||
await musicDataSource.insertArtist(artist);
|
await musicDataSource.insertArtist(artist);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<int, int>> updateAlbums() async {
|
Future<Map<int, int>> updateAlbums(List<AlbumModel> albums) async {
|
||||||
await musicDataSource.deleteAllAlbums();
|
await musicDataSource.deleteAllAlbums();
|
||||||
final List<AlbumModel> albums = await localMusicFetcher.getAlbums();
|
|
||||||
final Map<int, int> albumIdMap = {};
|
final Map<int, int> albumIdMap = {};
|
||||||
|
|
||||||
final Directory dir = await getApplicationSupportDirectory();
|
final Directory dir = await getApplicationSupportDirectory();
|
||||||
|
@ -111,9 +110,8 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
||||||
return albumIdMap;
|
return albumIdMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateSongs(Map<int, int> albumIdMap) async {
|
Future<void> updateSongs(List<SongModel> songs, Map<int, int> albumIdMap) async {
|
||||||
final Directory dir = await getApplicationSupportDirectory();
|
final Directory dir = await getApplicationSupportDirectory();
|
||||||
final List<SongModel> songs = await localMusicFetcher.getSongs();
|
|
||||||
|
|
||||||
final List<SongModel> songsToInsert = [];
|
final List<SongModel> songsToInsert = [];
|
||||||
for (final SongModel song in songs) {
|
for (final SongModel song in songs) {
|
||||||
|
@ -133,4 +131,9 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
|
||||||
Future<void> toggleNextSongLink(Song song) async {
|
Future<void> toggleNextSongLink(Song song) async {
|
||||||
musicDataSource.toggleNextSongLink(song as SongModel);
|
musicDataSource.toggleNextSongLink(song as SongModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<Album>> getArtistAlbumStream(Artist artist) {
|
||||||
|
return musicDataSource.getArtistAlbumStream(artist as ArtistModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue