Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android Auto #578

Merged
merged 48 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6a2238d
Move _includeItemTypes() to TabContentType
puff Jan 17, 2024
8da2b09
Basic Android Auto support
puff Jan 17, 2024
d168fb7
Try using downloaded parent before going online in Android Auto
puff Jan 17, 2024
f877624
Add shuffle to Android Auto
puff Jan 18, 2024
56d4f2a
Use correct shuffle status for Android Auto
puff Jan 18, 2024
05e5f72
Artists now start instant mix in Android Auto
puff Jan 20, 2024
efb14bd
synchronize internal queue and Android media queue
Chaphasilor Jan 21, 2024
0789c31
fix adding to next up when shuffle is active
Chaphasilor Jan 21, 2024
7a24508
Add sorting to Android Auto and make offline sort by name case-insens…
puff Jan 22, 2024
aad1edf
Remove unused parameter in Android Auto getBaseItems
puff Jan 22, 2024
99d01f2
Only sort downloaded items if not playing them in Android Auto
puff Jan 22, 2024
b3656c8
Use a ContentProvider to resolve artwork for Android Auto
puff Jan 23, 2024
ea10a21
Basic voice search for songs in Android Auto
puff Feb 4, 2024
4200036
Merge branch 'redesign' into pr/puff/578
Chaphasilor Feb 6, 2024
430ee62
enable showing search results for android auto voice search
Chaphasilor Feb 6, 2024
c85c07b
rename use parent instead of category
Chaphasilor Feb 6, 2024
121a87e
implement starting instant mixes from search results
Chaphasilor Feb 6, 2024
8d2d825
improved playBySearch using extras
Chaphasilor Feb 7, 2024
0c34cfc
Download images in MediaItemContentProvider
puff Feb 16, 2024
bcc22c0
replaced manual passing and parsing of media IDs with serializable Me…
Chaphasilor Feb 16, 2024
c0355e2
use item ID instead of parent ID
Chaphasilor Feb 16, 2024
72cc75a
Remove redundant TabContentType resolves in Android Auto code
puff Feb 17, 2024
b66d3ec
Merge branch 'redesign' into pr/puff/578
Chaphasilor Feb 17, 2024
32d213f
shuffle all for empty search query ("play some music")
Chaphasilor Feb 17, 2024
a03199f
increase timeout for http requests
Chaphasilor Feb 19, 2024
1febde3
Re-implement MediaItemContentProvider in Kotlin
puff Mar 8, 2024
4e70118
improve search using metadata where possible
Chaphasilor Mar 9, 2024
d8a11e2
prefer playlists for voice search
Chaphasilor Mar 9, 2024
294fa6f
Merge branch 'redesign' into pr/puff/578
Chaphasilor Mar 9, 2024
be80fb6
make voice search artist match case insensitive
Chaphasilor Mar 9, 2024
ea6ec93
Use correct item type id in Android Auto
puff Mar 10, 2024
09df0c1
only resolve songs from downloads in online mode, add artist playback…
Chaphasilor Mar 10, 2024
40e5bb5
also use grid layout in Android Auto if enabled
Chaphasilor Mar 10, 2024
c4baf85
improve offline mode for Android Auto
Chaphasilor Mar 10, 2024
d1aefd1
merge branch 'redesign'
Chaphasilor Mar 23, 2024
5797b16
disable artwork preloading in media session due to issues with custom…
Chaphasilor Mar 23, 2024
a2cb78b
Merge branch 'redesign' into pr/puff/578
Chaphasilor May 24, 2024
f37784d
added offline support for voice commands
Chaphasilor May 26, 2024
895fe77
explicitly handle request for recent ("For you") root
Chaphasilor May 26, 2024
289f3b2
fix syntax error
Chaphasilor May 26, 2024
adf2a81
improvements based on review
Chaphasilor May 27, 2024
6d12f78
fix errors, properly join file paths
Chaphasilor May 27, 2024
03470f9
fix issues with content provider custom artwork URIs
Chaphasilor May 27, 2024
ddc66e8
fix album name always being shown
Chaphasilor May 27, 2024
4274089
search multiple item types
Chaphasilor May 30, 2024
68d3b3d
added like button to media notification, added settings for customizi…
Chaphasilor May 30, 2024
1901816
Merge branch 'redesign' into pr/puff/578
Chaphasilor May 30, 2024
ae315fb
Merge branch 'redesign' into pr/puff/578
Chaphasilor Jun 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 31 additions & 23 deletions lib/services/android_auto_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class AndroidAutoHelper {
}

