Merge pull request #30 from moritz-weber/29-support-more-file-types
29 support more file types
This commit is contained in:
commit
6fedee7d94
11 changed files with 174 additions and 62 deletions
|
@ -1,7 +1,7 @@
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audiotagger/audiotagger.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:on_audio_query/on_audio_query.dart';
|
||||
|
||||
import 'domain/actors/audio_player_actor.dart';
|
||||
import 'domain/actors/persistence_actor.dart';
|
||||
|
@ -341,7 +341,7 @@ Future<void> setupGetIt() async {
|
|||
|
||||
getIt.registerFactory<AudioPlayer>(() => AudioPlayer());
|
||||
|
||||
getIt.registerLazySingleton<Audiotagger>(() => Audiotagger());
|
||||
getIt.registerLazySingleton<OnAudioQuery>(() => OnAudioQuery());
|
||||
|
||||
// actors
|
||||
getIt.registerSingleton<PlatformIntegrationActor>(
|
||||
|
|
|
@ -14,8 +14,8 @@ import '../theming.dart';
|
|||
import '../widgets/highlight_album.dart';
|
||||
import '../widgets/highlight_artist.dart';
|
||||
import '../widgets/history_widget.dart';
|
||||
import '../widgets/playlists_widget.dart';
|
||||
import '../widgets/shuffle_all_button.dart';
|
||||
import '../widgets/smart_lists.dart';
|
||||
import 'home_settings_page.dart';
|
||||
import 'settings_page.dart';
|
||||
|
||||
|
@ -54,7 +54,7 @@ class _HomePageInner extends StatelessWidget {
|
|||
final NavigationStore navStore = GetIt.I<NavigationStore>();
|
||||
final MusicDataStore musicDataStore = GetIt.I<MusicDataStore>();
|
||||
|
||||
final songStream = musicDataStore.songStream;
|
||||
|
||||
|
||||
print('HomePage.build');
|
||||
return SafeArea(
|
||||
|
@ -84,7 +84,8 @@ class _HomePageInner extends StatelessWidget {
|
|||
),
|
||||
body: Observer(
|
||||
builder: (context) {
|
||||
if (songStream.value?.isEmpty ?? true) {
|
||||
final songListIsEmpty = musicDataStore.songListIsEmpty;
|
||||
if (songListIsEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
|
@ -160,7 +161,7 @@ Widget _createHomeWidget(HomeWidget homeWidget) {
|
|||
);
|
||||
case HomeWidgetType.playlists:
|
||||
return SliverToBoxAdapter(
|
||||
child: SmartLists(homePlaylists: homeWidget as HomePlaylists),
|
||||
child: PlaylistsWidget(homePlaylists: homeWidget as HomePlaylists),
|
||||
);
|
||||
case HomeWidgetType.history:
|
||||
return SliverToBoxAdapter(
|
||||
|
|
|
@ -64,6 +64,9 @@ abstract class _MusicDataStore with Store {
|
|||
late ObservableStream<Artist?> artistOfDay =
|
||||
_musicDataRepository.artistOfDayStream.asObservable();
|
||||
|
||||
@computed
|
||||
bool get songListIsEmpty => songStream.value?.isEmpty ?? true;
|
||||
|
||||
ObservableStream<List<CustomList>> getCustomLists({
|
||||
required HomePlaylistsOrder orderCriterion,
|
||||
required OrderDirection orderDirection,
|
||||
|
|
|
@ -9,6 +9,14 @@ part of 'music_data_store.dart';
|
|||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||
|
||||
mixin _$MusicDataStore on _MusicDataStore, Store {
|
||||
Computed<bool>? _$songListIsEmptyComputed;
|
||||
|
||||
@override
|
||||
bool get songListIsEmpty =>
|
||||
(_$songListIsEmptyComputed ??= Computed<bool>(() => super.songListIsEmpty,
|
||||
name: '_MusicDataStore.songListIsEmpty'))
|
||||
.value;
|
||||
|
||||
late final _$songStreamAtom =
|
||||
Atom(name: '_MusicDataStore.songStream', context: context);
|
||||
|
||||
|
@ -155,7 +163,8 @@ playlistsStream: ${playlistsStream},
|
|||
smartListsStream: ${smartListsStream},
|
||||
isUpdatingDatabase: ${isUpdatingDatabase},
|
||||
albumOfDay: ${albumOfDay},
|
||||
artistOfDay: ${artistOfDay}
|
||||
artistOfDay: ${artistOfDay},
|
||||
songListIsEmpty: ${songListIsEmpty}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import '../theming.dart';
|
|||
import 'play_shuffle_button.dart';
|
||||
import 'playlist_cover.dart';
|
||||
|
||||
class SmartLists extends StatelessWidget {
|
||||
const SmartLists({Key? key, required this.homePlaylists}) : super(key: key);
|
||||
class PlaylistsWidget extends StatelessWidget {
|
||||
const PlaylistsWidget({Key? key, required this.homePlaylists}) : super(key: key);
|
||||
|
||||
final HomePlaylists homePlaylists;
|
||||
|
||||
|
@ -34,6 +34,7 @@ class SmartLists extends StatelessWidget {
|
|||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
print('PlaylistsWidget.build -> Observer');
|
||||
final customLists = customListsStream.value ?? [];
|
||||
|
||||
return Card(
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:audiotagger/audiotagger.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_fimber/flutter_fimber.dart';
|
||||
import 'package:on_audio_query/on_audio_query.dart' as aq;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../models/album_model.dart';
|
||||
|
@ -14,11 +14,11 @@ import 'music_data_source_contract.dart';
|
|||
import 'settings_data_source.dart';
|
||||
|
||||
class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
||||
LocalMusicFetcherImpl(this._settingsDataSource, this._audiotagger, this._musicDataSource);
|
||||
LocalMusicFetcherImpl(this._settingsDataSource, this._musicDataSource, this._onAudioQuery);
|
||||
|
||||
static final _log = FimberLog('LocalMusicFetcher');
|
||||
|
||||
final Audiotagger _audiotagger;
|
||||
final aq.OnAudioQuery _onAudioQuery;
|
||||
final SettingsDataSource _settingsDataSource;
|
||||
final MusicDataSource _musicDataSource;
|
||||
|
||||
|
@ -30,14 +30,11 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
final musicDirectories = await _settingsDataSource.libraryFoldersStream.first;
|
||||
final libDirs = musicDirectories.map((e) => Directory(e));
|
||||
|
||||
final List<File> files = [];
|
||||
final List<aq.SongModel> aqSongs = [];
|
||||
|
||||
for (final libDir in libDirs) {
|
||||
await for (final entity in libDir.list(recursive: true, followLinks: false)) {
|
||||
if (entity is File && entity.path.endsWith('.mp3')) {
|
||||
files.add(entity);
|
||||
}
|
||||
}
|
||||
await _onAudioQuery.scanMedia(libDir.path);
|
||||
aqSongs.addAll(await _onAudioQuery.querySongs(path: libDir.path));
|
||||
}
|
||||
|
||||
final List<SongModel> songs = [];
|
||||
|
@ -58,14 +55,15 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
|
||||
final Directory dir = await getApplicationSupportDirectory();
|
||||
|
||||
for (final file in files.toSet()) {
|
||||
final fileStats = file.statSync();
|
||||
for (final aqSong in aqSongs.toSet()) {
|
||||
if (aqSong.data.toLowerCase().endsWith('.wma')) continue;
|
||||
final data = aqSong.getMap;
|
||||
// changed includes the creation time
|
||||
// => also update, when the file was created later (and wasn't really changed)
|
||||
// this is used as a workaround because android
|
||||
// doesn't seem to return the correct modification time
|
||||
final lastModified = _dateMax(fileStats.modified, fileStats.changed);
|
||||
final song = await _musicDataSource.getSongByPath(file.path);
|
||||
final lastModified = DateTime.fromMillisecondsSinceEpoch((aqSong.dateModified ?? 0) * 1000);
|
||||
final song = await _musicDataSource.getSongByPath(aqSong.data);
|
||||
|
||||
int? albumId;
|
||||
String albumString;
|
||||
|
@ -97,30 +95,25 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
}
|
||||
}
|
||||
// completely new song -> new album ids should start after existing ones
|
||||
final tags = await _audiotagger.readTags(path: file.path);
|
||||
final audioFile = await _audiotagger.readAudioFile(path: file.path);
|
||||
|
||||
if (tags == null || audioFile == null) {
|
||||
_log.w('Could not read ${file.path}');
|
||||
continue;
|
||||
}
|
||||
// this is new information
|
||||
// is the album ID still correct or do we find another album with the same properties?
|
||||
albumString = '${tags.album}___${tags.albumArtist}__${tags.year}';
|
||||
final String albumArtist = data['album_artist'] as String? ?? '';
|
||||
final String year = data['year'] as String? ?? '';
|
||||
albumString = '${aqSong.album}___${albumArtist}__$year';
|
||||
|
||||
String? albumArtPath;
|
||||
if (!albumIdMap.containsKey(albumString)) {
|
||||
// we haven't seen an album with these properties in the files yet, but there might be an entry in the database
|
||||
// in this case, we should use the corresponding ID
|
||||
albumId ??= await _musicDataSource.getAlbumId(
|
||||
tags.album,
|
||||
tags.albumArtist,
|
||||
int.tryParse(tags.year ?? ''),
|
||||
aqSong.album,
|
||||
albumArtist,
|
||||
int.tryParse(year),
|
||||
) ??
|
||||
newAlbumId++;
|
||||
albumIdMap[albumString] = albumId;
|
||||
|
||||
final albumArt = await _audiotagger.readArtwork(path: file.path);
|
||||
final albumArt = await _onAudioQuery.queryArtwork(aqSong.albumId ?? -1, aq.ArtworkType.ALBUM, size: 640);
|
||||
|
||||
if (albumArt != null && albumArt.isNotEmpty) {
|
||||
albumArtPath = '${dir.path}/$albumId';
|
||||
|
@ -129,9 +122,9 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
albumArtMap[albumString] = albumArtPath;
|
||||
}
|
||||
|
||||
final String albumArtist = tags.albumArtist ?? '';
|
||||
final String songArtist = tags.artist ?? '';
|
||||
final String artistName = albumArtist != '' ? albumArtist : (songArtist != '' ? songArtist : DEF_ARTIST);
|
||||
final String songArtist = aqSong.artist ?? '';
|
||||
final String artistName =
|
||||
albumArtist != '' ? albumArtist : (songArtist != '' ? songArtist : DEF_ARTIST);
|
||||
|
||||
final artist = artistsInDb.firstWhereOrNull((a) => a.name == artistName);
|
||||
if (artist != null) {
|
||||
|
@ -141,11 +134,9 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
artistSet.add(ArtistModel(name: artistName, id: newArtistId++));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
albums.add(
|
||||
AlbumModel.fromAudiotagger(albumId: albumId, tag: tags, albumArtPath: albumArtPath),
|
||||
AlbumModel.fromOnAudioQuery(
|
||||
albumId: albumId, songModel: aqSong, albumArtPath: albumArtPath),
|
||||
);
|
||||
} else {
|
||||
// an album with the same properties is already stored in the list
|
||||
|
@ -155,10 +146,9 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
}
|
||||
|
||||
songs.add(
|
||||
SongModel.fromAudiotagger(
|
||||
path: file.path,
|
||||
tag: tags,
|
||||
audioFile: audioFile,
|
||||
SongModel.fromOnAudioQuery(
|
||||
path: aqSong.data,
|
||||
songModel: aqSong,
|
||||
albumId: albumId,
|
||||
albumArtPath: albumArtPath,
|
||||
lastModified: lastModified,
|
||||
|
@ -173,8 +163,3 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
DateTime _dateMax(DateTime a, DateTime b) {
|
||||
if (a.isAfter(b)) return a;
|
||||
return b;
|
||||
}
|
||||
|
|
|
@ -153,26 +153,34 @@ class PersistentStateDao extends DatabaseAccessor<MoorDatabase>
|
|||
Future<Playable> get playable async {
|
||||
final entry = await (select(keyValueEntries)
|
||||
..where((tbl) => tbl.key.equals(PERSISTENT_PLAYABLE)))
|
||||
.getSingle();
|
||||
.getSingleOrNull();
|
||||
if (entry == null) return AllSongs();
|
||||
|
||||
final data = jsonDecode(entry.value);
|
||||
final playableType = (data['type'] as String).toPlayableType();
|
||||
|
||||
Playable? result;
|
||||
|
||||
switch (playableType) {
|
||||
case PlayableType.all:
|
||||
return AllSongs();
|
||||
result = AllSongs();
|
||||
break;
|
||||
case PlayableType.album:
|
||||
return (select(albums)..where((tbl) => tbl.id.equals(int.parse(data['id'] as String))))
|
||||
.getSingle()
|
||||
.then((value) => AlbumModel.fromMoor(value));
|
||||
result = await (select(albums)..where((tbl) => tbl.id.equals(int.parse(data['id'] as String))))
|
||||
.getSingleOrNull()
|
||||
.then((value) => value == null ? null : AlbumModel.fromMoor(value));
|
||||
break;
|
||||
case PlayableType.artist:
|
||||
return (select(artists)..where((tbl) => tbl.name.equals(data['id'] as String)))
|
||||
.getSingle()
|
||||
.then((value) => ArtistModel.fromMoor(value));
|
||||
result = await (select(artists)..where((tbl) => tbl.name.equals(data['id'] as String)))
|
||||
.getSingleOrNull()
|
||||
.then((value) => value == null ? null : ArtistModel.fromMoor(value));
|
||||
break;
|
||||
case PlayableType.playlist:
|
||||
final plId = int.parse(data['id'] as String);
|
||||
// TODO: need proper getter for this
|
||||
final moorPl = await (select(playlists)..where((tbl) => tbl.id.equals(plId))).getSingle();
|
||||
return PlaylistModel.fromMoor(moorPl);
|
||||
result = PlaylistModel.fromMoor(moorPl);
|
||||
break;
|
||||
case PlayableType.smartlist:
|
||||
final slId = int.parse(data['id'] as String);
|
||||
final sl = await (select(smartLists)..where((tbl) => tbl.id.equals(slId))).getSingle();
|
||||
|
@ -182,10 +190,17 @@ class PersistentStateDao extends DatabaseAccessor<MoorDatabase>
|
|||
[innerJoin(artists, artists.name.equalsExp(smartListArtists.artistName))],
|
||||
)).map((p0) => p0.readTable(artists)).get();
|
||||
|
||||
return SmartListModel.fromMoor(sl, slArtists);
|
||||
result = SmartListModel.fromMoor(sl, slArtists);
|
||||
break;
|
||||
case PlayableType.search:
|
||||
return SearchQuery(data['id'] as String);
|
||||
result = SearchQuery(data['id'] as String);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
return AllSongs();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:audiotagger/models/tag.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:on_audio_query/on_audio_query.dart' as aq;
|
||||
|
||||
import '../../domain/entities/album.dart';
|
||||
import '../datasources/moor_database.dart';
|
||||
|
@ -39,6 +40,24 @@ class AlbumModel extends Album {
|
|||
);
|
||||
}
|
||||
|
||||
factory AlbumModel.fromOnAudioQuery({
|
||||
required aq.SongModel songModel,
|
||||
required int albumId,
|
||||
String? albumArtPath,
|
||||
}) {
|
||||
final data = songModel.getMap;
|
||||
final albumArtist = data['album_artist'] as String? ?? '';
|
||||
final artist = albumArtist != '' ? albumArtist : songModel.artist;
|
||||
|
||||
return AlbumModel(
|
||||
id: albumId,
|
||||
title: songModel.album ?? DEF_ALBUM,
|
||||
artist: artist ?? DEF_ARTIST,
|
||||
albumArtPath: albumArtPath,
|
||||
pubYear: data['year'] == null ? null : parseYear(data['year'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$title';
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:audio_service/audio_service.dart';
|
|||
import 'package:audiotagger/models/audiofile.dart';
|
||||
import 'package:audiotagger/models/tag.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:on_audio_query/on_audio_query.dart' as aq;
|
||||
|
||||
import '../../domain/entities/song.dart';
|
||||
import '../datasources/moor_database.dart';
|
||||
|
@ -98,6 +99,39 @@ class SongModel extends Song {
|
|||
);
|
||||
}
|
||||
|
||||
factory SongModel.fromOnAudioQuery({
|
||||
required String path,
|
||||
required aq.SongModel songModel,
|
||||
String? albumArtPath,
|
||||
required int albumId,
|
||||
required DateTime lastModified,
|
||||
}) {
|
||||
|
||||
final data = songModel.getMap;
|
||||
final trackNumber = _parseTrackNumber(songModel.track);
|
||||
|
||||
return SongModel(
|
||||
title: songModel.title,
|
||||
artist: songModel.artist ?? DEF_ARTIST,
|
||||
album: songModel.album ?? DEF_ALBUM,
|
||||
albumId: albumId,
|
||||
path: path,
|
||||
duration: Duration(milliseconds: songModel.duration ?? DEF_DURATION),
|
||||
blockLevel: 0,
|
||||
discNumber: trackNumber[0],
|
||||
trackNumber: trackNumber[1],
|
||||
albumArtPath: albumArtPath,
|
||||
next: false,
|
||||
previous: false,
|
||||
likeCount: 0,
|
||||
playCount: 0,
|
||||
skipCount: 0,
|
||||
year: parseYear(data['year'] as String?),
|
||||
timeAdded: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
lastModified: lastModified,
|
||||
);
|
||||
}
|
||||
|
||||
final int albumId;
|
||||
final DateTime lastModified;
|
||||
|
||||
|
@ -141,7 +175,7 @@ class SongModel extends Song {
|
|||
trackNumber: trackNumber ?? this.trackNumber,
|
||||
likeCount: likeCount ?? this.likeCount,
|
||||
skipCount: skipCount ?? this.skipCount,
|
||||
playCount: playCount ?? this.playCount,
|
||||
playCount: playCount ?? this.playCount,
|
||||
timeAdded: timeAdded ?? this.timeAdded,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
year: year ?? this.year,
|
||||
|
@ -210,6 +244,22 @@ class SongModel extends Song {
|
|||
}
|
||||
return int.parse(numberString);
|
||||
}
|
||||
|
||||
static List<int> _parseTrackNumber(int? number) {
|
||||
if (number == null) return [1, 1];
|
||||
|
||||
final numString = number.toString();
|
||||
final firstZero = numString.indexOf('0');
|
||||
|
||||
if (firstZero < 0 || firstZero == numString.length - 1) {
|
||||
return [1, number];
|
||||
}
|
||||
|
||||
final disc = numString.substring(0, firstZero);
|
||||
final track = numString.substring(firstZero + 1);
|
||||
|
||||
return [int.parse(disc), int.parse(track)];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: maybe move to another file
|
||||
|
|
|
@ -373,6 +373,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
id3:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: id3
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -485,6 +492,27 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
on_audio_query:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: on_audio_query
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
on_audio_query_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: on_audio_query_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
on_audio_query_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: on_audio_query_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.2+2"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -26,6 +26,7 @@ dependencies:
|
|||
get_it: ^7.1.3 # MIT
|
||||
just_audio: ^0.9.18 # MIT
|
||||
mobx: ^2.0.1 # MIT
|
||||
on_audio_query: ^2.6.1 # BSD 3
|
||||
path: ^1.8.0 # BSD 3
|
||||
path_provider: ^2.0.2 # BSD 3
|
||||
permission_handler: ^8.3.0 # MIT
|
||||
|
|
Loading…
Add table
Reference in a new issue