This repository was archived by the owner on Mar 21, 2026. It is now read-only.
forked from edde746/plezy
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add Discord Rich Presence support #23
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
04f0799
feat: Add Discord Rich Presence support
google-labs-jules[bot] cafc963
fix: Resolve lint warnings in media controls manager
google-labs-jules[bot] 205a174
fix: Address PR comments on Discord RPC integration
google-labs-jules[bot] c16c79a
feat: Add Discord RPC toggle setting
google-labs-jules[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; | ||
| import '../utils/app_logger.dart'; | ||
| import 'settings_service.dart'; | ||
|
|
||
| class DiscordRPCService { | ||
| static final DiscordRPCService _instance = DiscordRPCService._internal(); | ||
|
|
||
| factory DiscordRPCService() { | ||
| return _instance; | ||
| } | ||
|
|
||
| DiscordRPCService._internal(); | ||
|
|
||
| bool _isInitialized = false; | ||
| bool _isEnabled = true; | ||
|
|
||
| // Discord Application ID from environment variables | ||
| // Pass with --dart-define=DISCORD_APP_ID=your_id_here | ||
| static const String _applicationId = String.fromEnvironment('DISCORD_APP_ID'); | ||
|
|
||
| Future<void> initialize() async { | ||
| final settings = await SettingsService.getInstance(); | ||
| _isEnabled = settings.getEnableDiscordRpc(); | ||
|
|
||
| if (!_isEnabled || _isInitialized || _applicationId.isEmpty) { | ||
| if (_applicationId.isEmpty && _isEnabled) { | ||
| appLogger.w('Discord RPC initialized without Application ID. Use --dart-define=DISCORD_APP_ID=...'); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| await FlutterDiscordRPC.initialize(_applicationId); | ||
| FlutterDiscordRPC.instance.connect(); | ||
| _isInitialized = true; | ||
| appLogger.d('Discord RPC initialized'); | ||
| } catch (e) { | ||
| appLogger.e('Failed to initialize Discord RPC', error: e); | ||
| } | ||
| } | ||
|
|
||
| Future<void> updateSettings(bool enabled) async { | ||
| if (_isEnabled == enabled) return; | ||
| _isEnabled = enabled; | ||
|
|
||
| if (enabled) { | ||
| await initialize(); | ||
| } else { | ||
| dispose(); | ||
| } | ||
| } | ||
|
|
||
| void updatePresence({ | ||
| required String title, | ||
| String? subtitle, | ||
| String? state, | ||
| int? startTime, | ||
| int? endTime, | ||
| String? largeImageKey, | ||
| String? largeImageText, | ||
| String? smallImageKey, | ||
| String? smallImageText, | ||
| }) { | ||
| if (!_isInitialized || !_isEnabled) return; | ||
|
|
||
| try { | ||
| FlutterDiscordRPC.instance.setActivity( | ||
| activity: RPCActivity( | ||
| details: title, | ||
| state: subtitle ?? state, | ||
| timestamps: (startTime != null || endTime != null) | ||
| ? RPCTimestamps( | ||
| start: startTime, | ||
| end: endTime, | ||
| ) | ||
| : null, | ||
| // Note: Image keys refer to assets uploaded to the Discord Developer Portal application | ||
| assets: RPCAssets( | ||
| largeImage: largeImageKey, | ||
| largeText: largeImageText, | ||
|
Doezer marked this conversation as resolved.
|
||
| smallImage: smallImageKey, | ||
| smallText: smallImageText, | ||
| ), | ||
| ), | ||
| ); | ||
| } catch (e) { | ||
| appLogger.w('Failed to update Discord presence', error: e); | ||
| } | ||
| } | ||
|
|
||
| void clearActivity() { | ||
| if (!_isInitialized) return; | ||
| try { | ||
| FlutterDiscordRPC.instance.clearActivity(); | ||
| } catch (e) { | ||
| appLogger.w('Failed to clear Discord activity', error: e); | ||
| } | ||
| } | ||
|
|
||
| void dispose() { | ||
| if (_isInitialized) { | ||
| FlutterDiscordRPC.instance.disconnect(); | ||
| FlutterDiscordRPC.instance.dispose(); | ||
| _isInitialized = false; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import 'package:os_media_controls/os_media_controls.dart'; | |
| import 'package:rate_limiter/rate_limiter.dart'; | ||
|
|
||
| import 'plex_client.dart'; | ||
| import 'discord_rpc_service.dart'; | ||
| import '../models/plex_metadata.dart'; | ||
| import '../utils/content_utils.dart'; | ||
| import '../utils/app_logger.dart'; | ||
|
|
@@ -24,6 +25,12 @@ class MediaControlsManager { | |
| bool? _lastCanGoNext; | ||
| bool? _lastCanGoPrevious; | ||
|
|
||
| final DiscordRPCService _discordRpc = DiscordRPCService(); | ||
|
|
||
| // Cache for discord RPC | ||
| PlexMetadata? _currentMetadata; | ||
| Duration? _currentDuration; | ||
|
|
||
| MediaControlsManager() { | ||
| _throttledUpdate = throttle( | ||
| _doUpdatePlaybackState, | ||
|
|
@@ -37,6 +44,9 @@ class MediaControlsManager { | |
| /// | ||
| /// This includes title, artist, artwork, and duration. | ||
| Future<void> updateMetadata({required PlexMetadata metadata, PlexClient? client, Duration? duration}) async { | ||
| _currentMetadata = metadata; | ||
| _currentDuration = duration; | ||
|
|
||
| try { | ||
| // Build artwork URL if client is available | ||
| String? artworkUrl; | ||
|
|
@@ -59,6 +69,9 @@ class MediaControlsManager { | |
| ), | ||
| ); | ||
|
|
||
| // Update Discord RPC | ||
| _updateDiscordPresence(isPlaying: false, position: Duration.zero); | ||
|
|
||
| appLogger.d('Updated media controls metadata: ${metadata.title}'); | ||
| } catch (e) { | ||
| appLogger.w('Failed to update media controls metadata', error: e); | ||
|
|
@@ -96,6 +109,12 @@ class MediaControlsManager { | |
| speed: params.speed, | ||
| ), | ||
| ); | ||
|
|
||
| _updateDiscordPresence( | ||
| isPlaying: params.isPlaying, | ||
| position: params.position, | ||
| speed: params.speed, | ||
| ); | ||
| } catch (e) { | ||
| appLogger.w('Failed to update media controls playback state', error: e); | ||
| } | ||
|
|
@@ -140,7 +159,10 @@ class MediaControlsManager { | |
| Future<void> clear() async { | ||
| try { | ||
| await OsMediaControls.clear(); | ||
| _discordRpc.clearActivity(); | ||
| _throttledUpdate.cancel(); | ||
| _currentMetadata = null; | ||
| _currentDuration = null; | ||
| appLogger.d('Media controls cleared'); | ||
| } catch (e) { | ||
| appLogger.w('Failed to clear media controls', error: e); | ||
|
|
@@ -184,6 +206,33 @@ class MediaControlsManager { | |
|
|
||
| return ''; | ||
| } | ||
|
|
||
| void _updateDiscordPresence({required bool isPlaying, required Duration position, double speed = 1.0}) { | ||
| if (_currentMetadata == null) return; | ||
|
|
||
| final title = _currentMetadata!.title; | ||
| final artist = _buildArtist(_currentMetadata!); | ||
|
|
||
| int? endTime; | ||
|
|
||
| if (isPlaying && _currentDuration != null) { | ||
| final now = DateTime.now().millisecondsSinceEpoch; | ||
| final remaining = (_currentDuration!.inMilliseconds - position.inMilliseconds) ~/ speed; | ||
| endTime = now + remaining; | ||
| } | ||
|
|
||
| _discordRpc.updatePresence( | ||
| title: title, | ||
| subtitle: artist.isNotEmpty ? artist : null, | ||
| state: isPlaying ? null : 'Paused', | ||
| startTime: isPlaying && endTime == null ? DateTime.now().millisecondsSinceEpoch : null, | ||
| endTime: endTime, | ||
| largeImageKey: 'plezy', | ||
| largeImageText: 'Plezy', | ||
| smallImageKey: isPlaying ? 'play' : 'pause', | ||
| smallImageText: isPlaying ? 'Playing' : 'Paused', | ||
|
Comment on lines
+224
to
+233
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make comments simple and concise, remove if unnecessary and "make it simple" for the implem
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simplified the logic and removed unnecessary comments. |
||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Parameters for playback state update (used with throttle) | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.