feat(tautulli): add ability to open media in Plex

This commit is contained in:
Jagandeep Brar
2023-02-16 17:07:56 -05:00
parent 47ff2628a2
commit 5cf1a06cec
11 changed files with 149 additions and 19 deletions

View File

@@ -7,4 +7,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.android.vending.BILLING" />
<queries>
<package android:name="com.plexapp.android" />
</queries>
</manifest>

View File

@@ -40,4 +40,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.android.vending.BILLING" />
<queries>
<package android:name="com.plexapp.android" />
</queries>
</manifest>

View File

@@ -8,4 +8,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.android.vending.BILLING" />
<queries>
<package android:name="com.plexapp.android" />
</queries>
</manifest>

View File

@@ -61,6 +61,11 @@
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>plex</string>
<string>https</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@@ -1,17 +1,17 @@
import 'package:lunasea/system/logger.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
extension StringAsLinksExtension on String {
Future<bool> _launchUniversal(uri) async {
return await launchUrl(
Future<bool> _launchUniversal(String uri) async {
return await launchUrlString(
uri,
webOnlyWindowName: '_blank',
mode: LaunchMode.externalNonBrowserApplication,
mode: LaunchMode.externalApplication,
);
}
Future<bool> _launchDefault(uri) async {
return await launchUrl(
Future<bool> _launchDefault(String uri) async {
return await launchUrlString(
uri,
webOnlyWindowName: '_blank',
mode: LaunchMode.platformDefault,
@@ -20,9 +20,8 @@ extension StringAsLinksExtension on String {
Future<void> openLink() async {
try {
Uri uri = Uri.parse(this);
if (await _launchUniversal(uri)) return;
await _launchDefault(uri);
if (await _launchUniversal(this)) return;
await _launchDefault(this);
} catch (error, stack) {
LunaLogger().error(
'Unable to open URL',
@@ -32,6 +31,10 @@ extension StringAsLinksExtension on String {
}
}
Future<bool> canOpenUrl() async {
return canLaunchUrlString(this);
}
Future<void> openImdb() async =>
await 'https://www.imdb.com/title/$this'.openLink();

View File

@@ -33,6 +33,7 @@ class TautulliState extends LunaModuleState {
_playCountByPlatformStreamTypeGraph = null;
_playCountByUserStreamTypeGraph = null;
_librariesTable = null;
_serverIdentity = null;
_searchQuery = '';
// Clear user data
@@ -52,6 +53,7 @@ class TautulliState extends LunaModuleState {
resetActivity();
resetUsers();
resetHistory();
resetServerIdentity();
notifyListeners();
}
@@ -194,6 +196,24 @@ class TautulliState extends LunaModuleState {
notifyListeners();
}
///////////////////////
/// SERVER IDENTITY ///
///////////////////////
Future<TautulliServerIdentity>? _serverIdentity;
Future<TautulliServerIdentity>? get serverIdentity => _serverIdentity;
set serverIdentity(Future<TautulliServerIdentity>? serverIdentity) {
_serverIdentity = serverIdentity;
notifyListeners();
}
void resetServerIdentity() {
if (_api != null) {
_serverIdentity = _api!.miscellaneous.getServerIdentity();
}
notifyListeners();
}
//////////////////
/// STATISTICS ///
//////////////////

View File

@@ -42,6 +42,12 @@ class _State extends State<MediaDetailsRoute> {
title: 'Media Details',
scrollControllers: TautulliMediaDetailsNavigationBar.scrollControllers,
pageController: _pageController,
actions: [
TautulliMediaDetailsOpenPlexButton(
ratingKey: widget.ratingKey,
mediaType: widget.mediaType,
),
],
);
}

View File

@@ -4,3 +4,4 @@ export 'widgets/metadata_header.dart';
export 'widgets/metadata_metadata.dart';
export 'widgets/metadata_summary.dart';
export 'widgets/navigation_bar.dart';
export 'widgets/open_plex_button.dart';

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/extensions/string/links.dart';
import 'package:lunasea/modules/tautulli.dart';
import 'package:lunasea/utils/links.dart';
class TautulliMediaDetailsOpenPlexButton extends StatelessWidget {
final TautulliMediaType mediaType;
final int ratingKey;
const TautulliMediaDetailsOpenPlexButton({
Key? key,
required this.mediaType,
required this.ratingKey,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: context.watch<TautulliState>().serverIdentity,
builder: (context, snapshot) {
if (_isValidMediaType() && snapshot.hasData) {
return LunaIconButton.appBar(
icon: LunaIcons.PLEX,
onPressed: () => _openPlex(snapshot.data as TautulliServerIdentity),
);
}
return const SizedBox();
},
);
}
bool _isValidMediaType() {
const invalidTypes = [
TautulliMediaType.TRACK,
TautulliMediaType.PHOTO,
];
return !invalidTypes.contains(mediaType);
}
Future<void> _openPlex(TautulliServerIdentity identity) async {
final mobile = LunaLinkedContent.plexMobile(
identity.machineIdentifier!,
ratingKey,
);
if (await mobile.canOpenUrl()) {
mobile.openLink();
return;
}
final web = LunaLinkedContent.plexWeb(
identity.machineIdentifier!,
ratingKey,
mediaType == TautulliMediaType.CLIP,
);
web.openLink();
}
}

View File

@@ -1,5 +1,6 @@
import 'package:lunasea/core.dart';
import 'package:lunasea/extensions/string/links.dart';
import 'package:lunasea/system/platform.dart';
enum LinkedContentType {
MOVIE,
@@ -28,29 +29,55 @@ enum LunaLinkedContent {
static String? imdb(String? id) {
if (id == null) return null;
String base = 'https://www.imdb.com';
const base = 'https://www.imdb.com';
return '$base/title/$id';
}
static String? letterboxd(int? id) {
if (id == null) return null;
String base = 'https://letterboxd.com';
const base = 'https://letterboxd.com';
return '$base/tmdb/$id';
}
static String? musicBrainz(String? id) {
if (id == null) return null;
String base = 'https://musicbrainz.org/artist';
const base = 'https://musicbrainz.org/artist';
return '$base/$id';
}
static String plexMobile(
String plexIdentifier,
int ratingKey,
) {
if (LunaPlatform.isAndroid) {
const base = 'plex://server://';
const path = '/com.plexapp.plugins.library/library/metadata/';
return '$base$plexIdentifier$path$ratingKey';
} else {
const base = 'plex://preplay/?server=';
const path = '&metadataKey=/library/metadata/';
return '$base$plexIdentifier$path$ratingKey';
}
}
static String plexWeb(
String plexIdentifier,
int ratingKey, [
bool useLegacy = false,
]) {
const base = 'https://app.plex.tv/desktop#!/server/';
const path = '/details?key=%2Flibrary%2Fmetadata%2F';
final legacy = useLegacy ? '&legacy=1' : '';
return '$base$plexIdentifier$path$ratingKey$legacy';
}
static String? theMovieDB(dynamic id, LinkedContentType type) {
if (id == null) return null;
String base = 'https://www.themoviedb.org';
String baseImage = 'https://image.tmdb.org/t/p';
const base = 'https://www.themoviedb.org';
const baseImage = 'https://image.tmdb.org/t/p';
switch (type) {
case LinkedContentType.MOVIE:
@@ -70,7 +97,7 @@ enum LunaLinkedContent {
static String? trakt(int? id, LinkedContentType type) {
if (id == null) return null;
String base = 'https://trakt.tv';
const base = 'https://trakt.tv';
switch (type) {
case LinkedContentType.MOVIE:
@@ -84,14 +111,14 @@ enum LunaLinkedContent {
static String? tvMaze(int? id) {
if (id == null) return null;
String base = 'https://www.tvmaze.com';
const base = 'https://www.tvmaze.com';
return '$base/shows/$id';
}
static String? theTVDB(int? id, LinkedContentType type) {
if (id == null) return null;
String base = 'https://thetvdb.com';
const base = 'https://thetvdb.com';
switch (type) {
case LinkedContentType.MOVIE:
@@ -107,7 +134,7 @@ enum LunaLinkedContent {
static String? youtube(String? id) {
if (id == null) return null;
String base = 'https://www.youtube.com';
const base = 'https://www.youtube.com';
return '$base/watch?v=$id';
}

View File

@@ -1612,5 +1612,5 @@ packages:
source: hosted
version: "3.1.1"
sdks:
dart: ">=2.18.0 <4.0.0"
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.3.0"