Skip to content
This repository was archived by the owner on Apr 3, 2025. It is now read-only.

Commit 5cf1a06

Browse files
committed
feat(tautulli): add ability to open media in Plex
1 parent 47ff262 commit 5cf1a06

File tree

11 files changed

+149
-19
lines changed

11 files changed

+149
-19
lines changed

android/app/src/debug/AndroidManifest.xml

+3
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@
77
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
88
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
99
<uses-permission android:name="com.android.vending.BILLING" />
10+
<queries>
11+
<package android:name="com.plexapp.android" />
12+
</queries>
1013
</manifest>

android/app/src/main/AndroidManifest.xml

+3
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@
4040
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
4141
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
4242
<uses-permission android:name="com.android.vending.BILLING" />
43+
<queries>
44+
<package android:name="com.plexapp.android" />
45+
</queries>
4346
</manifest>

android/app/src/profile/AndroidManifest.xml

+3
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
99
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
1010
<uses-permission android:name="com.android.vending.BILLING" />
11+
<queries>
12+
<package android:name="com.plexapp.android" />
13+
</queries>
1114
</manifest>

ios/Runner/Info.plist

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
<string>Main</string>
6262
<key>UIStatusBarHidden</key>
6363
<false/>
64+
<key>LSApplicationQueriesSchemes</key>
65+
<array>
66+
<string>plex</string>
67+
<string>https</string>
68+
</array>
6469
<key>UISupportedInterfaceOrientations</key>
6570
<array>
6671
<string>UIInterfaceOrientationPortrait</string>

lib/extensions/string/links.dart

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import 'package:lunasea/system/logger.dart';
2-
import 'package:url_launcher/url_launcher.dart';
2+
import 'package:url_launcher/url_launcher_string.dart';
33

