This commit is contained in:
edde746
2025-10-22 12:45:29 +02:00
commit a6effe208b
152 changed files with 16289 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
import 'package:json_annotation/json_annotation.dart';
import 'plex_metadata.dart';
import 'plex_library.dart';
part 'media_container.g.dart';
@JsonSerializable()
class MediaContainer<T> {
final int? size;
final int? totalSize;
final int? offset;
final String? identifier;
@JsonKey(name: 'Directory')
final List<PlexLibrary>? directories;
@JsonKey(name: 'Metadata')
final List<PlexMetadata>? metadata;
MediaContainer({
this.size,
this.totalSize,
this.offset,
this.identifier,
this.directories,
this.metadata,
});
factory MediaContainer.fromJson(Map<String, dynamic> json) =>
_$MediaContainerFromJson(json);
Map<String, dynamic> toJson() => _$MediaContainerToJson(this);
}
+31
View File
@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'media_container.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MediaContainer<T> _$MediaContainerFromJson<T>(Map<String, dynamic> json) =>
MediaContainer<T>(
size: (json['size'] as num?)?.toInt(),
totalSize: (json['totalSize'] as num?)?.toInt(),
offset: (json['offset'] as num?)?.toInt(),
identifier: json['identifier'] as String?,
directories: (json['Directory'] as List<dynamic>?)
?.map((e) => PlexLibrary.fromJson(e as Map<String, dynamic>))
.toList(),
metadata: (json['Metadata'] as List<dynamic>?)
?.map((e) => PlexMetadata.fromJson(e as Map<String, dynamic>))
.toList(),
);
Map<String, dynamic> _$MediaContainerToJson<T>(MediaContainer<T> instance) =>
<String, dynamic>{
'size': instance.size,
'totalSize': instance.totalSize,
'offset': instance.offset,
'identifier': instance.identifier,
'Directory': instance.directories,
'Metadata': instance.metadata,
};
+55
View File
@@ -0,0 +1,55 @@
class PlexFilter {
final String filter;
final String filterType;
final String key;
final String title;
final String type;
PlexFilter({
required this.filter,
required this.filterType,
required this.key,
required this.title,
required this.type,
});
factory PlexFilter.fromJson(Map<String, dynamic> json) {
return PlexFilter(
filter: json['filter'] ?? '',
filterType: json['filterType'] ?? 'string',
key: json['key'] ?? '',
title: json['title'] ?? '',
type: json['type'] ?? 'filter',
);
}
Map<String, dynamic> toJson() {
return {
'filter': filter,
'filterType': filterType,
'key': key,
'title': title,
'type': type,
};
}
}
class PlexFilterValue {
final String key;
final String title;
final String? type;
PlexFilterValue({required this.key, required this.title, this.type});
factory PlexFilterValue.fromJson(Map<String, dynamic> json) {
return PlexFilterValue(
key: json['key'] ?? '',
title: json['title'] ?? '',
type: json['type'],
);
}
Map<String, dynamic> toJson() {
return {'key': key, 'title': title, if (type != null) 'type': type};
}
}
+33
View File
@@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
part 'plex_library.g.dart';
@JsonSerializable()
class PlexLibrary {
final String key;
final String title;
final String type;
final String? agent;
final String? scanner;
final String? language;
final String? uuid;
final int? updatedAt;
final int? createdAt;
PlexLibrary({
required this.key,
required this.title,
required this.type,
this.agent,
this.scanner,
this.language,
this.uuid,
this.updatedAt,
this.createdAt,
});
factory PlexLibrary.fromJson(Map<String, dynamic> json) =>
_$PlexLibraryFromJson(json);
Map<String, dynamic> toJson() => _$PlexLibraryToJson(this);
}
+32
View File
@@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_library.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexLibrary _$PlexLibraryFromJson(Map<String, dynamic> json) => PlexLibrary(
key: json['key'] as String,
title: json['title'] as String,
type: json['type'] as String,
agent: json['agent'] as String?,
scanner: json['scanner'] as String?,
language: json['language'] as String?,
uuid: json['uuid'] as String?,
updatedAt: (json['updatedAt'] as num?)?.toInt(),
createdAt: (json['createdAt'] as num?)?.toInt(),
);
Map<String, dynamic> _$PlexLibraryToJson(PlexLibrary instance) =>
<String, dynamic>{
'key': instance.key,
'title': instance.title,
'type': instance.type,
'agent': instance.agent,
'scanner': instance.scanner,
'language': instance.language,
'uuid': instance.uuid,
'updatedAt': instance.updatedAt,
'createdAt': instance.createdAt,
};
+104
View File
@@ -0,0 +1,104 @@
class PlexMediaInfo {
final String videoUrl;
final List<PlexAudioTrack> audioTracks;
final List<PlexSubtitleTrack> subtitleTracks;
final List<PlexChapter> chapters;
PlexMediaInfo({
required this.videoUrl,
required this.audioTracks,
required this.subtitleTracks,
required this.chapters,
});
}
class PlexAudioTrack {
final int id;
final int? index;
final String? codec;
final String? language;
final String? languageCode;
final String? title;
final String? displayTitle;
final int? channels;
final bool selected;
PlexAudioTrack({
required this.id,
this.index,
this.codec,
this.language,
this.languageCode,
this.title,
this.displayTitle,
this.channels,
required this.selected,
});
String get label {
if (displayTitle != null) return displayTitle!;
final parts = <String>[];
if (language != null) parts.add(language!);
if (codec != null) parts.add(codec!.toUpperCase());
if (channels != null) parts.add('${channels!}ch');
return parts.isEmpty ? 'Track ${index ?? id}' : parts.join(' · ');
}
}
class PlexSubtitleTrack {
final int id;
final int? index;
final String? codec;
final String? language;
final String? languageCode;
final String? title;
final String? displayTitle;
final bool selected;
final bool forced;
final String? key;
PlexSubtitleTrack({
required this.id,
this.index,
this.codec,
this.language,
this.languageCode,
this.title,
this.displayTitle,
required this.selected,
required this.forced,
this.key,
});
String get label {
if (displayTitle != null) return displayTitle!;
final parts = <String>[];
if (language != null) parts.add(language!);
if (forced) parts.add('Forced');
return parts.isEmpty ? 'Track ${index ?? id}' : parts.join(' · ');
}
}
class PlexChapter {
final int id;
final int? index;
final int? startTimeOffset;
final int? endTimeOffset;
final String? title;
final String? thumb;
PlexChapter({
required this.id,
this.index,
this.startTimeOffset,
this.endTimeOffset,
this.title,
this.thumb,
});
String get label => title ?? 'Chapter ${(index ?? 0) + 1}';
Duration get startTime => Duration(milliseconds: startTimeOffset ?? 0);
Duration? get endTime =>
endTimeOffset != null ? Duration(milliseconds: endTimeOffset!) : null;
}
+150
View File
@@ -0,0 +1,150 @@
import 'package:json_annotation/json_annotation.dart';
part 'plex_metadata.g.dart';
@JsonSerializable()
class PlexMetadata {
final String ratingKey;
final String key;
final String? guid;
final String? studio;
final String type;
final String title;
final String? contentRating;
final String? summary;
final int? rating;
final int? year;
final String? thumb;
final String? art;
final int? duration;
final int? addedAt;
final int? updatedAt;
final String? grandparentTitle; // Show title for episodes
final String? grandparentThumb; // Show poster for episodes
final String? grandparentArt; // Show art for episodes
final String? grandparentRatingKey; // Show rating key for episodes
final String? parentTitle; // Season title for episodes
final String? parentRatingKey; // Season rating key for episodes
final int? parentIndex; // Season number
final int? index; // Episode number
final String? grandparentTheme; // Show theme music
final int? viewOffset; // Resume position in ms
final int? viewCount;
final int? leafCount; // Total number of episodes in a series/season
final int? viewedLeafCount; // Number of watched episodes in a series/season
// Transient field for clear logo (extracted from Image array)
String? _clearLogo;
String? get clearLogo => _clearLogo;
PlexMetadata({
required this.ratingKey,
required this.key,
this.guid,
this.studio,
required this.type,
required this.title,
this.contentRating,
this.summary,
this.rating,
this.year,
this.thumb,
this.art,
this.duration,
this.addedAt,
this.updatedAt,
this.grandparentTitle,
this.grandparentThumb,
this.grandparentArt,
this.grandparentRatingKey,
this.parentTitle,
this.parentRatingKey,
this.parentIndex,
this.index,
this.grandparentTheme,
this.viewOffset,
this.viewCount,
this.leafCount,
this.viewedLeafCount,
});
// Extract clearLogo from Image array in raw JSON
void _extractClearLogo(Map<String, dynamic> json) {
if (!json.containsKey('Image')) return;
final images = json['Image'] as List?;
if (images == null) return;
for (var image in images) {
if (image is Map && image['type'] == 'clearLogo') {
_clearLogo = image['url'] as String?;
return;
}
}
}
// Custom factory that extracts clearLogo
factory PlexMetadata.fromJsonWithImages(Map<String, dynamic> json) {
final metadata = PlexMetadata.fromJson(json);
metadata._extractClearLogo(json);
return metadata;
}
// Helper to get the display title (show name for episodes/seasons, title otherwise)
String get displayTitle {
final itemType = type.toLowerCase();
// For episodes and seasons, prefer grandparent title (show name)
if ((itemType == 'episode' || itemType == 'season') &&
grandparentTitle != null) {
return grandparentTitle!;
}
// For seasons without grandparent, check if this IS the show (parentTitle might have show name)
if (itemType == 'season' && parentTitle != null) {
return parentTitle!;
}
return title;
}
// Helper to get the subtitle (episode/season title)
String? get displaySubtitle {
final itemType = type.toLowerCase();
if (itemType == 'episode' || itemType == 'season') {
// If we showed grandparent/parent as title, show this item's title as subtitle
if (grandparentTitle != null ||
(itemType == 'season' && parentTitle != null)) {
return title;
}
}
return null;
}
// Helper to get the poster (show poster for episodes/seasons, thumb otherwise)
String? get posterThumb {
final itemType = type.toLowerCase();
// For episodes and seasons, prefer grandparent thumb (show poster)
if ((itemType == 'episode' || itemType == 'season') &&
grandparentThumb != null) {
return grandparentThumb!;
}
return thumb;
}
// Helper to determine if content is watched
bool get isWatched {
// For series/seasons, check if all episodes are watched
if (leafCount != null && viewedLeafCount != null) {
return viewedLeafCount! >= leafCount!;
}
// For individual items (movies, episodes), check viewCount
return viewCount != null && viewCount! > 0;
}
factory PlexMetadata.fromJson(Map<String, dynamic> json) =>
_$PlexMetadataFromJson(json);
Map<String, dynamic> toJson() => _$PlexMetadataToJson(this);
}
+70
View File
@@ -0,0 +1,70 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_metadata.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
ratingKey: json['ratingKey'] as String,
key: json['key'] as String,
guid: json['guid'] as String?,
studio: json['studio'] as String?,
type: json['type'] as String,
title: json['title'] as String,
contentRating: json['contentRating'] as String?,
summary: json['summary'] as String?,
rating: (json['rating'] as num?)?.toInt(),
year: (json['year'] as num?)?.toInt(),
thumb: json['thumb'] as String?,
art: json['art'] as String?,
duration: (json['duration'] as num?)?.toInt(),
addedAt: (json['addedAt'] as num?)?.toInt(),
updatedAt: (json['updatedAt'] as num?)?.toInt(),
grandparentTitle: json['grandparentTitle'] as String?,
grandparentThumb: json['grandparentThumb'] as String?,
grandparentArt: json['grandparentArt'] as String?,
grandparentRatingKey: json['grandparentRatingKey'] as String?,
parentTitle: json['parentTitle'] as String?,
parentRatingKey: json['parentRatingKey'] as String?,
parentIndex: (json['parentIndex'] as num?)?.toInt(),
index: (json['index'] as num?)?.toInt(),
grandparentTheme: json['grandparentTheme'] as String?,
viewOffset: (json['viewOffset'] as num?)?.toInt(),
viewCount: (json['viewCount'] as num?)?.toInt(),
leafCount: (json['leafCount'] as num?)?.toInt(),
viewedLeafCount: (json['viewedLeafCount'] as num?)?.toInt(),
);
Map<String, dynamic> _$PlexMetadataToJson(PlexMetadata instance) =>
<String, dynamic>{
'ratingKey': instance.ratingKey,
'key': instance.key,
'guid': instance.guid,
'studio': instance.studio,
'type': instance.type,
'title': instance.title,
'contentRating': instance.contentRating,
'summary': instance.summary,
'rating': instance.rating,
'year': instance.year,
'thumb': instance.thumb,
'art': instance.art,
'duration': instance.duration,
'addedAt': instance.addedAt,
'updatedAt': instance.updatedAt,
'grandparentTitle': instance.grandparentTitle,
'grandparentThumb': instance.grandparentThumb,
'grandparentArt': instance.grandparentArt,
'grandparentRatingKey': instance.grandparentRatingKey,
'parentTitle': instance.parentTitle,
'parentRatingKey': instance.parentRatingKey,
'parentIndex': instance.parentIndex,
'index': instance.index,
'grandparentTheme': instance.grandparentTheme,
'viewOffset': instance.viewOffset,
'viewCount': instance.viewCount,
'leafCount': instance.leafCount,
'viewedLeafCount': instance.viewedLeafCount,
};
+29
View File
@@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';
part 'plex_server_info.g.dart';
@JsonSerializable()
class PlexServerInfo {
final String name;
final String? host;
final int? port;
final String? machineIdentifier;
final String version;
final bool? owned;
final bool? https;
PlexServerInfo({
required this.name,
this.host,
this.port,
this.machineIdentifier,
required this.version,
this.owned,
this.https,
});
factory PlexServerInfo.fromJson(Map<String, dynamic> json) =>
_$PlexServerInfoFromJson(json);
Map<String, dynamic> toJson() => _$PlexServerInfoToJson(this);
}
+29
View File
@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'plex_server_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PlexServerInfo _$PlexServerInfoFromJson(Map<String, dynamic> json) =>
PlexServerInfo(
name: json['name'] as String,
host: json['host'] as String?,
port: (json['port'] as num?)?.toInt(),
machineIdentifier: json['machineIdentifier'] as String?,
version: json['version'] as String,
owned: json['owned'] as bool?,
https: json['https'] as bool?,
);
Map<String, dynamic> _$PlexServerInfoToJson(PlexServerInfo instance) =>
<String, dynamic>{
'name': instance.name,
'host': instance.host,
'port': instance.port,
'machineIdentifier': instance.machineIdentifier,
'version': instance.version,
'owned': instance.owned,
'https': instance.https,
};
+83
View File
@@ -0,0 +1,83 @@
/// Represents a Plex user's profile preferences
/// Fetched from https://clients.plex.tv/api/v2/user
class PlexUserProfile {
final bool autoSelectAudio;
final int defaultAudioAccessibility;
final String? defaultAudioLanguage;
final List<String>? defaultAudioLanguages;
final String? defaultSubtitleLanguage;
final List<String>? defaultSubtitleLanguages;
final int autoSelectSubtitle;
final int defaultSubtitleAccessibility;
final int defaultSubtitleForced;
final int watchedIndicator;
final int mediaReviewsVisibility;
final List<String>? mediaReviewsLanguages;
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 PlexUserProfile(
autoSelectAudio: profile['autoSelectAudio'] as bool? ?? true,
defaultAudioAccessibility:
profile['defaultAudioAccessibility'] as int? ?? 0,
defaultAudioLanguage: profile['defaultAudioLanguage'] as String?,
defaultAudioLanguages: profile['defaultAudioLanguages'] != null
? List<String>.from(profile['defaultAudioLanguages'] as List)
: null,
defaultSubtitleLanguage: profile['defaultSubtitleLanguage'] as String?,
defaultSubtitleLanguages: profile['defaultSubtitleLanguages'] != null
? List<String>.from(profile['defaultSubtitleLanguages'] as List)
: null,
autoSelectSubtitle: profile['autoSelectSubtitle'] as int? ?? 0,
defaultSubtitleAccessibility:
profile['defaultSubtitleAccessibility'] as int? ?? 0,
defaultSubtitleForced: profile['defaultSubtitleForced'] as int? ?? 1,
watchedIndicator: profile['watchedIndicator'] as int? ?? 1,
mediaReviewsVisibility: profile['mediaReviewsVisibility'] as int? ?? 0,
mediaReviewsLanguages: profile['mediaReviewsLanguages'] != null
? List<String>.from(profile['mediaReviewsLanguages'] as List)
: null,
);
}
Map<String, dynamic> toJson() {
return {
'profile': {
'autoSelectAudio': autoSelectAudio,
'defaultAudioAccessibility': defaultAudioAccessibility,
'defaultAudioLanguage': defaultAudioLanguage,
'defaultAudioLanguages': defaultAudioLanguages,
'defaultSubtitleLanguage': defaultSubtitleLanguage,
'defaultSubtitleLanguages': defaultSubtitleLanguages,
'autoSelectSubtitle': autoSelectSubtitle,
'defaultSubtitleAccessibility': defaultSubtitleAccessibility,
'defaultSubtitleForced': defaultSubtitleForced,
'watchedIndicator': watchedIndicator,
'mediaReviewsVisibility': mediaReviewsVisibility,
'mediaReviewsLanguages': mediaReviewsLanguages,
},
};
}
/// Returns true if subtitles should be automatically selected
bool get shouldAutoSelectSubtitle => autoSelectSubtitle > 0;
/// Returns true if forced subtitles should be preferred
bool get preferForcedSubtitles => defaultSubtitleForced == 1;
}