Skip to content

Commit

Permalink
✨ Write offline listens to a file
Browse files Browse the repository at this point in the history
Useful if you want to manually export them to ListenBrainz or similar later on.
  • Loading branch information
Maxr1998 committed Nov 1, 2023
1 parent 4e1f166 commit 3301c65
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 38 deletions.
48 changes: 27 additions & 21 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:finamp/services/finamp_settings_helper.dart';
import 'package:finamp/services/finamp_user_helper.dart';
import 'package:finamp/services/offline_listen_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
Expand All @@ -18,37 +19,37 @@ import 'package:logging/logging.dart';
import 'package:uuid/uuid.dart';

import 'generate_material_color.dart';
import 'models/finamp_models.dart';
import 'models/jellyfin_models.dart';
import 'models/locale_adapter.dart';
import 'models/theme_mode_adapter.dart';
import 'screens/language_selection_screen.dart';
import 'services/locale_helper.dart';
import 'services/theme_mode_helper.dart';
import 'setup_logging.dart';
import 'screens/user_selector.dart';
import 'screens/music_screen.dart';
import 'screens/view_selector.dart';
import 'screens/add_download_location_screen.dart';
import 'screens/add_to_playlist_screen.dart';
import 'screens/album_screen.dart';
import 'screens/player_screen.dart';
import 'screens/splash_screen.dart';
import 'screens/artist_screen.dart';
import 'screens/audio_service_settings_screen.dart';
import 'screens/downloads_error_screen.dart';
import 'screens/downloads_screen.dart';
import 'screens/artist_screen.dart';
import 'screens/downloads_settings_screen.dart';
import 'screens/language_selection_screen.dart';
import 'screens/layout_settings_screen.dart';
import 'screens/logs_screen.dart';
import 'screens/music_screen.dart';
import 'screens/player_screen.dart';
import 'screens/settings_screen.dart';
import 'screens/transcoding_settings_screen.dart';
import 'screens/downloads_settings_screen.dart';
import 'screens/add_download_location_screen.dart';
import 'screens/audio_service_settings_screen.dart';
import 'screens/splash_screen.dart';
import 'screens/tabs_settings_screen.dart';
import 'screens/add_to_playlist_screen.dart';
import 'screens/layout_settings_screen.dart';
import 'screens/transcoding_settings_screen.dart';
import 'screens/user_selector.dart';
import 'screens/view_selector.dart';
import 'services/audio_service_helper.dart';
import 'services/jellyfin_api_helper.dart';
import 'services/downloads_helper.dart';
import 'services/download_update_stream.dart';
import 'services/downloads_helper.dart';
import 'services/jellyfin_api_helper.dart';
import 'services/locale_helper.dart';
import 'services/music_player_background_task.dart';
import 'models/jellyfin_models.dart';
import 'models/finamp_models.dart';
import 'services/theme_mode_helper.dart';
import 'setup_logging.dart';