44
extension StringAsLinksExtension on String {
5-
Future<bool> _launchUniversal(uri) async {
6-
return await launchUrl(
5+
Future<bool> _launchUniversal(String uri) async {
6+
return await launchUrlString(
77
uri,
88
webOnlyWindowName: '_blank',
9-
mode: LaunchMode.externalNonBrowserApplication,
9+
mode: LaunchMode.externalApplication,
1010
);
1111
}
1212

13-
Future<bool> _launchDefault(uri) async {
14-
return await launchUrl(
13+
Future<bool> _launchDefault(String uri) async {
14+
return await launchUrlString(
1515
uri,
1616
webOnlyWindowName: '_blank',
1717
mode: LaunchMode.platformDefault,
@@ -20,9 +20,8 @@ extension StringAsLinksExtension on String {
2020

2121
Future<void> openLink() async {
2222
try {
23-
Uri uri = Uri.parse(this);
24-
if (await _launchUniversal(uri)) return;
25-
await _launchDefault(uri);
23+
if (await _launchUniversal(this)) return;
24+
await _launchDefault(this);
2625
} catch (error, stack) {
2726
LunaLogger().error(
2827
'Unable to open URL',
@@ -32,6 +31,10 @@ extension StringAsLinksExtension on String {
3231
}
3332
}
3433

34+
Future<bool> canOpenUrl() async {
35+
return canLaunchUrlString(this);
36+
}
37+
3538
Future<void> openImdb() async =>
3639
await 'https://www.imdb.com/title/$this'.openLink();
3740

lib/modules/tautulli/core/state.dart

+20
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class TautulliState extends LunaModuleState {
3333
_playCountByPlatformStreamTypeGraph = null;
3434
_playCountByUserStreamTypeGraph = null;
3535
_librariesTable = null;
36+
_serverIdentity = null;
3637
_searchQuery = '';
3738

3839
// Clear user data
@@ -52,6 +53,7 @@ class TautulliState extends LunaModuleState {
5253
resetActivity();
5354
resetUsers();
5455
resetHistory();
56+
resetServerIdentity();
5557
notifyListeners();
5658
}
5759

@@ -194,6 +196,24 @@ class TautulliState extends LunaModuleState {
194196
notifyListeners();
195197
}
196198

199+
///////////////////////
200+
/// SERVER IDENTITY ///
201+
///////////////////////
202+
203+
Future<TautulliServerIdentity>? _serverIdentity;
204+
Future<TautulliServerIdentity>? get serverIdentity => _serverIdentity;
205+
set serverIdentity(Future<TautulliServerIdentity>? serverIdentity) {
206+
_serverIdentity = serverIdentity;
207+
notifyListeners();
208+
}
209+
210+
void resetServerIdentity() {
211+
if (_api != null) {
212+
_serverIdentity = _api!.miscellaneous.getServerIdentity();
213+
}
214+
notifyListeners();
215+
}
216+
197217
//////////////////
198218
/// STATISTICS ///
199219
//////////////////

lib/modules/tautulli/routes/media_details/route.dart

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class _State extends State<MediaDetailsRoute> {
4242
title: 'Media Details',
4343
scrollControllers: TautulliMediaDetailsNavigationBar.scrollControllers,
4444
pageController: _pageController,
45+
actions: [
46+
TautulliMediaDetailsOpenPlexButton(
47+
ratingKey: widget.ratingKey,
48+
mediaType: widget.mediaType,
49+
),
50+
],
4551
);
4652
}
4753

lib/modules/tautulli/routes/media_details/widgets.dart

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export 'widgets/metadata_header.dart';
44
export 'widgets/metadata_metadata.dart';
55
export 'widgets/metadata_summary.dart';
66
export 'widgets/navigation_bar.dart';
7+
export 'widgets/open_plex_button.dart';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:lunasea/core.dart';
3+
import 'package:lunasea/extensions/string/links.dart';
4+
import 'package:lunasea/modules/tautulli.dart';
5+
import 'package:lunasea/utils/links.dart';
6+
7+
class TautulliMediaDetailsOpenPlexButton extends StatelessWidget {
8+
final TautulliMediaType mediaType;
9+
final int ratingKey;
10+
11+
const TautulliMediaDetailsOpenPlexButton({
12+
Key? key,
13+
required this.mediaType,
14+
required this.ratingKey,
15+
}) : super(key: key);
16+
17+
@override
18+
Widget build(BuildContext context) {
19+
return FutureBuilder(
20+
future: context.watch<TautulliState>().serverIdentity,
21+
builder: (context, snapshot) {
22+
if (_isValidMediaType() && snapshot.hasData) {
23+
return LunaIconButton.appBar(
24+
icon: LunaIcons.PLEX,
25+
onPressed: () => _openPlex(snapshot.data as TautulliServerIdentity),
26+
);
27+
}
28+
return const SizedBox();
29+
},
30+
);
31+
}
32+
33+
bool _isValidMediaType() {
34+
const invalidTypes = [
35+
TautulliMediaType.TRACK,
36+
TautulliMediaType.PHOTO,
37+
];
38+
return !invalidTypes.contains(mediaType);
39+
}
40+
41+
Future<void> _openPlex(TautulliServerIdentity identity) async {
42+
final mobile = LunaLinkedContent.plexMobile(
43+
identity.machineIdentifier!,
44+
ratingKey,
45+
);
46+
47+
if (await mobile.canOpenUrl()) {
48+
mobile.openLink();
49+
return;
50+
}
51+
52+
final web = LunaLinkedContent.plexWeb(
53+
identity.machineIdentifier!,
54+
ratingKey,
55+
mediaType == TautulliMediaType.CLIP,
56+
);
57+
web.openLink();
58+
}
59+
}

lib/utils/links.dart

+36-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:lunasea/core.dart';
22
import 'package:lunasea/extensions/string/links.dart';
3+
import 'package:lunasea/system/platform.dart';
34

45
enum LinkedContentType {
56
MOVIE,
@@ -28,29 +29,55 @@ enum LunaLinkedContent {
2829

2930
static String? imdb(String? id) {
3031
if (id == null) return null;
31-
String base = 'https://www.imdb.com';
32+
const base = 'https://www.imdb.com';
3233

3334
return '$base/title/$id';
3435
}
3536

3637
static String? letterboxd(int? id) {
3738
if (id == null) return null;
38-
String base = 'https://letterboxd.com';
39+
const base = 'https://letterboxd.com';
3940

4041
return '$base/tmdb/$id';
4142
}
4243

4344
static String? musicBrainz(String? id) {
4445
if (id == null) return null;
45-
String base = 'https://musicbrainz.org/artist';
46+
const base = 'https://musicbrainz.org/artist';
4647

4748
return '$base/$id';
4849
}
4950

51+
static String plexMobile(
52+
String plexIdentifier,
53+
int ratingKey,
54+
) {
55+
if (LunaPlatform.isAndroid) {
56+
const base = 'plex://server://';
57+
const path = '/com.plexapp.plugins.library/library/metadata/';
58+
return '$base$plexIdentifier$path$ratingKey';
59+
} else {
60+
const base = 'plex://preplay/?server=';
61+
const path = '&metadataKey=/library/metadata/';
62+
return '$base$plexIdentifier$path$ratingKey';
63+
}
64+
}
65+
66+
static String plexWeb(
67+
String plexIdentifier,
68+
int ratingKey, [
69+
bool useLegacy = false,
70+
]) {
71+
const base = 'https://app.plex.tv/desktop#!/server/';
72+
const path = '/details?key=%2Flibrary%2Fmetadata%2F';
73+
final legacy = useLegacy ? '&legacy=1' : '';
74+
return '$base$plexIdentifier$path$ratingKey$legacy';
75+
}
76+
5077
static String? theMovieDB(dynamic id, LinkedContentType type) {
5178
if (id == null) return null;
52-
String base = 'https://www.themoviedb.org';
53-
String baseImage = 'https://image.tmdb.org/t/p';
79+
const base = 'https://www.themoviedb.org';
80+
const baseImage = 'https://image.tmdb.org/t/p';
5481

5582
switch (type) {
5683
case LinkedContentType.MOVIE:
@@ -70,7 +97,7 @@ enum LunaLinkedContent {
7097

7198
static String? trakt(int? id, LinkedContentType type) {
7299
if (id == null) return null;
73-
String base = 'https://trakt.tv';
100+
const base = 'https://trakt.tv';
74101

75102
switch (type) {
76103
case LinkedContentType.MOVIE:
@@ -84,14 +111,14 @@ enum LunaLinkedContent {
84111

85112
static String? tvMaze(int? id) {
86113
if (id == null) return null;
87-
String base = 'https://www.tvmaze.com';
114+
const base = 'https://www.tvmaze.com';
88115

89116
return '$base/shows/$id';
90117
}
91118

92119
static String? theTVDB(int? id, LinkedContentType type) {
93120
if (id == null) return null;
94-
String base = 'https://thetvdb.com';
121+
const base = 'https://thetvdb.com';
95122

96123
switch (type) {
97124
case LinkedContentType.MOVIE:
@@ -107,7 +134,7 @@ enum LunaLinkedContent {
107134

108135
static String? youtube(String? id) {
109136
if (id == null) return null;
110-
String base = 'https://www.youtube.com';
137+
const base = 'https://www.youtube.com';
111138

112139
return '$base/watch?v=$id';
113140
}

pubspec.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1612,5 +1612,5 @@ packages:
16121612
source: hosted
16131613
version: "3.1.1"
16141614
sdks:
1615-
dart: ">=2.18.0 <4.0.0"
1615+
dart: ">=2.18.0 <3.0.0"
16161616
flutter: ">=3.3.0"

0 commit comments

Comments
 (0)