📊 Feature: History-Sync vom Navidrome-Server (Phase 2, Part 5/X)
- Neue NavidromeService APIs: scrobble() + getBookmark() - Lädt Wiedergabe-Position vom Server beim Song-Start (Subsonic-API) - Sendet Position alle 5s zum Server wenn verbunden - Priorisiert Server-Position über lokale Position - Full Sync: lokal speichern + Server synchen parallel Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJzQjUtnvYUtHdnTs3iCru
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
5202b130c5
commit
f93b2d35da
@@ -38,10 +38,15 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
|
||||
|
||||
// Wiedergabeposition alle ~5s persistieren, solange aktiv abgespielt wird.
|
||||
// Synche auch zum Server wenn verbunden.
|
||||
_positionRecordTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
final songId = mediaItem.value?.extras?['songId'] as String?;
|
||||
if (_player.playing && songId != null) {
|
||||
db.recordPlayback(songId, _player.position.inMilliseconds);
|
||||
final posMs = _player.position.inMilliseconds;
|
||||
db.recordPlayback(songId, posMs);
|
||||
if (_nav.istVerbunden) {
|
||||
_nav.scrobble(songId, (posMs ~/ 1000).toInt());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,12 +91,28 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
await _player.setAudioSources(sources, initialIndex: startIndex);
|
||||
|
||||
// Bei bekannter letzter Position an dieser Stelle fortsetzen,
|
||||
// statt immer von vorne zu beginnen.
|
||||
final songId = items[startIndex].extras?['songId'] as String?;
|
||||
// statt immer von vorne zu beginnen. Priorisiert Server-Position über lokal.
|
||||
final item = items[startIndex];
|
||||
final songId = item.extras?['songId'] as String?;
|
||||
if (songId != null) {
|
||||
final lastMs = await db.lastPosition(songId);
|
||||
if (lastMs != null && shouldResumeAt(lastMs, items[startIndex].duration)) {
|
||||
await _player.seek(Duration(milliseconds: lastMs), index: startIndex);
|
||||
int? resumeMs;
|
||||
|
||||
if (_nav.istVerbunden) {
|
||||
final serverMs = await _nav.getBookmark(songId);
|
||||
if (serverMs != null && shouldResumeAt(serverMs, item.duration)) {
|
||||
resumeMs = serverMs;
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeMs == null) {
|
||||
final localMs = await db.lastPosition(songId);
|
||||
if (localMs != null && shouldResumeAt(localMs, item.duration)) {
|
||||
resumeMs = localMs;
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeMs != null) {
|
||||
await _player.seek(Duration(milliseconds: resumeMs), index: startIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,6 +298,49 @@ class NavidromeService {
|
||||
return streamUrl(songId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Speichert die aktuelle Wiedergabe-Position für einen Song zum Server.
|
||||
/// Subsonic-API: scrobble.view mit `submission=true` und Position in Sekunden.
|
||||
Future<void> scrobble(String songId, int positionSeconds) async {
|
||||
if (!istVerbunden) return;
|
||||
try {
|
||||
final uri = _uri('scrobble.view', {
|
||||
'id': songId,
|
||||
'submission': 'true',
|
||||
'position': positionSeconds.toString(),
|
||||
});
|
||||
await http.get(uri).timeout(const Duration(seconds: 10));
|
||||
debugPrint('Scrobble erfolgreich: Song=$songId, Position=${positionSeconds}s');
|
||||
} catch (e) {
|
||||
debugPrint('Scrobble Fehler: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Lädt die gespeicherte Wiedergabe-Position für einen Song vom Server.
|
||||
/// Gibt Position in Millisekunden zurück oder null wenn nicht gespeichert.
|
||||
Future<int?> getBookmark(String songId) async {
|
||||
if (!istVerbunden) return null;
|
||||
try {
|
||||
final uri = _uri('getBookmarks.view', {'id': songId});
|
||||
final response = await http.get(uri).timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200) return null;
|
||||
|
||||
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final subsonicResponse = json['subsonic-response'] as Map<String, dynamic>?;
|
||||
if (subsonicResponse == null) return null;
|
||||
|
||||
final bookmarks = subsonicResponse['bookmark'] as List<dynamic>?;
|
||||
if (bookmarks == null || bookmarks.isEmpty) return null;
|
||||
|
||||
final position = (bookmarks[0] as Map<String, dynamic>)['position'] as int?;
|
||||
if (position == null) return null;
|
||||
debugPrint('Bookmark geladen: Song=$songId, Position=${position}ms');
|
||||
return position;
|
||||
} catch (e) {
|
||||
debugPrint('Bookmark Fehler: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SubsonicArtist {
|
||||
|
||||
Reference in New Issue
Block a user