Files
plezy/lib/services/plex_client/parts/playlists.dart
T
edde746 352b88109b refactor: extract shared mixins and helpers, drop dead abstractions
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.

Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
2026-07-26 06:09:48 +02:00

208 lines
7.1 KiB
Dart

part of '../../plex_client.dart';
mixin _PlexPlaylistMethods on _PlexClientInternals {
static const int _playlistPageSize = 200;
static const int _defaultPlaylistContainerSize = 100;
({List<PlexPlaylistDto> items, int totalSize}) _extractPlaylistListResult(
MediaServerResponse response, {
int? start,
int? size,
});
Future<_LibraryContentResult> _getPlaylist(String playlistId, {int? start, int? size, AbortController? abort}) =>
_fetchPaginatedList('/playlists/$playlistId/items', start: start, size: size, abort: abort);
Future<List<PlexPlaylistDto>> _getPlaylists({String playlistType = 'video', bool? smart}) async {
try {
return await drainPages<PlexPlaylistDto>((start, size) async {
final page = await _getPlaylistsPage(playlistType: playlistType, smart: smart, start: start, size: size);
return LibraryPage(items: page.items, totalCount: page.totalSize, offset: start);
}, pageSize: _playlistPageSize);
} catch (e, st) {
appLogger.e('Failed to get playlists', error: e, stackTrace: st);
return [];
}
}
Future<({List<PlexPlaylistDto> items, int totalSize})> _getPlaylistsPage({
String playlistType = 'video',
bool? smart,
int? start,
int? size,
AbortController? abort,
}) async {
final pageSize = size ?? _defaultPlaylistContainerSize;
final queryParams = <String, dynamic>{
if (playlistType.isNotEmpty) 'playlistType': playlistType,
..._buildPaginationParams(start, pageSize),
};
if (smart != null) queryParams['smart'] = smart ? '1' : '0';
final response = await _getWithFailover('/playlists', queryParameters: queryParams, abort: abort);
return _extractPlaylistListResult(response, start: start, size: pageSize);
}
Future<PlexPlaylistDto?> _getPlaylistMetadata(String playlistId) async {
try {
final response = await _getWithFailover('/playlists/$playlistId');
final container = _getMediaContainer(response);
final metadata = container?['Metadata'];
if (metadata is! List || metadata.isEmpty) return null;
return PlexPlaylistDto.fromJson(metadata.first as Map<String, dynamic>);
} catch (e) {
appLogger.e('Failed to get playlist metadata: $e');
return null;
}
}
@override
Future<List<MediaPlaylist>> fetchPlaylists({String playlistType = 'video', bool? smart}) async {
final playlists = await _getPlaylists(playlistType: playlistType, smart: smart);
return playlists.map(PlexMappers.mediaPlaylist).toList();
}
@override
Future<LibraryPage<MediaPlaylist>> fetchPlaylistsPage({
String playlistType = 'video',
bool? smart,
int? start,
int? size,
AbortController? abort,
}) async {
final result = await _getPlaylistsPage(
playlistType: playlistType,
smart: smart,
start: start,
size: size,
abort: abort,
);
return LibraryPage<MediaPlaylist>(
items: result.items.map(PlexMappers.mediaPlaylist).toList(),
totalCount: result.totalSize,
offset: start ?? 0,
);
}
@override
Future<MediaPlaylist?> fetchPlaylistMetadata(String id) async {
final playlist = await _getPlaylistMetadata(id);
return playlist == null ? null : PlexMappers.mediaPlaylist(playlist);
}
@override
Future<List<MediaItem>> fetchPlaylistItems(String id, {int offset = 0, int limit = 100}) async {
final page = await fetchPlaylistPage(id, start: offset, size: limit);
return page.items;
}
@override
Future<LibraryPage<MediaItem>> fetchPlaylistPage(
String playlistId, {
int? start,
int? size,
AbortController? abort,
}) async {
final result = await _getPlaylist(playlistId, start: start, size: size, abort: abort);
return LibraryPage<MediaItem>(
items: result.items.map(PlexMappers.mediaItem).toList(),
totalCount: result.totalSize,
offset: start ?? 0,
);
}
@override
Future<MediaPlaylist?> createPlaylist({required String title, required List<MediaItem> items}) async {
if (items.isEmpty) return createPlaylistFromUri(title: title);
final uri = await buildMetadataUri(items.map((item) => item.id).join(','));
return createPlaylistFromUri(title: title, uri: uri, type: items.first.kind.isMusic ? 'audio' : 'video');
}
Future<MediaPlaylist?> createPlaylistFromUri({
required String title,
String? uri,
int? playQueueId,
String type = 'video',
}) async {
final queryParameters = <String, dynamic>{
'type': type,
'title': title,
'smart': '0',
'uri': ?uri,
'playQueueID': ?playQueueId?.toString(),
};
final response = await _http.post('/playlists', queryParameters: queryParameters);
throwIfHttpError(response);
final metadata = _getMediaContainer(response)?['Metadata'];
if (metadata is! List || metadata.isEmpty) return null;
final dto = PlexPlaylistDto.fromJson(
metadata.first as Map<String, dynamic>,
).copyWith(serverId: serverId, serverName: serverName);
return PlexMappers.mediaPlaylist(dto);
}
@override
Future<bool> deletePlaylist(MediaPlaylist playlist) {
return _wrapBoolApiCall(() => _http.delete('/playlists/${playlist.id}'), 'Failed to delete playlist');
}
@override
Future<bool> addToPlaylist({required String playlistId, required List<MediaItem> items}) async {
if (items.isEmpty) return true;
final uri = await buildMetadataUri(items.map((item) => item.id).join(','));
return addItemsToPlaylistByUri(playlistId: playlistId, uri: uri);
}
Future<bool> addItemsToPlaylistByUri({required String playlistId, required String uri}) async {
appLogger.d(
'Adding to playlist $playlistId with URI: '
'${uri.substring(0, uri.length > 100 ? 100 : uri.length)}'
'${uri.length > 100 ? "..." : ""}',
);
final result = await _wrapBoolApiCall(
() => _http.put('/playlists/$playlistId/items', queryParameters: {'uri': uri}),
'Failed to add to playlist',
);
if (result) appLogger.d('Add to playlist response status: 200');
return result;
}
@override
Future<bool> removeFromPlaylist({required String playlistId, required MediaItem item}) {
if (item is! PlexMediaItem || item.playlistItemId == null) {
return Future.value(false);
}
return _wrapBoolApiCall(
() => _http.delete('/playlists/$playlistId/items/${item.playlistItemId}'),
'Failed to remove from playlist',
);
}
@override
Future<bool> movePlaylistItem({
required String playlistId,
required MediaItem item,
required int newIndex,
required MediaItem? afterItem,
}) async {
if (item is! PlexMediaItem || item.playlistItemId == null) return false;
final int after;
if (newIndex == 0) {
after = 0;
} else if (afterItem is PlexMediaItem && afterItem.playlistItemId != null) {
after = afterItem.playlistItemId!;
} else {
return false;
}
appLogger.d(
'Moving playlist item ${item.playlistItemId} after $after in playlist '
'$playlistId',
);
return _wrapBoolApiCall(
() => _http.put('/playlists/$playlistId/items/${item.playlistItemId}/move', queryParameters: {'after': after}),
'Failed to move playlist item',
);
}
}