feat: Sync offline/online playback when able (#431)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-08-03 13:35:56 +02:00 committed by GitHub
parent 15ac3566e2
commit 092836328f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1002 additions and 497 deletions

View file

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:fladder/fake/fake_jellyfin_open_api.dart';
@ -12,10 +13,13 @@ import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/credentials_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/providers/auth_provider.dart';
import 'package:fladder/providers/image_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/util/jellyfin_extension.dart';
@ -84,21 +88,78 @@ class JellyService {
Future<Response<ItemBaseModel>> usersUserIdItemsItemIdGet({
String? itemId,
}) async {
final response = await api.itemsItemIdGet(
userId: account?.id,
itemId: itemId,
);
return response.copyWith(body: ItemBaseModel.fromBaseDto(response.bodyOrThrow, ref));
try {
final response = await api.itemsItemIdGet(
userId: account?.id,
itemId: itemId,
);
return response.copyWith(body: ItemBaseModel.fromBaseDto(response.bodyOrThrow, ref));
} catch (e) {
final item = (await ref.read(syncProvider.notifier).getSyncedItem(itemId))?.itemModel;
return Response<ItemBaseModel>(
http.Response("", 202),
item,
);
}
}
Future<Response<BaseItemDto>> usersUserIdItemsItemIdGetBaseItem({
String? itemId,
}) async {
final response = await api.itemsItemIdGet(
try {
return await api.itemsItemIdGet(
userId: account?.id,
itemId: itemId,
);
} catch (e) {
return ref.read(syncProvider.notifier).getSyncedItem(itemId).then(
(value) => value?.data != null
? Response<BaseItemDto>(
http.Response("", 202),
value?.data,
)
: Response<BaseItemDto>(
http.Response("", 404),
null,
),
);
}
}
Future<Response<UserData>> userItemsItemIdUserDataGet({
String? itemId,
}) async {
final response = await api.userItemsItemIdUserDataGet(
userId: account?.id,
itemId: itemId,
);
return response;
return response.copyWith(
body: UserData.fromDto(response.bodyOrThrow),
);
}
Future<Response<UserData>?> userItemsItemIdUserDataPost({
String? itemId,
required UserData? body,
}) async {
if (body == null) {
return null;
}
final response = await api.userItemsItemIdUserDataPost(
userId: account?.id,
itemId: itemId,
body: UpdateUserItemDataDto(
playCount: body.playCount,
playbackPositionTicks: body.playbackPositionTicks,
isFavorite: body.isFavourite,
played: body.played,
lastPlayedDate: body.lastPlayed,
itemId: itemId,
),
);
return response.copyWith(
body: UserData.fromDto(response.bodyOrThrow),
);
}
Future<Response<ServerQueryResult>> itemsGet({
@ -491,8 +552,15 @@ class JellyService {
Future<Response> sessionsPlayingStoppedPost({
required PlaybackStopInfo? body,
}) =>
api.sessionsPlayingStoppedPost(body: body);
}) {
final positionTicks = body?.positionTicks;
if (positionTicks != null) {
ref
.read(syncProvider.notifier)
.updatePlaybackPosition(itemId: body?.itemId, position: Duration(milliseconds: positionTicks ~/ 10000));
}
return api.sessionsPlayingStoppedPost(body: body);
}
Future<Response> sessionsPlayingProgressPost({required PlaybackProgressInfo? body}) async =>
api.sessionsPlayingProgressPost(body: body);
@ -533,25 +601,51 @@ class JellyService {
bool? enableUserData,
ShowsSeriesIdEpisodesGetSortBy? sortBy,
}) async {
return api.showsSeriesIdEpisodesGet(
seriesId: seriesId,
userId: account?.id,
fields: [
...?fields,
ItemFields.parentid,
],
isMissing: isMissing,
limit: limit,
sortBy: sortBy,
enableUserData: enableUserData,
startIndex: startIndex,
adjacentTo: adjacentTo,
startItemId: startItemId,
season: season,
seasonId: seasonId,
enableImages: enableImages,
enableImageTypes: enableImageTypes,
);
try {
var response = await api.showsSeriesIdEpisodesGet(
seriesId: seriesId,
userId: account?.id,
fields: [
...?fields,
ItemFields.parentid,
],
isMissing: isMissing,
limit: limit,
sortBy: sortBy,
enableUserData: enableUserData,
startIndex: startIndex,
adjacentTo: adjacentTo,
startItemId: startItemId,
season: season,
seasonId: seasonId,
enableImages: enableImages,
enableImageTypes: enableImageTypes,
);
return response;
} catch (e) {
final seriesItem = await ref.read(syncProvider.notifier).getSyncedItem(seriesId);
if (seriesItem != null) {
final episodes = await ref.read(syncProvider.notifier).getNestedChildren(seriesItem)
..where((e) => e.itemModel is EpisodeModel);
return Response<BaseItemDtoQueryResult>(
http.Response("", 200),
BaseItemDtoQueryResult(
items: episodes.map((e) => e.data).nonNulls.toList(),
totalRecordCount: episodes.length,
startIndex: 0,
),
);
} else {
return Response<BaseItemDtoQueryResult>(
http.Response("", 400),
const BaseItemDtoQueryResult(
items: [],
totalRecordCount: 0,
startIndex: 0,
),
);
}
}
}
Future<List<ItemBaseModel>> fetchEpisodeFromShow({
@ -566,11 +660,22 @@ class JellyService {
String? itemId,
int? limit,
}) async {
return api.itemsItemIdSimilarGet(
userId: account?.id,
itemId: itemId,
limit: limit,
);
try {
return await api.itemsItemIdSimilarGet(userId: account?.id, itemId: itemId, limit: limit, fields: [
ItemFields.parentid,
ItemFields.candelete,
ItemFields.candownload,
]);
} catch (e) {
return Response<BaseItemDtoQueryResult>(
http.Response("", 400),
const BaseItemDtoQueryResult(
items: [],
totalRecordCount: 0,
startIndex: 0,
),
);
}
}
Future<Response<BaseItemDtoQueryResult>> usersUserIdItemsGet({
@ -692,8 +797,9 @@ class JellyService {
bool? enableUserData,
bool? isMissing,
List<ItemFields>? fields,
}) =>
api.showsSeriesIdSeasonsGet(
}) async {
try {
final response = await api.showsSeriesIdSeasonsGet(
seriesId: seriesId,
isMissing: isMissing,
enableUserData: enableUserData,
@ -702,6 +808,31 @@ class JellyService {
ItemFields.parentid,
],
);
return response;
} catch (e) {
final seriesItem = await ref.read(syncProvider.notifier).getSyncedItem(seriesId);
if (seriesItem != null) {
final seasons = await ref.read(syncProvider.notifier).getChildren(seriesItem.id);
return Response<BaseItemDtoQueryResult>(
http.Response("", 200),
BaseItemDtoQueryResult(
items: seasons.map((e) => e.data).nonNulls.toList(),
totalRecordCount: seasons.length,
startIndex: 0,
),
);
} else {
return Response<BaseItemDtoQueryResult>(
http.Response("", 400),
const BaseItemDtoQueryResult(
items: [],
totalRecordCount: 0,
startIndex: 0,
),
);
}
}
}
Future<Response<QueryFilters>> itemsFilters2Get({
String? parentId,
@ -817,33 +948,75 @@ class JellyService {
Future<Response<UserItemDataDto>> usersUserIdFavoriteItemsItemIdPost({
required String? itemId,
}) =>
api.userFavoriteItemsItemIdPost(
}) async {
Response<UserItemDataDto>? response;
try {
response = await api.userFavoriteItemsItemIdPost(
itemId: itemId,
userId: account?.id,
);
} finally {
await ref
.read(syncProvider.notifier)
.updateFavoriteItem(itemId, isFavorite: true, responseSuccessful: response?.isSuccessful ?? false);
}
return response;
}
Future<Response<UserItemDataDto>> usersUserIdFavoriteItemsItemIdDelete({
required String? itemId,
}) =>
api.userFavoriteItemsItemIdDelete(
}) async {
Response<UserItemDataDto>? response;
try {
response = await api.userFavoriteItemsItemIdDelete(
itemId: itemId,
userId: account?.id,
);
} finally {
await ref
.read(syncProvider.notifier)
.updateFavoriteItem(itemId, isFavorite: false, responseSuccessful: response?.isSuccessful ?? false);
}
return response;
}
Future<Response<UserItemDataDto>> usersUserIdPlayedItemsItemIdPost({
required String? itemId,
DateTime? datePlayed,
}) =>
api.userPlayedItemsItemIdPost(itemId: itemId, userId: account?.id, datePlayed: datePlayed);
}) async {
Response<UserItemDataDto>? response;
try {
response = await api.userPlayedItemsItemIdPost(itemId: itemId, userId: account?.id, datePlayed: datePlayed);
} finally {
await ref.read(syncProvider.notifier).updatePlayedItem(
itemId,
datePlayed: datePlayed,
played: true,
responseSuccessful: response?.isSuccessful ?? false,
);
}
return response;
}
Future<Response<UserItemDataDto>> usersUserIdPlayedItemsItemIdDelete({
required String? itemId,
}) =>
api.userPlayedItemsItemIdDelete(
}) async {
Response<UserItemDataDto>? response;
try {
response = await api.userPlayedItemsItemIdDelete(
itemId: itemId,
userId: account?.id,
);
} finally {
await ref.read(syncProvider.notifier).updatePlayedItem(
itemId,
played: false,
responseSuccessful: response?.isSuccessful ?? false,
);
}
return response;
}
Future<Response<MediaSegmentsModel>?> mediaSegmentsGet({
required String id,