Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/data/services/plugin_sync_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,12 @@ class PluginSyncService extends ChangeNotifier {
UserPreferences.homeRowsStyle,
enumValues: prefs.HomeRowsStyle.values,
);
_applyString(
resolved,
'recommendationSystemSource',
UserPreferences.recommendationSystemSource,
enumValues: prefs.RecommendationSystemSource.values,
);
_applyString(
resolved,
'detailScreenStyle',
Expand Down Expand Up @@ -1553,6 +1559,8 @@ class PluginSyncService extends ChangeNotifier {
.name,
'cardFocusExpansion': _prefs.get(UserPreferences.cardFocusExpansion),
'homeRowsStyle': _prefs.get(UserPreferences.homeRowsStyle).name,
'recommendationSystemSource':
_prefs.get(UserPreferences.recommendationSystemSource).name,
'detailScreenStyle': _prefs.get(UserPreferences.detailScreenStyle).name,
'homeImageTypeContinueWatching': _prefs
.get(UserPreferences.homeRowImageType(prefs.HomeSectionType.resume))
Expand Down
57 changes: 39 additions & 18 deletions lib/data/services/row_data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1845,9 +1845,35 @@ class RowDataSource {
}

final baseItem = baseItems[sourceIdx];
final itemDetail = baseItem.rawData;
final baseItemName = baseItem.name;

final recommendedItems = await getRecommendations(
serverId: serverId,
baseItem: baseItem,
isLocal: isLocal,
candidateItemTypes: candidateItemTypes,
limit: 15,
);

return HomeRow(
id: 'sinceYouWatched$rowIndex',
title: 'Since you watched "$baseItemName"',
rowType: HomeRowType.latestMedia,
items: recommendedItems,
);
}

Future<List<AggregatedItem>> getRecommendations({
required String serverId,
required AggregatedItem baseItem,
required bool isLocal,
List<String>? candidateItemTypes,
int limit = 15,
bool? includeWatched,
}) async {
final prefs = GetIt.instance<UserPreferences>();
final itemDetail = baseItem.rawData;
final types = candidateItemTypes ?? (baseItem.type == 'Series' ? const ['Series'] : const ['Movie']);
List<AggregatedItem> recommendedItems = [];

if (isLocal) {
Expand Down Expand Up @@ -1894,7 +1920,7 @@ class RowDataSource {
if (genres.isNotEmpty) {
futures.add(() async {
try {
final cacheKey = '$serverId:genres:${candidateItemTypes.join(",")}:${genres.join(",")}';
final cacheKey = '$serverId:genres:${types.join(",")}:${genres.join(",")}';
if (_recommendationCache.containsKey(cacheKey)) {
final cached = _recommendationCache[cacheKey]!;
for (final item in cached) {
Expand All @@ -1904,7 +1930,7 @@ class RowDataSource {
return;
}
final res = await _client.itemsApi.getItems(
includeItemTypes: candidateItemTypes,
includeItemTypes: types,
genres: genres,
recursive: true,
limit: 80,
Expand All @@ -1927,7 +1953,7 @@ class RowDataSource {
if (tags.isNotEmpty) {
futures.add(() async {
try {
final cacheKey = '$serverId:tags:${candidateItemTypes.join(",")}:${tags.join(",")}';
final cacheKey = '$serverId:tags:${types.join(",")}:${tags.join(",")}';
if (_recommendationCache.containsKey(cacheKey)) {
final cached = _recommendationCache[cacheKey]!;
for (final item in cached) {
Expand All @@ -1937,7 +1963,7 @@ class RowDataSource {
return;
}
final res = await _client.itemsApi.getItems(
includeItemTypes: candidateItemTypes,
includeItemTypes: types,
tags: tags,
recursive: true,
limit: 80,
Expand Down Expand Up @@ -1965,7 +1991,7 @@ class RowDataSource {
if (allPersonIds.isNotEmpty) {
futures.add(() async {
try {
final cacheKey = '$serverId:people:${candidateItemTypes.join(",")}:${allPersonIds.join(",")}';
final cacheKey = '$serverId:people:${types.join(",")}:${allPersonIds.join(",")}';
if (_recommendationCache.containsKey(cacheKey)) {
final cached = _recommendationCache[cacheKey]!;
for (final item in cached) {
Expand All @@ -1975,7 +2001,7 @@ class RowDataSource {
return;
}
final res = await _client.itemsApi.getItems(
includeItemTypes: candidateItemTypes,
includeItemTypes: types,
personIds: allPersonIds,
recursive: true,
limit: 80,
Expand All @@ -1996,7 +2022,7 @@ class RowDataSource {

await Future.wait(futures);

final includeWatched = prefs.get(UserPreferences.sinceYouWatchedIncludeWatched);
final bool effectiveIncludeWatched = includeWatched ?? prefs.get(UserPreferences.sinceYouWatchedIncludeWatched);
final sourceRating = baseItem.officialRating;
final sourceRatingLevel = _getRatingLevel(sourceRating);

Expand All @@ -2009,7 +2035,7 @@ class RowDataSource {
// Played check
final userData = candidate['UserData'] as Map?;
final isPlayed = userData?['Played'] as bool? ?? false;
if (!includeWatched && isPlayed) continue;
if (!effectiveIncludeWatched && isPlayed) continue;

// Parental rating constraint upper bound
final candRating = candidate['OfficialRating'] as String?;
Expand Down Expand Up @@ -2060,7 +2086,7 @@ class RowDataSource {
});

recommendedItems = scoredCandidates
.take(15)
.take(limit)
.map((e) => AggregatedItem(
id: e.key['Id']?.toString() ?? '',
serverId: serverId,
Expand Down Expand Up @@ -2154,7 +2180,7 @@ class RowDataSource {
'SeerrMediaType': type == 'Series' ? 'tv' : 'movie',
},
);
}).take(15).toList();
}).take(limit).toList();
}
} catch (e) {
print('[RowDataSource] Direct TMDB recommendation query failed: $e');
Expand Down Expand Up @@ -2199,20 +2225,15 @@ class RowDataSource {
'SeerrMediaType': item.mediaType,
},
);
}).take(15).toList();
}).take(limit).toList();
}
} catch (_) {}
}
}
}
}

return HomeRow(
id: 'sinceYouWatched$rowIndex',
title: 'Since you watched "$baseItemName"',
rowType: HomeRowType.latestMedia,
items: recommendedItems,
);
return recommendedItems;
}

Future<HomeRow> loadRewatchRow(String serverId) async {
Expand Down
33 changes: 32 additions & 1 deletion lib/data/viewmodels/item_detail_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';
import 'package:server_core/server_core.dart';

import '../../preference/preference_constants.dart';
import '../../preference/user_preferences.dart';
import '../models/aggregated_item.dart';
import '../models/lyrics.dart';
import '../services/row_data_source.dart';
import '../repositories/item_mutation_repository.dart';
import '../repositories/mdblist_repository.dart';
import '../repositories/tmdb_repository.dart';
Expand Down Expand Up @@ -875,8 +877,37 @@ class ItemDetailViewModel extends ChangeNotifier {
}

Future<void> _loadSimilar() async {
final item = _item;
if (item != null && (item.type == 'Movie' || item.type == 'Series')) {
try {
final prefs = GetIt.instance<UserPreferences>();
final sourceSetting = prefs.get(UserPreferences.recommendationSystemSource);
final isLocal = sourceSetting == RecommendationSystemSource.local;
final serverId = _serverId ?? _client.baseUrl;
final dataSource = GetIt.instance<RowDataSource>();

final recommended = await dataSource.getRecommendations(
serverId: serverId,
baseItem: item,
isLocal: isLocal,
limit: 15,
includeWatched: true,
);
// Only short-circuit when we actually have results. An empty list (e.g.
// the online source without Seerr configured, or no local matches)
// falls through to Jellyfin's similar-items below.
if (recommended.isNotEmpty) {
_similar = recommended;
notifyListeners();
return;
}
} catch (e) {
debugPrint('[ItemDetailViewModel] Custom recommendation system failed: $e');
}
}

try {
final data = await _client.itemsApi.getSimilarItems(itemId, limit: 12);
final data = await _client.itemsApi.getSimilarItems(itemId, limit: 15);
final items = (data['Items'] as List?) ?? [];
_similar = _mapItems(items);
notifyListeners();
Expand Down
16 changes: 16 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,22 @@
"@detailScreenStyleModern": {
"description": "Detail screen style option: the responsive cinematic layout"
},
"recommendationSystem": "Recommendation System",
"@recommendationSystem": {
"description": "Label for the Recommendation System setting"
},
"recommendationSystemSubtitle": "Use the Moonfin Recommends local-library algorithm or the online TMDb's Similarity Metrics. Note: Online recommendations require Seerr integration.",
"@recommendationSystemSubtitle": {
"description": "Explanation under the recommendation system setting"
},
"recommendationSystemMoonfin": "Moonfin Recommends",
"@recommendationSystemMoonfin": {
"description": "Recommendation system option: Moonfin Recommends"
},
"recommendationSystemTmdb": "TMDb Similarity",
"@recommendationSystemTmdb": {
"description": "Recommendation system option: TMDb Similarity"
},
"interfaceStyle": "Interface Style",
"@interfaceStyle": {
"description": "Label for the Automatic/Apple/Material interface style setting"
Expand Down
24 changes: 24 additions & 0 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,30 @@ abstract class AppLocalizations {
/// **'Modern'**
String get detailScreenStyleModern;

/// Label for the Recommendation System setting
///
/// In en, this message translates to:
/// **'Recommendation System'**
String get recommendationSystem;

/// Explanation under the recommendation system setting
///
/// In en, this message translates to:
/// **'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.'**
String get recommendationSystemSubtitle;

/// Recommendation system option: Moonfin Recommends
///
/// In en, this message translates to:
/// **'Moonfin Recommends'**
String get recommendationSystemMoonfin;

/// Recommendation system option: TMDb Similarity
///
/// In en, this message translates to:
/// **'TMDb Similarity'**
String get recommendationSystemTmdb;

/// Label for the Automatic/Apple/Material interface style setting
///
/// In en, this message translates to:
Expand Down
13 changes: 13 additions & 0 deletions lib/l10n/app_localizations_af.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ class AppLocalizationsAf extends AppLocalizations {
@override
String get detailScreenStyleModern => 'Modern';

@override
String get recommendationSystem => 'Recommendation System';

@override
String get recommendationSystemSubtitle =>
'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.';

@override
String get recommendationSystemMoonfin => 'Moonfin Recommends';

@override
String get recommendationSystemTmdb => 'TMDb Similarity';

@override
String get interfaceStyle => '';

Expand Down
13 changes: 13 additions & 0 deletions lib/l10n/app_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get detailScreenStyleModern => 'Modern';

@override
String get recommendationSystem => 'Recommendation System';

@override
String get recommendationSystemSubtitle =>
'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.';

@override
String get recommendationSystemMoonfin => 'Moonfin Recommends';

@override
String get recommendationSystemTmdb => 'TMDb Similarity';

@override
String get interfaceStyle => '';

Expand Down
13 changes: 13 additions & 0 deletions lib/l10n/app_localizations_be.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ class AppLocalizationsBe extends AppLocalizations {
@override
String get detailScreenStyleModern => 'Modern';

@override
String get recommendationSystem => 'Recommendation System';

@override
String get recommendationSystemSubtitle =>
'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.';

@override
String get recommendationSystemMoonfin => 'Moonfin Recommends';

@override
String get recommendationSystemTmdb => 'TMDb Similarity';

@override
String get interfaceStyle => '';

Expand Down
13 changes: 13 additions & 0 deletions lib/l10n/app_localizations_bg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get detailScreenStyleModern => 'Modern';

@override
String get recommendationSystem => 'Recommendation System';

@override
String get recommendationSystemSubtitle =>
'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.';

@override
String get recommendationSystemMoonfin => 'Moonfin Recommends';

@override
String get recommendationSystemTmdb => 'TMDb Similarity';

@override
String get interfaceStyle => '';

Expand Down
13 changes: 13 additions & 0 deletions lib/l10n/app_localizations_bn.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ class AppLocalizationsBn extends AppLocalizations {
@override
String get detailScreenStyleModern => 'Modern';

@override
String get recommendationSystem => 'Recommendation System';

@override
String get recommendationSystemSubtitle =>
'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.';

@override
String get recommendationSystemMoonfin => 'Moonfin Recommends';

@override
String get recommendationSystemTmdb => 'TMDb Similarity';

@override
String get interfaceStyle => '';

Expand Down
13 changes: 13 additions & 0 deletions lib/l10n/app_localizations_ca.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,19 @@ class AppLocalizationsCa extends AppLocalizations {
@override
String get detailScreenStyleModern => 'Modern';

@override
String get recommendationSystem => 'Recommendation System';

@override
String get recommendationSystemSubtitle =>
'Use the Moonfin Recommends local-library algorithm or the online TMDb\'s Similarity Metrics. Note: Online recommendations require Seerr integration.';

@override
String get recommendationSystemMoonfin => 'Moonfin Recommends';

@override
String get recommendationSystemTmdb => 'TMDb Similarity';

@override
String get interfaceStyle => '';

Expand Down
Loading
Loading