feat: jellyfin

This commit is contained in:
edde746
2026-05-01 01:20:36 +02:00
parent 4080c812d2
commit 31d2d9dc98
408 changed files with 56022 additions and 13747 deletions
+46
View File
@@ -0,0 +1,46 @@
import '../../media/media_item.dart';
/// Response from Plex play queue API.
/// Contains queue metadata and a window of items in neutral [MediaItem] form.
class PlayQueueResponse {
final int playQueueID;
final int? playQueueSelectedItemID;
final int? playQueueSelectedItemOffset;
final String? playQueueSelectedMetadataItemID;
final bool playQueueShuffled;
final String? playQueueSourceURI;
final int? playQueueTotalCount;
final int playQueueVersion;
final int? size; // Number of items in this response window
final List<MediaItem>? items;
PlayQueueResponse({
required this.playQueueID,
this.playQueueSelectedItemID,
this.playQueueSelectedItemOffset,
this.playQueueSelectedMetadataItemID,
required this.playQueueShuffled,
this.playQueueSourceURI,
required this.playQueueTotalCount,
required this.playQueueVersion,
this.size,
this.items,
});
/// Get the current selected item from the queue. Items in a Plex
/// `PlayQueueResponse` are always [PlexMediaItem]; the cast is safe.
MediaItem? get selectedItem {
if (items == null || playQueueSelectedItemID == null) return null;
try {
return items!.firstWhere((item) => item is PlexMediaItem && item.playQueueItemId == playQueueSelectedItemID);
} catch (e) {
return null;
}
}
/// Get the index of the selected item in the current window
int? get selectedItemIndex {
if (items == null || playQueueSelectedItemID == null) return null;
return items!.indexWhere((item) => item is PlexMediaItem && item.playQueueItemId == playQueueSelectedItemID);
}
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
part 'plex_activity.g.dart';
/// Represents a running background task on a Plex Media Server (from /activities endpoint).
@JsonSerializable(createToJson: false)
class PlexActivity {
@JsonKey(defaultValue: '')
final String uuid;
@JsonKey(defaultValue: '')
final String type;
@JsonKey(defaultValue: '')
final String title;
final String? subtitle;
@JsonKey(defaultValue: 0)
final int progress; // 0100
@JsonKey(defaultValue: false)
final bool cancellable;
const PlexActivity({
required this.uuid,
required this.type,
required this.title,
this.subtitle,
required this.progress,
required this.cancellable,
});
factory PlexActivity.fromJson(Map<String, dynamic> json) => _$PlexActivityFromJson(json);
}
+16
View File
@@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_activity.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexActivity _$PlexActivityFromJson(Map<String, dynamic> json) => PlexActivity(
uuid: json['uuid'] as String? ?? '',
type: json['type'] as String? ?? '',
title: json['title'] as String? ?? '',
subtitle: json['subtitle'] as String?,
progress: (json['progress'] as num?)?.toInt() ?? 0,
cancellable: json['cancellable'] as bool? ?? false,
);
+92
View File
@@ -0,0 +1,92 @@
import 'package:package_info_plus/package_info_plus.dart';
class PlexConfig {
final String baseUrl;
final String? token;
final String clientIdentifier;
final String product;
final String version;
final String platform;
final String? device;
final bool acceptJson;
final String? machineIdentifier;
PlexConfig({
required this.baseUrl,
this.token,
required this.clientIdentifier,
required this.product,
required this.version,
this.platform = 'Flutter',
this.device,
this.acceptJson = true,
this.machineIdentifier,
});
static Future<PlexConfig> create({
required String baseUrl,
String? token,
required String clientIdentifier,
String? product,
String? platform,
String? device,
bool acceptJson = true,
String? machineIdentifier,
}) async {
final packageInfo = await PackageInfo.fromPlatform();
return PlexConfig(
baseUrl: baseUrl,
token: token,
clientIdentifier: clientIdentifier,
product: product ?? 'Plezy',
version: packageInfo.version,
platform: platform ?? 'Flutter',
device: device,
acceptJson: acceptJson,
machineIdentifier: machineIdentifier,
);
}
Map<String, String> get headers {
final headers = {
'X-Plex-Client-Identifier': clientIdentifier,
'X-Plex-Product': product,
'X-Plex-Version': version,
'X-Plex-Platform': platform,
'X-Plex-Client-Profile-Name': 'Generic',
'X-Plex-Device': ?device,
if (acceptJson) 'Accept': 'application/json',
'Accept-Charset': 'utf-8',
};
if (token != null) {
headers['X-Plex-Token'] = token!;
}
return headers;
}
PlexConfig copyWith({
String? baseUrl,
String? token,
String? clientIdentifier,
String? product,
String? version,
String? platform,
String? device,
bool? acceptJson,
String? machineIdentifier,
}) {
return PlexConfig(
baseUrl: baseUrl ?? this.baseUrl,
token: token ?? this.token,
clientIdentifier: clientIdentifier ?? this.clientIdentifier,
product: product ?? this.product,
version: version ?? this.version,
platform: platform ?? this.platform,
device: device ?? this.device,
acceptJson: acceptJson ?? this.acceptJson,
machineIdentifier: machineIdentifier ?? this.machineIdentifier,
);
}
}
+52
View File
@@ -0,0 +1,52 @@
import 'package:json_annotation/json_annotation.dart';
import 'plex_home_user.dart';
part 'plex_home.g.dart';
@JsonSerializable()
class PlexHome {
@JsonKey(defaultValue: 0)
final int id;
@JsonKey(defaultValue: '')
final String name;
final int? guestUserID;
@JsonKey(defaultValue: '')
final String guestUserUUID;
@JsonKey(defaultValue: false)
final bool guestEnabled;
@JsonKey(defaultValue: false)
final bool subscription;
@JsonKey(defaultValue: <PlexHomeUser>[])
final List<PlexHomeUser> users;
PlexHome({
required this.id,
required this.name,
required this.guestUserID,
required this.guestUserUUID,
required this.guestEnabled,
required this.subscription,
required this.users,
});
factory PlexHome.fromJson(Map<String, dynamic> json) => _$PlexHomeFromJson(json);
Map<String, dynamic> toJson() => _$PlexHomeToJson(this);
PlexHomeUser? get adminUser => users.where((user) => user.admin).firstOrNull;
List<PlexHomeUser> get managedUsers => users.where((user) => !user.admin).toList();
List<PlexHomeUser> get restrictedUsers => users.where((user) => user.restricted).toList();
PlexHomeUser? getUserByUUID(String uuid) {
try {
return users.firstWhere((user) => user.uuid == uuid);
} catch (e) {
return null;
}
}
bool get hasMultipleUsers => users.length > 1;
}
+27
View File
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_home.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexHome _$PlexHomeFromJson(Map<String, dynamic> json) => PlexHome(
id: (json['id'] as num?)?.toInt() ?? 0,
name: json['name'] as String? ?? '',
guestUserID: (json['guestUserID'] as num?)?.toInt(),
guestUserUUID: json['guestUserUUID'] as String? ?? '',
guestEnabled: json['guestEnabled'] as bool? ?? false,
subscription: json['subscription'] as bool? ?? false,
users: (json['users'] as List<dynamic>?)?.map((e) => PlexHomeUser.fromJson(e as Map<String, dynamic>)).toList() ?? [],
);
Map<String, dynamic> _$PlexHomeToJson(PlexHome instance) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'guestUserID': instance.guestUserID,
'guestUserUUID': instance.guestUserUUID,
'guestEnabled': instance.guestEnabled,
'subscription': instance.subscription,
'users': instance.users,
};
+56
View File
@@ -0,0 +1,56 @@
import 'package:json_annotation/json_annotation.dart';
part 'plex_home_user.g.dart';
@JsonSerializable()
class PlexHomeUser {
@JsonKey(defaultValue: 0)
final int id;
@JsonKey(defaultValue: '')
final String uuid;
@JsonKey(defaultValue: 'Unknown')
final String title;
final String? username;
final String? email;
final String? friendlyName;
@JsonKey(defaultValue: '')
final String thumb;
@JsonKey(defaultValue: false)
final bool hasPassword;
@JsonKey(defaultValue: false)
final bool restricted;
final int? updatedAt;
@JsonKey(defaultValue: false)
final bool admin;
@JsonKey(defaultValue: false)
final bool guest;
@JsonKey(defaultValue: false)
final bool protected;
PlexHomeUser({
required this.id,
required this.uuid,
required this.title,
this.username,
this.email,
this.friendlyName,
required this.thumb,
required this.hasPassword,
required this.restricted,
required this.updatedAt,
required this.admin,
required this.guest,
required this.protected,
});
factory PlexHomeUser.fromJson(Map<String, dynamic> json) => _$PlexHomeUserFromJson(json);
Map<String, dynamic> toJson() => _$PlexHomeUserToJson(this);
String get displayName => friendlyName ?? title;
bool get isAdminUser => admin;
bool get isRestrictedUser => restricted;
bool get isGuestUser => guest;
bool get requiresPassword => protected;
}
+39
View File
@@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_home_user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexHomeUser _$PlexHomeUserFromJson(Map<String, dynamic> json) => PlexHomeUser(
id: (json['id'] as num?)?.toInt() ?? 0,
uuid: json['uuid'] as String? ?? '',
title: json['title'] as String? ?? 'Unknown',
username: json['username'] as String?,
email: json['email'] as String?,
friendlyName: json['friendlyName'] as String?,
thumb: json['thumb'] as String? ?? '',
hasPassword: json['hasPassword'] as bool? ?? false,
restricted: json['restricted'] as bool? ?? false,
updatedAt: (json['updatedAt'] as num?)?.toInt(),
admin: json['admin'] as bool? ?? false,
guest: json['guest'] as bool? ?? false,
protected: json['protected'] as bool? ?? false,
);
Map<String, dynamic> _$PlexHomeUserToJson(PlexHomeUser instance) => <String, dynamic>{
'id': instance.id,
'uuid': instance.uuid,
'title': instance.title,
'username': instance.username,
'email': instance.email,
'friendlyName': instance.friendlyName,
'thumb': instance.thumb,
'hasPassword': instance.hasPassword,
'restricted': instance.restricted,
'updatedAt': instance.updatedAt,
'admin': instance.admin,
'guest': instance.guest,
'protected': instance.protected,
};
+40
View File
@@ -0,0 +1,40 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
part 'plex_match_result.g.dart';
@JsonSerializable()
class PlexMatchResult {
@JsonKey(readValue: readStringField, defaultValue: '')
final String guid;
@JsonKey(readValue: readStringField, defaultValue: '')
final String name;
@JsonKey(fromJson: flexibleInt)
final int? year;
@JsonKey(fromJson: flexibleInt)
final int? score;
@JsonKey(readValue: readStringField)
final String? thumb;
@JsonKey(readValue: readStringField)
final String? summary;
@JsonKey(readValue: readStringField)
final String? type;
@JsonKey(fromJson: flexibleBool)
final bool matched;
PlexMatchResult({
required this.guid,
required this.name,
this.year,
this.score,
this.thumb,
this.summary,
this.type,
this.matched = false,
});
factory PlexMatchResult.fromJson(Map<String, dynamic> json) => _$PlexMatchResultFromJson(json);
Map<String, dynamic> toJson() => _$PlexMatchResultToJson(this);
}
+29
View File
@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_match_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexMatchResult _$PlexMatchResultFromJson(Map<String, dynamic> json) => PlexMatchResult(
guid: readStringField(json, 'guid') as String? ?? '',
name: readStringField(json, 'name') as String? ?? '',
year: flexibleInt(json['year']),
score: flexibleInt(json['score']),
thumb: readStringField(json, 'thumb') as String?,
summary: readStringField(json, 'summary') as String?,
type: readStringField(json, 'type') as String?,
matched: json['matched'] == null ? false : flexibleBool(json['matched']),
);
Map<String, dynamic> _$PlexMatchResultToJson(PlexMatchResult instance) => <String, dynamic>{
'guid': instance.guid,
'name': instance.name,
'year': instance.year,
'score': instance.score,
'thumb': instance.thumb,
'summary': instance.summary,
'type': instance.type,
'matched': instance.matched,
};
@@ -0,0 +1,57 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
part 'plex_subtitle_search_result.g.dart';
int _flexibleIntOrZero(Object? v) => flexibleInt(v) ?? 0;
@JsonSerializable()
class PlexSubtitleSearchResult {
@JsonKey(fromJson: _flexibleIntOrZero)
final int id;
@JsonKey(readValue: readStringField, defaultValue: '')
final String key;
@JsonKey(readValue: readStringField)
final String? codec;
@JsonKey(readValue: readStringField)
final String? language;
@JsonKey(readValue: readStringField)
final String? languageCode;
@JsonKey(fromJson: flexibleDouble)
final double? score;
@JsonKey(readValue: readStringField)
final String? providerTitle;
@JsonKey(readValue: readStringField)
final String? title;
@JsonKey(readValue: readStringField)
final String? displayTitle;
@JsonKey(fromJson: flexibleBool)
final bool hearingImpaired;
@JsonKey(fromJson: flexibleBool)
final bool perfectMatch;
@JsonKey(fromJson: flexibleBool)
final bool downloaded;
@JsonKey(fromJson: flexibleBool)
final bool forced;
PlexSubtitleSearchResult({
required this.id,
required this.key,
this.codec,
this.language,
this.languageCode,
this.score,
this.providerTitle,
this.title,
this.displayTitle,
this.hearingImpaired = false,
this.perfectMatch = false,
this.downloaded = false,
this.forced = false,
});
factory PlexSubtitleSearchResult.fromJson(Map<String, dynamic> json) => _$PlexSubtitleSearchResultFromJson(json);
Map<String, dynamic> toJson() => _$PlexSubtitleSearchResultToJson(this);
}
@@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_subtitle_search_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexSubtitleSearchResult _$PlexSubtitleSearchResultFromJson(Map<String, dynamic> json) => PlexSubtitleSearchResult(
id: _flexibleIntOrZero(json['id']),
key: readStringField(json, 'key') as String? ?? '',
codec: readStringField(json, 'codec') as String?,
language: readStringField(json, 'language') as String?,
languageCode: readStringField(json, 'languageCode') as String?,
score: flexibleDouble(json['score']),
providerTitle: readStringField(json, 'providerTitle') as String?,
title: readStringField(json, 'title') as String?,
displayTitle: readStringField(json, 'displayTitle') as String?,
hearingImpaired: json['hearingImpaired'] == null ? false : flexibleBool(json['hearingImpaired']),
perfectMatch: json['perfectMatch'] == null ? false : flexibleBool(json['perfectMatch']),
downloaded: json['downloaded'] == null ? false : flexibleBool(json['downloaded']),
forced: json['forced'] == null ? false : flexibleBool(json['forced']),
);
Map<String, dynamic> _$PlexSubtitleSearchResultToJson(PlexSubtitleSearchResult instance) => <String, dynamic>{
'id': instance.id,
'key': instance.key,
'codec': instance.codec,
'language': instance.language,
'languageCode': instance.languageCode,
'score': instance.score,
'providerTitle': instance.providerTitle,
'title': instance.title,
'displayTitle': instance.displayTitle,
'hearingImpaired': instance.hearingImpaired,
'perfectMatch': instance.perfectMatch,
'downloaded': instance.downloaded,
'forced': instance.forced,
};
+60
View File
@@ -0,0 +1,60 @@
import 'package:json_annotation/json_annotation.dart';
import '../../media/media_server_user_profile.dart';
part 'plex_user_profile.g.dart';
/// Represents a Plex user's profile preferences
/// Fetched from https://clients.plex.tv/api/v2/user
@JsonSerializable()
class PlexUserProfile implements MediaServerUserProfile {
@JsonKey(defaultValue: true)
@override
final bool autoSelectAudio;
@JsonKey(defaultValue: 0)
final int defaultAudioAccessibility;
@override
final String? defaultAudioLanguage;
@override
final List<String>? defaultAudioLanguages;
@override
final String? defaultSubtitleLanguage;
@override
final List<String>? defaultSubtitleLanguages;
@JsonKey(defaultValue: 0)
final int autoSelectSubtitle;
@JsonKey(defaultValue: 0)
final int defaultSubtitleAccessibility;
@JsonKey(defaultValue: 1)
final int defaultSubtitleForced;
@JsonKey(defaultValue: 1)
final int watchedIndicator;
@JsonKey(defaultValue: 0)
final int mediaReviewsVisibility;
final List<String>? mediaReviewsLanguages;
@override
SubtitlePlaybackMode? get subtitleMode => null;
PlexUserProfile({
required this.autoSelectAudio,
required this.defaultAudioAccessibility,
this.defaultAudioLanguage,
this.defaultAudioLanguages,
this.defaultSubtitleLanguage,
this.defaultSubtitleLanguages,
required this.autoSelectSubtitle,
required this.defaultSubtitleAccessibility,
required this.defaultSubtitleForced,
required this.watchedIndicator,
required this.mediaReviewsVisibility,
this.mediaReviewsLanguages,
});
factory PlexUserProfile.fromJson(Map<String, dynamic> json) {
final profile = json['profile'] as Map<String, dynamic>? ?? json;
return _$PlexUserProfileFromJson(profile);
}
Map<String, dynamic> toJson() => {'profile': _$PlexUserProfileToJson(this)};
}
+37
View File
@@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_user_profile.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexUserProfile _$PlexUserProfileFromJson(Map<String, dynamic> json) => PlexUserProfile(
autoSelectAudio: json['autoSelectAudio'] as bool? ?? true,
defaultAudioAccessibility: (json['defaultAudioAccessibility'] as num?)?.toInt() ?? 0,
defaultAudioLanguage: json['defaultAudioLanguage'] as String?,
defaultAudioLanguages: (json['defaultAudioLanguages'] as List<dynamic>?)?.map((e) => e as String).toList(),
defaultSubtitleLanguage: json['defaultSubtitleLanguage'] as String?,
defaultSubtitleLanguages: (json['defaultSubtitleLanguages'] as List<dynamic>?)?.map((e) => e as String).toList(),
autoSelectSubtitle: (json['autoSelectSubtitle'] as num?)?.toInt() ?? 0,
defaultSubtitleAccessibility: (json['defaultSubtitleAccessibility'] as num?)?.toInt() ?? 0,
defaultSubtitleForced: (json['defaultSubtitleForced'] as num?)?.toInt() ?? 1,
watchedIndicator: (json['watchedIndicator'] as num?)?.toInt() ?? 1,
mediaReviewsVisibility: (json['mediaReviewsVisibility'] as num?)?.toInt() ?? 0,
mediaReviewsLanguages: (json['mediaReviewsLanguages'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$PlexUserProfileToJson(PlexUserProfile instance) => <String, dynamic>{
'autoSelectAudio': instance.autoSelectAudio,
'defaultAudioAccessibility': instance.defaultAudioAccessibility,
'defaultAudioLanguage': instance.defaultAudioLanguage,
'defaultAudioLanguages': instance.defaultAudioLanguages,
'defaultSubtitleLanguage': instance.defaultSubtitleLanguage,
'defaultSubtitleLanguages': instance.defaultSubtitleLanguages,
'autoSelectSubtitle': instance.autoSelectSubtitle,
'defaultSubtitleAccessibility': instance.defaultSubtitleAccessibility,
'defaultSubtitleForced': instance.defaultSubtitleForced,
'watchedIndicator': instance.watchedIndicator,
'mediaReviewsVisibility': instance.mediaReviewsVisibility,
'mediaReviewsLanguages': instance.mediaReviewsLanguages,
};
@@ -0,0 +1,31 @@
import '../../media/media_source_info.dart';
import '../../media/media_version.dart';
/// Consolidated data model containing all information needed for video playback.
/// This model combines data from multiple Plex API endpoints to reduce redundant requests.
class PlexVideoPlaybackData {
/// Direct video URL for playback
final String? videoUrl;
/// Media information including audio/subtitle tracks and chapters
final MediaSourceInfo? mediaInfo;
/// Available media versions/qualities for this content
final List<MediaVersion> availableVersions;
/// Markers for intro/credits skip functionality
final List<MediaMarker> markers;
PlexVideoPlaybackData({
required this.videoUrl,
required this.mediaInfo,
required this.availableVersions,
this.markers = const [],
});
/// Returns true if this playback data has a valid video URL
bool get hasValidVideoUrl => videoUrl != null && videoUrl!.isNotEmpty;
/// Returns true if media info is available
bool get hasMediaInfo => mediaInfo != null;
}