Future<List<BaseItemDto>> getBaseItems(MediaItemId itemId) async {
// offline mode only supports albums and playlists for now (no offline instant mix for others yet)
if (FinampSettingsHelper.finampSettings.isOffline && (itemId.contentType == TabContentType.artists || itemId.contentType == TabContentType.genres)) {
return [];
}

Chaphasilor marked this conversation as resolved.
Show resolved Hide resolved
// limit amount so it doesn't crash on large libraries
// TODO: add pagination
const limit = 100;
Expand All @@ -75,7 +80,10 @@ class AndroidAutoHelper {
var downloadedParent = await _downloadService.getCollectionInfo(id: itemId.itemId);
if (downloadedParent != null && downloadedParent.baseItem != null) {
final downloadedItems = await _downloadService.getCollectionSongs(downloadedParent.baseItem!);
//TODO enforce page limit
if (downloadedItems.length >= limit) {
downloadedItems.removeRange(limit, downloadedItems.length - 1);
}

// only sort items if we are not playing them
return _isPlayable(itemId.contentType) ? downloadedItems : _sortItems(downloadedItems, sortBy, sortOrder);
}
Expand All @@ -85,17 +93,17 @@ class AndroidAutoHelper {

// select the item type that each parent holds
final includeItemTypes = itemId.parentType == MediaItemParentType.collection // if we are browsing a root library. e.g. browsing the list of all albums or artists
Chaphasilor marked this conversation as resolved.
Show resolved Hide resolved
? (itemId.contentType == TabContentType.albums ? TabContentType.songs.itemType.name // get an album's songs
: itemId.contentType == TabContentType.artists ? TabContentType.albums.itemType.name // get an artist's albums
: itemId.contentType == TabContentType.playlists ? TabContentType.songs.itemType.name // get a playlist's songs
: itemId.contentType == TabContentType.genres ? TabContentType.albums.itemType.name // get a genre's albums
: TabContentType.songs.itemType.name ) // if we don't have one of these categories, we are probably dealing with stray songs
: itemId.contentType.itemType.name; // get the root library
? (itemId.contentType == TabContentType.albums ? TabContentType.songs.itemType.idString // get an album's songs
: itemId.contentType == TabContentType.artists ? TabContentType.albums.itemType.idString // get an artist's albums
: itemId.contentType == TabContentType.playlists ? TabContentType.songs.itemType.idString // get a playlist's songs
: itemId.contentType == TabContentType.genres ? TabContentType.albums.itemType.idString // get a genre's albums
: TabContentType.songs.itemType.idString ) // if we don't have one of these categories, we are probably dealing with stray songs
: itemId.contentType.itemType.idString; // get the root library

// if parent id is defined, use that to get items.
// otherwise, use the current view as fallback to ensure we get the correct items.
final parentItem = itemId.parentType == MediaItemParentType.collection
? BaseItemDto(id: itemId.itemId!, type: itemId.contentType.itemType.name)
? BaseItemDto(id: itemId.itemId!, type: itemId.contentType.itemType.idString)
: _finampUserHelper.currentUser?.currentView;

final items = await _jellyfinApiHelper.getItems(parentItem: parentItem, sortBy: sortBy.jellyfinName(itemId.contentType), sortOrder: sortOrder.toString(), includeItemTypes: includeItemTypes, limit: limit);
Expand Down Expand Up @@ -132,7 +140,7 @@ class AndroidAutoHelper {
try {
searchResultExactQuery = await jellyfinApiHelper.getItems(
parentItem: finampUserHelper.currentUser?.currentView,
includeItemTypes: "Audio",
includeItemTypes: TabContentType.songs.itemType.idString,
searchTerm: searchQuery.query.trim(),
startIndex: 0,
limit: 7,
Expand All @@ -143,7 +151,7 @@ class AndroidAutoHelper {
try {
searchResultAdjustedQuery = await jellyfinApiHelper.getItems(
parentItem: finampUserHelper.currentUser?.currentView,
includeItemTypes: "Audio",
includeItemTypes: TabContentType.songs.itemType.idString,
searchTerm: searchQuery.extras!["android.intent.extra.title"].trim(),
startIndex: 0,
limit: (searchResultExactQuery != null && searchResultExactQuery.isNotEmpty) ? 13 : 20,
Expand All @@ -157,7 +165,7 @@ class AndroidAutoHelper {
} else {
searchResult = await jellyfinApiHelper.getItems(
parentItem: finampUserHelper.currentUser?.currentView,
includeItemTypes: "Audio",
includeItemTypes: TabContentType.songs.itemType.idString,
searchTerm: searchQuery.query.trim(),
startIndex: 0,
limit: 20,
Expand Down Expand Up @@ -220,21 +228,21 @@ class AndroidAutoHelper {
return await shuffleAllSongs();
}

String itemType = "Audio";
String? itemType = TabContentType.songs.itemType.idString;
String? alternativeQuery;
bool searchForPlaylists = false;

if (searchQuery.extras?["android.intent.extra.album"] != null && searchQuery.extras?["android.intent.extra.artist"] != null && searchQuery.extras?["android.intent.extra.title"] != null) {
// if all metadata is provided, search for song
itemType = "Audio";
itemType = TabContentType.songs.itemType.idString;
alternativeQuery = searchQuery.extras?["android.intent.extra.title"];
} else if (searchQuery.extras?["android.intent.extra.album"] != null && searchQuery.extras?["android.intent.extra.artist"] != null && searchQuery.extras?["android.intent.extra.title"] == null) {
// if only album is provided, search for album
itemType = "MusicAlbum";
itemType = TabContentType.albums.itemType.idString;
alternativeQuery = searchQuery.extras?["android.intent.extra.album"];
} else if (searchQuery.extras?["android.intent.extra.artist"] != null && searchQuery.extras?["android.intent.extra.title"] == null) {
// if only artist is provided, search for artist
itemType = "MusicArtist";
itemType = TabContentType.artists.itemType.idString;
alternativeQuery = searchQuery.extras?["android.intent.extra.artist"];
} else {
// if no metadata is provided, search for song *and* playlists, preferring playlists
Expand All @@ -246,14 +254,14 @@ class AndroidAutoHelper {
try {
List<BaseItemDto>? searchResult = await jellyfinApiHelper.getItems(
parentItem: finampUserHelper.currentUser?.currentView,
includeItemTypes: "Playlist",
includeItemTypes: TabContentType.playlists.itemType.idString,
searchTerm: alternativeQuery?.trim() ?? searchQuery.query.trim(),
startIndex: 0,
limit: 1,
);

final playlist = searchResult![0];
final items = await _jellyfinApiHelper.getItems(parentItem: playlist, includeItemTypes: "Audio", sortBy: "ParentIndexNumber,IndexNumber,SortName", sortOrder: "Ascending", limit: 200);
final items = await _jellyfinApiHelper.getItems(parentItem: playlist, includeItemTypes: TabContentType.songs.itemType.idString, sortBy: "ParentIndexNumber,IndexNumber,SortName", sortOrder: "Ascending", limit: 200);
_androidAutoHelperLogger.info("Playing playlist: ${playlist.name} (${items?.length} songs)");

await queueService.startPlayback(items: items ?? [], source: QueueItemSource(
Expand Down Expand Up @@ -301,9 +309,9 @@ class AndroidAutoHelper {
}

final selectedResult = searchResult.firstWhere((element) {
if (itemType == "Audio" && searchQuery.extras?["android.intent.extra.artist"] != null) {
if (itemType == TabContentType.songs.itemType.idString && searchQuery.extras?["android.intent.extra.artist"] != null) {
return element.albumArtists?.any((artist) => (artist.name?.isNotEmpty ?? false) && (searchQuery.extras?["android.intent.extra.artist"]?.toString().toLowerCase().contains(artist.name?.toLowerCase() ?? "") ?? false)) ?? false;
} else if (itemType == "MusicAlbum" && searchQuery.extras?["android.intent.extra.artist"] != null) {
} else if (itemType == TabContentType.songs.itemType.idString && searchQuery.extras?["android.intent.extra.artist"] != null) {
return element.albumArtists?.any((artist) => (artist.name?.isNotEmpty ?? false) && (searchQuery.extras?["android.intent.extra.artist"]?.toString().toLowerCase().contains(artist.name?.toLowerCase() ?? "") ?? false)) ?? false;
} else {
return false;
Expand All @@ -313,9 +321,9 @@ class AndroidAutoHelper {

_androidAutoHelperLogger.info("Playing from search: ${selectedResult.name}");

if (itemType == "MusicAlbum") {
if (itemType == TabContentType.albums.itemType.idString) {
final album = await _jellyfinApiHelper.getItemById(selectedResult.id);
final items = await _jellyfinApiHelper.getItems(parentItem: album, includeItemTypes: "Audio", sortBy: "ParentIndexNumber,IndexNumber,SortName", sortOrder: "Ascending", limit: 200);
final items = await _jellyfinApiHelper.getItems(parentItem: album, includeItemTypes: TabContentType.songs.itemType.idString, sortBy: "ParentIndexNumber,IndexNumber,SortName", sortOrder: "Ascending", limit: 200);
_androidAutoHelperLogger.info("Playing album: ${album.name} (${items?.length} songs)");

await queueService.startPlayback(items: items ?? [], source: QueueItemSource(
Expand All @@ -328,7 +336,7 @@ class AndroidAutoHelper {
),
order: FinampPlaybackOrder.linear, //TODO add a setting that sets the default (because Android Auto doesn't give use the prompt as an extra), or use the current order?
);
} else if (itemType == "MusicArtist") {
} else if (itemType == TabContentType.artists.itemType.idString) {
await audioServiceHelper.startInstantMixForArtists([selectedResult]).then((value) => 1);
} else {
await audioServiceHelper.startInstantMixForItem(selectedResult).then((value) => 1);
Expand Down Expand Up @@ -374,7 +382,7 @@ class AndroidAutoHelper {
_androidAutoHelperLogger.warning("Tried to play from media id with invalid parent type '${itemId.parentType.name}' or null id");
return;
}
// get all songs of current parrent
// get all songs of current parent
final parentItem = await getParentFromId(itemId.itemId!);

// start instant mix for artists
Expand Down
68 changes: 36 additions & 32 deletions lib/services/music_player_background_task.dart
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,41 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler {
}
}

List<MediaItem> _getRootMenu() {
return !FinampSettingsHelper.finampSettings.isOffline ? [
MediaItem(
id: MediaItemId(contentType: TabContentType.albums, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.albums ?? TabContentType.albums.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.artists, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.artists ?? TabContentType.artists.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.playlists, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.playlists ?? TabContentType.playlists.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.genres, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.genres ?? TabContentType.genres.toString(),
playable: false
)] : [ // display only albums and playlists if in offline mode
MediaItem(
id: MediaItemId(contentType: TabContentType.albums, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.albums ?? TabContentType.albums.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.playlists, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.playlists ?? TabContentType.playlists.toString(),
playable: false
),
];
}

// menus
@override
Chaphasilor marked this conversation as resolved.
Show resolved Hide resolved
Future<List<MediaItem>> getChildren(String parentMediaId, [Map<String, dynamic>? options]) async {
Expand All @@ -386,38 +421,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler {
_localizationsInitialized = true;
}

return !FinampSettingsHelper.finampSettings.isOffline ? [
MediaItem(
id: MediaItemId(contentType: TabContentType.albums, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.albums ?? TabContentType.albums.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.artists, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.artists ?? TabContentType.artists.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.playlists, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.playlists ?? TabContentType.playlists.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.genres, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.genres ?? TabContentType.genres.toString(),
playable: false
)] : [ // display only albums and playlists if in offline mode
MediaItem(
id: MediaItemId(contentType: TabContentType.albums, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.albums ?? TabContentType.albums.toString(),
playable: false
),
MediaItem(
id: MediaItemId(contentType: TabContentType.playlists, parentType: MediaItemParentType.rootCollection).toString(),
title: _appLocalizations?.playlists ?? TabContentType.playlists.toString(),
playable: false
),
];
return _getRootMenu();
}
// else if (parentMediaId == AudioService.recentRootId) {
// return await _androidAutoHelper.getRecentItems();
Expand Down