void main() async {
// If the app has failed, this is set to true. If true, we don't attempt to run the main app since the error app has started.
Expand All @@ -60,6 +61,7 @@ void main() async {
_migrateSortOptions();
_setupFinampUserHelper();
_setupJellyfinApiData();
_setupOfflineListenLogHelper();
await _setupDownloader();
await _setupDownloadsHelper();
await _setupAudioServiceHelper();
Expand Down Expand Up @@ -91,6 +93,10 @@ void _setupJellyfinApiData() {
GetIt.instance.registerSingleton(JellyfinApiHelper());
}

void _setupOfflineListenLogHelper() {
GetIt.instance.registerSingleton(OfflineListenLogHelper());
}

Future<void> _setupDownloadsHelper() async {
GetIt.instance.registerSingleton(DownloadsHelper());
final downloadsHelper = GetIt.instance<DownloadsHelper>();
Expand Down Expand Up @@ -485,4 +491,4 @@ class _DummyCallback {
IsolateNameServer.lookupPortByName('downloader_send_port');
send!.send([id, status, progress]);
}
}
}
48 changes: 31 additions & 17 deletions lib/services/music_player_background_task.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dart:math';
import 'package:android_id/android_id.dart';
import 'package:audio_service/audio_service.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:finamp/services/offline_listen_helper.dart';
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:just_audio/just_audio.dart';
Expand Down Expand Up @@ -97,6 +98,7 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler {
);
final _audioServiceBackgroundTaskLogger = Logger("MusicPlayerBackgroundTask");
final _jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
final _offlineListenLogHelper = GetIt.instance<OfflineListenLogHelper>();
final _finampUserHelper = GetIt.instance<FinampUserHelper>();

/// Set when shuffle mode is changed. If true, [onUpdateQueue] will create a
Expand Down Expand Up @@ -203,6 +205,12 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler {
if (playbackInfo != null) {
await _jellyfinApiHelper.stopPlaybackProgress(playbackInfo);
}
} else {
final currentIndex = _player.currentIndex;
if (_queueAudioSource.length != 0 && currentIndex != null) {
final item = _getQueueItem(currentIndex);
_offlineListenLogHelper.logOfflineListen(item);
}
}

// Stop playing audio.
Expand Down Expand Up @@ -446,34 +454,40 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler {
MediaItem? previousItem,
PlaybackState? previousState,
) async {
if (FinampSettingsHelper.finampSettings.isOffline) {
return;
}

final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
final isOffline = FinampSettingsHelper.finampSettings.isOffline;

if (previousItem != null &&
previousState != null &&
// don't submit stop events for idle tracks (at position 0 and not playing)
(previousState.playing ||
previousState.updatePosition != Duration.zero)) {
final playbackData = generatePlaybackProgressInfoFromState(
previousItem,
previousState,
);
if (!isOffline) {
final playbackData = generatePlaybackProgressInfoFromState(
previousItem,
previousState,
);

if (playbackData != null) {
await jellyfinApiHelper.stopPlaybackProgress(playbackData);
if (playbackData != null) {
try {
await _jellyfinApiHelper.stopPlaybackProgress(playbackData);
} catch (e) {
_offlineListenLogHelper.logOfflineListen(previousItem);
}
}
} else {
_offlineListenLogHelper.logOfflineListen(previousItem);
}
}

final playbackData = generatePlaybackProgressInfoFromState(
currentItem,
currentState,
);
if (!isOffline) {
final playbackData = generatePlaybackProgressInfoFromState(
currentItem,
currentState,
);

if (playbackData != null) {
await jellyfinApiHelper.reportPlaybackStart(playbackData);
if (playbackData != null) {
await _jellyfinApiHelper.reportPlaybackStart(playbackData);
}
}
}

Expand Down
64 changes: 64 additions & 0 deletions lib/services/offline_listen_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:convert';
import 'dart:io';

import 'package:audio_service/audio_service.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';

/// Logs offline listens or failed submissions to a file.
class OfflineListenLogHelper {
final _logger = Logger("OfflineListenLogHelper");

Future<Directory> get _logDirectory async {
if (!Platform.isAndroid) {
return await getApplicationDocumentsDirectory();
}

final List<Directory>? dirs =
await getExternalStorageDirectories(type: StorageDirectory.documents);
return dirs?.first ?? await getApplicationDocumentsDirectory();
}

Future<File> get _logFile async {
final Directory directory = await _logDirectory;
return File("${directory.path}/listens.json");
}

Future<void> logOfflineListen(MediaItem item) async {
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final itemJson = item.extras!["itemJson"];
final id = itemJson["Id"];
final name = itemJson["Name"];
final albumArtist = itemJson["AlbumArtist"];
final album = itemJson["Album"];
final trackMbid = itemJson["ProviderIds"]?["MusicBrainzTrack"];

await _logOfflineListen(timestamp, id, name, albumArtist, album, trackMbid);
}

Future<void> _logOfflineListen(
int timestamp,
String id,
String name,
String? artist,
String? album,
String? trackMbid,
) async {
final data = {
'timestamp': timestamp,
'id': id,
'title': name,
'artist': artist,
'album': album,
'track_mbid': trackMbid,
};
final content = json.encode(data) + Platform.lineTerminator;

final file = await _logFile;
try {
file.writeAsString(content, mode: FileMode.append, flush: true);
} catch (e) {
_logger.warning("Failed to write listen to file: $content");
}
}
}

0 comments on commit 3301c65

Please sign in to comment.