refactor(player): move PlayableMedia to playable_media.dart

The class moves verbatim and is re-exported from music_player_service.dart.
The toJson/fromJson artwork round trips stay: toJson drops non-positive
numbers, blank formats and empty artwork, so a copyWith would not be an
exact replacement for them.
This commit is contained in:
zarzet committed 2026-10-18 23:53:44 +07:00
1 parent 992e21e297
commit 548567eff3
2 files changed
+146 -141

No files matched your search

+2 -141
View File
@@ -35,13 +35,13 @@ import 'package:spotiflac_android/services/automix_effect_renderer.dart';
import 'package:spotiflac_android/services/automix_status.dart';
import 'package:spotiflac_android/services/music_haptics.dart';
import 'package:spotiflac_android/utils/int_utils.dart';
import 'package:spotiflac_android/utils/ios_container_paths.dart';
import 'package:spotiflac_android/utils/playback_artwork.dart';
import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
import 'package:spotiflac_android/services/playable_media.dart';
export 'music_player_runtime.dart';
export 'playable_media.dart';
export 'playback_metadata.dart';
part 'music_player_automix.dart';
@@ -102,145 +102,6 @@ final AudioContext _musicAudioContext = AudioContext(
),
);
class PlayableMedia {
final String id;
final String source;
final String title;
final String artist;
final String album;
final String? artUri;
final String? networkArtworkSource;
final Duration? duration;
final int? bitDepth;
final int? sampleRate;
final int? bitrate;
final String? format;
final bool explicit;
final String? genre;
final bool autoplay;
const PlayableMedia({
required this.id,
required this.source,
required this.title,
required this.artist,
this.album = '',
this.artUri,
this.networkArtworkSource,
this.duration,
this.bitDepth,
this.sampleRate,
this.bitrate,
this.format,
this.explicit = false,
this.genre,
this.autoplay = false,
});
bool get isContentUri => source.startsWith('content://');
bool get isNetwork =>
const {'network', 'http', 'https'}.contains(Uri.tryParse(source)?.scheme);
Map<String, dynamic> toJson() => {
'id': id,
'source': source,
'title': title,
'artist': artist,
'album': album,
if (artUri != null && artUri!.isNotEmpty && networkArtworkSource == null)
'artUri': artUri,
if (networkArtworkSource != null)
'networkArtworkSource': networkArtworkSource,
if (duration != null) 'durationMs': duration!.inMilliseconds,
if (bitDepth != null && bitDepth! > 0) 'bitDepth': bitDepth,
if (sampleRate != null && sampleRate! > 0) 'sampleRate': sampleRate,
if (bitrate != null && bitrate! > 0) 'bitrate': bitrate,
if (format != null && format!.trim().isNotEmpty) 'format': format,
if (explicit) 'explicit': true,
if (genre != null) 'genre': genre,
if (autoplay) 'autoplay': true,
};
static PlayableMedia? fromJson(
Map<String, dynamic> json, {
String? iosDocumentsPath,
}) {
final id = json['id'] as String?;
final source = json['source'] as String?;
if (id == null || id.isEmpty || source == null || source.isEmpty) {
return null;
}
final durationMs = (json['durationMs'] as num?)?.toInt();
var artwork = json['artUri'] as String?;
if (artwork != null && iosDocumentsPath != null) {
final uri = Uri.tryParse(artwork);
if (uri != null && uri.scheme == 'file' && uri.host.isEmpty) {
try {
final path = uri.toFilePath();
final rebased = rebaseIosSandboxPath(path, iosDocumentsPath);
if (rebased != path) artwork = Uri.file(rebased).toString();
} on UnsupportedError {
// Leave malformed or non-local artwork URIs to the image fallback.
}
}
}
return PlayableMedia(
id: id,
source: source,
title: json['title'] as String? ?? '',
artist: json['artist'] as String? ?? '',
album: json['album'] as String? ?? '',
artUri: artwork,
networkArtworkSource: json['networkArtworkSource'] as String?,
duration: (durationMs != null && durationMs > 0)
? Duration(milliseconds: durationMs)
: null,
bitDepth: readPositiveInt(json['bitDepth']),
sampleRate: readPositiveInt(json['sampleRate']),
bitrate: readPositiveInt(json['bitrate']),
format: json['format']?.toString(),
explicit: parseExplicitFlag(json['explicit']) == true,
genre: json['genre'] as String?,
autoplay: json['autoplay'] == true,
);
}
MediaItem toMediaItem({
String? resolvedSource,
String? resolvedArtwork,
String? unknownTitle,
String? unknownArtist,
}) {
final artwork = resolvedArtwork ?? artUri;
return MediaItem(
id: id,
title: title.isEmpty
? unknownTitle ?? musicPlayerRuntime.unknownTitle
: title,
artist: artist.isEmpty
? unknownArtist ?? musicPlayerRuntime.unknownArtist
: artist,
album: album.isEmpty ? null : album,
duration: duration,
artUri: (artwork != null && artwork.isNotEmpty)
? Uri.tryParse(artwork)
: null,
extras: {
'source': source,
if (resolvedSource != null && resolvedSource.isNotEmpty)
'resolvedSource': resolvedSource,
if (bitDepth != null && bitDepth! > 0) 'bit_depth': bitDepth,
if (sampleRate != null && sampleRate! > 0) 'sample_rate': sampleRate,
if (bitrate != null && bitrate! > 0) 'bitrate': bitrate,
if (format != null && format!.trim().isNotEmpty)
'format': format!.trim(),
if (explicit) 'explicit': true,
if (autoplay) 'autoplay': true,
},
);
}
}
/// Returns a safe source-start position for restored playback. A completed
/// snapshot starts over instead of immediately completing again on resume.
Duration normalizedPlaybackResumePosition(
+144
View File
@@ -0,0 +1,144 @@
import 'package:audio_service/audio_service.dart' show MediaItem;
import 'package:spotiflac_android/services/music_player_runtime.dart';
import 'package:spotiflac_android/utils/int_utils.dart';
import 'package:spotiflac_android/utils/ios_container_paths.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
class PlayableMedia {
final String id;
final String source;
final String title;
final String artist;
final String album;
final String? artUri;
final String? networkArtworkSource;
final Duration? duration;
final int? bitDepth;
final int? sampleRate;
final int? bitrate;
final String? format;
final bool explicit;
final String? genre;
final bool autoplay;
const PlayableMedia({
required this.id,
required this.source,
required this.title,
required this.artist,
this.album = '',
this.artUri,
this.networkArtworkSource,
this.duration,
this.bitDepth,
this.sampleRate,
this.bitrate,
this.format,
this.explicit = false,
this.genre,
this.autoplay = false,
});
bool get isContentUri => source.startsWith('content://');
bool get isNetwork =>
const {'network', 'http', 'https'}.contains(Uri.tryParse(source)?.scheme);
Map<String, dynamic> toJson() => {
'id': id,
'source': source,
'title': title,
'artist': artist,
'album': album,
if (artUri != null && artUri!.isNotEmpty && networkArtworkSource == null)
'artUri': artUri,
if (networkArtworkSource != null)
'networkArtworkSource': networkArtworkSource,
if (duration != null) 'durationMs': duration!.inMilliseconds,
if (bitDepth != null && bitDepth! > 0) 'bitDepth': bitDepth,
if (sampleRate != null && sampleRate! > 0) 'sampleRate': sampleRate,
if (bitrate != null && bitrate! > 0) 'bitrate': bitrate,
if (format != null && format!.trim().isNotEmpty) 'format': format,
if (explicit) 'explicit': true,
if (genre != null) 'genre': genre,
if (autoplay) 'autoplay': true,
};
static PlayableMedia? fromJson(
Map<String, dynamic> json, {
String? iosDocumentsPath,
}) {
final id = json['id'] as String?;
final source = json['source'] as String?;
if (id == null || id.isEmpty || source == null || source.isEmpty) {
return null;
}
final durationMs = (json['durationMs'] as num?)?.toInt();
var artwork = json['artUri'] as String?;
if (artwork != null && iosDocumentsPath != null) {
final uri = Uri.tryParse(artwork);
if (uri != null && uri.scheme == 'file' && uri.host.isEmpty) {
try {
final path = uri.toFilePath();
final rebased = rebaseIosSandboxPath(path, iosDocumentsPath);
if (rebased != path) artwork = Uri.file(rebased).toString();
} on UnsupportedError {
// Leave malformed or non-local artwork URIs to the image fallback.
}
}
}
return PlayableMedia(
id: id,
source: source,
title: json['title'] as String? ?? '',
artist: json['artist'] as String? ?? '',
album: json['album'] as String? ?? '',
artUri: artwork,
networkArtworkSource: json['networkArtworkSource'] as String?,
duration: (durationMs != null && durationMs > 0)
? Duration(milliseconds: durationMs)
: null,
bitDepth: readPositiveInt(json['bitDepth']),
sampleRate: readPositiveInt(json['sampleRate']),
bitrate: readPositiveInt(json['bitrate']),
format: json['format']?.toString(),
explicit: parseExplicitFlag(json['explicit']) == true,
genre: json['genre'] as String?,
autoplay: json['autoplay'] == true,
);
}
MediaItem toMediaItem({
String? resolvedSource,
String? resolvedArtwork,
String? unknownTitle,
String? unknownArtist,
}) {
final artwork = resolvedArtwork ?? artUri;
return MediaItem(
id: id,
title: title.isEmpty
? unknownTitle ?? musicPlayerRuntime.unknownTitle
: title,
artist: artist.isEmpty
? unknownArtist ?? musicPlayerRuntime.unknownArtist
: artist,
album: album.isEmpty ? null : album,
duration: duration,
artUri: (artwork != null && artwork.isNotEmpty)
? Uri.tryParse(artwork)
: null,
extras: {
'source': source,
if (resolvedSource != null && resolvedSource.isNotEmpty)
'resolvedSource': resolvedSource,
if (bitDepth != null && bitDepth! > 0) 'bit_depth': bitDepth,
if (sampleRate != null && sampleRate! > 0) 'sample_rate': sampleRate,
if (bitrate != null && bitrate! > 0) 'bitrate': bitrate,
if (format != null && format!.trim().isNotEmpty)
'format': format!.trim(),
if (explicit) 'explicit': true,
if (autoplay) 'autoplay': true,
},
);
}
}