filter music by directory; ArtistDetailsPage added

This commit is contained in:
Moritz Weber 2020-12-29 21:01:31 +01:00
parent 26756bb0d5
commit f0fbf8e526
11 changed files with 183 additions and 11 deletions

View file

@ -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);

View file

@ -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()));

View 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,
),
),
);
},
);
},
),
),
),
);
}
}

View file

@ -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(

View file

@ -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);
} }

View file

@ -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},

View file

@ -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();
}
} }

View file

@ -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();

View file

@ -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)

View file

@ -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);

View file

@ -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);
}
} }